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 d201646e81c..c7ee5a7d00c 100644 --- a/homeassistant/components/openhome/__init__.py +++ b/homeassistant/components/openhome/__init__.py @@ -17,7 +17,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.MEDIA_PLAYER] +PLATFORMS = [Platform.MEDIA_PLAYER, Platform.UPDATE] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index 61d425895bf..de6c56a01dd 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/openhome", "iot_class": "local_polling", "loggers": ["async_upnp_client", "openhomedevice"], - "requirements": ["openhomedevice==2.0.2"], + "requirements": ["openhomedevice==2.2.0"], "ssdp": [ { "st": "urn:av-openhome-org:service:Product:1" diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index c0941906e40..77ab0ac0aaf 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -126,9 +126,9 @@ class OpenhomeDevice(MediaPlayerEntity): identifiers={ (DOMAIN, self._device.uuid()), }, - manufacturer=self._device.device.manufacturer, - model=self._device.device.model_name, - name=self._device.device.friendly_name, + manufacturer=self._device.manufacturer(), + model=self._device.model_name(), + name=self._device.friendly_name(), ) @property diff --git a/homeassistant/components/openhome/update.py b/homeassistant/components/openhome/update.py new file mode 100644 index 00000000000..22bffad44d8 --- /dev/null +++ b/homeassistant/components/openhome/update.py @@ -0,0 +1,102 @@ +"""Update entities for Linn devices.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any + +import aiohttp +from async_upnp_client.client import UpnpError + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up update entities for Reolink component.""" + + _LOGGER.debug("Setting up config entry: %s", config_entry.unique_id) + + device = hass.data[DOMAIN][config_entry.entry_id] + + entity = OpenhomeUpdateEntity(device) + + await entity.async_update() + + async_add_entities([entity]) + + +class OpenhomeUpdateEntity(UpdateEntity): + """Update entity for a Linn DS device.""" + + _attr_device_class = UpdateDeviceClass.FIRMWARE + _attr_supported_features = UpdateEntityFeature.INSTALL + _attr_has_entity_name = True + + def __init__(self, device): + """Initialize a Linn DS update entity.""" + self._device = device + self._attr_unique_id = f"{device.uuid()}-update" + + @property + def device_info(self): + """Return a device description for device registry.""" + return DeviceInfo( + identifiers={ + (DOMAIN, self._device.uuid()), + }, + manufacturer=self._device.manufacturer(), + model=self._device.model_name(), + name=self._device.friendly_name(), + ) + + async def async_update(self) -> None: + """Update state of entity.""" + + software_status = await self._device.software_status() + + if not software_status: + self._attr_installed_version = None + self._attr_latest_version = None + self._attr_release_summary = None + self._attr_release_url = None + return + + self._attr_installed_version = software_status["current_software"]["version"] + + if software_status["status"] == "update_available": + self._attr_latest_version = software_status["update_info"]["updates"][0][ + "version" + ] + self._attr_release_summary = software_status["update_info"]["updates"][0][ + "description" + ] + self._attr_release_url = software_status["update_info"]["releasenotesuri"] + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install the latest firmware version.""" + try: + if self.latest_version: + await self._device.update_firmware() + except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError) as err: + raise HomeAssistantError( + f"Error updating {self._device.device.friendly_name}: {err}" + ) from err diff --git a/requirements_all.txt b/requirements_all.txt index 875c138aa12..b3cb0d2a6a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1348,7 +1348,7 @@ openerz-api==0.2.0 openevsewifi==1.1.2 # homeassistant.components.openhome -openhomedevice==2.0.2 +openhomedevice==2.2.0 # homeassistant.components.opensensemap opensensemap-api==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7af3d317830..4d309c5f18b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1026,7 +1026,7 @@ openai==0.27.2 openerz-api==0.2.0 # homeassistant.components.openhome -openhomedevice==2.0.2 +openhomedevice==2.2.0 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/tests/components/openhome/test_update.py b/tests/components/openhome/test_update.py new file mode 100644 index 00000000000..d975cc29af4 --- /dev/null +++ b/tests/components/openhome/test_update.py @@ -0,0 +1,172 @@ +"""Tests for the Openhome update platform.""" +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.openhome.const import DOMAIN +from homeassistant.components.update import ( + ATTR_INSTALLED_VERSION, + ATTR_LATEST_VERSION, + ATTR_RELEASE_SUMMARY, + ATTR_RELEASE_URL, + DOMAIN as PLATFORM_DOMAIN, + SERVICE_INSTALL, + UpdateDeviceClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + CONF_HOST, + STATE_ON, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from tests.common import MockConfigEntry + +LATEST_FIRMWARE_INSTALLED = { + "status": "on_latest", + "current_software": {"version": "4.100.502", "topic": "main", "channel": "release"}, +} + +FIRMWARE_UPDATE_AVAILABLE = { + "status": "update_available", + "current_software": {"version": "4.99.491", "topic": "main", "channel": "release"}, + "update_info": { + "legal": { + "licenseurl": "http://products.linn.co.uk/VersionInfo/licenseV2.txt", + "privacyurl": "https://www.linn.co.uk/privacy", + "privacyuri": "https://products.linn.co.uk/VersionInfo/PrivacyV1.json", + "privacyversion": 1, + }, + "releasenotesuri": "http://docs.linn.co.uk/wiki/index.php/ReleaseNotes", + "updates": [ + { + "channel": "release", + "date": "07 Jun 2023 12:29:48", + "description": "Release build version 4.100.502 (07 Jun 2023 12:29:48)", + "exaktlink": "3", + "manifest": "https://cloud.linn.co.uk/update/components/836/4.100.502/manifest.json", + "topic": "main", + "variant": "836", + "version": "4.100.502", + } + ], + "exaktUpdates": [], + }, +} + + +async def setup_integration( + hass: HomeAssistant, + software_status: dict, + update_firmware: AsyncMock, +) -> None: + """Load an openhome platform with mocked device.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "http://localhost"}, + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.openhome.PLATFORMS", [Platform.UPDATE]), patch( + "homeassistant.components.openhome.Device", MagicMock() + ) as mock_device: + mock_device.return_value.init = AsyncMock() + mock_device.return_value.uuid = MagicMock(return_value="uuid") + mock_device.return_value.manufacturer = MagicMock(return_value="manufacturer") + mock_device.return_value.model_name = MagicMock(return_value="model_name") + mock_device.return_value.friendly_name = MagicMock(return_value="friendly_name") + mock_device.return_value.software_status = AsyncMock( + return_value=software_status + ) + mock_device.return_value.update_firmware = update_firmware + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + +async def test_not_supported(hass: HomeAssistant): + """Ensure update entity works if service not supported.""" + + update_firmware = AsyncMock() + await setup_integration(hass, None, update_firmware) + + state = hass.states.get("update.friendly_name") + + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert state.attributes[ATTR_INSTALLED_VERSION] is None + assert state.attributes[ATTR_LATEST_VERSION] is None + assert state.attributes[ATTR_RELEASE_URL] is None + assert state.attributes[ATTR_RELEASE_SUMMARY] is None + update_firmware.assert_not_called() + + +async def test_on_latest_firmware(hass: HomeAssistant): + """Test device on latest firmware.""" + + update_firmware = AsyncMock() + await setup_integration(hass, LATEST_FIRMWARE_INSTALLED, update_firmware) + + state = hass.states.get("update.friendly_name") + + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert state.attributes[ATTR_INSTALLED_VERSION] == "4.100.502" + assert state.attributes[ATTR_LATEST_VERSION] is None + assert state.attributes[ATTR_RELEASE_URL] is None + assert state.attributes[ATTR_RELEASE_SUMMARY] is None + update_firmware.assert_not_called() + + +async def test_update_available(hass: HomeAssistant): + """Test device has firmware update available.""" + + update_firmware = AsyncMock() + await setup_integration(hass, FIRMWARE_UPDATE_AVAILABLE, update_firmware) + + state = hass.states.get("update.friendly_name") + + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert state.attributes[ATTR_INSTALLED_VERSION] == "4.99.491" + assert state.attributes[ATTR_LATEST_VERSION] == "4.100.502" + assert ( + state.attributes[ATTR_RELEASE_URL] + == "http://docs.linn.co.uk/wiki/index.php/ReleaseNotes" + ) + assert ( + state.attributes[ATTR_RELEASE_SUMMARY] + == "Release build version 4.100.502 (07 Jun 2023 12:29:48)" + ) + + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.friendly_name"}, + blocking=True, + ) + await hass.async_block_till_done() + + update_firmware.assert_called_once() + + +async def test_firmware_update_not_required(hass: HomeAssistant): + """Ensure firmware install does nothing if up to date.""" + + update_firmware = AsyncMock() + await setup_integration(hass, LATEST_FIRMWARE_INSTALLED, update_firmware) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.friendly_name"}, + blocking=True, + ) + update_firmware.assert_not_called() From 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 bcc19bfb55d..331a1bc151a 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 4bc5e189cf2..f3d3d48fdd2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum 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 9c67835c544..e857abd31e5 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 bd65b3a0925..7c94c07c8cd 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -65,7 +65,6 @@ DISCOVERY_SCHEMAS = [ entity_description=MatterBinarySensorEntityDescription( key="HueMotionSensor", device_class=BinarySensorDeviceClass.MOTION, - name="Motion", measurement_to_ha=lambda x: (x & 1 == 1) if x is not None else None, ), entity_class=MatterBinarySensor, @@ -78,7 +77,6 @@ DISCOVERY_SCHEMAS = [ entity_description=MatterBinarySensorEntityDescription( key="ContactSensor", device_class=BinarySensorDeviceClass.DOOR, - name="Contact", # value is inverted on matter to what we expect measurement_to_ha=lambda x: not x, ), @@ -90,7 +88,6 @@ DISCOVERY_SCHEMAS = [ entity_description=MatterBinarySensorEntityDescription( key="OccupancySensor", device_class=BinarySensorDeviceClass.OCCUPANCY, - name="Occupancy", # The first bit = if occupied measurement_to_ha=lambda x: (x & 1 == 1) if x is not None else None, ), @@ -102,7 +99,6 @@ DISCOVERY_SCHEMAS = [ entity_description=MatterBinarySensorEntityDescription( key="BatteryChargeLevel", device_class=BinarySensorDeviceClass.BATTERY, - name="Battery Status", measurement_to_ha=lambda x: x != clusters.PowerSource.Enums.BatChargeLevelEnum.kOk, ), diff --git a/homeassistant/components/matter/cover.py b/homeassistant/components/matter/cover.py index 61c5d4cd2ff..590f325cf22 100644 --- a/homeassistant/components/matter/cover.py +++ b/homeassistant/components/matter/cover.py @@ -199,7 +199,7 @@ class MatterCover(MatterEntity, CoverEntity): DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.COVER, - entity_description=CoverEntityDescription(key="MatterCover"), + entity_description=CoverEntityDescription(key="MatterCover", name=None), entity_class=MatterCover, required_attributes=( clusters.WindowCovering.Attributes.OperationalStatus, @@ -212,7 +212,9 @@ DISCOVERY_SCHEMAS = [ ), MatterDiscoverySchema( platform=Platform.COVER, - entity_description=CoverEntityDescription(key="MatterCoverPositionAwareLift"), + entity_description=CoverEntityDescription( + key="MatterCoverPositionAwareLift", name=None + ), entity_class=MatterCover, required_attributes=( clusters.WindowCovering.Attributes.OperationalStatus, @@ -225,7 +227,9 @@ DISCOVERY_SCHEMAS = [ ), MatterDiscoverySchema( platform=Platform.COVER, - entity_description=CoverEntityDescription(key="MatterCoverPositionAwareTilt"), + entity_description=CoverEntityDescription( + key="MatterCoverPositionAwareTilt", name=None + ), entity_class=MatterCover, required_attributes=( clusters.WindowCovering.Attributes.OperationalStatus, @@ -239,7 +243,7 @@ DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.COVER, entity_description=CoverEntityDescription( - key="MatterCoverPositionAwareLiftAndTilt" + key="MatterCoverPositionAwareLiftAndTilt", name=None ), entity_class=MatterCover, required_attributes=( diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 4c220ab85ea..facdb6752d3 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -358,7 +358,7 @@ class MatterLight(MatterEntity, LightEntity): DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.LIGHT, - entity_description=LightEntityDescription(key="MatterLight"), + entity_description=LightEntityDescription(key="MatterLight", name=None), entity_class=MatterLight, required_attributes=(clusters.OnOff.Attributes.OnOff,), optional_attributes=( @@ -380,7 +380,9 @@ DISCOVERY_SCHEMAS = [ # Additional schema to match (HS Color) lights with incorrect/missing device type MatterDiscoverySchema( platform=Platform.LIGHT, - entity_description=LightEntityDescription(key="MatterHSColorLightFallback"), + entity_description=LightEntityDescription( + key="MatterHSColorLightFallback", name=None + ), entity_class=MatterLight, required_attributes=( clusters.OnOff.Attributes.OnOff, @@ -398,7 +400,9 @@ DISCOVERY_SCHEMAS = [ # Additional schema to match (XY Color) lights with incorrect/missing device type MatterDiscoverySchema( platform=Platform.LIGHT, - entity_description=LightEntityDescription(key="MatterXYColorLightFallback"), + entity_description=LightEntityDescription( + key="MatterXYColorLightFallback", name=None + ), entity_class=MatterLight, required_attributes=( clusters.OnOff.Attributes.OnOff, @@ -417,7 +421,7 @@ DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.LIGHT, entity_description=LightEntityDescription( - key="MatterColorTemperatureLightFallback" + key="MatterColorTemperatureLightFallback", name=None ), entity_class=MatterLight, required_attributes=( @@ -430,7 +434,9 @@ DISCOVERY_SCHEMAS = [ # Additional schema to match generic dimmable lights with incorrect/missing device type MatterDiscoverySchema( platform=Platform.LIGHT, - entity_description=LightEntityDescription(key="MatterDimmableLightFallback"), + entity_description=LightEntityDescription( + key="MatterDimmableLightFallback", name=None + ), entity_class=MatterLight, required_attributes=( clusters.OnOff.Attributes.OnOff, diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index f78529b7268..7df6d84c794 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -147,7 +147,7 @@ class DoorLockFeature(IntFlag): DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.LOCK, - entity_description=LockEntityDescription(key="MatterLock"), + entity_description=LockEntityDescription(key="MatterLock", name=None), entity_class=MatterLock, required_attributes=(clusters.DoorLock.Attributes.LockState,), optional_attributes=(clusters.DoorLock.Attributes.DoorState,), diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 84e68695d63..027dcda65a7 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -68,7 +68,6 @@ DISCOVERY_SCHEMAS = [ platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( key="TemperatureSensor", - name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, measurement_to_ha=lambda x: x / 100, @@ -80,7 +79,6 @@ DISCOVERY_SCHEMAS = [ platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( key="PressureSensor", - name="Pressure", native_unit_of_measurement=UnitOfPressure.KPA, device_class=SensorDeviceClass.PRESSURE, measurement_to_ha=lambda x: x / 10, @@ -92,9 +90,8 @@ DISCOVERY_SCHEMAS = [ platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( key="FlowSensor", - name="Flow", native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, - device_class=SensorDeviceClass.WATER, # what is the device class here ? + translation_key="flow", measurement_to_ha=lambda x: x / 10, ), entity_class=MatterSensor, @@ -104,7 +101,6 @@ DISCOVERY_SCHEMAS = [ platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( key="HumiditySensor", - name="Humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, measurement_to_ha=lambda x: x / 100, @@ -118,7 +114,6 @@ DISCOVERY_SCHEMAS = [ platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( key="LightSensor", - name="Illuminance", native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, measurement_to_ha=lambda x: round(pow(10, ((x - 1) / 10000)), 1), @@ -130,7 +125,6 @@ DISCOVERY_SCHEMAS = [ platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( key="PowerSource", - name="Battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, # value has double precision diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 594998c236f..dc5eb30df51 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -43,5 +43,12 @@ "install_addon": "Please wait while the Matter Server add-on installation finishes. This can take several minutes.", "start_addon": "Please wait while the Matter Server add-on starts. This add-on is what powers Matter in Home Assistant. This may take some seconds." } + }, + "entity": { + "sensor": { + "flow": { + "name": "Flow" + } + } } } diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index 2eb3c22c1f7..56c51d144d8 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -63,7 +63,7 @@ DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.SWITCH, entity_description=SwitchEntityDescription( - key="MatterPlug", device_class=SwitchDeviceClass.OUTLET + key="MatterPlug", device_class=SwitchDeviceClass.OUTLET, name=None ), entity_class=MatterSwitch, required_attributes=(clusters.OnOff.Attributes.OnOff,), diff --git a/tests/components/matter/test_binary_sensor.py b/tests/components/matter/test_binary_sensor.py index 743619ddde9..d7982e1d5ae 100644 --- a/tests/components/matter/test_binary_sensor.py +++ b/tests/components/matter/test_binary_sensor.py @@ -31,7 +31,7 @@ async def test_contact_sensor( contact_sensor_node: MatterNode, ) -> None: """Test contact sensor.""" - state = hass.states.get("binary_sensor.mock_contact_sensor_contact") + state = hass.states.get("binary_sensor.mock_contact_sensor_door") assert state assert state.state == "off" @@ -40,7 +40,7 @@ async def test_contact_sensor( hass, matter_client, data=(contact_sensor_node.node_id, "1/69/0", False) ) - state = hass.states.get("binary_sensor.mock_contact_sensor_contact") + state = hass.states.get("binary_sensor.mock_contact_sensor_door") assert state assert state.state == "on" From 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 f704a8baa33..5b82b5dae72 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -8,6 +8,7 @@ import logging import re from typing import Any, Literal +from hassil.recognize import RecognizeResult import voluptuous as vol from homeassistant import core @@ -353,6 +354,10 @@ async def websocket_hass_agent_debug( } for entity_key, entity in result.entities.items() }, + "targets": { + state.entity_id: {"matched": is_matched} + for state, is_matched in _get_debug_targets(hass, result) + }, } if result is not None else None @@ -362,6 +367,49 @@ async def websocket_hass_agent_debug( ) +def _get_debug_targets( + hass: HomeAssistant, + result: RecognizeResult, +) -> Iterable[tuple[core.State, bool]]: + """Yield state/is_matched pairs for a hassil recognition.""" + entities = result.entities + + name: str | None = None + area_name: str | None = None + domains: set[str] | None = None + device_classes: set[str] | None = None + state_names: set[str] | None = None + + if "name" in entities: + name = str(entities["name"].value) + + if "area" in entities: + area_name = str(entities["area"].value) + + if "domain" in entities: + domains = set(cv.ensure_list(entities["domain"].value)) + + if "device_class" in entities: + device_classes = set(cv.ensure_list(entities["device_class"].value)) + + if "state" in entities: + # HassGetState only + state_names = set(cv.ensure_list(entities["state"].value)) + + states = intent.async_match_states( + hass, + name=name, + area_name=area_name, + domains=domains, + device_classes=device_classes, + ) + + for state in states: + # For queries, a target is "matched" based on its state + is_matched = (state_names is None) or (state.state in state_names) + yield state, is_matched + + class ConversationProcessView(http.HomeAssistantView): """View to process text.""" diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 23afe9ce3f7..8ef0cef52f9 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -382,6 +382,11 @@ 'intent': dict({ 'name': 'HassTurnOn', }), + 'targets': dict({ + 'light.kitchen': dict({ + 'matched': True, + }), + }), }), dict({ 'entities': dict({ @@ -394,6 +399,60 @@ 'intent': dict({ 'name': 'HassTurnOff', }), + 'targets': dict({ + 'light.kitchen': dict({ + 'matched': True, + }), + }), + }), + dict({ + 'entities': dict({ + 'area': dict({ + 'name': 'area', + 'text': 'kitchen', + 'value': 'kitchen', + }), + 'domain': dict({ + 'name': 'domain', + 'text': '', + 'value': 'light', + }), + }), + 'intent': dict({ + 'name': 'HassTurnOn', + }), + 'targets': dict({ + 'light.kitchen': dict({ + 'matched': True, + }), + }), + }), + dict({ + 'entities': dict({ + 'area': dict({ + 'name': 'area', + 'text': 'kitchen', + 'value': 'kitchen', + }), + 'domain': dict({ + 'name': 'domain', + 'text': 'lights', + 'value': 'light', + }), + 'state': dict({ + 'name': 'state', + 'text': 'on', + 'value': 'on', + }), + }), + 'intent': dict({ + 'name': 'HassGetState', + }), + 'targets': dict({ + 'light.kitchen': dict({ + 'matched': False, + }), + }), }), None, ]), diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index ec2128e3bd7..6ad9beb3362 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -1652,16 +1652,22 @@ async def test_ws_hass_agent_debug( hass: HomeAssistant, init_components, hass_ws_client: WebSocketGenerator, + area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test homeassistant agent debug websocket command.""" client = await hass_ws_client(hass) + kitchen_area = area_registry.async_create("kitchen") entity_registry.async_get_or_create( "light", "demo", "1234", suggested_object_id="kitchen" ) - entity_registry.async_update_entity("light.kitchen", aliases={"my cool light"}) + entity_registry.async_update_entity( + "light.kitchen", + aliases={"my cool light"}, + area_id=kitchen_area.id, + ) hass.states.async_set("light.kitchen", "off") on_calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on") @@ -1673,6 +1679,8 @@ async def test_ws_hass_agent_debug( "sentences": [ "turn on my cool light", "turn my cool light off", + "turn on all lights in the kitchen", + "how many lights are on in the kitchen?", "this will not match anything", # null in results ], } From 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 ae5718e108b..fa1c382926e 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -20,15 +20,15 @@ "zigpy_znp" ], "requirements": [ - "bellows==0.35.5", + "bellows==0.35.6", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.101", "zigpy-deconz==0.21.0", - "zigpy==0.56.0", - "zigpy-xbee==0.18.0", + "zigpy==0.56.1", + "zigpy-xbee==0.18.1", "zigpy-zigate==0.11.0", - "zigpy-znp==0.11.1" + "zigpy-znp==0.11.2" ], "usb": [ { diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 9fbfa03b928..29214083d27 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -250,11 +250,12 @@ class ZhaRadioManager: assert self.current_settings is not None + metadata = self.current_settings.network_info.metadata["ezsp"] + if ( self.current_settings.node_info.ieee == self.chosen_backup.node_info.ieee - or not self.current_settings.network_info.metadata["ezsp"][ - "can_write_custom_eui64" - ] + or metadata["can_rewrite_custom_eui64"] + or not metadata["can_burn_userdata_custom_eui64"] ): # No point in prompting the user if the backup doesn't have a new IEEE # address or if there is no way to overwrite the IEEE address a second time diff --git a/requirements_all.txt b/requirements_all.txt index b3cb0d2a6a3..79f8926932c 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 4d309c5f18b..5e1af46951c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -418,7 +418,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.35.5 +bellows==0.35.6 # homeassistant.components.bmw_connected_drive bimmer-connected==0.13.7 @@ -2017,16 +2017,16 @@ zha-quirks==0.0.101 zigpy-deconz==0.21.0 # homeassistant.components.zha -zigpy-xbee==0.18.0 +zigpy-xbee==0.18.1 # homeassistant.components.zha zigpy-zigate==0.11.0 # homeassistant.components.zha -zigpy-znp==0.11.1 +zigpy-znp==0.11.2 # homeassistant.components.zha -zigpy==0.56.0 +zigpy==0.56.1 # homeassistant.components.zwave_js zwave-js-server-python==0.49.0 diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 48c45dd241d..17665994806 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -74,6 +74,12 @@ def mock_app(): mock_app = AsyncMock() mock_app.backups = create_autospec(BackupManager, instance=True) mock_app.backups.backups = [] + mock_app.state.network_info.metadata = { + "ezsp": { + "can_burn_userdata_custom_eui64": True, + "can_rewrite_custom_eui64": False, + } + } with patch( "zigpy.application.ControllerApplication.new", AsyncMock(return_value=mock_app) @@ -1517,6 +1523,7 @@ async def test_ezsp_restore_without_settings_change_ieee( mock_app.state.node_info = backup.node_info mock_app.state.network_info = copy.deepcopy(backup.network_info) mock_app.state.network_info.network_key.tx_counter += 10000 + mock_app.state.network_info.metadata["ezsp"] = {} # Include the overwrite option, just in case someone uploads a backup with it backup.network_info.metadata["ezsp"] = {EZSP_OVERWRITE_EUI64: True} From 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 6b031ff7142..8ac929e13b6 100644 --- a/homeassistant/components/conversation/services.yaml +++ b/homeassistant/components/conversation/services.yaml @@ -20,4 +20,4 @@ process: description: Assist engine to process your request example: homeassistant selector: - text: + conversation_agent: diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index afd38bf7636..84c0f769c7c 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -500,6 +500,34 @@ class ConstantSelector(Selector[ConstantSelectorConfig]): return self.config["value"] +class ConversationAgentSelectorConfig(TypedDict, total=False): + """Class to represent a conversation agent selector config.""" + + language: str + + +@SELECTORS.register("conversation_agent") +class COnversationAgentSelector(Selector[ConversationAgentSelectorConfig]): + """Selector for a conversation agent.""" + + selector_type = "conversation_agent" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("language"): str, + } + ) + + def __init__(self, config: ConversationAgentSelectorConfig) -> None: + """Instantiate a selector.""" + super().__init__(config) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + agent: str = vol.Schema(str)(data) + return agent + + class DateSelectorConfig(TypedDict): """Class to represent a date selector config.""" diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index f95da5c6e66..c518ad227a7 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -979,3 +979,25 @@ def test_constant_selector_schema_error(schema) -> None: """Test constant selector.""" with pytest.raises(vol.Invalid): selector.validate_selector({"constant": schema}) + + +@pytest.mark.parametrize( + ("schema", "valid_selections", "invalid_selections"), + ( + ( + {}, + ("home_assistant", "2j4hp3uy4p87wyrpiuhk34"), + (None, True, 1), + ), + ( + {"language": "nl"}, + ("home_assistant", "2j4hp3uy4p87wyrpiuhk34"), + (None, True, 1), + ), + ), +) +def test_conversation_agent_selector_schema( + schema, valid_selections, invalid_selections +) -> None: + """Test conversation agent selector.""" + _test_selector("conversation_agent", schema, valid_selections, invalid_selections) From 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 | 321 +++++++++++-------- 1 file changed, 192 insertions(+), 129 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index afaefe117ba..271b0b9aa16 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] +class ESPHomeManager: + """Class to manage an ESPHome connection.""" - 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, + __slots__ = ( + "hass", + "host", + "password", + "entry", + "cli", + "device_id", + "domain_data", + "voice_assistant_udp_server", + "reconnect_logic", + "zeroconf_instance", + "entry_data", ) - 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) + 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 - 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) - - async def on_stop(event: Event) -> None: + async def on_stop(self, event: Event) -> None: """Cleanup the socket client on HA stop.""" - await _cleanup_instance(hass, entry) + await _cleanup_instance(self.hass, self.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) - ) + @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 @@ async def async_setup_entry( # noqa: C901 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 @@ async def async_setup_entry( # noqa: C901 **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 @@ async def async_setup_entry( # noqa: C901 ) 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 async_setup_entry( # noqa: C901 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 async_setup_entry( # noqa: C901 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 async_setup_entry( # noqa: C901 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 async_setup_entry( # noqa: C901 # 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 async_setup_entry( # noqa: C901 InvalidAuthAPIError, ), ): - entry.async_start_reauth(hass) + self.entry.async_start_reauth(self.hass) - reconnect_logic = ReconnectLogic( - client=cli, - on_connect=on_connect, - on_disconnect=on_disconnect, + 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] + port = entry.data[CONF_PORT] + password = entry.data[CONF_PASSWORD] + noise_psk = entry.data.get(CONF_NOISE_PSK) + + zeroconf_instance = await zeroconf.async_get_instance(hass) + + cli = APIClient( + host, + port, + password, + client_info=f"Home Assistant {ha_version}", zeroconf_instance=zeroconf_instance, - name=host, - on_connect_error=on_connect_error, + noise_psk=noise_psk, ) - 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) + 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) - 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)) + 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 f1221290c74..d1fcb115180 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -413,23 +413,20 @@ class BluetoothManager: # 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 @@ class BluetoothManager: "%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 345480da363..594abc69c13 100644 --- a/homeassistant/components/voip/manifest.json +++ b/homeassistant/components/voip/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/voip", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["voip-utils==0.0.7"] + "requirements": ["voip-utils==0.1.0"] } diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index 32cfbd70337..3d0681a8475 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -11,7 +11,13 @@ import time from typing import TYPE_CHECKING import async_timeout -from voip_utils import CallInfo, RtpDatagramProtocol, SdpInfo, VoipDatagramProtocol +from voip_utils import ( + CallInfo, + RtcpState, + RtpDatagramProtocol, + SdpInfo, + VoipDatagramProtocol, +) from homeassistant.components import stt, tts from homeassistant.components.assist_pipeline import ( @@ -46,7 +52,10 @@ _LOGGER = logging.getLogger(__name__) def make_protocol( - hass: HomeAssistant, devices: VoIPDevices, call_info: CallInfo + hass: HomeAssistant, + devices: VoIPDevices, + call_info: CallInfo, + rtcp_state: RtcpState | None = None, ) -> VoipDatagramProtocol: """Plays a pre-recorded message if pipeline is misconfigured.""" voip_device = devices.async_get_or_create(call_info) @@ -70,6 +79,7 @@ def make_protocol( hass, "problem.pcm", opus_payload_type=call_info.opus_payload_type, + rtcp_state=rtcp_state, ) vad_sensitivity = pipeline_select.get_vad_sensitivity( @@ -86,6 +96,7 @@ def make_protocol( Context(user_id=devices.config_entry.data["user"]), opus_payload_type=call_info.opus_payload_type, silence_seconds=VadSensitivity.to_seconds(vad_sensitivity), + rtcp_state=rtcp_state, ) @@ -101,13 +112,14 @@ class HassVoipDatagramProtocol(VoipDatagramProtocol): session_name="voip_hass", version=__version__, ), - valid_protocol_factory=lambda call_info: make_protocol( - hass, devices, call_info + valid_protocol_factory=lambda call_info, rtcp_state: make_protocol( + hass, devices, call_info, rtcp_state ), - invalid_protocol_factory=lambda call_info: PreRecordMessageProtocol( + invalid_protocol_factory=lambda call_info, rtcp_state: PreRecordMessageProtocol( hass, "not_configured.pcm", opus_payload_type=call_info.opus_payload_type, + rtcp_state=rtcp_state, ), ) self.hass = hass @@ -147,6 +159,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): tone_delay: float = 0.2, tts_extra_timeout: float = 1.0, silence_seconds: float = 1.0, + rtcp_state: RtcpState | None = None, ) -> None: """Set up pipeline RTP server.""" super().__init__( @@ -154,6 +167,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): width=WIDTH, channels=CHANNELS, opus_payload_type=opus_payload_type, + rtcp_state=rtcp_state, ) self.hass = hass @@ -454,6 +468,7 @@ class PreRecordMessageProtocol(RtpDatagramProtocol): opus_payload_type: int, message_delay: float = 1.0, loop_delay: float = 2.0, + rtcp_state: RtcpState | None = None, ) -> None: """Set up RTP server.""" super().__init__( @@ -461,6 +476,7 @@ class PreRecordMessageProtocol(RtpDatagramProtocol): width=WIDTH, channels=CHANNELS, opus_payload_type=opus_payload_type, + rtcp_state=rtcp_state, ) self.hass = hass self.file_name = file_name diff --git a/requirements_all.txt b/requirements_all.txt index 79f8926932c..b0d6425d442 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 5e1af46951c..51f614a685f 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 693d2550f53..72629544895 100644 --- a/homeassistant/components/youtube/coordinator.py +++ b/homeassistant/components/youtube/coordinator.py @@ -65,7 +65,7 @@ class YouTubeDataUpdateCoordinator(DataUpdateCoordinator): channels = self.config_entry.options[CONF_CHANNELS] while received_channels < len(channels): # We're slicing the channels in chunks of 50 to avoid making the URI too long - end = min(received_channels + 50, len(channels) - 1) + end = min(received_channels + 50, len(channels)) channel_request: HttpRequest = service.channels().list( part="snippet,statistics", id=",".join(channels[received_channels:end]), From 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 cc713d1034c..e8c237114b5 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -30,6 +30,7 @@ from homeassistant.const import ( CONF_PORT, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, + SERVICE_RELOAD, Platform, ) from homeassistant.core import Event, HomeAssistant, ServiceCall @@ -312,6 +313,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA, ) + async def _reload_integration(call: ServiceCall) -> None: + """Reload the integration.""" + await hass.config_entries.async_reload(entry.entry_id) + hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context) + + async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_integration) + await register_panel(hass) return True diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index d95a1573872..0ad497a30a2 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -106,3 +106,6 @@ exposure_register: default: false selector: boolean: +reload: + name: Reload + description: Reload the KNX integration. diff --git a/tests/components/knx/test_services.py b/tests/components/knx/test_services.py index 5e6f856ee44..5796eae8393 100644 --- a/tests/components/knx/test_services.py +++ b/tests/components/knx/test_services.py @@ -1,7 +1,10 @@ """Test KNX services.""" +from unittest.mock import patch + import pytest from xknx.telegram.apci import GroupValueResponse, GroupValueWrite +from homeassistant.components.knx import async_unload_entry as knx_async_unload_entry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -250,3 +253,24 @@ async def test_exposure_register(hass: HomeAssistant, knx: KNXTestKit) -> None: hass.states.async_set(test_entity, STATE_OFF, {test_attribute: 25}) await knx.assert_telegram_count(1) await knx.assert_write(test_address, (25,)) + + +async def test_reload_service( + hass: HomeAssistant, + knx: KNXTestKit, +) -> None: + """Test reload service.""" + await knx.setup_integration({}) + + with patch( + "homeassistant.components.knx.async_unload_entry", wraps=knx_async_unload_entry + ) as mock_unload_entry, patch( + "homeassistant.components.knx.async_setup_entry" + ) as mock_setup_entry: + await hass.services.async_call( + "knx", + "reload", + blocking=True, + ) + mock_unload_entry.assert_called_once() + mock_setup_entry.assert_called_once() From 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 99a1a4ac2ff..1b1e51db035 100644 --- a/homeassistant/components/mazda/button.py +++ b/homeassistant/components/mazda/button.py @@ -78,21 +78,25 @@ BUTTON_ENTITIES = [ key="start_engine", name="Start engine", icon="mdi:engine", + is_supported=lambda data: not data["isElectric"], ), MazdaButtonEntityDescription( key="stop_engine", name="Stop engine", icon="mdi:engine-off", + is_supported=lambda data: not data["isElectric"], ), MazdaButtonEntityDescription( key="turn_on_hazard_lights", name="Turn on hazard lights", icon="mdi:hazard-lights", + is_supported=lambda data: not data["isElectric"], ), MazdaButtonEntityDescription( key="turn_off_hazard_lights", name="Turn off hazard lights", icon="mdi:hazard-lights", + is_supported=lambda data: not data["isElectric"], ), MazdaButtonEntityDescription( key="refresh_vehicle_status", diff --git a/tests/components/mazda/test_button.py b/tests/components/mazda/test_button.py index 535ddcc963b..ba80c10b38d 100644 --- a/tests/components/mazda/test_button.py +++ b/tests/components/mazda/test_button.py @@ -65,40 +65,6 @@ async def test_button_setup_electric_vehicle(hass: HomeAssistant) -> None: entity_registry = er.async_get(hass) - entry = entity_registry.async_get("button.my_mazda3_start_engine") - assert entry - assert entry.unique_id == "JM000000000000000_start_engine" - state = hass.states.get("button.my_mazda3_start_engine") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start engine" - assert state.attributes.get(ATTR_ICON) == "mdi:engine" - - entry = entity_registry.async_get("button.my_mazda3_stop_engine") - assert entry - assert entry.unique_id == "JM000000000000000_stop_engine" - state = hass.states.get("button.my_mazda3_stop_engine") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop engine" - assert state.attributes.get(ATTR_ICON) == "mdi:engine-off" - - entry = entity_registry.async_get("button.my_mazda3_turn_on_hazard_lights") - assert entry - assert entry.unique_id == "JM000000000000000_turn_on_hazard_lights" - state = hass.states.get("button.my_mazda3_turn_on_hazard_lights") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn on hazard lights" - assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" - - entry = entity_registry.async_get("button.my_mazda3_turn_off_hazard_lights") - assert entry - assert entry.unique_id == "JM000000000000000_turn_off_hazard_lights" - state = hass.states.get("button.my_mazda3_turn_off_hazard_lights") - assert state - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn off hazard lights" - ) - assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" - entry = entity_registry.async_get("button.my_mazda3_refresh_status") assert entry assert entry.unique_id == "JM000000000000000_refresh_vehicle_status" @@ -109,20 +75,45 @@ async def test_button_setup_electric_vehicle(hass: HomeAssistant) -> None: @pytest.mark.parametrize( - ("entity_id_suffix", "api_method_name"), + ("electric_vehicle", "entity_id_suffix"), [ - ("start_engine", "start_engine"), - ("stop_engine", "stop_engine"), - ("turn_on_hazard_lights", "turn_on_hazard_lights"), - ("turn_off_hazard_lights", "turn_off_hazard_lights"), - ("refresh_status", "refresh_vehicle_status"), + (True, "start_engine"), + (True, "stop_engine"), + (True, "turn_on_hazard_lights"), + (True, "turn_off_hazard_lights"), + (False, "refresh_status"), + ], +) +async def test_button_not_created( + hass: HomeAssistant, electric_vehicle, entity_id_suffix +) -> None: + """Test that button entities are not created when they should not be.""" + await init_integration(hass, electric_vehicle=electric_vehicle) + + entity_registry = er.async_get(hass) + + entity_id = f"button.my_mazda3_{entity_id_suffix}" + entry = entity_registry.async_get(entity_id) + assert entry is None + state = hass.states.get(entity_id) + assert state is None + + +@pytest.mark.parametrize( + ("electric_vehicle", "entity_id_suffix", "api_method_name"), + [ + (False, "start_engine", "start_engine"), + (False, "stop_engine", "stop_engine"), + (False, "turn_on_hazard_lights", "turn_on_hazard_lights"), + (False, "turn_off_hazard_lights", "turn_off_hazard_lights"), + (True, "refresh_status", "refresh_vehicle_status"), ], ) async def test_button_press( - hass: HomeAssistant, entity_id_suffix, api_method_name + hass: HomeAssistant, electric_vehicle, entity_id_suffix, api_method_name ) -> None: """Test pressing the button entities.""" - client_mock = await init_integration(hass, electric_vehicle=True) + client_mock = await init_integration(hass, electric_vehicle=electric_vehicle) await hass.services.async_call( BUTTON_DOMAIN, From 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 ebfd445f62c..2beffc7c894 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 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 f4dab9e301b..f3767be9f3d 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 @@ from .const import DOMAIN 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 @@ class IOSSensor(SensorEntity): 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 2b486cc0c04..6c77209e317 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 a738952d2ca..0145d6f0906 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -106,6 +106,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): _attr_has_entity_name = True _attr_icon = "mdi:spotify" _attr_media_image_remotely_accessible = False + _attr_name = None def __init__( self, From 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 d5122862e2c..c2c9c207c02 100644 --- a/homeassistant/components/tuya/alarm_control_panel.py +++ b/homeassistant/components/tuya/alarm_control_panel.py @@ -89,6 +89,7 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity): """Tuya Alarm Entity.""" _attr_icon = "mdi:security" + _attr_name = None def __init__( self, diff --git a/homeassistant/components/tuya/camera.py b/homeassistant/components/tuya/camera.py index 182d0dfd85d..72216057aff 100644 --- a/homeassistant/components/tuya/camera.py +++ b/homeassistant/components/tuya/camera.py @@ -52,6 +52,7 @@ class TuyaCameraEntity(TuyaEntity, CameraEntity): _attr_supported_features = CameraEntityFeature.STREAM _attr_brand = "Tuya" + _attr_name = None def __init__( self, diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index bcb97327006..6b3b84ba349 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -125,6 +125,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): _set_humidity: IntegerTypeData | None = None _set_temperature: IntegerTypeData | None = None entity_description: TuyaClimateEntityDescription + _attr_name = None def __init__( self, diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 59cfee3506c..210cc5c7518 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -65,6 +65,7 @@ class TuyaFanEntity(TuyaEntity, FanEntity): _speed: IntegerTypeData | None = None _speeds: EnumTypeData | None = None _switch: DPCode | None = None + _attr_name = None def __init__( self, diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index 458a2681186..6d09ba4314c 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -86,6 +86,7 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity): _set_humidity: IntegerTypeData | None = None _switch_dpcode: DPCode | None = None entity_description: TuyaHumidifierEntityDescription + _attr_name = None def __init__( self, diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 44b3494ca77..3ab4c3568c4 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -79,6 +79,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "dc": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, @@ -90,6 +91,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "dd": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, @@ -102,6 +104,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "dj": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=(DPCode.BRIGHT_VALUE_V2, DPCode.BRIGHT_VALUE), color_temp=(DPCode.TEMP_VALUE_V2, DPCode.TEMP_VALUE), @@ -120,6 +123,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "fsd": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, @@ -128,6 +132,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { # Some ceiling fan lights use LIGHT for DPCode instead of SWITCH_LED TuyaLightEntityDescription( key=DPCode.LIGHT, + name=None, ), ), # Ambient Light @@ -135,6 +140,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "fwd": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, @@ -146,6 +152,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "gyd": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, @@ -157,6 +164,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "jsq": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_data=DPCode.COLOUR_DATA_HSV, @@ -195,6 +203,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "mbd": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_data=DPCode.COLOUR_DATA, @@ -206,6 +215,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "qjdcz": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_data=DPCode.COLOUR_DATA, @@ -296,6 +306,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "tyndj": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, @@ -307,6 +318,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "xdd": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, @@ -322,6 +334,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "ykq": ( TuyaLightEntityDescription( key=DPCode.SWITCH_CONTROLLER, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_CONTROLLER, color_temp=DPCode.TEMP_CONTROLLER, @@ -332,6 +345,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "fs": ( TuyaLightEntityDescription( key=DPCode.LIGHT, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, diff --git a/homeassistant/components/tuya/siren.py b/homeassistant/components/tuya/siren.py index a60e24eca86..c2dc8cea99b 100644 --- a/homeassistant/components/tuya/siren.py +++ b/homeassistant/components/tuya/siren.py @@ -27,7 +27,6 @@ SIRENS: dict[str, tuple[SirenEntityDescription, ...]] = { "dgnbj": ( SirenEntityDescription( key=DPCode.ALARM_SWITCH, - name="Siren", ), ), # Siren Alarm @@ -35,7 +34,6 @@ SIRENS: dict[str, tuple[SirenEntityDescription, ...]] = { "sgbj": ( SirenEntityDescription( key=DPCode.ALARM_SWITCH, - name="Siren", ), ), # Smart Camera @@ -43,7 +41,6 @@ SIRENS: dict[str, tuple[SirenEntityDescription, ...]] = { "sp": ( SirenEntityDescription( key=DPCode.SIREN_SWITCH, - name="Siren", ), ), } @@ -83,6 +80,7 @@ class TuyaSirenEntity(TuyaEntity, SirenEntity): """Tuya Siren Entity.""" _attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF + _attr_name = None def __init__( self, diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index 7827fb061ea..a2961a55d78 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -78,6 +78,7 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): _fan_speed: EnumTypeData | None = None _battery_level: IntegerTypeData | None = None + _attr_name = None def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: """Init Tuya vacuum.""" From 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 da6c710fed8..1da879cb02b 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -52,6 +52,7 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): | CoverEntityFeature.SET_POSITION ) _attr_translation_key = "cover" + _attr_name = None def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: """Initialize the Switchbot.""" diff --git a/homeassistant/components/switchbot/humidifier.py b/homeassistant/components/switchbot/humidifier.py index e5f1e3c46b4..5b53b410208 100644 --- a/homeassistant/components/switchbot/humidifier.py +++ b/homeassistant/components/switchbot/humidifier.py @@ -41,6 +41,7 @@ class SwitchBotHumidifier(SwitchbotSwitchedEntity, HumidifierEntity): _device: switchbot.SwitchbotHumidifier _attr_min_humidity = 1 _attr_translation_key = "humidifier" + _attr_name = None @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/switchbot/light.py b/homeassistant/components/switchbot/light.py index 5dc99c5d6f1..53b40bbf780 100644 --- a/homeassistant/components/switchbot/light.py +++ b/homeassistant/components/switchbot/light.py @@ -46,6 +46,7 @@ class SwitchbotLightEntity(SwitchbotEntity, LightEntity): """Representation of switchbot light bulb.""" _device: SwitchbotBaseLight + _attr_name = None def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: """Initialize the Switchbot light.""" diff --git a/homeassistant/components/switchbot/lock.py b/homeassistant/components/switchbot/lock.py index 69ce0aa4750..7710cde12a9 100644 --- a/homeassistant/components/switchbot/lock.py +++ b/homeassistant/components/switchbot/lock.py @@ -27,6 +27,7 @@ class SwitchBotLock(SwitchbotEntity, LockEntity): """Representation of a Switchbot lock.""" _attr_translation_key = "lock" + _attr_name = None _device: switchbot.SwitchbotLock def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 25b76ea24cb..f62e4d3f918 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -35,6 +35,7 @@ class SwitchBotSwitch(SwitchbotSwitchedEntity, SwitchEntity, RestoreEntity): _attr_device_class = SwitchDeviceClass.SWITCH _attr_translation_key = "bot" + _attr_name = None _device: switchbot.Switchbot def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: From 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 7df6d84c794..a5f625f9e73 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -33,6 +33,26 @@ class MatterLock(MatterEntity, LockEntity): features: int | None = None + @property + def code_format(self) -> str | None: + """Regex for code format or None if no code is required.""" + if self.get_matter_attribute_value( + clusters.DoorLock.Attributes.RequirePINforRemoteOperation + ): + min_pincode_length = int( + self.get_matter_attribute_value( + clusters.DoorLock.Attributes.MinPINCodeLength + ) + ) + max_pincode_length = int( + self.get_matter_attribute_value( + clusters.DoorLock.Attributes.MaxPINCodeLength + ) + ) + return f"^\\d{{{min_pincode_length},{max_pincode_length}}}$" + + return None + @property def supports_door_position_sensor(self) -> bool: """Return True if the lock supports door position sensor.""" diff --git a/tests/components/matter/test_door_lock.py b/tests/components/matter/test_door_lock.py index 072658044d8..003bfa3cf39 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_door_lock.py @@ -11,7 +11,7 @@ from homeassistant.components.lock import ( STATE_UNLOCKED, STATE_UNLOCKING, ) -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import ATTR_CODE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from .common import ( @@ -104,3 +104,46 @@ async def test_lock( state = hass.states.get("lock.mock_door_lock") assert state assert state.state == STATE_UNKNOWN + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_lock_requires_pin( + hass: HomeAssistant, + matter_client: MagicMock, + door_lock: MatterNode, +) -> None: + """Test door lock with PINCode.""" + + code = "1234567" + + # set RequirePINforRemoteOperation + set_node_attribute(door_lock, 1, 257, 51, True) + # set door state to unlocked + set_node_attribute(door_lock, 1, 257, 0, 2) + + with pytest.raises(ValueError): + # Lock door using invalid code format + await trigger_subscription_callback(hass, matter_client) + await hass.services.async_call( + "lock", + "lock", + {"entity_id": "lock.mock_door_lock", ATTR_CODE: "1234"}, + blocking=True, + ) + + # Lock door using valid code + await trigger_subscription_callback(hass, matter_client) + await hass.services.async_call( + "lock", + "lock", + {"entity_id": "lock.mock_door_lock", ATTR_CODE: code}, + blocking=True, + ) + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=door_lock.node_id, + endpoint_id=1, + command=clusters.DoorLock.Commands.LockDoor(code.encode()), + timed_request_timeout_ms=1000, + ) From 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 fb8f57b4d5a..f2097178a95 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -70,7 +70,7 @@ async def async_setup_platform( hass, DOMAIN, "deprecated_yaml_binary_sensor", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_platform_yaml", diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 90bc5b7d50e..553af2f0c86 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -73,7 +73,7 @@ async def async_setup_platform( hass, DOMAIN, "deprecated_yaml_cover", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_platform_yaml", diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index 2f4f20045d7..d00926eb0ee 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -43,7 +43,7 @@ def get_service( hass, DOMAIN, "deprecated_yaml_notify", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_platform_yaml", diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index c164c6636fa..1b865827e69 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -74,7 +74,7 @@ async def async_setup_platform( hass, DOMAIN, "deprecated_yaml_sensor", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_platform_yaml", diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 5beb06eea9d..8fbafd7a4d1 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -74,7 +74,7 @@ async def async_setup_platform( hass, DOMAIN, "deprecated_yaml_switch", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_platform_yaml", diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index d2834b8991b..6f3d48fc1bb 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -292,7 +292,7 @@ class Counter(collection.CollectionEntity, RestoreEntity): self.hass, DOMAIN, "deprecated_configure_service", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=True, is_persistent=True, severity=IssueSeverity.WARNING, diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 3e8ed2afbdc..f44d736b426 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -91,7 +91,7 @@ async def async_setup_platform( hass, DOMAIN, "deprecated_yaml", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 57be995a489..60a332446ce 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -312,7 +312,7 @@ class EzvizCamera(EzvizEntity, Camera): self.hass, DOMAIN, "service_depreciation_detection_sensibility", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=ir.IssueSeverity.WARNING, translation_key="service_depreciation_detection_sensibility", diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index def8f77994e..b922d98f25e 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -83,7 +83,7 @@ async def async_setup_platform( hass, DOMAIN, "deprecated_yaml", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 08179df5b7e..b4776b19c50 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -53,7 +53,7 @@ async def async_setup_platform( hass, DOMAIN, "deprecated_yaml", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", From 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 7c0dc8ff0a6..4ea4da602e3 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -2,17 +2,25 @@ from __future__ import annotations from datetime import date, timedelta +from typing import Any import holidays from holidays import DateLike, HolidayBase +import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.config_entries import ConfigEntry +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + BinarySensorEntity, +) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util from .const import ( @@ -24,11 +32,81 @@ from .const import ( CONF_PROVINCE, CONF_REMOVE_HOLIDAYS, CONF_WORKDAYS, + DEFAULT_EXCLUDES, + DEFAULT_NAME, + DEFAULT_OFFSET, + DEFAULT_WORKDAYS, DOMAIN, LOGGER, ) +def valid_country(value: Any) -> str: + """Validate that the given country is supported.""" + value = cv.string(value) + all_supported_countries = holidays.list_supported_countries() + + try: + raw_value = value.encode("utf-8") + except UnicodeError as err: + raise vol.Invalid( + "The country name or the abbreviation must be a valid UTF-8 string." + ) from err + if not raw_value: + raise vol.Invalid("Country name or the abbreviation must not be empty.") + if value not in all_supported_countries: + raise vol.Invalid("Country is not supported.") + return value + + +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_COUNTRY): valid_country, + vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES): vol.All( + cv.ensure_list, [vol.In(ALLOWED_DAYS)] + ), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int), + vol.Optional(CONF_PROVINCE): cv.string, + vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS): vol.All( + cv.ensure_list, [vol.In(ALLOWED_DAYS)] + ), + vol.Optional(CONF_ADD_HOLIDAYS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_REMOVE_HOLIDAYS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + } +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Workday sensor.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: diff --git a/homeassistant/components/workday/config_flow.py b/homeassistant/components/workday/config_flow.py index bfa6c299b57..7153dac1bcb 100644 --- a/homeassistant/components/workday/config_flow.py +++ b/homeassistant/components/workday/config_flow.py @@ -155,6 +155,33 @@ class WorkdayConfigFlow(ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return WorkdayOptionsFlowHandler(config_entry) + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a configuration from config.yaml.""" + + abort_match = { + CONF_COUNTRY: config[CONF_COUNTRY], + CONF_EXCLUDES: config[CONF_EXCLUDES], + CONF_OFFSET: config[CONF_OFFSET], + CONF_WORKDAYS: config[CONF_WORKDAYS], + CONF_ADD_HOLIDAYS: config[CONF_ADD_HOLIDAYS], + CONF_REMOVE_HOLIDAYS: config[CONF_REMOVE_HOLIDAYS], + CONF_PROVINCE: config.get(CONF_PROVINCE), + } + new_config = config.copy() + new_config[CONF_PROVINCE] = config.get(CONF_PROVINCE) + LOGGER.debug("Importing with %s", new_config) + + self._async_abort_entries_match(abort_match) + + self.data[CONF_NAME] = config.get(CONF_NAME, DEFAULT_NAME) + self.data[CONF_COUNTRY] = config[CONF_COUNTRY] + LOGGER.debug( + "No duplicate, next step with name %s for country %s", + self.data[CONF_NAME], + self.data[CONF_COUNTRY], + ) + return await self.async_step_options(user_input=new_config) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/workday/strings.json b/homeassistant/components/workday/strings.json index 5af69e29a8b..e6753b39dce 100644 --- a/homeassistant/components/workday/strings.json +++ b/homeassistant/components/workday/strings.json @@ -64,6 +64,12 @@ "already_configured": "Service with this configuration already exist" } }, + "issues": { + "deprecated_yaml": { + "title": "The Workday YAML configuration is being removed", + "description": "Configuring Workday using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Workday YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + }, "selector": { "province": { "options": { diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index d2ae9895544..71dd23c19a3 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -4,7 +4,9 @@ from typing import Any from freezegun.api import FrozenDateTimeFactory import pytest +import voluptuous as vol +from homeassistant.components.workday import binary_sensor from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import UTC @@ -28,6 +30,21 @@ from . import ( ) +async def test_valid_country_yaml() -> None: + """Test valid country from yaml.""" + # Invalid UTF-8, must not contain U+D800 to U+DFFF + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("\ud800") + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("\udfff") + # Country MUST NOT be empty + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("") + # Country must be supported by holidays + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("HomeAssistantLand") + + @pytest.mark.parametrize( ("config", "expected_state"), [ @@ -62,6 +79,34 @@ async def test_setup( } +async def test_setup_from_import( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test setup from various configs.""" + freezer.move_to(datetime(2022, 4, 15, 12, tzinfo=UTC)) # Monday + await async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "workday", + "country": "DE", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.workday_sensor") + assert state.state == "off" + assert state.attributes == { + "friendly_name": "Workday Sensor", + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + } + + async def test_setup_with_invalid_province_from_yaml(hass: HomeAssistant) -> None: """Test setup invalid province with import.""" diff --git a/tests/components/workday/test_config_flow.py b/tests/components/workday/test_config_flow.py index ce4dd127778..7e28471c78c 100644 --- a/tests/components/workday/test_config_flow.py +++ b/tests/components/workday/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components.workday.const import ( CONF_REMOVE_HOLIDAYS, CONF_WORKDAYS, DEFAULT_EXCLUDES, + DEFAULT_NAME, DEFAULT_OFFSET, DEFAULT_WORKDAYS, DOMAIN, @@ -23,6 +24,8 @@ from homeassistant.data_entry_flow import FlowResultType from . import init_integration +from tests.common import MockConfigEntry + pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -111,6 +114,142 @@ async def test_form_no_subdivision(hass: HomeAssistant) -> None: } +async def test_import_flow_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_NAME: DEFAULT_NAME, + CONF_COUNTRY: "DE", + CONF_EXCLUDES: DEFAULT_EXCLUDES, + CONF_OFFSET: DEFAULT_OFFSET, + CONF_WORKDAYS: DEFAULT_WORKDAYS, + CONF_ADD_HOLIDAYS: [], + CONF_REMOVE_HOLIDAYS: [], + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Workday Sensor" + assert result["options"] == { + "name": "Workday Sensor", + "country": "DE", + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": [], + "remove_holidays": [], + "province": None, + } + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_NAME: "Workday Sensor 2", + CONF_COUNTRY: "DE", + CONF_PROVINCE: "BW", + CONF_EXCLUDES: DEFAULT_EXCLUDES, + CONF_OFFSET: DEFAULT_OFFSET, + CONF_WORKDAYS: DEFAULT_WORKDAYS, + CONF_ADD_HOLIDAYS: [], + CONF_REMOVE_HOLIDAYS: [], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Workday Sensor 2" + assert result2["options"] == { + "name": "Workday Sensor 2", + "country": "DE", + "province": "BW", + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": [], + "remove_holidays": [], + } + + +async def test_import_flow_already_exist(hass: HomeAssistant) -> None: + """Test import of yaml already exist.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + "name": "Workday Sensor", + "country": "DE", + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": [], + "remove_holidays": [], + "province": None, + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_NAME: "Workday sensor 2", + CONF_COUNTRY: "DE", + CONF_EXCLUDES: ["sat", "sun", "holiday"], + CONF_OFFSET: 0, + CONF_WORKDAYS: ["mon", "tue", "wed", "thu", "fri"], + CONF_ADD_HOLIDAYS: [], + CONF_REMOVE_HOLIDAYS: [], + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_import_flow_province_no_conflict(hass: HomeAssistant) -> None: + """Test import of yaml with province.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + "name": "Workday Sensor", + "country": "DE", + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": [], + "remove_holidays": [], + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_NAME: "Workday sensor 2", + CONF_COUNTRY: "DE", + CONF_PROVINCE: "BW", + CONF_EXCLUDES: ["sat", "sun", "holiday"], + CONF_OFFSET: 0, + CONF_WORKDAYS: ["mon", "tue", "wed", "thu", "fri"], + CONF_ADD_HOLIDAYS: [], + CONF_REMOVE_HOLIDAYS: [], + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + + async def test_options_form(hass: HomeAssistant) -> None: """Test we get the form in options.""" From 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 479d1d648b8..896d3f8b5a8 100644 --- a/homeassistant/components/snapcast/config_flow.py +++ b/homeassistant/components/snapcast/config_flow.py @@ -51,3 +51,13 @@ class SnapcastConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=SNAPCAST_SCHEMA, errors=errors ) + + async def async_step_import(self, import_config: dict[str, str]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + self._async_abort_entries_match( + { + CONF_HOST: (import_config[CONF_HOST]), + CONF_PORT: (import_config[CONF_PORT]), + } + ) + return self.async_create_entry(title=DEFAULT_TITLE, data=import_config) diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 377a3d1e2b6..096e3829bc7 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -1,19 +1,24 @@ """Support for interacting with Snapcast clients.""" from __future__ import annotations -from snapcast.control.server import Snapserver +import logging + +from snapcast.control.server import CONTROL_PORT, Snapserver import voluptuous as vol from homeassistant.components.media_player import ( + PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( ATTR_LATENCY, @@ -30,6 +35,12 @@ from .const import ( SERVICE_UNJOIN, ) +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT): cv.port} +) + STREAM_STATUS = { "idle": MediaPlayerState.IDLE, "playing": MediaPlayerState.PLAYING, @@ -82,6 +93,32 @@ async def async_setup_entry( ].hass_async_add_entities = async_add_entities +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Snapcast platform.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + + config[CONF_PORT] = config.get(CONF_PORT, CONTROL_PORT) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) + + async def handle_async_join(entity, service_call): """Handle the entity service join.""" if not isinstance(entity, SnapcastClientDevice): diff --git a/homeassistant/components/snapcast/strings.json b/homeassistant/components/snapcast/strings.json index 766bca63495..0087b70d820 100644 --- a/homeassistant/components/snapcast/strings.json +++ b/homeassistant/components/snapcast/strings.json @@ -17,5 +17,11 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_host": "[%key:common::config_flow::error::invalid_host%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Snapcast YAML configuration is being removed", + "description": "Configuring Snapcast using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Snapcast YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/tests/components/snapcast/test_config_flow.py b/tests/components/snapcast/test_config_flow.py index bb07eae2140..b6ff43503a6 100644 --- a/tests/components/snapcast/test_config_flow.py +++ b/tests/components/snapcast/test_config_flow.py @@ -93,3 +93,18 @@ async def test_abort( assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_import(hass: HomeAssistant) -> None: + """Test successful import.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=TEST_CONNECTION, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Snapcast" + assert result["data"] == {CONF_HOST: "snapserver.test", CONF_PORT: 1705} From 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 ac41d03e998..54c47c53895 100644 --- a/homeassistant/components/qbittorrent/config_flow.py +++ b/homeassistant/components/qbittorrent/config_flow.py @@ -1,6 +1,7 @@ """Config flow for qBittorrent.""" from __future__ import annotations +import logging from typing import Any from qbittorrent.client import LoginRequired @@ -8,12 +9,20 @@ from requests.exceptions import RequestException import voluptuous as vol from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL +from homeassistant.const import ( + CONF_NAME, + CONF_PASSWORD, + CONF_URL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_NAME, DEFAULT_URL, DOMAIN from .helpers import setup_client +_LOGGER = logging.getLogger(__name__) + USER_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_URL, default=DEFAULT_URL): str, @@ -52,3 +61,16 @@ class QbittorrentConfigFlow(ConfigFlow, domain=DOMAIN): schema = self.add_suggested_values_to_schema(USER_DATA_SCHEMA, user_input) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + self._async_abort_entries_match({CONF_URL: config[CONF_URL]}) + return self.async_create_entry( + title=config.get(CONF_NAME, DEFAULT_NAME), + data={ + CONF_URL: config[CONF_URL], + CONF_USERNAME: config[CONF_USERNAME], + CONF_PASSWORD: config[CONF_PASSWORD], + CONF_VERIFY_SSL: True, + }, + ) diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index b6d9fe63e4b..15a634cf7a9 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -24,8 +24,10 @@ from homeassistant.const import ( UnitOfDataRate, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DEFAULT_NAME, DOMAIN @@ -68,6 +70,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the qBittorrent platform.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) + ir.async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.11.0", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, diff --git a/homeassistant/components/qbittorrent/strings.json b/homeassistant/components/qbittorrent/strings.json index 66c9430911e..24d1885a917 100644 --- a/homeassistant/components/qbittorrent/strings.json +++ b/homeassistant/components/qbittorrent/strings.json @@ -17,5 +17,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The qBittorrent YAML configuration is being removed", + "description": "Configuring qBittorrent using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the qBittorrent YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/tests/components/qbittorrent/test_config_flow.py b/tests/components/qbittorrent/test_config_flow.py index bbfeee20d8a..b7244ccef8d 100644 --- a/tests/components/qbittorrent/test_config_flow.py +++ b/tests/components/qbittorrent/test_config_flow.py @@ -4,7 +4,7 @@ from requests.exceptions import RequestException import requests_mock from homeassistant.components.qbittorrent.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import ( CONF_PASSWORD, CONF_SOURCE, @@ -26,6 +26,12 @@ USER_INPUT = { CONF_VERIFY_SSL: True, } +YAML_IMPORT = { + CONF_URL: "http://localhost:8080", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", +} + async def test_flow_user(hass: HomeAssistant, mock_api: requests_mock.Mocker) -> None: """Test the user flow.""" @@ -98,3 +104,33 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_flow_import(hass: HomeAssistant) -> None: + """Test import step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_IMPORT}, + data=YAML_IMPORT, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_URL: "http://localhost:8080", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_VERIFY_SSL: True, + } + + +async def test_flow_import_already_configured(hass: HomeAssistant) -> None: + """Test import step already configured.""" + entry = MockConfigEntry(domain=DOMAIN, data=USER_INPUT) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_IMPORT}, + data=YAML_IMPORT, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" From 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 932febd80f0..5f66338ecc1 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -83,6 +83,7 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): | VacuumEntityFeature.START ) _attr_translation_key = DOMAIN + _attr_name = None def __init__( self, From 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 c116a1bb635..c0fb80971ca 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -107,6 +107,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): """Representation of a Broadlink remote.""" _attr_has_entity_name = True + _attr_name = None def __init__(self, device, codes, flags): """Initialize the remote.""" From 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 | 51 ++++++++- .../components/brottsplatskartan/strings.json | 6 + .../brottsplatskartan/test_config_flow.py | 108 ++++++++++++++++++ 4 files changed, 176 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/brottsplatskartan/config_flow.py b/homeassistant/components/brottsplatskartan/config_flow.py index 09d6cd96087..1de24ffa76c 100644 --- a/homeassistant/components/brottsplatskartan/config_flow.py +++ b/homeassistant/components/brottsplatskartan/config_flow.py @@ -34,6 +34,21 @@ class BPKConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a configuration from config.yaml.""" + + if config.get(CONF_LATITUDE): + config[CONF_LOCATION] = { + CONF_LATITUDE: config[CONF_LATITUDE], + CONF_LONGITUDE: config[CONF_LONGITUDE], + } + if not config.get(CONF_AREA): + config[CONF_AREA] = "none" + else: + config[CONF_AREA] = config[CONF_AREA][0] + + return await self.async_step_user(user_input=config) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index 63af7530b79..a70b9c134d0 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -5,19 +5,62 @@ from collections import defaultdict from datetime import timedelta from brottsplatskartan import ATTRIBUTION, BrottsplatsKartan +import voluptuous as vol -from homeassistant.components.sensor import SensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SensorEntity, +) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_APP_ID, CONF_AREA, DOMAIN, LOGGER +from .const import AREAS, CONF_APP_ID, CONF_AREA, DEFAULT_NAME, DOMAIN, LOGGER SCAN_INTERVAL = timedelta(minutes=30) +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( + { + vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_AREA, default=[]): vol.All(cv.ensure_list, [vol.In(AREAS)]), + } +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Brottsplatskartan platform.""" + + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/brottsplatskartan/strings.json b/homeassistant/components/brottsplatskartan/strings.json index f10120f7884..8d9677a0af4 100644 --- a/homeassistant/components/brottsplatskartan/strings.json +++ b/homeassistant/components/brottsplatskartan/strings.json @@ -16,6 +16,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "title": "The Brottsplatskartan YAML configuration is being removed", + "description": "Configuring Brottsplatskartan using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Brottsplatskartan YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + }, "selector": { "areas": { "options": { diff --git a/tests/components/brottsplatskartan/test_config_flow.py b/tests/components/brottsplatskartan/test_config_flow.py index efd259fa73c..dd3139dc2b9 100644 --- a/tests/components/brottsplatskartan/test_config_flow.py +++ b/tests/components/brottsplatskartan/test_config_flow.py @@ -1,6 +1,8 @@ """Test the Brottsplatskartan config flow.""" from __future__ import annotations +from unittest.mock import patch + import pytest from homeassistant import config_entries @@ -9,6 +11,8 @@ from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from tests.common import MockConfigEntry + pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -99,3 +103,107 @@ async def test_form_area(hass: HomeAssistant) -> None: "area": "Stockholms län", "app_id": "ha-1234567890", } + + +async def test_import_flow_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml.""" + + with patch( + "homeassistant.components.brottsplatskartan.sensor.BrottsplatsKartan", + ): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Brottsplatskartan HOME" + assert result2["data"] == { + "latitude": hass.config.latitude, + "longitude": hass.config.longitude, + "area": None, + "app_id": "ha-1234567890", + } + + +async def test_import_flow_location_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml with location.""" + + with patch( + "homeassistant.components.brottsplatskartan.sensor.BrottsplatsKartan", + ): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_LATITUDE: 59.32, + CONF_LONGITUDE: 18.06, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Brottsplatskartan 59.32, 18.06" + assert result2["data"] == { + "latitude": 59.32, + "longitude": 18.06, + "area": None, + "app_id": "ha-1234567890", + } + + +async def test_import_flow_location_area_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml with location and area.""" + + with patch( + "homeassistant.components.brottsplatskartan.sensor.BrottsplatsKartan", + ): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_LATITUDE: 59.32, + CONF_LONGITUDE: 18.06, + CONF_AREA: ["Blekinge län"], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Brottsplatskartan Blekinge län" + assert result2["data"] == { + "latitude": None, + "longitude": None, + "area": "Blekinge län", + "app_id": "ha-1234567890", + } + + +async def test_import_flow_already_exist(hass: HomeAssistant) -> None: + """Test import of yaml already exist.""" + + MockConfigEntry( + domain=DOMAIN, + data={ + "latitude": hass.config.latitude, + "longitude": hass.config.longitude, + "area": None, + "app_id": "ha-1234567890", + }, + unique_id="bpk-home", + ).add_to_hass(hass) + + with patch( + "homeassistant.components.brottsplatskartan.sensor.BrottsplatsKartan", + ): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.ABORT + assert result3["reason"] == "already_configured" From 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 834202ba58d..3eceb448fa4 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -61,6 +61,7 @@ class NestCamera(Camera): """Devices that support cameras.""" _attr_has_entity_name = True + _attr_name = None def __init__(self, device: Device) -> None: """Initialize the camera.""" diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index ab0ce20a9a1..ca975ed055d 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -99,6 +99,7 @@ class ThermostatEntity(ClimateEntity): _attr_max_temp = MAX_TEMP _attr_has_entity_name = True _attr_should_poll = False + _attr_name = None def __init__(self, device: Device) -> None: """Initialize ThermostatEntity.""" From 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 f3a764bc99f..8e76706b7fd 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -96,20 +96,24 @@ class MatterAdapter: ) self.config_entry.async_on_unload( - self.matter_client.subscribe( + self.matter_client.subscribe_events( endpoint_added_callback, EventType.ENDPOINT_ADDED ) ) self.config_entry.async_on_unload( - self.matter_client.subscribe( + self.matter_client.subscribe_events( endpoint_removed_callback, EventType.ENDPOINT_REMOVED ) ) self.config_entry.async_on_unload( - self.matter_client.subscribe(node_removed_callback, EventType.NODE_REMOVED) + self.matter_client.subscribe_events( + node_removed_callback, EventType.NODE_REMOVED + ) ) self.config_entry.async_on_unload( - self.matter_client.subscribe(node_added_callback, EventType.NODE_ADDED) + self.matter_client.subscribe_events( + node_added_callback, EventType.NODE_ADDED + ) ) def _setup_node(self, node: MatterNode) -> None: diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index d4e90508afa..0457cfaa810 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -81,7 +81,7 @@ class MatterEntity(Entity): self._attributes_map[attr_cls] = attr_path sub_paths.append(attr_path) self._unsubscribes.append( - self.matter_client.subscribe( + self.matter_client.subscribe_events( callback=self._on_matter_event, event_filter=EventType.ATTRIBUTE_UPDATED, node_filter=self._endpoint.node.node_id, @@ -93,7 +93,7 @@ class MatterEntity(Entity): ) # subscribe to node (availability changes) self._unsubscribes.append( - self.matter_client.subscribe( + self.matter_client.subscribe_events( callback=self._on_matter_event, event_filter=EventType.NODE_UPDATED, node_filter=self._endpoint.node.node_id, diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 0bf900c8812..85434407a10 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==3.6.0"] + "requirements": ["python-matter-server==3.6.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index b0d6425d442..aab14224dc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2105,7 +2105,7 @@ python-kasa==0.5.1 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==3.6.0 +python-matter-server==3.6.3 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51f614a685f..2863b4c90f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1543,7 +1543,7 @@ python-juicenet==1.1.0 python-kasa==0.5.1 # homeassistant.components.matter -python-matter-server==3.6.0 +python-matter-server==3.6.3 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/tests/components/matter/common.py b/tests/components/matter/common.py index 7582b9c415d..a0935154054 100644 --- a/tests/components/matter/common.py +++ b/tests/components/matter/common.py @@ -71,6 +71,6 @@ async def trigger_subscription_callback( data: Any = None, ) -> None: """Trigger a subscription callback.""" - callback = client.subscribe.call_args.kwargs["callback"] + callback = client.subscribe_events.call_args.kwargs["callback"] callback(event, data) await hass.async_block_till_done() diff --git a/tests/components/matter/test_adapter.py b/tests/components/matter/test_adapter.py index 0d7cfc9c2ed..62ed847bf28 100644 --- a/tests/components/matter/test_adapter.py +++ b/tests/components/matter/test_adapter.py @@ -136,10 +136,10 @@ async def test_node_added_subscription( integration: MagicMock, ) -> None: """Test subscription to new devices work.""" - assert matter_client.subscribe.call_count == 4 - assert matter_client.subscribe.call_args[0][1] == EventType.NODE_ADDED + assert matter_client.subscribe_events.call_count == 4 + assert matter_client.subscribe_events.call_args[0][1] == EventType.NODE_ADDED - node_added_callback = matter_client.subscribe.call_args[0][0] + node_added_callback = matter_client.subscribe_events.call_args[0][0] node_data = load_and_parse_node_fixture("onoff-light") node = MatterNode( dataclass_from_dict( From 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 c6ca70bdc84..bdd55bb2dad 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -70,6 +70,7 @@ class PhilipsTVMediaPlayer( _attr_device_class = MediaPlayerDeviceClass.TV _attr_has_entity_name = True + _attr_name = None def __init__( self, From 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 9a25a28aa1c..514c218b101 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -168,7 +168,7 @@ BRIDGE_SCHEMA = vol.All( vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string), vol.Optional(CONF_ADVERTISE_IP): vol.All( - cv.ensure_list, ipaddress.ip_address, cv.string + cv.ensure_list, [ipaddress.ip_address], [cv.string] ), vol.Optional(CONF_FILTER, default={}): BASE_FILTER_SCHEMA, vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 5c154a50bec..112c138a843 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from typing import Any from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from uuid import uuid1 @@ -24,6 +25,7 @@ from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, + CONF_ADVERTISE_IP, DEFAULT_PORT, DOMAIN, HOMEKIT, @@ -322,6 +324,80 @@ async def test_homekit_setup_ip_address( ) +async def test_homekit_with_single_advertise_ips( + hass: HomeAssistant, + hk_driver, + mock_async_zeroconf: None, + hass_storage: dict[str, Any], +) -> None: + """Test setup with a single advertise ips.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345, CONF_ADVERTISE_IP: "1.3.4.4"}, + source=SOURCE_IMPORT, + ) + entry.add_to_hass(hass) + with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: + mock_driver.async_start = AsyncMock() + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mock_driver.assert_called_with( + hass, + entry.entry_id, + ANY, + entry.title, + loop=hass.loop, + address=[None], + port=ANY, + persist_file=ANY, + advertised_address="1.3.4.4", + async_zeroconf_instance=mock_async_zeroconf, + zeroconf_server=ANY, + loader=ANY, + iid_storage=ANY, + ) + + +async def test_homekit_with_many_advertise_ips( + hass: HomeAssistant, + hk_driver, + mock_async_zeroconf: None, + hass_storage: dict[str, Any], +) -> None: + """Test setup with many advertise ips.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_NAME: "mock_name", + CONF_PORT: 12345, + CONF_ADVERTISE_IP: ["1.3.4.4", "4.3.2.2"], + }, + source=SOURCE_IMPORT, + ) + entry.add_to_hass(hass) + with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: + mock_driver.async_start = AsyncMock() + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mock_driver.assert_called_with( + hass, + entry.entry_id, + ANY, + entry.title, + loop=hass.loop, + address=[None], + port=ANY, + persist_file=ANY, + advertised_address=["1.3.4.4", "4.3.2.2"], + async_zeroconf_instance=mock_async_zeroconf, + zeroconf_server=ANY, + loader=ANY, + iid_storage=ANY, + ) + + async def test_homekit_setup_advertise_ips( hass: HomeAssistant, hk_driver, mock_async_zeroconf: None ) -> None: From 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 4ea4da602e3..51560161faa 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -178,6 +178,7 @@ class IsWorkdaySensor(BinarySensorEntity): """Implementation of a Workday sensor.""" _attr_has_entity_name = True + _attr_name = None def __init__( self, From 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 8ac929e13b6..1a28044dcb5 100644 --- a/homeassistant/components/conversation/services.yaml +++ b/homeassistant/components/conversation/services.yaml @@ -7,6 +7,7 @@ process: name: Text description: Transcribed text example: Turn all lights on + required: true selector: text: language: From 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 c1eac31886e..de6dbdbe075 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -112,8 +112,8 @@ ENTITY_TRIGGERS = { {CONF_TYPE: CONF_NO_LIGHT}, ], BinarySensorDeviceClass.LOCK: [ - {CONF_TYPE: CONF_LOCKED}, {CONF_TYPE: CONF_NOT_LOCKED}, + {CONF_TYPE: CONF_LOCKED}, ], BinarySensorDeviceClass.MOISTURE: [ {CONF_TYPE: CONF_MOIST}, From 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 862f317ee82..5a99805da62 100644 --- a/homeassistant/components/androidtv_remote/entity.py +++ b/homeassistant/components/androidtv_remote/entity.py @@ -16,6 +16,7 @@ from .const import DOMAIN class AndroidTVRemoteBaseEntity(Entity): """Android TV Remote Base Entity.""" + _attr_name = None _attr_has_entity_name = True _attr_should_poll = False From 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 7e6c210a164..526ddd2bcc7 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -190,6 +190,7 @@ async def async_setup_entry( class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Representation of a Sonos entity.""" + _attr_name = None _attr_supported_features = ( MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.CLEAR_PLAYLIST diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 2c684013765..1eb8074bbc1 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -118,7 +118,9 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): # Segment 0 uses a simpler name, which is more natural for when using # a single segment / using WLED with one big LED strip. - if segment != 0: + if segment == 0: + self._attr_name = None + else: self._attr_name = f"Segment {segment}" self._attr_unique_id = ( diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 10fd1f2406b..34a7b949646 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -187,6 +187,7 @@ class MiroboVacuum( ): """Representation of a Xiaomi Vacuum cleaner robot.""" + _attr_name = None _attr_supported_features = ( VacuumEntityFeature.STATE | VacuumEntityFeature.PAUSE From 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 3801ccbecc9..90ca13c5146 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -2,6 +2,8 @@ from typing import Any +from roborock.api import AttributeCache +from roborock.command_cache import CacheableAttribute from roborock.containers import Status from roborock.exceptions import RoborockException from roborock.local_api import RoborockLocalClient @@ -27,6 +29,15 @@ class RoborockEntity(Entity): self._attr_device_info = device_info self._api = api + @property + def api(self) -> RoborockLocalClient: + """Returns the api.""" + return self._api + + def get_cache(self, attribute: CacheableAttribute) -> AttributeCache: + """Get an item from the api cache.""" + return self._api.cache.get(attribute) + async def send( self, command: RoborockCommand, params: dict[str, Any] | list[Any] | None = None ) -> dict: diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 39e13412e90..baab687e64a 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.23.6"] + "requirements": ["python-roborock==0.29.2"] } diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index d16d7437202..a0b3d5be295 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -1,23 +1,27 @@ """Support for Roborock switch.""" +from __future__ import annotations + import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass import logging from typing import Any -from roborock.exceptions import RoborockException -from roborock.roborock_typing import RoborockCommand +from roborock.api import AttributeCache +from roborock.command_cache import CacheableAttribute +from roborock.local_api import RoborockLocalClient from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from .const import DOMAIN from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockCoordinatedEntity, RoborockEntity +from .device import RoborockEntity _LOGGER = logging.getLogger(__name__) @@ -27,23 +31,11 @@ class RoborockSwitchDescriptionMixin: """Define an entity description mixin for switch entities.""" # Gets the status of the switch - get_value: Callable[[RoborockEntity], Coroutine[Any, Any, dict]] - # Evaluate the result of get_value to determine a bool - evaluate_value: Callable[[dict], bool] + cache_key: CacheableAttribute # Sets the status of the switch - set_command: Callable[[RoborockEntity, bool], Coroutine[Any, Any, dict]] - # Check support of this feature - check_support: Callable[[RoborockDataUpdateCoordinator], Coroutine[Any, Any, dict]] - - -@dataclass -class RoborockCoordinatedSwitchDescriptionMixIn: - """Define an entity description mixin for switch entities.""" - - get_value: Callable[[RoborockCoordinatedEntity], bool] - set_command: Callable[[RoborockCoordinatedEntity, bool], Coroutine[Any, Any, dict]] - # Check support of this feature - check_support: Callable[[RoborockDataUpdateCoordinator], dict] + update_value: Callable[[AttributeCache, bool], Coroutine[Any, Any, dict]] + # Attribute from cache + attribute: str @dataclass @@ -53,59 +45,42 @@ class RoborockSwitchDescription( """Class to describe an Roborock switch entity.""" -@dataclass -class RoborockCoordinatedSwitchDescription( - SwitchEntityDescription, RoborockCoordinatedSwitchDescriptionMixIn -): - """Class to describe an Roborock switch entity that needs a coordinator.""" - - SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [ RoborockSwitchDescription( - set_command=lambda entity, value: entity.send( - RoborockCommand.SET_CHILD_LOCK_STATUS, {"lock_status": 1 if value else 0} + cache_key=CacheableAttribute.child_lock_status, + update_value=lambda cache, value: cache.update_value( + {"lock_status": 1 if value else 0} ), - get_value=lambda data: data.send(RoborockCommand.GET_CHILD_LOCK_STATUS), - check_support=lambda data: data.api.send_command( - RoborockCommand.GET_CHILD_LOCK_STATUS - ), - evaluate_value=lambda data: data["lock_status"] == 1, + attribute="lock_status", key="child_lock", translation_key="child_lock", icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), RoborockSwitchDescription( - set_command=lambda entity, value: entity.send( - RoborockCommand.SET_FLOW_LED_STATUS, {"status": 1 if value else 0} + cache_key=CacheableAttribute.flow_led_status, + update_value=lambda cache, value: cache.update_value( + {"status": 1 if value else 0} ), - get_value=lambda data: data.send(RoborockCommand.GET_FLOW_LED_STATUS), - check_support=lambda data: data.api.send_command( - RoborockCommand.GET_FLOW_LED_STATUS - ), - evaluate_value=lambda data: data["status"] == 1, + attribute="status", key="status_indicator", translation_key="status_indicator", icon="mdi:alarm-light-outline", entity_category=EntityCategory.CONFIG, ), -] - -COORDINATED_SWITCH_DESCRIPTION = [ - RoborockCoordinatedSwitchDescription( - set_command=lambda entity, value: entity.send( - RoborockCommand.SET_DND_TIMER, + RoborockSwitchDescription( + cache_key=CacheableAttribute.dnd_timer, + update_value=lambda cache, value: cache.update_value( [ - entity.coordinator.roborock_device_info.props.dnd_timer.start_hour, - entity.coordinator.roborock_device_info.props.dnd_timer.start_minute, - entity.coordinator.roborock_device_info.props.dnd_timer.end_hour, - entity.coordinator.roborock_device_info.props.dnd_timer.end_minute, - ], + cache.value.get("start_hour"), + cache.value.get("start_minute"), + cache.value.get("end_hour"), + cache.value.get("end_minute"), + ] ) if value - else entity.send(RoborockCommand.CLOSE_DND_TIMER), - check_support=lambda data: data.roborock_device_info.props.dnd_timer, - get_value=lambda data: data.coordinator.roborock_device_info.props.dnd_timer.enabled, + else cache.close_value(), + attribute="enabled", key="dnd_switch", translation_key="dnd_switch", icon="mdi:bell-cancel", @@ -120,114 +95,74 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock switch platform.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ config_entry.entry_id ] possible_entities: list[ - tuple[str, RoborockDataUpdateCoordinator, RoborockSwitchDescription] + tuple[RoborockDataUpdateCoordinator, RoborockSwitchDescription] ] = [ - (device_id, coordinator, description) - for device_id, coordinator in coordinators.items() + (coordinator, description) + for coordinator in coordinators.values() for description in SWITCH_DESCRIPTIONS ] # We need to check if this function is supported by the device. results = await asyncio.gather( *( - description.check_support(coordinator) - for _, coordinator, description in possible_entities + coordinator.api.cache.get(description.cache_key).async_value() + for coordinator, description in possible_entities ), return_exceptions=True, ) - valid_entities: list[RoborockNonCoordinatedSwitchEntity] = [] - for posible_entity, result in zip(possible_entities, results): - if isinstance(result, Exception): - if not isinstance(result, RoborockException): - raise result + valid_entities: list[RoborockSwitch] = [] + for (coordinator, description), result in zip(possible_entities, results): + if result is None or isinstance(result, Exception): _LOGGER.debug("Not adding entity because of %s", result) else: valid_entities.append( - RoborockNonCoordinatedSwitchEntity( - f"{posible_entity[2].key}_{slugify(posible_entity[0])}", - posible_entity[1], - posible_entity[2], - result, + RoborockSwitch( + f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}", + coordinator.device_info, + description, + coordinator.api, ) ) - async_add_entities( - valid_entities, - True, - ) - async_add_entities( - ( - RoborockCoordinatedSwitchEntity( - f"{description.key}_{slugify(device_id)}", - coordinator, - description, - ) - for device_id, coordinator in coordinators.items() - for description in COORDINATED_SWITCH_DESCRIPTION - if description.check_support(coordinator) is not None - ) - ) + async_add_entities(valid_entities) -class RoborockNonCoordinatedSwitchEntity(RoborockEntity, SwitchEntity): - """A class to let you turn functionality on Roborock devices on and off that does not need a coordinator.""" +class RoborockSwitch(RoborockEntity, SwitchEntity): + """A class to let you turn functionality on Roborock devices on and off that does need a coordinator.""" entity_description: RoborockSwitchDescription def __init__( self, unique_id: str, - coordinator: RoborockDataUpdateCoordinator, - entity_description: RoborockSwitchDescription, - initial_value: bool, + device_info: DeviceInfo, + description: RoborockSwitchDescription, + api: RoborockLocalClient, ) -> None: - """Create a switch entity.""" - self.entity_description = entity_description - super().__init__(unique_id, coordinator.device_info, coordinator.api) - self._attr_is_on = initial_value + """Initialize the entity.""" + super().__init__(unique_id, device_info, api) + self.entity_description = description async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" - await self.entity_description.set_command(self, False) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn on the switch.""" - await self.entity_description.set_command(self, True) - - async def async_update(self) -> None: - """Update switch.""" - self._attr_is_on = self.entity_description.evaluate_value( - await self.entity_description.get_value(self) + await self.entity_description.update_value( + self.get_cache(self.entity_description.cache_key), False ) - -class RoborockCoordinatedSwitchEntity(RoborockCoordinatedEntity, SwitchEntity): - """A class to let you turn functionality on Roborock devices on and off that does need a coordinator.""" - - entity_description: RoborockCoordinatedSwitchDescription - - def __init__( - self, - unique_id: str, - coordinator: RoborockDataUpdateCoordinator, - entity_description: RoborockCoordinatedSwitchDescription, - ) -> None: - """Create a switch entity.""" - self.entity_description = entity_description - super().__init__(unique_id, coordinator) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn off the switch.""" - await self.entity_description.set_command(self, False) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" - await self.entity_description.set_command(self, True) + await self.entity_description.update_value( + self.get_cache(self.entity_description.cache_key), True + ) @property def is_on(self) -> bool | None: - """Use the coordinator to determine if the switch is on.""" - return self.entity_description.get_value(self) + """Return True if entity is on.""" + return ( + self.get_cache(self.entity_description.cache_key).value.get( + self.entity_description.attribute + ) + == 1 + ) diff --git a/requirements_all.txt b/requirements_all.txt index aab14224dc9..851359306ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2139,7 +2139,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.23.6 +python-roborock==0.29.2 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2863b4c90f7..74612b98385 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1565,7 +1565,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.23.6 +python-roborock==0.29.2 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index d9c11bead74..eb281076825 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -20,11 +20,17 @@ from tests.common import MockConfigEntry @pytest.fixture(name="bypass_api_fixture") def bypass_api_fixture() -> None: """Skip calls to the API.""" - with patch("homeassistant.components.roborock.RoborockMqttClient.connect"), patch( - "homeassistant.components.roborock.RoborockMqttClient.send_command" + with patch( + "homeassistant.components.roborock.RoborockMqttClient.async_connect" + ), patch( + "homeassistant.components.roborock.RoborockMqttClient._send_command" ), patch( "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", return_value=PROP, + ), patch( + "roborock.api.AttributeCache.async_value" + ), patch( + "roborock.api.AttributeCache.value" ): yield diff --git a/tests/components/roborock/mock_data.py b/tests/components/roborock/mock_data.py index 15e69cee9d9..6a2e1f4b5f1 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -367,7 +367,12 @@ STATUS = S7Status.from_dict( "unsave_map_flag": 0, } ) -PROP = DeviceProp(STATUS, DND_TIMER, CLEAN_SUMMARY, CONSUMABLE, CLEAN_RECORD) +PROP = DeviceProp( + status=STATUS, + clean_summary=CLEAN_SUMMARY, + consumable=CONSUMABLE, + last_clean_record=CLEAN_RECORD, +) NETWORK_INFO = NetworkInfo( ip="123.232.12.1", ssid="wifi", mac="ac:cc:cc:cc:cc", bssid="bssid", rssi=90 diff --git a/tests/components/roborock/snapshots/test_diagnostics.ambr b/tests/components/roborock/snapshots/test_diagnostics.ambr index 432bad167cd..eb70e04110f 100644 --- a/tests/components/roborock/snapshots/test_diagnostics.ambr +++ b/tests/components/roborock/snapshots/test_diagnostics.ambr @@ -221,21 +221,6 @@ 'sideBrushWorkTime': 74382, 'strainerWorkTimes': 65, }), - 'dndTimer': dict({ - 'enabled': 1, - 'endHour': 7, - 'endMinute': 0, - 'endTime': dict({ - '__type': "", - 'isoformat': '07:00:00', - }), - 'startHour': 22, - 'startMinute': 0, - 'startTime': dict({ - '__type': "", - 'isoformat': '22:00:00', - }), - }), 'lastCleanRecord': dict({ 'area': 20965000, 'avoidCount': 19, diff --git a/tests/components/roborock/test_select.py b/tests/components/roborock/test_select.py index 3b0ba8183b3..bcea4e6246b 100644 --- a/tests/components/roborock/test_select.py +++ b/tests/components/roborock/test_select.py @@ -26,6 +26,8 @@ async def test_update_success( value: str, ) -> None: """Test allowed changing values for select entities.""" + # Ensure that the entity exist, as these test can pass even if there is no entity. + assert hass.states.get(entity_id) is not None with patch( "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" ) as mock_send_message: diff --git a/tests/components/roborock/test_switch.py b/tests/components/roborock/test_switch.py index 9c079ef85b6..40ecdc267ed 100644 --- a/tests/components/roborock/test_switch.py +++ b/tests/components/roborock/test_switch.py @@ -2,11 +2,9 @@ from unittest.mock import patch import pytest -from roborock.exceptions import RoborockException from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry @@ -26,6 +24,8 @@ async def test_update_success( entity_id: str, ) -> None: """Test turning switch entities on and off.""" + # Ensure that the entity exist, as these test can pass even if there is no entity. + assert hass.states.get(entity_id) is not None with patch( "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" ) as mock_send_message: @@ -48,22 +48,3 @@ async def test_update_success( target={"entity_id": entity_id}, ) assert mock_send_message.assert_called_once - - -async def test_update_failure( - hass: HomeAssistant, - bypass_api_fixture, - setup_entry: MockConfigEntry, -) -> None: - """Test that changing a value will raise a homeassistanterror when it fails.""" - with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message", - side_effect=RoborockException(), - ), pytest.raises(HomeAssistantError): - await hass.services.async_call( - "switch", - SERVICE_TURN_ON, - service_data=None, - blocking=True, - target={"entity_id": "switch.roborock_s7_maxv_child_lock"}, - ) From 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 509983c25f6..891a97d4d02 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230628.0"] + "requirements": ["home-assistant-frontend==20230629.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7b3e7fe7c4b..c850bb6790b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.0 hass-nabucasa==0.69.0 hassil==1.0.6 home-assistant-bluetooth==1.10.0 -home-assistant-frontend==20230628.0 +home-assistant-frontend==20230629.0 home-assistant-intents==2023.6.28 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 851359306ad..8b905cbe408 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -980,7 +980,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230628.0 +home-assistant-frontend==20230629.0 # homeassistant.components.conversation home-assistant-intents==2023.6.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 74612b98385..bb50d6f1b57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -763,7 +763,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230628.0 +home-assistant-frontend==20230629.0 # homeassistant.components.conversation home-assistant-intents==2023.6.28 From 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 5d0d2526bf2..86f61f0ed87 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -60,6 +60,7 @@ from .const import ( EVENT_TIME_FIELDS, EVENT_TYPES, EVENT_UID, + LIST_EVENT_FIELDS, CalendarEntityFeature, ) @@ -415,6 +416,17 @@ def _api_event_dict_factory(obj: Iterable[tuple[str, Any]]) -> dict[str, Any]: return result +def _list_events_dict_factory( + obj: Iterable[tuple[str, Any]] +) -> dict[str, JsonValueType]: + """Convert CalendarEvent dataclass items to dictionary of attributes.""" + return { + name: value + for name, value in obj + if name in LIST_EVENT_FIELDS and value is not None + } + + def _get_datetime_local( dt_or_d: datetime.datetime | datetime.date, ) -> datetime.datetime: @@ -782,9 +794,9 @@ async def async_list_events_service( else: end = service_call.data[EVENT_END_DATETIME] calendar_event_list = await calendar.async_get_events(calendar.hass, start, end) - events: list[JsonValueType] = [ - dataclasses.asdict(event) for event in calendar_event_list - ] return { - "events": events, + "events": [ + dataclasses.asdict(event, dict_factory=_list_events_dict_factory) + for event in calendar_event_list + ] } diff --git a/homeassistant/components/calendar/const.py b/homeassistant/components/calendar/const.py index 2d4f0dfe0ba..e667510325b 100644 --- a/homeassistant/components/calendar/const.py +++ b/homeassistant/components/calendar/const.py @@ -41,3 +41,12 @@ EVENT_TIME_FIELDS = { } EVENT_TYPES = "event_types" EVENT_DURATION = "duration" + +# Fields for the list events service +LIST_EVENT_FIELDS = { + "start", + "end", + EVENT_SUMMARY, + EVENT_DESCRIPTION, + EVENT_LOCATION, +} diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 97292221819..9fdc76abe03 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from http import HTTPStatus from typing import Any -from unittest.mock import patch +from unittest.mock import ANY, patch import pytest import voluptuous as vol @@ -405,11 +405,17 @@ async def test_list_events_service(hass: HomeAssistant) -> None: blocking=True, return_response=True, ) - assert response - assert "events" in response - events = response["events"] - assert len(events) == 1 - assert events[0]["summary"] == "Future Event" + assert response == { + "events": [ + { + "start": ANY, + "end": ANY, + "summary": "Future Event", + "description": "Future Description", + "location": "Future Location", + } + ] + } @pytest.mark.parametrize( diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index c8f494a0071..7e46dc0d0bd 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1769,9 +1769,6 @@ async def test_execute_script_complex_response( "summary": "Future Event", "description": "Future Description", "location": "Future Location", - "uid": None, - "recurrence_id": None, - "rrule": None, } ] } From 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 4c9337e54ce..8e194ac27b1 100644 --- a/homeassistant/components/ibeacon/device_tracker.py +++ b/homeassistant/components/ibeacon/device_tracker.py @@ -48,6 +48,8 @@ async def async_setup_entry( class IBeaconTrackerEntity(IBeaconEntity, BaseTrackerEntity): """An iBeacon Tracker entity.""" + _attr_name = None + def __init__( self, coordinator: IBeaconCoordinator, diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 877e58233d5..a8c1cf4698c 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -108,6 +108,7 @@ async def async_setup_entry( class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): """Representation of a Roku media player on the network.""" + _attr_name = None _attr_supported_features = ( MediaPlayerEntityFeature.PREVIOUS_TRACK | MediaPlayerEntityFeature.NEXT_TRACK diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index fceac67a477..0271e4a0f73 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -37,6 +37,8 @@ async def async_setup_entry( class RokuRemote(RokuEntity, RemoteEntity): """Device that sends commands to an Roku.""" + _attr_name = None + @property def is_on(self) -> bool: """Return true if device is on.""" From 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 6f00568cf2b..5711aff2a4a 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -62,7 +62,7 @@ "issues": { "service_depreciation_detection_sensibility": { "title": "Ezviz Detection sensitivity service is being removed", - "description": "Ezviz Detection sensitivity service is deprecated and will be removed in Home Assistant 2023.8; Please adjust the automation or script that uses the service and select submit below to mark this issue as resolved." + "description": "Ezviz Detection sensitivity service is deprecated and will be removed in Home Assistant 2023.12; Please adjust the automation or script that uses the service and select submit below to mark this issue as resolved." } } } From 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 2d96897ef9d..dbe8ac3f1ab 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,7 +18,7 @@ "bleak-retry-connector==3.0.2", "bluetooth-adapters==0.15.3", "bluetooth-auto-recovery==1.2.0", - "bluetooth-data-tools==1.2.0", + "bluetooth-data-tools==1.3.0", "dbus-fast==1.86.0" ] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index de47c904dd7..404339d4f30 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "aioesphomeapi==15.0.0", - "bluetooth-data-tools==1.2.0", + "bluetooth-data-tools==1.3.0", "esphome-dashboard-api==1.2.3" ], "zeroconf": ["_esphomelib._tcp.local."] diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index b15eb343c06..6eaf2885d89 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/ld2410_ble/", "integration_type": "device", "iot_class": "local_push", - "requirements": ["bluetooth-data-tools==1.2.0", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.3.0", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index b788ec21052..cdc270f2e99 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -32,5 +32,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/led_ble/", "iot_class": "local_polling", - "requirements": ["bluetooth-data-tools==1.2.0", "led-ble==1.0.0"] + "requirements": ["bluetooth-data-tools==1.3.0", "led-ble==1.0.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c850bb6790b..e6e506273ad 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ bleak-retry-connector==3.0.2 bleak==0.20.2 bluetooth-adapters==0.15.3 bluetooth-auto-recovery==1.2.0 -bluetooth-data-tools==1.2.0 +bluetooth-data-tools==1.3.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 8b905cbe408..f4bd9865a77 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -537,7 +537,7 @@ bluetooth-auto-recovery==1.2.0 # homeassistant.components.esphome # homeassistant.components.ld2410_ble # homeassistant.components.led_ble -bluetooth-data-tools==1.2.0 +bluetooth-data-tools==1.3.0 # homeassistant.components.bond bond-async==0.1.23 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb50d6f1b57..5d216e6c844 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -448,7 +448,7 @@ bluetooth-auto-recovery==1.2.0 # homeassistant.components.esphome # homeassistant.components.ld2410_ble # homeassistant.components.led_ble -bluetooth-data-tools==1.2.0 +bluetooth-data-tools==1.3.0 # homeassistant.components.bond bond-async==0.1.23 From 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 271b0b9aa16..fedb2edd899 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -405,7 +405,9 @@ class ESPHomeManager: 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 5f426fc49c5..f52b78b5a52 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -142,6 +142,7 @@ async def async_setup_entry( ): device_info = DeviceInfo( identifiers=device.identifiers, + connections=device.connections, ) else: device_info = None diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 1e26d5e211a..5cb9e594cb2 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1469,6 +1469,7 @@ async def test_device_id(hass: HomeAssistant) -> None: source_device_entry = device_registry.async_get_or_create( config_entry_id=source_config_entry.entry_id, identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, ) source_entity = entity_registry.async_get_or_create( "sensor", From 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 fa1c382926e..d7acc9788c4 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -20,7 +20,7 @@ "zigpy_znp" ], "requirements": [ - "bellows==0.35.6", + "bellows==0.35.7", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.101", diff --git a/requirements_all.txt b/requirements_all.txt index f4bd9865a77..749a064f952 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -497,7 +497,7 @@ beautifulsoup4==4.11.1 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.35.6 +bellows==0.35.7 # homeassistant.components.bmw_connected_drive bimmer-connected==0.13.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d216e6c844..5761609b4b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -418,7 +418,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.35.6 +bellows==0.35.7 # homeassistant.components.bmw_connected_drive bimmer-connected==0.13.7 From 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 084acbe73e7..216c5d9335e 100644 --- a/homeassistant/components/wiz/light.py +++ b/homeassistant/components/wiz/light.py @@ -71,6 +71,8 @@ async def async_setup_entry( class WizBulbEntity(WizToggleEntity, LightEntity): """Representation of WiZ Light bulb.""" + _attr_name = None + def __init__(self, wiz_data: WizData, name: str) -> None: """Initialize an WiZLight.""" super().__init__(wiz_data, name) From 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 52de9215535..65d9e0b3894 100644 --- a/homeassistant/components/google_generative_ai_conversation/manifest.json +++ b/homeassistant/components/google_generative_ai_conversation/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google_generative_ai_conversation", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["google-generativeai==0.1.0rc2"] + "requirements": ["google-generativeai==0.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 749a064f952..bc0d3208b35 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -880,7 +880,7 @@ google-cloud-pubsub==2.13.11 google-cloud-texttospeech==2.12.3 # homeassistant.components.google_generative_ai_conversation -google-generativeai==0.1.0rc2 +google-generativeai==0.1.0 # homeassistant.components.nest google-nest-sdm==2.2.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5761609b4b3..57e7adf7964 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,7 +690,7 @@ google-api-python-client==2.71.0 google-cloud-pubsub==2.13.11 # homeassistant.components.google_generative_ai_conversation -google-generativeai==0.1.0rc2 +google-generativeai==0.1.0 # homeassistant.components.nest google-nest-sdm==2.2.5 From 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 e2791f6000f..db2a8d9512e 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -18,18 +18,11 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( ) from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_ENABLE_CONVERSATION_AGENT, - CONF_LANGUAGE_CODE, - DATA_MEM_STORAGE, - DATA_SESSION, - DOMAIN, -) +from .const import DATA_MEM_STORAGE, DATA_SESSION, DOMAIN, SUPPORTED_LANGUAGE_CODES from .helpers import ( GoogleAssistantSDKAudioView, InMemoryStorage, async_send_text_commands, - default_language_code, ) SERVICE_SEND_TEXT_COMMAND = "send_text_command" @@ -82,8 +75,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_setup_service(hass) - entry.async_on_unload(entry.add_update_listener(update_listener)) - await update_listener(hass, entry) + agent = GoogleAssistantConversationAgent(hass, entry) + conversation.async_set_agent(hass, entry, agent) return True @@ -100,8 +93,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for service_name in hass.services.async_services()[DOMAIN]: hass.services.async_remove(DOMAIN, service_name) - if entry.options.get(CONF_ENABLE_CONVERSATION_AGENT, False): - conversation.async_unset_agent(hass, entry) + conversation.async_unset_agent(hass, entry) return True @@ -125,15 +117,6 @@ async def async_setup_service(hass: HomeAssistant) -> None: ) -async def update_listener(hass, entry): - """Handle options update.""" - if entry.options.get(CONF_ENABLE_CONVERSATION_AGENT, False): - agent = GoogleAssistantConversationAgent(hass, entry) - conversation.async_set_agent(hass, entry, agent) - else: - conversation.async_unset_agent(hass, entry) - - class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): """Google Assistant SDK conversation agent.""" @@ -143,6 +126,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): self.entry = entry self.assistant: TextAssistant | None = None self.session: OAuth2Session | None = None + self.language: str | None = None @property def attribution(self): @@ -155,10 +139,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): @property def supported_languages(self) -> list[str]: """Return a list of supported languages.""" - language_code = self.entry.options.get( - CONF_LANGUAGE_CODE, default_language_code(self.hass) - ) - return [language_code] + return SUPPORTED_LANGUAGE_CODES async def async_process( self, user_input: conversation.ConversationInput @@ -172,12 +153,10 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): if not session.valid_token: await session.async_ensure_token_valid() self.assistant = None - if not self.assistant: + if not self.assistant or user_input.language != self.language: credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) - language_code = self.entry.options.get( - CONF_LANGUAGE_CODE, default_language_code(self.hass) - ) - self.assistant = TextAssistant(credentials, language_code) + self.language = user_input.language + self.assistant = TextAssistant(credentials, self.language) resp = self.assistant.assist(user_input.text) text_response = resp[0] or "" diff --git a/homeassistant/components/google_assistant_sdk/config_flow.py b/homeassistant/components/google_assistant_sdk/config_flow.py index b93a3be93f2..b4f617ca029 100644 --- a/homeassistant/components/google_assistant_sdk/config_flow.py +++ b/homeassistant/components/google_assistant_sdk/config_flow.py @@ -13,13 +13,7 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow -from .const import ( - CONF_ENABLE_CONVERSATION_AGENT, - CONF_LANGUAGE_CODE, - DEFAULT_NAME, - DOMAIN, - SUPPORTED_LANGUAGE_CODES, -) +from .const import CONF_LANGUAGE_CODE, DEFAULT_NAME, DOMAIN, SUPPORTED_LANGUAGE_CODES from .helpers import default_language_code _LOGGER = logging.getLogger(__name__) @@ -114,12 +108,6 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_LANGUAGE_CODE, default=self.config_entry.options.get(CONF_LANGUAGE_CODE), ): vol.In(SUPPORTED_LANGUAGE_CODES), - vol.Required( - CONF_ENABLE_CONVERSATION_AGENT, - default=self.config_entry.options.get( - CONF_ENABLE_CONVERSATION_AGENT - ), - ): bool, } ), ) diff --git a/homeassistant/components/google_assistant_sdk/const.py b/homeassistant/components/google_assistant_sdk/const.py index c9f86160bb4..d63aec0ebd5 100644 --- a/homeassistant/components/google_assistant_sdk/const.py +++ b/homeassistant/components/google_assistant_sdk/const.py @@ -5,7 +5,6 @@ DOMAIN: Final = "google_assistant_sdk" DEFAULT_NAME: Final = "Google Assistant SDK" -CONF_ENABLE_CONVERSATION_AGENT: Final = "enable_conversation_agent" CONF_LANGUAGE_CODE: Final = "language_code" DATA_MEM_STORAGE: Final = "mem_storage" diff --git a/homeassistant/components/google_assistant_sdk/strings.json b/homeassistant/components/google_assistant_sdk/strings.json index d4c85be91e5..66a2b975b5e 100644 --- a/homeassistant/components/google_assistant_sdk/strings.json +++ b/homeassistant/components/google_assistant_sdk/strings.json @@ -31,10 +31,8 @@ "step": { "init": { "data": { - "enable_conversation_agent": "Enable the conversation agent", "language_code": "Language code" - }, - "description": "Set language for interactions with Google Assistant and whether you want to enable the conversation agent." + } } } }, diff --git a/tests/components/google_assistant_sdk/test_config_flow.py b/tests/components/google_assistant_sdk/test_config_flow.py index 1d350a8fe4f..c65477b18b1 100644 --- a/tests/components/google_assistant_sdk/test_config_flow.py +++ b/tests/components/google_assistant_sdk/test_config_flow.py @@ -223,65 +223,39 @@ async def test_options_flow( assert result["type"] == "form" assert result["step_id"] == "init" data_schema = result["data_schema"].schema - assert set(data_schema) == {"enable_conversation_agent", "language_code"} + assert set(data_schema) == {"language_code"} result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"enable_conversation_agent": False, "language_code": "es-ES"}, + user_input={"language_code": "es-ES"}, ) assert result["type"] == "create_entry" - assert config_entry.options == { - "enable_conversation_agent": False, - "language_code": "es-ES", - } + assert config_entry.options == {"language_code": "es-ES"} # Retrigger options flow, not change language result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == "form" assert result["step_id"] == "init" data_schema = result["data_schema"].schema - assert set(data_schema) == {"enable_conversation_agent", "language_code"} + assert set(data_schema) == {"language_code"} result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"enable_conversation_agent": False, "language_code": "es-ES"}, + user_input={"language_code": "es-ES"}, ) assert result["type"] == "create_entry" - assert config_entry.options == { - "enable_conversation_agent": False, - "language_code": "es-ES", - } + assert config_entry.options == {"language_code": "es-ES"} # Retrigger options flow, change language result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == "form" assert result["step_id"] == "init" data_schema = result["data_schema"].schema - assert set(data_schema) == {"enable_conversation_agent", "language_code"} + assert set(data_schema) == {"language_code"} result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"enable_conversation_agent": False, "language_code": "en-US"}, + user_input={"language_code": "en-US"}, ) assert result["type"] == "create_entry" - assert config_entry.options == { - "enable_conversation_agent": False, - "language_code": "en-US", - } - - # Retrigger options flow, enable conversation agent - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" - assert result["step_id"] == "init" - data_schema = result["data_schema"].schema - assert set(data_schema) == {"enable_conversation_agent", "language_code"} - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={"enable_conversation_agent": True, "language_code": "en-US"}, - ) - assert result["type"] == "create_entry" - assert config_entry.options == { - "enable_conversation_agent": True, - "language_code": "en-US", - } + assert config_entry.options == {"language_code": "en-US"} diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index 25066f73b6d..99f264e4a3a 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -9,8 +9,9 @@ import pytest from homeassistant.components import conversation from homeassistant.components.google_assistant_sdk import DOMAIN +from homeassistant.components.google_assistant_sdk.const import SUPPORTED_LANGUAGE_CODES from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant +from homeassistant.core import Context, HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -29,13 +30,9 @@ async def fetch_api_url(hass_client, url): return response.status, contents -@pytest.mark.parametrize( - "enable_conversation_agent", [False, True], ids=["", "enable_conversation_agent"] -) async def test_setup_success( hass: HomeAssistant, setup_integration: ComponentSetup, - enable_conversation_agent: bool, ) -> None: """Test successful setup and unload.""" await setup_integration() @@ -44,12 +41,6 @@ async def test_setup_success( assert len(entries) == 1 assert entries[0].state is ConfigEntryState.LOADED - if enable_conversation_agent: - hass.config_entries.async_update_entry( - entries[0], options={"enable_conversation_agent": True} - ) - await hass.async_block_till_done() - await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() @@ -333,30 +324,21 @@ async def test_conversation_agent( assert len(entries) == 1 entry = entries[0] assert entry.state is ConfigEntryState.LOADED - hass.config_entries.async_update_entry( - entry, options={"enable_conversation_agent": True} - ) - await hass.async_block_till_done() agent = await conversation._get_agent_manager(hass).async_get_agent(entry.entry_id) - assert agent.supported_languages == ["en-US"] + assert agent.attribution.keys() == {"name", "url"} + assert agent.supported_languages == SUPPORTED_LANGUAGE_CODES text1 = "tell me a joke" text2 = "tell me another one" with patch( "homeassistant.components.google_assistant_sdk.TextAssistant" ) as mock_text_assistant: - await hass.services.async_call( - "conversation", - "process", - {"text": text1, "agent_id": config_entry.entry_id}, - blocking=True, + await conversation.async_converse( + hass, text1, None, Context(), "en-US", config_entry.entry_id ) - await hass.services.async_call( - "conversation", - "process", - {"text": text2, "agent_id": config_entry.entry_id}, - blocking=True, + await conversation.async_converse( + hass, text2, None, Context(), "en-US", config_entry.entry_id ) # Assert constructor is called only once since it's reused across requests @@ -381,21 +363,14 @@ async def test_conversation_agent_refresh_token( assert len(entries) == 1 entry = entries[0] assert entry.state is ConfigEntryState.LOADED - hass.config_entries.async_update_entry( - entry, options={"enable_conversation_agent": True} - ) - await hass.async_block_till_done() text1 = "tell me a joke" text2 = "tell me another one" with patch( "homeassistant.components.google_assistant_sdk.TextAssistant" ) as mock_text_assistant: - await hass.services.async_call( - "conversation", - "process", - {"text": text1, "agent_id": config_entry.entry_id}, - blocking=True, + await conversation.async_converse( + hass, text1, None, Context(), "en-US", config_entry.entry_id ) # Expire the token between requests @@ -411,11 +386,8 @@ async def test_conversation_agent_refresh_token( }, ) - await hass.services.async_call( - "conversation", - "process", - {"text": text2, "agent_id": config_entry.entry_id}, - blocking=True, + await conversation.async_converse( + hass, text2, None, Context(), "en-US", config_entry.entry_id ) # Assert constructor is called twice since the token was expired @@ -426,3 +398,38 @@ async def test_conversation_agent_refresh_token( ) mock_text_assistant.assert_has_calls([call().assist(text1)]) mock_text_assistant.assert_has_calls([call().assist(text2)]) + + +async def test_conversation_agent_language_changed( + hass: HomeAssistant, + config_entry: MockConfigEntry, + setup_integration: ComponentSetup, +) -> None: + """Test GoogleAssistantConversationAgent when language is changed.""" + await setup_integration() + + assert await async_setup_component(hass, "conversation", {}) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.state is ConfigEntryState.LOADED + + text1 = "tell me a joke" + text2 = "cuéntame un chiste" + with patch( + "homeassistant.components.google_assistant_sdk.TextAssistant" + ) as mock_text_assistant: + await conversation.async_converse( + hass, text1, None, Context(), "en-US", config_entry.entry_id + ) + await conversation.async_converse( + hass, text2, None, Context(), "es-ES", config_entry.entry_id + ) + + # Assert constructor is called twice since the language was changed + assert mock_text_assistant.call_count == 2 + mock_text_assistant.assert_has_calls([call(ExpectedCredentials(), "en-US")]) + mock_text_assistant.assert_has_calls([call(ExpectedCredentials(), "es-ES")]) + mock_text_assistant.assert_has_calls([call().assist(text1)]) + mock_text_assistant.assert_has_calls([call().assist(text2)]) From 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 6170bd165e9..fa9f609ba10 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -90,6 +90,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): _attr_target_temperature_step = PRECISION_WHOLE _attr_max_temp = 32 _attr_min_temp = 16 + _attr_name = None _attr_hvac_modes = [ HVACMode.OFF, From 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 90fbf849389..c33838029b4 100644 --- a/homeassistant/components/airzone_cloud/sensor.py +++ b/homeassistant/components/airzone_cloud/sensor.py @@ -141,6 +141,8 @@ class AirzoneSensor(AirzoneEntity, SensorEntity): class AirzoneAidooSensor(AirzoneAidooEntity, AirzoneSensor): """Define an Airzone Cloud Aidoo sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: AirzoneUpdateCoordinator, @@ -151,7 +153,6 @@ class AirzoneAidooSensor(AirzoneAidooEntity, AirzoneSensor): """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 @@ class AirzoneAidooSensor(AirzoneAidooEntity, AirzoneSensor): class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor): """Define an Airzone Cloud WebServer sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: AirzoneUpdateCoordinator, @@ -171,7 +174,6 @@ class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor): """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 @@ class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor): class AirzoneZoneSensor(AirzoneZoneEntity, AirzoneSensor): """Define an Airzone Cloud Zone sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: AirzoneUpdateCoordinator, @@ -191,7 +195,6 @@ class AirzoneZoneSensor(AirzoneZoneEntity, AirzoneSensor): """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 35d4d2eefbf..892bcac5bf9 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 @@ CONFIG_SCHEMA = vol.Schema( 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 00000000000..ebc0a819d1d --- /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 c3e7f13d5e3..f50e108c2aa 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 ceacbf1645a..88f3d7fadef 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 f63c844701f..d9b306da4dd 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 @@ ZODIAC_ICONS = { } -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 cbae6ead433..8cf0e22237e 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 3145c5cdc49..2925ea3425c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -534,6 +534,7 @@ FLOWS = { "zerproc", "zeversolar", "zha", + "zodiac", "zwave_js", "zwave_me", ], diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 26833d62368..98571b6905e 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 00000000000..18a512e0b45 --- /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 dbb1d2739a5..9fa151c87d5 100644 --- a/tests/components/zodiac/test_sensor.py +++ b/tests/components/zodiac/test_sensor.py @@ -24,6 +24,8 @@ from homeassistant.helpers import entity_registry as er 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 = datetime(2020, 4, 21, tzinfo=dt_util.UTC) (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 9afdff43ef0..4ff63a25455 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -163,6 +163,8 @@ async def async_setup_entry( class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): """Representation of a Sensibo device.""" + _attr_name = None + def __init__( self, coordinator: SensiboDataUpdateCoordinator, device_id: str ) -> None: From 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 e6e506273ad..98e2df2d97b 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 e857abd31e5..8256ce2d060 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 f4f2608b597..31e5812dadf 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 c90f4c9031c..f5c4090dc87 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.core import HomeAssistant 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 @@ class PurpleAirEntity(CoordinatorEntity[PurpleAirDataUpdateCoordinator]): 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 604bcb28c0e..c7988c02e6a 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -31,7 +31,7 @@ from homeassistant.helpers.selector import ( 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 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): 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 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): 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 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): """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 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): 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 60f51a9e7dd..e3ea7807a21 100644 --- a/homeassistant/components/purpleair/const.py +++ b/homeassistant/components/purpleair/const.py @@ -7,3 +7,4 @@ LOGGER = logging.getLogger(__package__) 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 3d18fef3906..836496d0ca8 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 ce911183dfd..503ba23e052 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 5ebd7e34409..046ea237560 100644 --- a/homeassistant/components/persistent_notification/services.yaml +++ b/homeassistant/components/persistent_notification/services.yaml @@ -4,14 +4,14 @@ create: fields: message: name: Message - description: Message body of the notification. [Templates accepted] + description: Message body of the notification. required: true example: Please check your configuration.yaml. selector: text: title: name: Title - description: Optional title for your notification. [Templates accepted] + description: Optional title for your notification. example: Test notification selector: text: From 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 d93d7c48823..fdd7537e9e1 100644 --- a/homeassistant/components/radio_browser/__init__.py +++ b/homeassistant/components/radio_browser/__init__.py @@ -1,6 +1,7 @@ """The Radio Browser integration.""" from __future__ import annotations +from aiodns.error import DNSError from radios import RadioBrowser, RadioBrowserError from homeassistant.config_entries import ConfigEntry @@ -23,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await radios.stats() - except RadioBrowserError as err: + except (DNSError, RadioBrowserError) as err: raise ConfigEntryNotReady("Could not connect to Radio Browser API") from err hass.data[DOMAIN] = radios From 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 2546f762912..66a2e3b0db5 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -34,6 +34,7 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity): """An alarm_control_panel implementation for Abode.""" _attr_icon = ICON + _attr_name = None _attr_code_arm_required = False _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 17d7b820d45..afe017bfcc7 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -39,6 +39,7 @@ class AbodeCamera(AbodeDevice, Camera): """Representation of an Abode camera.""" _device: AbodeCam + _attr_name = None def __init__(self, data: AbodeSystem, device: AbodeDev, event: Event) -> None: """Initialize the Abode device.""" diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index 507b1284362..d504040ee90 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -29,6 +29,7 @@ class AbodeCover(AbodeDevice, CoverEntity): """Representation of an Abode cover.""" _device: AbodeCV + _attr_name = None @property def is_closed(self) -> bool: diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index be69897431f..539b89a5546 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -42,6 +42,7 @@ class AbodeLight(AbodeDevice, LightEntity): """Representation of an Abode light.""" _device: AbodeLT + _attr_name = None def turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index 039b2423099..c110b3fd558 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -29,6 +29,7 @@ class AbodeLock(AbodeDevice, LockEntity): """Representation of an Abode lock.""" _device: AbodeLK + _attr_name = None def lock(self, **kwargs: Any) -> None: """Lock the device.""" diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index 11821773938..1e238783221 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -53,7 +53,6 @@ class AbodeSensor(AbodeDevice, SensorEntity): """A sensor implementation for Abode devices.""" _device: AbodeSense - _attr_has_entity_name = True def __init__( self, diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index ab83e3a20c1..14bdf4e0caf 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -44,6 +44,7 @@ class AbodeSwitch(AbodeDevice, SwitchEntity): """Representation of an Abode switch.""" _device: AbodeSW + _attr_name = None def turn_on(self, **kwargs: Any) -> None: """Turn on the device.""" diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index bbc8738c4ae..9e4f92e8c98 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -84,6 +84,8 @@ class AdvantageAirZoneEntity(AdvantageAirAcEntity): class AdvantageAirThingEntity(AdvantageAirEntity): """Parent class for Advantage Air Things Entities.""" + _attr_name = None + def __init__(self, instance: AdvantageAirData, thing: dict[str, Any]) -> None: """Initialize common aspects of an Advantage Air Things entity.""" super().__init__(instance) diff --git a/homeassistant/components/advantage_air/light.py b/homeassistant/components/advantage_air/light.py index 13a77d5cab3..7815354dd92 100644 --- a/homeassistant/components/advantage_air/light.py +++ b/homeassistant/components/advantage_air/light.py @@ -41,6 +41,7 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity): """Representation of Advantage Air Light.""" _attr_supported_color_modes = {ColorMode.ONOFF} + _attr_name = None def __init__(self, instance: AdvantageAirData, light: dict[str, Any]) -> None: """Initialize an Advantage Air Light.""" diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 2ab23ff2d37..038e71750dd 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -80,6 +80,7 @@ class AnthemAVR(MediaPlayerEntity): self._attr_name = f"zone {zone_number}" self._attr_unique_id = f"{mac_address}_{zone_number}" else: + self._attr_name = None self._attr_unique_id = mac_address self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 009536a9adb..b8744865898 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -221,6 +221,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): _attr_assumed_state = False _attr_has_entity_name = True + _attr_name = None def __init__(self, device, *args, **kwargs): """Initialize the switch.""" diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index a70b9c134d0..5512bcd1176 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -83,6 +83,7 @@ class BrottsplatskartanSensor(SensorEntity): _attr_attribution = ATTRIBUTION _attr_has_entity_name = True + _attr_name = None def __init__(self, bpk: BrottsplatsKartan, name: str, entry_id: str) -> None: """Initialize the Brottsplatskartan sensor.""" diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 47afdf1539b..dc403611da2 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -71,6 +71,7 @@ class BSBLANClimate( """Defines a BSBLAN climate device.""" _attr_has_entity_name = True + _attr_name = None # Determine preset modes _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE diff --git a/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py b/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py index eafd1e63b1f..d2608ed43c7 100644 --- a/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py +++ b/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py @@ -8,6 +8,8 @@ from .devolo_device import DevoloDeviceEntity class DevoloMultiLevelSwitchDeviceEntity(DevoloDeviceEntity): """Representation of a multi level switch device within devolo Home Control. Something like a dimmer or a thermostat.""" + _attr_name = None + def __init__( self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str ) -> None: diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py index 24b1d3545de..9b96e58da60 100644 --- a/homeassistant/components/devolo_home_control/switch.py +++ b/homeassistant/components/devolo_home_control/switch.py @@ -41,6 +41,8 @@ async def async_setup_entry( class DevoloSwitch(DevoloDeviceEntity, SwitchEntity): """Representation of a switch.""" + _attr_name = None + def __init__( self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str ) -> None: diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 47da87306a3..f74ec04476f 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -47,6 +47,7 @@ async def async_setup_entry( class ElgatoLight(ElgatoEntity, LightEntity): """Defines an Elgato Light.""" + _attr_name = None _attr_min_mireds = 143 _attr_max_mireds = 344 diff --git a/homeassistant/components/homewizard/switch.py b/homeassistant/components/homewizard/switch.py index bec72b5d32d..cddcabc841e 100644 --- a/homeassistant/components/homewizard/switch.py +++ b/homeassistant/components/homewizard/switch.py @@ -45,6 +45,7 @@ class HomeWizardSwitchEntityDescription( SWITCHES = [ HomeWizardSwitchEntityDescription( key="power_on", + name=None, device_class=SwitchDeviceClass.OUTLET, create_fn=lambda coordinator: coordinator.supports_state(), available_fn=lambda data: data.state is not None and not data.state.switch_lock, diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index dd33da56297..db31baa53a6 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -101,6 +101,7 @@ class HoneywellUSThermostat(ClimateEntity): """Representation of a Honeywell US Thermostat.""" _attr_has_entity_name = True + _attr_name = None def __init__( self, diff --git a/homeassistant/components/jellyfin/media_player.py b/homeassistant/components/jellyfin/media_player.py index 2025e1a2a6c..bcd8e975823 100644 --- a/homeassistant/components/jellyfin/media_player.py +++ b/homeassistant/components/jellyfin/media_player.py @@ -90,6 +90,7 @@ class JellyfinMediaPlayer(JellyfinEntity, MediaPlayerEntity): sw_version=self.app_version, via_device=(DOMAIN, coordinator.server_id), ) + self._attr_name = None else: self._attr_device_info = None self._attr_has_entity_name = False diff --git a/homeassistant/components/jellyfin/sensor.py b/homeassistant/components/jellyfin/sensor.py index 1957adfc6eb..cd0e9ab21a2 100644 --- a/homeassistant/components/jellyfin/sensor.py +++ b/homeassistant/components/jellyfin/sensor.py @@ -42,6 +42,7 @@ def _count_now_playing(data: JellyfinDataT) -> int: SENSOR_TYPES: dict[str, JellyfinSensorEntityDescription] = { "sessions": JellyfinSensorEntityDescription( key="watching", + name=None, icon="mdi:television-play", native_unit_of_measurement="Watching", value_fn=_count_now_playing, diff --git a/homeassistant/components/jvc_projector/remote.py b/homeassistant/components/jvc_projector/remote.py index e33eef74c48..45f797a5aaa 100644 --- a/homeassistant/components/jvc_projector/remote.py +++ b/homeassistant/components/jvc_projector/remote.py @@ -52,6 +52,8 @@ async def async_setup_entry( class JvcProjectorRemote(JvcProjectorEntity, RemoteEntity): """Representation of a JVC Projector device.""" + _attr_name = None + @property def is_on(self) -> bool: """Return True if entity is on.""" From 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 a2854479abd..e5f70bc25a0 100644 --- a/homeassistant/components/melnor/switch.py +++ b/homeassistant/components/melnor/switch.py @@ -45,6 +45,7 @@ ZONE_ENTITY_DESCRIPTIONS = [ device_class=SwitchDeviceClass.SWITCH, icon="mdi:sprinkler", key="manual", + name=None, on_off_fn=lambda valve, bool: valve.set_is_watering(bool), state_fn=lambda valve: valve.is_watering, ), diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index 2d06b20a30a..b23abb54f8b 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -34,6 +34,7 @@ class OpenMeteoWeatherEntity( """Defines an Open-Meteo weather entity.""" _attr_has_entity_name = True + _attr_name = None _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR diff --git a/homeassistant/components/openhome/update.py b/homeassistant/components/openhome/update.py index 22bffad44d8..54c2d16fb2b 100644 --- a/homeassistant/components/openhome/update.py +++ b/homeassistant/components/openhome/update.py @@ -48,6 +48,7 @@ class OpenhomeUpdateEntity(UpdateEntity): _attr_device_class = UpdateDeviceClass.FIRMWARE _attr_supported_features = UpdateEntityFeature.INSTALL _attr_has_entity_name = True + _attr_name = None def __init__(self, device): """Initialize a Linn DS update entity.""" diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index f7941d1f02d..36626c2324e 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -42,6 +42,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): """Representation of an Plugwise thermostat.""" _attr_has_entity_name = True + _attr_name = None _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN diff --git a/homeassistant/components/prusalink/sensor.py b/homeassistant/components/prusalink/sensor.py index 4f93fd3407e..1ee4274e5bb 100644 --- a/homeassistant/components/prusalink/sensor.py +++ b/homeassistant/components/prusalink/sensor.py @@ -47,6 +47,7 @@ SENSORS: dict[str, tuple[PrusaLinkSensorEntityDescription, ...]] = { "printer": ( PrusaLinkSensorEntityDescription[PrinterInfo]( key="printer.state", + name=None, icon="mdi:printer-3d", value_fn=lambda data: ( "pausing" diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 38f3c03fb03..e915c52c9dc 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -67,6 +67,7 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity) self._attr_name = imported_name self._attr_has_entity_name = False else: + self._attr_name = None self._attr_has_entity_name = True self._state = None self._duration_minutes = duration_minutes diff --git a/homeassistant/components/recollect_waste/calendar.py b/homeassistant/components/recollect_waste/calendar.py index 120ab77c3b3..c439f647da5 100644 --- a/homeassistant/components/recollect_waste/calendar.py +++ b/homeassistant/components/recollect_waste/calendar.py @@ -48,6 +48,7 @@ class ReCollectWasteCalendar(ReCollectWasteEntity, CalendarEntity): """Define a ReCollect Waste calendar.""" _attr_icon = "mdi:delete-empty" + _attr_name = None def __init__( self, diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 0edd6f82195..3544abcfdd1 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -548,6 +548,8 @@ class RfxtrxCommandEntity(RfxtrxEntity): Contains the common logic for Rfxtrx lights and switches. """ + _attr_name = None + def __init__( self, device: rfxtrxmod.RFXtrxDevice, diff --git a/homeassistant/components/ridwell/calendar.py b/homeassistant/components/ridwell/calendar.py index 57919ed1feb..3ef3bbdc5ae 100644 --- a/homeassistant/components/ridwell/calendar.py +++ b/homeassistant/components/ridwell/calendar.py @@ -50,6 +50,7 @@ class RidwellCalendar(RidwellEntity, CalendarEntity): """Define a Ridwell calendar.""" _attr_icon = "mdi:delete-empty" + _attr_name = None def __init__( self, coordinator: RidwellDataUpdateCoordinator, account: RidwellAccount diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index a6083e51430..77776704a60 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -36,6 +36,7 @@ class RitualsSwitchEntityDescription( ENTITY_DESCRIPTIONS = ( RitualsSwitchEntityDescription( key="is_on", + name=None, icon="mdi:fan", is_on_fn=lambda diffuser: diffuser.is_on, turn_on_fn=lambda diffuser: diffuser.turn_on(), From 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 404339d4f30..085437fb02e 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ - "aioesphomeapi==15.0.0", + "aioesphomeapi==15.0.1", "bluetooth-data-tools==1.3.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index bc0d3208b35..a58b9f7e0d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -234,7 +234,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.0.0 +aioesphomeapi==15.0.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57e7adf7964..b2989389fc2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.0.0 +aioesphomeapi==15.0.1 # homeassistant.components.flo aioflo==2021.11.0 From 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 27eaef09b43..4c440610838 100644 --- a/homeassistant/components/advantage_air/diagnostics.py +++ b/homeassistant/components/advantage_air/diagnostics.py @@ -9,17 +9,30 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN -TO_REDACT = ["dealerPhoneNumber", "latitude", "logoPIN", "longitude", "postCode"] +TO_REDACT = [ + "dealerPhoneNumber", + "latitude", + "logoPIN", + "longitude", + "postCode", + "rid", + "deviceNames", + "deviceIds", + "deviceIdsV2", + "backupId", +] async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]["coordinator"].data + data = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id].coordinator.data # Return only the relevant children return { - "aircons": data["aircons"], + "aircons": data.get("aircons"), + "myLights": data.get("myLights"), + "myThings": data.get("myThings"), "system": async_redact_data(data["system"], TO_REDACT), } diff --git a/tests/components/advantage_air/snapshots/test_diagnostics.ambr b/tests/components/advantage_air/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..a472d4fa1fc --- /dev/null +++ b/tests/components/advantage_air/snapshots/test_diagnostics.ambr @@ -0,0 +1,292 @@ +# serializer version: 1 +# name: test_select_async_setup_entry + dict({ + 'aircons': dict({ + 'ac1': dict({ + 'info': dict({ + 'aaAutoFanModeEnabled': False, + 'climateControlModeEnabled': False, + 'climateControlModeIsRunning': False, + 'countDownToOff': 10, + 'countDownToOn': 0, + 'fan': 'high', + 'filterCleanStatus': 0, + 'freshAirStatus': 'off', + 'mode': 'vent', + 'myAutoModeEnabled': False, + 'myAutoModeIsRunning': False, + 'myZone': 1, + 'name': 'myzone', + 'setTemp': 24, + 'state': 'on', + }), + 'zones': dict({ + 'z01': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 25, + 'minDamper': 0, + 'motion': 20, + 'motionConfig': 2, + 'name': 'Zone open with Sensor', + 'number': 1, + 'rssi': 40, + 'setTemp': 24, + 'state': 'open', + 'type': 1, + 'value': 100, + }), + 'z02': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 25, + 'minDamper': 0, + 'motion': 21, + 'motionConfig': 2, + 'name': 'Zone closed with Sensor', + 'number': 2, + 'rssi': 10, + 'setTemp': 24, + 'state': 'close', + 'type': 1, + 'value': 0, + }), + 'z03': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 25, + 'minDamper': 0, + 'motion': 22, + 'motionConfig': 2, + 'name': 'Zone 3', + 'number': 3, + 'rssi': 25, + 'setTemp': 24, + 'state': 'close', + 'type': 1, + 'value': 0, + }), + 'z04': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 25, + 'minDamper': 0, + 'motion': 1, + 'motionConfig': 1, + 'name': 'Zone 4', + 'number': 4, + 'rssi': 75, + 'setTemp': 24, + 'state': 'close', + 'type': 1, + 'value': 0, + }), + 'z05': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 25, + 'minDamper': 0, + 'motion': 5, + 'motionConfig': 1, + 'name': 'Zone 5', + 'number': 5, + 'rssi': 100, + 'setTemp': 24, + 'state': 'close', + 'type': 1, + 'value': 0, + }), + }), + }), + 'ac2': dict({ + 'info': dict({ + 'aaAutoFanModeEnabled': True, + 'climateControlModeEnabled': True, + 'climateControlModeIsRunning': False, + 'countDownToOff': 0, + 'countDownToOn': 20, + 'fan': 'autoAA', + 'filterCleanStatus': 1, + 'freshAirStatus': 'none', + 'mode': 'cool', + 'myAutoModeCurrentSetMode': 'cool', + 'myAutoModeEnabled': False, + 'myAutoModeIsRunning': False, + 'myZone': 1, + 'name': 'mytemp', + 'setTemp': 24, + 'state': 'off', + }), + 'zones': dict({ + 'z01': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 25, + 'minDamper': 0, + 'motion': 20, + 'motionConfig': 2, + 'name': 'Zone A', + 'number': 1, + 'rssi': 40, + 'setTemp': 24, + 'state': 'open', + 'type': 1, + 'value': 100, + }), + 'z02': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 26, + 'minDamper': 0, + 'motion': 21, + 'motionConfig': 2, + 'name': 'Zone B', + 'number': 2, + 'rssi': 10, + 'setTemp': 23, + 'state': 'open', + 'type': 1, + 'value': 50, + }), + }), + }), + 'ac3': dict({ + 'info': dict({ + 'aaAutoFanModeEnabled': True, + 'climateControlModeEnabled': False, + 'climateControlModeIsRunning': False, + 'countDownToOff': 0, + 'countDownToOn': 0, + 'fan': 'autoAA', + 'filterCleanStatus': 1, + 'freshAirStatus': 'none', + 'mode': 'myauto', + 'myAutoCoolTargetTemp': 24, + 'myAutoHeatTargetTemp': 20, + 'myAutoModeCurrentSetMode': 'cool', + 'myAutoModeEnabled': True, + 'myAutoModeIsRunning': True, + 'myZone': 0, + 'name': 'myauto', + 'setTemp': 24, + 'state': 'on', + }), + 'zones': dict({ + 'z01': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 0, + 'minDamper': 0, + 'motion': 0, + 'motionConfig': 0, + 'name': 'Zone Y', + 'number': 1, + 'rssi': 0, + 'setTemp': 24, + 'state': 'open', + 'type': 0, + 'value': 100, + }), + 'z02': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 0, + 'minDamper': 0, + 'motion': 0, + 'motionConfig': 0, + 'name': 'Zone Z', + 'number': 2, + 'rssi': 0, + 'setTemp': 24, + 'state': 'close', + 'type': 0, + 'value': 0, + }), + }), + }), + }), + 'myLights': dict({ + 'lights': dict({ + '100': dict({ + 'id': '100', + 'moduleType': 'RM2', + 'name': 'Light A', + 'relay': True, + 'state': 'off', + }), + '101': dict({ + 'id': '101', + 'name': 'Light B', + 'state': 'on', + 'value': 50, + }), + }), + }), + 'myThings': dict({ + 'things': dict({ + '200': dict({ + 'buttonType': 'upDown', + 'channelDipState': 1, + 'id': '200', + 'name': 'Blind 1', + 'value': 100, + }), + '201': dict({ + 'buttonType': 'upDown', + 'channelDipState': 2, + 'id': '201', + 'name': 'Blind 2', + 'value': 0, + }), + '202': dict({ + 'buttonType': 'openClose', + 'channelDipState': 3, + 'id': '202', + 'name': 'Garage', + 'value': 100, + }), + '203': dict({ + 'buttonType': 'onOff', + 'channelDipState': 4, + 'id': '203', + 'name': 'Thing Light', + 'value': 100, + }), + '204': dict({ + 'buttonType': 'upDown', + 'channelDipState': 5, + 'id': '204', + 'name': 'Thing Light Dimmable', + 'value': 100, + }), + '205': dict({ + 'buttonType': 'onOff', + 'channelDipState': 8, + 'id': '205', + 'name': 'Relay', + 'value': 100, + }), + '206': dict({ + 'buttonType': 'onOff', + 'channelDipState': 9, + 'id': '206', + 'name': 'Fan', + 'value': 100, + }), + }), + }), + 'system': dict({ + 'hasAircons': True, + 'hasLights': True, + 'hasSensors': False, + 'hasThings': True, + 'hasThingsBOG': False, + 'hasThingsLight': False, + 'myAppRev': '1.234', + 'name': 'testname', + 'needsUpdate': False, + 'rid': '**REDACTED**', + 'sysType': 'e-zone', + }), + }) +# --- diff --git a/tests/components/advantage_air/test_diagnostics.py b/tests/components/advantage_air/test_diagnostics.py new file mode 100644 index 00000000000..ebd026c6cc7 --- /dev/null +++ b/tests/components/advantage_air/test_diagnostics.py @@ -0,0 +1,32 @@ +"""Test the Advantage Air Diagnostics.""" +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from . import ( + TEST_SYSTEM_DATA, + TEST_SYSTEM_URL, + add_mock_config, +) + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.test_util.aiohttp import AiohttpClientMocker +from tests.typing import ClientSessionGenerator + + +async def test_select_async_setup_entry( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + snapshot: SnapshotAssertion, +) -> None: + """Test select platform.""" + + aioclient_mock.get( + TEST_SYSTEM_URL, + text=TEST_SYSTEM_DATA, + ) + + entry = await add_mock_config(hass) + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + assert diag == snapshot From 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 675d368873a..66a74edf8f9 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -608,19 +608,12 @@ class EntityPlatform: 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 @@ class EntityPlatform: 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 @@ class EntityPlatform: 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 46806510f40..df4f4d1c643 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1827,3 +1827,53 @@ async def test_translated_device_class_name_influences_entity_id( 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 891a97d4d02..4a1edd4096e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230629.0"] + "requirements": ["home-assistant-frontend==20230630.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 98e2df2d97b..84b67ded6ae 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.0 hass-nabucasa==0.69.0 hassil==1.0.6 home-assistant-bluetooth==1.10.0 -home-assistant-frontend==20230629.0 +home-assistant-frontend==20230630.0 home-assistant-intents==2023.6.28 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index a58b9f7e0d1..ef07e91eb7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -980,7 +980,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230629.0 +home-assistant-frontend==20230630.0 # homeassistant.components.conversation home-assistant-intents==2023.6.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2989389fc2..9b22fd45f88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -763,7 +763,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230629.0 +home-assistant-frontend==20230630.0 # homeassistant.components.conversation home-assistant-intents==2023.6.28 From 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 5bd270ccfd5..b64b74c5fa6 100644 --- a/homeassistant/components/conversation/trigger.py +++ b/homeassistant/components/conversation/trigger.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from hassil.recognize import PUNCTUATION import voluptuous as vol from homeassistant.const import CONF_COMMAND, CONF_PLATFORM @@ -15,10 +16,22 @@ from . import HOME_ASSISTANT_AGENT, _get_agent_manager from .const import DOMAIN from .default_agent import DefaultAgent + +def has_no_punctuation(value: list[str]) -> list[str]: + """Validate result does not contain punctuation.""" + for sentence in value: + if PUNCTUATION.search(sentence): + raise vol.Invalid("sentence should not contain punctuation") + + return value + + TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): DOMAIN, - vol.Required(CONF_COMMAND): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_COMMAND): vol.All( + cv.ensure_list, [cv.string], has_no_punctuation + ), } ) diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index 74a5e4df8e2..522162fa457 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -1,7 +1,9 @@ """Test conversation triggers.""" import pytest +import voluptuous as vol from homeassistant.core import HomeAssistant +from homeassistant.helpers import trigger from homeassistant.setup import async_setup_component from tests.common import async_mock_service @@ -165,3 +167,24 @@ async def test_same_sentence_multiple_triggers( ("trigger1", "conversation", "hello"), ("trigger2", "conversation", "hello"), } + + +@pytest.mark.parametrize( + "command", + ["hello?", "hello!", "4 a.m."], +) +async def test_fails_on_punctuation(hass: HomeAssistant, command: str) -> None: + """Test that validation fails when sentences contain punctuation.""" + with pytest.raises(vol.Invalid): + await trigger.async_validate_trigger_config( + hass, + [ + { + "id": "trigger1", + "platform": "conversation", + "command": [ + command, + ], + }, + ], + ) From 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 793e8edc769..af04da27406 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -108,6 +108,7 @@ async def async_setup_entry( ): device_info = DeviceInfo( identifiers=device.identifiers, + connections=device.connections, ) else: device_info = None diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 513e9597572..2d1d7a93afc 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -357,6 +357,7 @@ async def test_device_id(hass: HomeAssistant) -> None: source_device_entry = device_registry.async_get_or_create( config_entry_id=source_config_entry.entry_id, identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, ) source_entity = entity_registry.async_get_or_create( "sensor", From 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 cf0e6e91ffb..cf2a6da9e08 100644 --- a/homeassistant/components/utility_meter/select.py +++ b/homeassistant/components/utility_meter/select.py @@ -56,6 +56,7 @@ async def async_setup_entry( ): device_info = DeviceInfo( identifiers=device.identifiers, + connections=device.connections, ) else: device_info = None From 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 f7b8c9c097c..09f928303bf 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -96,6 +96,7 @@ async def async_setup_entry( ): device_info = DeviceInfo( identifiers=device.identifiers, + connections=device.connections, ) else: device_info = None diff --git a/tests/components/threshold/test_binary_sensor.py b/tests/components/threshold/test_binary_sensor.py index 2180d0aed7f..e26781029c5 100644 --- a/tests/components/threshold/test_binary_sensor.py +++ b/tests/components/threshold/test_binary_sensor.py @@ -600,6 +600,7 @@ async def test_device_id(hass: HomeAssistant) -> None: source_device_entry = device_registry.async_get_or_create( config_entry_id=source_config_entry.entry_id, identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, ) source_entity = entity_registry.async_get_or_create( "sensor", From 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 ad0f96dd540..af4248e5e3b 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -162,6 +162,7 @@ async def async_setup_entry( ): device_info = DeviceInfo( identifiers=device.identifiers, + connections=device.connections, ) else: device_info = None diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 5b3734bd1be..a552d401681 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -688,6 +688,7 @@ async def test_device_id(hass: HomeAssistant) -> None: source_device_entry = device_registry.async_get_or_create( config_entry_id=source_config_entry.entry_id, identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, ) source_entity = entity_registry.async_get_or_create( "sensor", From 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 82c3a25590a..711c2987735 100644 --- a/homeassistant/components/wirelesstag/binary_sensor.py +++ b/homeassistant/components/wirelesstag/binary_sensor.py @@ -100,6 +100,7 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorEntity): 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 e4505e59666..fd9a7898f92 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -100,6 +100,7 @@ class WirelessTagSensor(WirelessTagBaseSensor, SensorEntity): 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 26c7d9384a6..df0f72aca18 100644 --- a/homeassistant/components/wirelesstag/switch.py +++ b/homeassistant/components/wirelesstag/switch.py @@ -82,6 +82,7 @@ class WirelessTagSwitch(WirelessTagBaseSensor, SwitchEntity): 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 d7acc9788c4..293822987c3 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -20,7 +20,7 @@ "zigpy_znp" ], "requirements": [ - "bellows==0.35.7", + "bellows==0.35.8", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.101", diff --git a/requirements_all.txt b/requirements_all.txt index ef07e91eb7d..fe24b288f63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -497,7 +497,7 @@ beautifulsoup4==4.11.1 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.35.7 +bellows==0.35.8 # homeassistant.components.bmw_connected_drive bimmer-connected==0.13.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b22fd45f88..306606bc46c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -418,7 +418,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.35.7 +bellows==0.35.8 # homeassistant.components.bmw_connected_drive bimmer-connected==0.13.7 From 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 5b8c4048530..1352d9cb42e 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -518,7 +518,6 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity): 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 f1e3d96a219..16ea12a5d96 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -119,3 +119,5 @@ class OverkizDescriptiveEntity(OverkizEntity): # In case of sub device, use the provided label # and append the name of the type of entity self._attr_name = f"{self.device.label} {description.name}" + elif isinstance(description.name, str): + self._attr_name = description.name From 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 37225fc052f..96ef152307d 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -21,3 +21,9 @@ UNIT_MAP = { "LITERS": UnitOfVolume.LITERS, "GALLONS": UnitOfVolume.GALLONS, } + +SCAN_INTERVALS = { + "china": 300, + "north_america": 600, + "rest_of_world": 300, +} diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py index f6354422312..4a586aab373 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -15,10 +15,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN +from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS -DEFAULT_SCAN_INTERVAL_SECONDS = 300 -SCAN_INTERVAL = timedelta(seconds=DEFAULT_SCAN_INTERVAL_SECONDS) _LOGGER = logging.getLogger(__name__) @@ -50,7 +48,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]): hass, _LOGGER, name=f"{DOMAIN}-{entry.data['username']}", - update_interval=SCAN_INTERVAL, + update_interval=timedelta(seconds=SCAN_INTERVALS[entry.data[CONF_REGION]]), ) async def _async_update_data(self) -> None: From 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 05642c12991..20822dc9973 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -218,7 +218,7 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): 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 14598921a61..b503e72d3a6 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -69,7 +69,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): 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 e915c52c9dc..ceca9c71c36 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -73,7 +73,7 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity) 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 752a65ff051..f0684b6b01d 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -85,7 +85,7 @@ class VeSyncBaseEntity(Entity): 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 179576334a9..dfb439133f6 100644 --- a/homeassistant/components/tesla_wall_connector/__init__.py +++ b/homeassistant/components/tesla_wall_connector/__init__.py @@ -148,10 +148,10 @@ class WallConnectorEntity(CoordinatorEntity): """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 d6cd3f43b10..c662c6754f4 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 fff7f9b9f4f..9545510e635 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -42,8 +42,5 @@ class KnxEntity(Entity): 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 4400c304193..dbfe8e9bd5e 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations 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 @@ class KNXSystemSensor(SensorEntity): 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 9fb21b9f9b4..12ae0ac7d0e 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 00000000000..9e6da0ea8f7 --- /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 6b0056b14fa..56569d4f23b 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -25,13 +25,13 @@ from homeassistant.const import ( 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 @@ from .const import ( PROTOCOL_SSH, PROTOCOL_TELNET, ) -from .router import get_api, get_nvram_info LABEL_MAC = "LABEL_MAC" @@ -143,16 +142,15 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): 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 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): _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 f80643f078d..1733d4c09c3 100644 --- a/homeassistant/components/asuswrt/const.py +++ b/homeassistant/components/asuswrt/const.py @@ -13,6 +13,10 @@ DEFAULT_DNSMASQ = "/var/lib/misc" 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 4291c21d0ed..c782a8f0f3b 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -6,22 +6,12 @@ from datetime import datetime, timedelta 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.event import async_track_time_interval 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 @@ class AsusWrtSensorDataHandler: """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 @@ class AsusWrtSensorDataHandler: 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 @@ class AsusWrtRouter: 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 @@ class AsusWrtRouter: } 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 @@ class AsusWrtRouter: 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 @@ class AsusWrtRouter: 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 @@ class AsusWrtRouter: 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 @@ class AsusWrtRouter: 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 @@ class AsusWrtRouter: @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 @@ class AsusWrtRouter: @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 @@ class AsusWrtRouter: @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 @@ class AsusWrtRouter: 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 95724ec3bb5..accd1eba59b 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -26,13 +26,15 @@ from homeassistant.helpers.update_coordinator import ( 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 aabf5d6d46b..bdee4f82f90 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 553902b66fd..c28d71c1a29 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -32,7 +32,7 @@ from homeassistant.util.dt import utcnow 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 1e7be48979b..9ce59eb3b56 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -49,6 +49,9 @@ async def async_setup_entry( class SimpliSafeLock(SimpliSafeEntity, LockEntity): """Define a SimpliSafe lock.""" + _attr_name = None + _device: Lock + def __init__(self, simplisafe: SimpliSafe, system: SystemV3, lock: Lock) -> None: """Initialize.""" super().__init__( @@ -58,8 +61,6 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): additional_websocket_events=WEBSOCKET_EVENTS_TO_LISTEN_FOR, ) - self._device: Lock - async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" try: From 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 fa0e57d501c..715a960de5d 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -240,7 +240,7 @@ class SelectedEntities: 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 f6299312b53..291a1744d20 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 2c2aafa960e..01f77cb2d38 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["pymazda"], "quality_scale": "platinum", - "requirements": ["pymazda==0.3.8"] + "requirements": ["pymazda==0.3.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index fe24b288f63..619a8721552 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1810,7 +1810,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.8 +pymazda==0.3.9 # homeassistant.components.mediaroom pymediaroom==0.6.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 306606bc46c..b1b5b9f2cbe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1338,7 +1338,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.8 +pymazda==0.3.9 # homeassistant.components.melcloud pymelcloud==2.5.8 From 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 1f684ce137c..ac01033f2c7 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -92,8 +92,8 @@ def async_migrate_unique_ids( device_name = slugify(coordinator.device.name) for key in ("reboot", "self_test", "mute", "unmute"): - if entity_entry.unique_id.startswith(device_name): - old_unique_id = entity_entry.unique_id + old_unique_id = f"{device_name}_{key}" + if entity_entry.unique_id == old_unique_id: new_unique_id = f"{coordinator.mac}_{key}" LOGGER.debug( "Migrating unique_id for %s entity from [%s] to [%s]", From 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 11deb5bb486..53c8577be44 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -40,6 +40,8 @@ ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk" ESPHOME_URL = "https://esphome.io/" _LOGGER = logging.getLogger(__name__) +ZERO_NOISE_PSK = "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=" + class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a esphome config flow.""" @@ -149,11 +151,22 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): async def _async_try_fetch_device_info(self) -> FlowResult: error = await self.fetch_device_info() - if ( - error == ERROR_REQUIRES_ENCRYPTION_KEY - and await self._retrieve_encryption_key_from_dashboard() - ): - error = await self.fetch_device_info() + if error == ERROR_REQUIRES_ENCRYPTION_KEY: + if not self._device_name and not self._noise_psk: + # If device name is not set we can send a zero noise psk + # to get the device name which will allow us to populate + # the device name and hopefully get the encryption key + # from the dashboard. + self._noise_psk = ZERO_NOISE_PSK + error = await self.fetch_device_info() + self._noise_psk = None + + if ( + self._device_name + and await self._retrieve_encryption_key_from_dashboard() + ): + error = await self.fetch_device_info() + # If the fetched key is invalid, unset it again. if error == ERROR_INVALID_ENCRYPTION_KEY: self._noise_psk = None @@ -323,7 +336,10 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._device_info = await cli.device_info() except RequiresEncryptionAPIError: return ERROR_REQUIRES_ENCRYPTION_KEY - except InvalidEncryptionKeyAPIError: + except InvalidEncryptionKeyAPIError as ex: + if ex.received_name: + self._device_name = ex.received_name + self._name = ex.received_name return ERROR_INVALID_ENCRYPTION_KEY except ResolveAPIError: return "resolve_error" @@ -334,9 +350,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._name = self._device_info.friendly_name or self._device_info.name self._device_name = self._device_info.name - await self.async_set_unique_id( - self._device_info.mac_address, raise_on_progress=False - ) + mac_address = format_mac(self._device_info.mac_address) + await self.async_set_unique_id(mac_address, raise_on_progress=False) if not self._reauth_entry: self._abort_if_unique_id_configured( updates={CONF_HOST: self._host, CONF_PORT: self._port} @@ -373,14 +388,13 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): Return boolean if a key was retrieved. """ - if self._device_name is None: - return False - - if (dashboard := async_get_dashboard(self.hass)) is None: + if ( + self._device_name is None + or (dashboard := async_get_dashboard(self.hass)) is None + ): return False await dashboard.async_request_refresh() - if not dashboard.last_update_success: return False diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 085437fb02e..8f5e6b95c39 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ - "aioesphomeapi==15.0.1", + "aioesphomeapi==15.1.1", "bluetooth-data-tools==1.3.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 619a8721552..7241667fb82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -234,7 +234,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.0.1 +aioesphomeapi==15.1.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1b5b9f2cbe..ab6fd014adc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.0.1 +aioesphomeapi==15.1.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index affe65949b2..4a99de77c1a 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1,4 +1,5 @@ """Test config flow.""" +import asyncio from unittest.mock import AsyncMock, MagicMock, patch from aioesphomeapi import ( @@ -10,6 +11,7 @@ from aioesphomeapi import ( RequiresEncryptionAPIError, ResolveAPIError, ) +import aiohttp import pytest from homeassistant import config_entries, data_entry_flow @@ -35,6 +37,7 @@ from . import VALID_NOISE_PSK from tests.common import MockConfigEntry INVALID_NOISE_PSK = "lSYBYEjQI1bVL8s2Vask4YytGMj1f1epNtmoim2yuTM=" +WRONG_NOISE_PSK = "GP+ciK+nVfTQ/gcz6uOdS+oKEdJgesU+jeu8Ssj2how=" @pytest.fixture(autouse=False) @@ -115,6 +118,58 @@ async def test_user_connection_updates_host( assert entry.data[CONF_HOST] == "127.0.0.1" +async def test_user_sets_unique_id( + hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None +) -> None: + """Test that the user flow sets the unique id.""" + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + addresses=["192.168.43.183"], + hostname="test8266.local.", + name="mock_name", + port=6053, + properties={ + "mac": "1122334455aa", + }, + type="mock_type", + ) + discovery_result = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + ) + + assert discovery_result["type"] == FlowResultType.FORM + assert discovery_result["step_id"] == "discovery_confirm" + + discovery_result = await hass.config_entries.flow.async_configure( + discovery_result["flow_id"], + {}, + ) + assert discovery_result["type"] == FlowResultType.CREATE_ENTRY + assert discovery_result["data"] == { + CONF_HOST: "192.168.43.183", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: "", + CONF_DEVICE_NAME: "test", + } + + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data=None, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + async def test_user_resolve_error( hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None ) -> None: @@ -140,6 +195,53 @@ async def test_user_resolve_error( assert len(mock_client.disconnect.mock_calls) == 1 +async def test_user_causes_zeroconf_to_abort( + hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None +) -> None: + """Test that the user flow sets the unique id and aborts the zeroconf flow.""" + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + addresses=["192.168.43.183"], + hostname="test8266.local.", + name="mock_name", + port=6053, + properties={ + "mac": "1122334455aa", + }, + type="mock_type", + ) + discovery_result = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + ) + + assert discovery_result["type"] == FlowResultType.FORM + assert discovery_result["step_id"] == "discovery_confirm" + + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data=None, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: "", + CONF_DEVICE_NAME: "test", + } + + assert not hass.config_entries.flow.async_progress_by_handler(DOMAIN) + + async def test_user_connection_error( hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None ) -> None: @@ -217,6 +319,211 @@ async def test_user_invalid_password( assert result["errors"] == {"base": "invalid_auth"} +async def test_user_dashboard_has_wrong_key( + hass: HomeAssistant, + mock_client, + mock_dashboard, + mock_zeroconf: None, + mock_setup_entry: None, +) -> None: + """Test user step with key from dashboard that is incorrect.""" + mock_client.device_info.side_effect = [ + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError, + DeviceInfo( + uses_password=False, + name="test", + mac_address="11:22:33:44:55:AA", + ), + ] + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=WRONG_NOISE_PSK, + ): + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "encryption_key" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: VALID_NOISE_PSK, + CONF_DEVICE_NAME: "test", + } + assert mock_client.noise_psk == VALID_NOISE_PSK + + +async def test_user_discovers_name_and_gets_key_from_dashboard( + hass: HomeAssistant, + mock_client, + mock_dashboard, + mock_zeroconf: None, + mock_setup_entry: None, +) -> None: + """Test user step can discover the name and get the key from the dashboard.""" + mock_client.device_info.side_effect = [ + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError("Wrong key", "test"), + DeviceInfo( + uses_password=False, + name="test", + mac_address="11:22:33:44:55:AA", + ), + ] + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + await dashboard.async_get_dashboard(hass).async_refresh() + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ): + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: VALID_NOISE_PSK, + CONF_DEVICE_NAME: "test", + } + assert mock_client.noise_psk == VALID_NOISE_PSK + + +async def test_user_discovers_name_and_gets_key_from_dashboard_fails( + hass: HomeAssistant, + mock_client, + mock_dashboard, + mock_zeroconf: None, + mock_setup_entry: None, +) -> None: + """Test user step can discover the name and get the key from the dashboard.""" + mock_client.device_info.side_effect = [ + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError("Wrong key", "test"), + DeviceInfo( + uses_password=False, + name="test", + mac_address="11:22:33:44:55:aa", + ), + ] + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + await dashboard.async_get_dashboard(hass).async_refresh() + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + side_effect=aiohttp.ClientError, + ): + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "encryption_key" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: VALID_NOISE_PSK, + CONF_DEVICE_NAME: "test", + } + assert mock_client.noise_psk == VALID_NOISE_PSK + + +async def test_user_discovers_name_and_dashboard_is_unavailable( + hass: HomeAssistant, + mock_client, + mock_dashboard, + mock_zeroconf: None, + mock_setup_entry: None, +) -> None: + """Test user step can discover the name but the dashboard is unavailable.""" + mock_client.device_info.side_effect = [ + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError("Wrong key", "test"), + DeviceInfo( + uses_password=False, + name="test", + mac_address="11:22:33:44:55:AA", + ), + ] + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + + with patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", + side_effect=asyncio.TimeoutError, + ): + await dashboard.async_get_dashboard(hass).async_refresh() + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "encryption_key" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: VALID_NOISE_PSK, + CONF_DEVICE_NAME: "test", + } + assert mock_client.noise_psk == VALID_NOISE_PSK + + async def test_login_connection_error( hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None ) -> None: @@ -398,9 +705,9 @@ async def test_user_requires_psk( assert result["step_id"] == "encryption_key" assert result["errors"] == {} - assert len(mock_client.connect.mock_calls) == 1 - assert len(mock_client.device_info.mock_calls) == 1 - assert len(mock_client.disconnect.mock_calls) == 1 + assert len(mock_client.connect.mock_calls) == 2 + assert len(mock_client.device_info.mock_calls) == 2 + assert len(mock_client.disconnect.mock_calls) == 2 async def test_encryption_key_valid_psk( @@ -894,7 +1201,7 @@ async def test_zeroconf_encryption_key_via_dashboard( DeviceInfo( uses_password=False, name="test8266", - mac_address="11:22:33:44:55:aa", + mac_address="11:22:33:44:55:AA", ), ] From 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 cce35731c72..bf0d7214c6e 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -159,7 +159,7 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): 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 d5e89e887d1..534e2e6e9e6 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 01945f9e242..959b0a8e8df 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -9,6 +9,7 @@ from homeassistant.components.humidifier import ( MODE_AWAY, MODE_NORMAL, PLATFORM_SCHEMA, + HumidifierAction, HumidifierDeviceClass, HumidifierEntity, HumidifierEntityFeature, @@ -158,6 +159,7 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): 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 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): """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 341571fe9ad..dcb1608b710 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 6683e7e4583..eaa1acc11bf 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -137,5 +137,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa==0.5.1"] + "requirements": ["python-kasa[speedups]==0.5.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7241667fb82..c14527f7348 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 ab6fd014adc..7076fab3e95 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 9134a92c799..1c5d25dfb3d 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.69.0"] + "requirements": ["zeroconf==0.70.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 84b67ded6ae..47bd964c002 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 c14527f7348..87f74d66523 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 7076fab3e95..86e22c3644c 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 0c26d2c7dd5..6cddf81b2bf 100644 --- a/homeassistant/components/lametric/sensor.py +++ b/homeassistant/components/lametric/sensor.py @@ -39,7 +39,6 @@ SENSORS = [ 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 e001450fab0..833c47dffb0 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -23,6 +23,7 @@ from homeassistant.const import ( 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 @@ class LaCrosseViewSensor( 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 619fc913e09..c733a96ca9d 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -3,12 +3,13 @@ from __future__ import annotations 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 319188dae21..a554001970b 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -33,6 +33,20 @@ BinaryHandler = Callable[[HomeAssistant, "ActiveConnection", bytes], None] 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 6ac0e10a76c..728405b5d96 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -63,6 +63,21 @@ class WebSocketAdapter(logging.LoggerAdapter): 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 @@ class WebSocketHandler: 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 cab55c20c02..87a9fa4da0e 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 @@ _LOGGER = logging.getLogger(__name__) 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 cbae7f0df76..7751f6b6a29 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 61080052ee5..2d35ad2787f 100644 --- a/homeassistant/components/kaleidescape/remote.py +++ b/homeassistant/components/kaleidescape/remote.py @@ -47,6 +47,8 @@ VALID_COMMANDS = { 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 23d40684c13..183036f3973 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 @@ SENSOR_TYPES: tuple[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 @@ SENSOR_TYPES: tuple[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 @@ SENSOR_TYPES: tuple[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 @@ SENSOR_TYPES: tuple[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 @@ class KaleidescapeSensor(KaleidescapeEntity, SensorEntity): 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 92b9c931acd..30c22a8ca0e 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 0ae2dc15619..3fbff29e3e9 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 5e89767d0e0..0961b7c10c5 100644 --- a/homeassistant/components/notion/const.py +++ b/homeassistant/components/notion/const.py @@ -9,6 +9,7 @@ SENSOR_DOOR = "door" 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 e6ff3eaab69..6f011523a2a 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant 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 @@ class NotionSensor(NotionEntity, SensorEntity): @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 47b52d9ff76..a30aed22322 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -31,7 +31,6 @@ from typing import ( TYPE_CHECKING, Any, Generic, - NamedTuple, ParamSpec, TypeVar, cast, @@ -964,12 +963,11 @@ class Event: 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 @@ class EventBus: 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 @@ class EventBus: 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 @@ class EventBus: _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 @@ class EventBus: @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 8eb607b2056..a74d0f3a54b 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 2578437acf4..a452d015a2b 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 6a2a0db3ea4..75402c71325 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 3f8f27187f3..7e09c3c8147 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 00000000000..f4fca22c9b4 --- /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 00000000000..fdf007c3b68 --- /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 00000000000..b996a214a05 --- /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 00000000000..4d40bb3356b --- /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 00000000000..969583f050a --- /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 00000000000..e28dcbd0661 --- /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 00000000000..79d8bf80fee --- /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 2925ea3425c..d8ffa25b765 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -325,6 +325,7 @@ FLOWS = { "opentherm_gw", "openuv", "openweathermap", + "opower", "oralb", "otbr", "overkiz", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 98571b6905e..314e8ffa092 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 87f74d66523..74c0b80ad57 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 86e22c3644c..09706afca23 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 00000000000..71aea27a698 --- /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 00000000000..17c6896593b --- /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 00000000000..7f6a847f52e --- /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 a5521596208..a280369e7c8 100644 --- a/homeassistant/components/nam/button.py +++ b/homeassistant/components/nam/button.py @@ -23,7 +23,6 @@ _LOGGER = logging.getLogger(__name__) 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 3f9821a1e34..3c0b8bc9ba4 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -338,7 +338,6 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = ( ), 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 e60855b882c..e443a398984 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 dad8ebe7f11..801b27ee971 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -237,7 +237,6 @@ class MinecraftServerEntity(Entity): ) -> 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 0bf4cdab859..ecf7d747770 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 2499dd8b75b..5d056d98dd1 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -74,6 +74,8 @@ class MinecraftServerSensorEntity(MinecraftServerEntity, SensorEntity): 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 @@ class MinecraftServerVersionSensor(MinecraftServerSensorEntity): 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 @@ class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): 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 @@ class MinecraftServerLatencyTimeSensor(MinecraftServerSensorEntity): 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 @@ class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): 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 @@ class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): 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 9e546a3cdfa..b4d68bc6117 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 79b80a6d7b5..e0f9c7d3bf6 100644 --- a/homeassistant/components/melnor/number.py +++ b/homeassistant/components/melnor/number.py @@ -48,7 +48,7 @@ ZONE_ENTITY_DESCRIPTIONS: list[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 @@ ZONE_ENTITY_DESCRIPTIONS: list[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 @@ ZONE_ENTITY_DESCRIPTIONS: list[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 b4a1d44a291..edb906cc80f 100644 --- a/homeassistant/components/melnor/sensor.py +++ b/homeassistant/components/melnor/sensor.py @@ -87,7 +87,6 @@ DEVICE_ENTITY_DESCRIPTIONS: list[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 @@ DEVICE_ENTITY_DESCRIPTIONS: list[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 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneSensorEntityDescription] = [ 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 2fefa32b6bc..51ca18b0b3d 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 e5f70bc25a0..03bd28faa9d 100644 --- a/homeassistant/components/melnor/switch.py +++ b/homeassistant/components/melnor/switch.py @@ -53,7 +53,7 @@ ZONE_ENTITY_DESCRIPTIONS = [ 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 7abdf62e20c..943a7996aeb 100644 --- a/homeassistant/components/melnor/time.py +++ b/homeassistant/components/melnor/time.py @@ -42,7 +42,7 @@ ZONE_ENTITY_DESCRIPTIONS: list[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 0a1240c7471..88df3b3b615 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -16,6 +16,7 @@ from homeassistant.components.sensor import ( 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 @@ class MeaterProbeTemperature( """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 110661b1c5c..5719c881d1f 100644 --- a/homeassistant/components/lifx/binary_sensor.py +++ b/homeassistant/components/lifx/binary_sensor.py @@ -18,7 +18,7 @@ from .util import lifx_features 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 00d216351a0..86e3bc569b1 100644 --- a/homeassistant/components/lifx/button.py +++ b/homeassistant/components/lifx/button.py @@ -17,7 +17,6 @@ from .entity import LIFXEntity 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 a86bda53cfd..5f08b6e7884 100644 --- a/homeassistant/components/lifx/entity.py +++ b/homeassistant/components/lifx/entity.py @@ -14,6 +14,8 @@ from .coordinator import LIFXUpdateCoordinator 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 cb901dcbe47..0e56155832f 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 @@ class LIFXLight(LIFXEntity, LightEntity): 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 9ad457e0270..183e31dec1f 100644 --- a/homeassistant/components/lifx/select.py +++ b/homeassistant/components/lifx/select.py @@ -23,14 +23,14 @@ THEME_NAMES = [theme_name.lower() for theme_name in ThemeLibrary().themes] 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 @@ class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity): 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 654b5285756..e10f9579bc3 100644 --- a/homeassistant/components/lifx/sensor.py +++ b/homeassistant/components/lifx/sensor.py @@ -22,7 +22,7 @@ SCAN_INTERVAL = timedelta(seconds=30) 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 93d3bd62abe..69055d6bbc6 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 c757d2f0080..5eecc0b3f59 100644 --- a/homeassistant/components/loqed/config_flow.py +++ b/homeassistant/components/loqed/config_flow.py @@ -44,9 +44,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) cloud_client = cloud_loqed.LoqedCloudAPI(cloud_api_client) lock_data = await cloud_client.async_get_locks() - except aiohttp.ClientError: + except aiohttp.ClientError as err: _LOGGER.error("HTTP Connection error to loqed API") - raise CannotConnect from aiohttp.ClientError + raise CannotConnect from err try: selected_lock = next( @@ -137,7 +137,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" else: await self.async_set_unique_id( - re.sub(r"LOQED-([a-f0-9]+)\.local", r"\1", info["bridge_mdns_hostname"]) + re.sub( + r"LOQED-([a-f0-9]+)\.local", r"\1", info["bridge_mdns_hostname"] + ), + raise_on_progress=False, ) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/loqed/const.py b/homeassistant/components/loqed/const.py index 0374e72d5f0..6b1c0311a2d 100644 --- a/homeassistant/components/loqed/const.py +++ b/homeassistant/components/loqed/const.py @@ -2,5 +2,3 @@ DOMAIN = "loqed" -OAUTH2_AUTHORIZE = "https://app.loqed.com/API/integration_oauth3/login.php" -OAUTH2_TOKEN = "https://app.loqed.com/API/integration_oauth3/token.php" diff --git a/homeassistant/components/loqed/coordinator.py b/homeassistant/components/loqed/coordinator.py index ec7d5467b49..507debc02ab 100644 --- a/homeassistant/components/loqed/coordinator.py +++ b/homeassistant/components/loqed/coordinator.py @@ -8,8 +8,8 @@ from loqedAPI import loqed from homeassistant.components import webhook from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_WEBHOOK_ID -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import CONF_NAME, CONF_WEBHOOK_ID +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN @@ -79,17 +79,16 @@ class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]): ) -> None: """Initialize the Loqed Data Update coordinator.""" super().__init__(hass, _LOGGER, name="Loqed sensors") - self._hass = hass self._api = api self._entry = entry self.lock = lock + self.device_name = self._entry.data[CONF_NAME] async def _async_update_data(self) -> StatusMessage: """Fetch data from API endpoint.""" async with async_timeout.timeout(10): return await self._api.async_get_lock_details() - @callback async def _handle_webhook( self, hass: HomeAssistant, webhook_id: str, request: Request ) -> None: @@ -116,7 +115,7 @@ class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]): self.hass, DOMAIN, "Loqed", webhook_id, self._handle_webhook ) webhook_url = webhook.async_generate_url(self.hass, webhook_id) - _LOGGER.info("Webhook URL: %s", webhook_url) + _LOGGER.debug("Webhook URL: %s", webhook_url) webhooks = await self.lock.getWebhooks() diff --git a/homeassistant/components/loqed/entity.py b/homeassistant/components/loqed/entity.py index 1b1731815b4..978fe844d61 100644 --- a/homeassistant/components/loqed/entity.py +++ b/homeassistant/components/loqed/entity.py @@ -23,7 +23,7 @@ class LoqedEntity(CoordinatorEntity[LoqedDataCoordinator]): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, lock_id)}, manufacturer="LOQED", - name="LOQED Lock", + name=coordinator.device_name, model="Touch Smart Lock", connections={(CONNECTION_NETWORK_MAC, lock_id)}, ) diff --git a/homeassistant/components/loqed/lock.py b/homeassistant/components/loqed/lock.py index 5a7540ba89e..d34df19e2d1 100644 --- a/homeassistant/components/loqed/lock.py +++ b/homeassistant/components/loqed/lock.py @@ -24,7 +24,7 @@ async def async_setup_entry( """Set up the Loqed lock platform.""" coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([LoqedLock(coordinator, entry.data["name"])]) + async_add_entities([LoqedLock(coordinator)]) class LoqedLock(LoqedEntity, LockEntity): @@ -32,17 +32,17 @@ class LoqedLock(LoqedEntity, LockEntity): _attr_supported_features = LockEntityFeature.OPEN - def __init__(self, coordinator: LoqedDataCoordinator, name: str) -> None: + def __init__(self, coordinator: LoqedDataCoordinator) -> None: """Initialize the lock.""" super().__init__(coordinator) self._lock = coordinator.lock self._attr_unique_id = self._lock.id - self._attr_name = name + self._attr_name = None @property def changed_by(self) -> str: """Return internal ID of last used key.""" - return "KeyID " + str(self._lock.last_key_id) + return f"KeyID {self._lock.last_key_id}" @property def is_locking(self) -> bool | None: diff --git a/homeassistant/components/loqed/strings.json b/homeassistant/components/loqed/strings.json index 5448f01b7f9..6f3316b283f 100644 --- a/homeassistant/components/loqed/strings.json +++ b/homeassistant/components/loqed/strings.json @@ -12,8 +12,7 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/homeassistant/components/loqed/translations/en.json b/homeassistant/components/loqed/translations/en.json deleted file mode 100644 index a961f10cb1b..00000000000 --- a/homeassistant/components/loqed/translations/en.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Device is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "flow_title": "LOQED Touch Smartlock setup", - "step": { - "user": { - "data": { - "api_key": "API Key", - "name": "Name of your lock in the LOQED app." - }, - "description": "Login at {config_url} and: \n* Create an API-key by clicking 'Create' \n* Copy the created access token." - } - } - } -} \ No newline at end of file diff --git a/tests/components/loqed/test_init.py b/tests/components/loqed/test_init.py index 89b67ee3258..960ad9def6b 100644 --- a/tests/components/loqed/test_init.py +++ b/tests/components/loqed/test_init.py @@ -15,31 +15,6 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture -async def test_webhook_rejects_invalid_message( - hass: HomeAssistant, - hass_client_no_auth, - integration: MockConfigEntry, - lock: loqed.Lock, -): - """Test webhook called with invalid message.""" - await async_setup_component(hass, "http", {"http": {}}) - client = await hass_client_no_auth() - - coordinator = hass.data[DOMAIN][integration.entry_id] - lock.receiveWebhook = AsyncMock(return_value={"error": "invalid hash"}) - - with patch.object(coordinator, "async_set_updated_data") as mock: - message = load_fixture("loqed/battery_update.json") - timestamp = 1653304609 - await client.post( - f"/api/webhook/{integration.data[CONF_WEBHOOK_ID]}", - data=message, - headers={"timestamp": str(timestamp), "hash": "incorrect hash"}, - ) - - mock.assert_not_called() - - async def test_webhook_accepts_valid_message( hass: HomeAssistant, hass_client_no_auth, @@ -49,20 +24,17 @@ async def test_webhook_accepts_valid_message( """Test webhook called with valid message.""" await async_setup_component(hass, "http", {"http": {}}) client = await hass_client_no_auth() - processed_message = json.loads(load_fixture("loqed/battery_update.json")) - coordinator = hass.data[DOMAIN][integration.entry_id] + processed_message = json.loads(load_fixture("loqed/lock_going_to_nightlock.json")) lock.receiveWebhook = AsyncMock(return_value=processed_message) - with patch.object(coordinator, "async_update_listeners") as mock: - message = load_fixture("loqed/battery_update.json") - timestamp = 1653304609 - await client.post( - f"/api/webhook/{integration.data[CONF_WEBHOOK_ID]}", - data=message, - headers={"timestamp": str(timestamp), "hash": "incorrect hash"}, - ) - - mock.assert_called() + message = load_fixture("loqed/battery_update.json") + timestamp = 1653304609 + await client.post( + f"/api/webhook/{integration.data[CONF_WEBHOOK_ID]}", + data=message, + headers={"timestamp": str(timestamp), "hash": "incorrect hash"}, + ) + lock.receiveWebhook.assert_called() async def test_setup_webhook_in_bridge( diff --git a/tests/components/loqed/test_lock.py b/tests/components/loqed/test_lock.py index 422b7ab6830..59e70212f92 100644 --- a/tests/components/loqed/test_lock.py +++ b/tests/components/loqed/test_lock.py @@ -21,7 +21,7 @@ async def test_lock_entity( integration: MockConfigEntry, ) -> None: """Test the lock entity.""" - entity_id = "lock.loqed_lock_home" + entity_id = "lock.home" state = hass.states.get(entity_id) @@ -37,7 +37,7 @@ async def test_lock_responds_to_bolt_state_updates( lock.bolt_state = "night_lock" coordinator.async_update_listeners() - entity_id = "lock.loqed_lock_home" + entity_id = "lock.home" state = hass.states.get(entity_id) @@ -50,7 +50,7 @@ async def test_lock_transition_to_unlocked( ) -> None: """Tests the lock transitions to unlocked state.""" - entity_id = "lock.loqed_lock_home" + entity_id = "lock.home" await hass.services.async_call( "lock", SERVICE_UNLOCK, {ATTR_ENTITY_ID: entity_id}, blocking=True @@ -64,7 +64,7 @@ async def test_lock_transition_to_locked( ) -> None: """Tests the lock transitions to locked state.""" - entity_id = "lock.loqed_lock_home" + entity_id = "lock.home" await hass.services.async_call( "lock", SERVICE_LOCK, {ATTR_ENTITY_ID: entity_id}, blocking=True @@ -78,7 +78,7 @@ async def test_lock_transition_to_open( ) -> None: """Tests the lock transitions to open state.""" - entity_id = "lock.loqed_lock_home" + entity_id = "lock.home" await hass.services.async_call( "lock", SERVICE_OPEN, {ATTR_ENTITY_ID: entity_id}, blocking=True From 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 ad09a1b5fdb..2477e849666 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -10,6 +10,7 @@ from decimal import Decimal, InvalidOperation as DecimalInvalidOperation 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 @@ ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" 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 @@ class SensorEntity(Entity): ) 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 d1da0a8166f..b5d425029d0 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -17,6 +17,7 @@ from homeassistant.components.sensor import ( 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 test_name(hass: HomeAssistant) -> None: 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 73854147372..cd0b5a2ab88 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 833c47dffb0..547772cad09 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -67,7 +67,6 @@ SENSOR_DESCRIPTIONS = { "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 @@ SENSOR_DESCRIPTIONS = { "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 @@ SENSOR_DESCRIPTIONS = { ), "Rain": LaCrosseSensorEntityDescription( key="Rain", - name="Rain", state_class=SensorStateClass.MEASUREMENT, value_fn=get_value, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, @@ -106,23 +102,23 @@ SENSOR_DESCRIPTIONS = { ), "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 @@ SENSOR_DESCRIPTIONS = { ), "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 @@ SENSOR_DESCRIPTIONS = { ), "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 160517793d8..8dc27ba259e 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 88df3b3b615..cf71455a81b 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -64,8 +64,8 @@ SENSOR_TYPES = ( # 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 @@ SENSOR_TYPES = ( # 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 @@ SENSOR_TYPES = ( # 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 @@ SENSOR_TYPES = ( # 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 @@ SENSOR_TYPES = ( # 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 @@ SENSOR_TYPES = ( # 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 @@ SENSOR_TYPES = ( # 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 @@ class MeaterProbeTemperature( ) -> 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 7f4a97a5b19..279841bb147 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 33ad67cc81a..7c1d2f09b04 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 00000000000..e5ca209e8a6 --- /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 059dab3da78..d8ba49adbec 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 4180d48cdef..906d9aee629 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 11e58fca775..ffe6d7f5433 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -10,7 +10,11 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, CONF_UNIQUE_ID, Platform from homeassistant.core import HomeAssistant, split_entity_id -from homeassistant.helpers import discovery, entity_registry as er +from homeassistant.helpers import ( + device_registry as dr, + discovery, + entity_registry as er, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -182,7 +186,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Utility Meter from a config entry.""" entity_registry = er.async_get(hass) - hass.data[DATA_UTILITY][entry.entry_id] = {} + hass.data[DATA_UTILITY][entry.entry_id] = { + "source": entry.options[CONF_SOURCE_SENSOR], + } hass.data[DATA_UTILITY][entry.entry_id][DATA_TARIFF_SENSORS] = [] try: @@ -218,8 +224,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update listener, called when the config entry options are changed.""" + old_source = hass.data[DATA_UTILITY][entry.entry_id]["source"] await hass.config_entries.async_reload(entry.entry_id) + if old_source == entry.options[CONF_SOURCE_SENSOR]: + return + + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + old_source_entity = entity_registry.async_get(old_source) + if not old_source_entity or not old_source_entity.device_id: + return + + device_registry.async_update_device( + old_source_entity.device_id, remove_config_entry_id=entry.entry_id + ) + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" diff --git a/tests/components/utility_meter/test_config_flow.py b/tests/components/utility_meter/test_config_flow.py index 302d3879a04..88a77407c07 100644 --- a/tests/components/utility_meter/test_config_flow.py +++ b/tests/components/utility_meter/test_config_flow.py @@ -7,6 +7,7 @@ from homeassistant import config_entries from homeassistant.components.utility_meter.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -266,3 +267,173 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() state = hass.states.get("sensor.electricity_meter") assert state.attributes["source"] == input_sensor2_entity_id + + +async def test_change_device_source(hass: HomeAssistant) -> None: + """Test remove the device registry configuration entry when the source entity changes.""" + + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + # Configure source entity 1 (with a linked device) + source_config_entry_1 = MockConfigEntry() + source_device_entry_1 = device_registry.async_get_or_create( + config_entry_id=source_config_entry_1.entry_id, + identifiers={("sensor", "identifier_test1")}, + connections={("mac", "20:31:32:33:34:35")}, + ) + source_entity_1 = entity_registry.async_get_or_create( + "sensor", + "test", + "source1", + config_entry=source_config_entry_1, + device_id=source_device_entry_1.id, + ) + + # Configure source entity 2 (with a linked device) + source_config_entry_2 = MockConfigEntry() + source_device_entry_2 = device_registry.async_get_or_create( + config_entry_id=source_config_entry_2.entry_id, + identifiers={("sensor", "identifier_test2")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + source_entity_2 = entity_registry.async_get_or_create( + "sensor", + "test", + "source2", + config_entry=source_config_entry_2, + device_id=source_device_entry_2.id, + ) + + # Configure source entity 3 (without a device) + source_config_entry_3 = MockConfigEntry() + source_entity_3 = entity_registry.async_get_or_create( + "sensor", + "test", + "source3", + config_entry=source_config_entry_3, + ) + + await hass.async_block_till_done() + + input_sensor_entity_id_1 = "sensor.test_source1" + input_sensor_entity_id_2 = "sensor.test_source2" + input_sensor_entity_id_3 = "sensor.test_source3" + + # Test the existence of configured source entities + assert entity_registry.async_get(input_sensor_entity_id_1) is not None + assert entity_registry.async_get(input_sensor_entity_id_2) is not None + assert entity_registry.async_get(input_sensor_entity_id_3) is not None + + # Setup the config entry with source entity 1 (with a linked device) + current_entity_source = source_entity_1 + utility_meter_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "cycle": "monthly", + "delta_values": False, + "name": "Energy", + "net_consumption": False, + "offset": 0, + "periodically_resetting": True, + "source": current_entity_source.entity_id, + "tariffs": [], + }, + title="Energy", + ) + utility_meter_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(utility_meter_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm that the configuration entry has been added to the source entity 1 (current) device registry + current_device = device_registry.async_get( + device_id=current_entity_source.device_id + ) + assert utility_meter_config_entry.entry_id in current_device.config_entries + + # Change configuration options to use source entity 2 (with a linked device) and reload the integration + previous_entity_source = source_entity_1 + current_entity_source = source_entity_2 + + result = await hass.config_entries.options.async_init( + utility_meter_config_entry.entry_id + ) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "periodically_resetting": True, + "source": current_entity_source.entity_id, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() + + # Confirm that the configuration entry has been removed from the source entity 1 (previous) device registry + previous_device = device_registry.async_get( + device_id=previous_entity_source.device_id + ) + assert utility_meter_config_entry.entry_id not in previous_device.config_entries + + # Confirm that the configuration entry has been added to the source entity 2 (current) device registry + current_device = device_registry.async_get( + device_id=current_entity_source.device_id + ) + assert utility_meter_config_entry.entry_id in current_device.config_entries + + # Change configuration options to use source entity 3 (without a device) and reload the integration + previous_entity_source = source_entity_2 + current_entity_source = source_entity_3 + + result = await hass.config_entries.options.async_init( + utility_meter_config_entry.entry_id + ) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "periodically_resetting": True, + "source": current_entity_source.entity_id, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() + + # Confirm that the configuration entry has been removed from the source entity 2 (previous) device registry + previous_device = device_registry.async_get( + device_id=previous_entity_source.device_id + ) + assert utility_meter_config_entry.entry_id not in previous_device.config_entries + + # Confirm that there is no device with the helper configuration entry + assert ( + dr.async_entries_for_config_entry( + device_registry, utility_meter_config_entry.entry_id + ) + == [] + ) + + # Change configuration options to use source entity 2 (with a linked device) and reload the integration + previous_entity_source = source_entity_3 + current_entity_source = source_entity_2 + + result = await hass.config_entries.options.async_init( + utility_meter_config_entry.entry_id + ) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "periodically_resetting": True, + "source": current_entity_source.entity_id, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() + + # Confirm that the configuration entry has been added to the source entity 2 (current) device registry + current_device = device_registry.async_get( + device_id=current_entity_source.device_id + ) + assert utility_meter_config_entry.entry_id in current_device.config_entries From 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 53c8577be44..731743e48c8 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import OrderedDict from collections.abc import Mapping +import json import logging from typing import Any @@ -408,6 +409,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): except aiohttp.ClientError as err: _LOGGER.error("Error talking to the dashboard: %s", err) return False + except json.JSONDecodeError as err: + _LOGGER.error( + "Error parsing response from dashboard: %s", err, exc_info=True + ) + return False self._noise_psk = noise_psk return True diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 4a99de77c1a..662816a53d8 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1,5 +1,6 @@ """Test config flow.""" import asyncio +import json from unittest.mock import AsyncMock, MagicMock, patch from aioesphomeapi import ( @@ -414,8 +415,13 @@ async def test_user_discovers_name_and_gets_key_from_dashboard( assert mock_client.noise_psk == VALID_NOISE_PSK +@pytest.mark.parametrize( + "dashboard_exception", + [aiohttp.ClientError(), json.JSONDecodeError("test", "test", 0)], +) async def test_user_discovers_name_and_gets_key_from_dashboard_fails( hass: HomeAssistant, + dashboard_exception: Exception, mock_client, mock_dashboard, mock_zeroconf: None, @@ -442,7 +448,7 @@ async def test_user_discovers_name_and_gets_key_from_dashboard_fails( with patch( "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", - side_effect=aiohttp.ClientError, + side_effect=dashboard_exception, ): result = await hass.config_entries.flow.async_init( "esphome", From 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 fda8717c3dd..c8fa05b2730 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -18,6 +18,11 @@ from aiohttp.typedefs import JSONDecoder, StrOrURL 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 @@ class HomeAssistantHTTP: "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 d30198bdc12..82426fbce08 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer-connected==0.13.7"] + "requirements": ["bimmer-connected==0.13.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 74c0b80ad57..95a0ad68a75 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 09706afca23..7d22f6021c0 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 b847b76ca17..8c1dd9b2197 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -317,7 +317,9 @@ class ControllerEvents: 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 @@ class NodeEvents: 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 8fba07198f2..4a8a9dec587 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.5.34"] + "requirements": ["env-canada==0.5.35"] } diff --git a/requirements_all.txt b/requirements_all.txt index 95a0ad68a75..81e71a831a2 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 7d22f6021c0..007cb5fdea7 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 00000000000..6da88533edc --- /dev/null +++ b/homeassistant/components/matter/climate.py @@ -0,0 +1,313 @@ +"""Matter climate platform.""" +from __future__ import annotations + +from enum import IntEnum +from typing import TYPE_CHECKING, Any + +from chip.clusters import Objects as clusters +from matter_server.client.models import device_types +from matter_server.common.helpers.util import create_attribute_path_from_attribute + +from homeassistant.components.climate import ( + ATTR_HVAC_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, + ClimateEntity, + ClimateEntityDescription, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, Platform, UnitOfTemperature +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import MatterEntity +from .helpers import get_matter +from .models import MatterDiscoverySchema + +if TYPE_CHECKING: + from matter_server.client import MatterClient + from matter_server.client.models.node import MatterEndpoint + + from .discovery import MatterEntityInfo + +TEMPERATURE_SCALING_FACTOR = 100 +HVAC_SYSTEM_MODE_MAP = { + HVACMode.OFF: 0, + HVACMode.HEAT_COOL: 1, + HVACMode.COOL: 3, + HVACMode.HEAT: 4, +} +SystemModeEnum = clusters.Thermostat.Enums.ThermostatSystemMode +ControlSequenceEnum = clusters.Thermostat.Enums.ThermostatControlSequence +ThermostatFeature = clusters.Thermostat.Bitmaps.ThermostatFeature + + +class ThermostatRunningState(IntEnum): + """Thermostat Running State, Matter spec Thermostat 7.33.""" + + Heat = 1 # 1 << 0 = 1 + Cool = 2 # 1 << 1 = 2 + Fan = 4 # 1 << 2 = 4 + HeatStage2 = 8 # 1 << 3 = 8 + CoolStage2 = 16 # 1 << 4 = 16 + FanStage2 = 32 # 1 << 5 = 32 + FanStage3 = 64 # 1 << 6 = 64 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Matter climate platform from Config Entry.""" + matter = get_matter(hass) + matter.register_platform_handler(Platform.CLIMATE, async_add_entities) + + +class MatterClimate(MatterEntity, ClimateEntity): + """Representation of a Matter climate entity.""" + + _attr_temperature_unit: str = UnitOfTemperature.CELSIUS + _attr_supported_features: ClimateEntityFeature = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) + _attr_hvac_mode: HVACMode = HVACMode.OFF + + def __init__( + self, + matter_client: MatterClient, + endpoint: MatterEndpoint, + entity_info: MatterEntityInfo, + ) -> None: + """Initialize the Matter climate entity.""" + super().__init__(matter_client, endpoint, entity_info) + + # set hvac_modes based on feature map + self._attr_hvac_modes: list[HVACMode] = [HVACMode.OFF] + feature_map = int( + self.get_matter_attribute_value(clusters.Thermostat.Attributes.FeatureMap) + ) + if feature_map & ThermostatFeature.kHeating: + self._attr_hvac_modes.append(HVACMode.HEAT) + if feature_map & ThermostatFeature.kCooling: + self._attr_hvac_modes.append(HVACMode.COOL) + if feature_map & ThermostatFeature.kAutoMode: + self._attr_hvac_modes.append(HVACMode.HEAT_COOL) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + target_hvac_mode: HVACMode | None = kwargs.get(ATTR_HVAC_MODE) + if target_hvac_mode is not None: + await self.async_set_hvac_mode(target_hvac_mode) + + current_mode = target_hvac_mode or self.hvac_mode + command = None + if current_mode in (HVACMode.HEAT, HVACMode.COOL): + # when current mode is either heat or cool, the temperature arg must be provided. + temperature: float | None = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + raise ValueError("Temperature must be provided") + if self.target_temperature is None: + raise ValueError("Current target_temperature should not be None") + command = self._create_optional_setpoint_command( + clusters.Thermostat.Enums.SetpointAdjustMode.kCool + if current_mode == HVACMode.COOL + else clusters.Thermostat.Enums.SetpointAdjustMode.kHeat, + temperature, + self.target_temperature, + ) + elif current_mode == HVACMode.HEAT_COOL: + temperature_low: float | None = kwargs.get(ATTR_TARGET_TEMP_LOW) + temperature_high: float | None = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if temperature_low is None or temperature_high is None: + raise ValueError( + "temperature_low and temperature_high must be provided" + ) + if ( + self.target_temperature_low is None + or self.target_temperature_high is None + ): + raise ValueError( + "current target_temperature_low and target_temperature_high should not be None" + ) + # due to ha send both high and low temperature, we need to check which one is changed + command = self._create_optional_setpoint_command( + clusters.Thermostat.Enums.SetpointAdjustMode.kHeat, + temperature_low, + self.target_temperature_low, + ) + if command is None: + command = self._create_optional_setpoint_command( + clusters.Thermostat.Enums.SetpointAdjustMode.kCool, + temperature_high, + self.target_temperature_high, + ) + if command: + await self.matter_client.send_device_command( + node_id=self._endpoint.node.node_id, + endpoint_id=self._endpoint.endpoint_id, + command=command, + ) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + system_mode_path = create_attribute_path_from_attribute( + endpoint_id=self._endpoint.endpoint_id, + attribute=clusters.Thermostat.Attributes.SystemMode, + ) + system_mode_value = HVAC_SYSTEM_MODE_MAP.get(hvac_mode) + if system_mode_value is None: + raise ValueError(f"Unsupported hvac mode {hvac_mode} in Matter") + await self.matter_client.write_attribute( + node_id=self._endpoint.node.node_id, + attribute_path=system_mode_path, + value=system_mode_value, + ) + # we need to optimistically update the attribute's value here + # to prevent a race condition when adjusting the mode and temperature + # in the same call + self._endpoint.set_attribute_value(system_mode_path, system_mode_value) + self._update_from_device() + + @callback + def _update_from_device(self) -> None: + """Update from device.""" + self._attr_current_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.LocalTemperature + ) + # update hvac_mode from SystemMode + system_mode_value = int( + self.get_matter_attribute_value(clusters.Thermostat.Attributes.SystemMode) + ) + match system_mode_value: + case SystemModeEnum.kAuto: + self._attr_hvac_mode = HVACMode.HEAT_COOL + case SystemModeEnum.kDry: + self._attr_hvac_mode = HVACMode.DRY + case SystemModeEnum.kFanOnly: + self._attr_hvac_mode = HVACMode.FAN_ONLY + case SystemModeEnum.kCool | SystemModeEnum.kPrecooling: + self._attr_hvac_mode = HVACMode.COOL + case SystemModeEnum.kHeat | SystemModeEnum.kEmergencyHeat: + self._attr_hvac_mode = HVACMode.HEAT + case _: + self._attr_hvac_mode = HVACMode.OFF + # running state is an optional attribute + # which we map to hvac_action if it exists (its value is not None) + self._attr_hvac_action = None + if running_state_value := self.get_matter_attribute_value( + clusters.Thermostat.Attributes.ThermostatRunningState + ): + match running_state_value: + case ThermostatRunningState.Heat | ThermostatRunningState.HeatStage2: + self._attr_hvac_action = HVACAction.HEATING + case ThermostatRunningState.Cool | ThermostatRunningState.CoolStage2: + self._attr_hvac_action = HVACAction.COOLING + case ( + ThermostatRunningState.Fan + | ThermostatRunningState.FanStage2 + | ThermostatRunningState.FanStage3 + ): + self._attr_hvac_action = HVACAction.FAN + case _: + self._attr_hvac_action = HVACAction.OFF + # update target_temperature + if self._attr_hvac_mode == HVACMode.HEAT_COOL: + self._attr_target_temperature = None + elif self._attr_hvac_mode == HVACMode.COOL: + self._attr_target_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedCoolingSetpoint + ) + else: + self._attr_target_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedHeatingSetpoint + ) + # update target temperature high/low + if self._attr_hvac_mode == HVACMode.HEAT_COOL: + self._attr_target_temperature_high = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedCoolingSetpoint + ) + self._attr_target_temperature_low = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedHeatingSetpoint + ) + else: + self._attr_target_temperature_high = None + self._attr_target_temperature_low = None + # update min_temp + if self._attr_hvac_mode == HVACMode.COOL: + attribute = clusters.Thermostat.Attributes.AbsMinCoolSetpointLimit + else: + attribute = clusters.Thermostat.Attributes.AbsMinHeatSetpointLimit + if (value := self._get_temperature_in_degrees(attribute)) is not None: + self._attr_min_temp = value + else: + self._attr_min_temp = DEFAULT_MIN_TEMP + # update max_temp + if self._attr_hvac_mode in (HVACMode.COOL, HVACMode.HEAT_COOL): + attribute = clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit + else: + attribute = clusters.Thermostat.Attributes.AbsMaxCoolSetpointLimit + if (value := self._get_temperature_in_degrees(attribute)) is not None: + self._attr_max_temp = value + else: + self._attr_max_temp = DEFAULT_MAX_TEMP + + def _get_temperature_in_degrees( + self, attribute: type[clusters.ClusterAttributeDescriptor] + ) -> float | None: + """Return the scaled temperature value for the given attribute.""" + if value := self.get_matter_attribute_value(attribute): + return float(value) / TEMPERATURE_SCALING_FACTOR + return None + + @staticmethod + def _create_optional_setpoint_command( + mode: clusters.Thermostat.Enums.SetpointAdjustMode, + target_temp: float, + current_target_temp: float, + ) -> clusters.Thermostat.Commands.SetpointRaiseLower | None: + """Create a setpoint command if the target temperature is different from the current one.""" + + temp_diff = int((target_temp - current_target_temp) * 10) + + if temp_diff == 0: + return None + + return clusters.Thermostat.Commands.SetpointRaiseLower( + mode, + temp_diff, + ) + + +# Discovery schema(s) to map Matter Attributes to HA entities +DISCOVERY_SCHEMAS = [ + MatterDiscoverySchema( + platform=Platform.CLIMATE, + entity_description=ClimateEntityDescription( + key="MatterThermostat", + name=None, + ), + entity_class=MatterClimate, + required_attributes=(clusters.Thermostat.Attributes.LocalTemperature,), + optional_attributes=( + clusters.Thermostat.Attributes.FeatureMap, + clusters.Thermostat.Attributes.ControlSequenceOfOperation, + clusters.Thermostat.Attributes.Occupancy, + clusters.Thermostat.Attributes.OccupiedCoolingSetpoint, + clusters.Thermostat.Attributes.OccupiedHeatingSetpoint, + clusters.Thermostat.Attributes.SystemMode, + clusters.Thermostat.Attributes.ThermostatRunningMode, + clusters.Thermostat.Attributes.ThermostatRunningState, + clusters.Thermostat.Attributes.TemperatureSetpointHold, + clusters.Thermostat.Attributes.UnoccupiedCoolingSetpoint, + clusters.Thermostat.Attributes.UnoccupiedHeatingSetpoint, + ), + device_type=(device_types.Thermostat,), + ), +] diff --git a/homeassistant/components/matter/discovery.py b/homeassistant/components/matter/discovery.py index 28f5b6b7f90..0b4bacf00ca 100644 --- a/homeassistant/components/matter/discovery.py +++ b/homeassistant/components/matter/discovery.py @@ -10,6 +10,7 @@ from homeassistant.const import Platform from homeassistant.core import callback from .binary_sensor import DISCOVERY_SCHEMAS as BINARY_SENSOR_SCHEMAS +from .climate import DISCOVERY_SCHEMAS as CLIMATE_SENSOR_SCHEMAS from .cover import DISCOVERY_SCHEMAS as COVER_SCHEMAS from .light import DISCOVERY_SCHEMAS as LIGHT_SCHEMAS from .lock import DISCOVERY_SCHEMAS as LOCK_SCHEMAS @@ -19,6 +20,7 @@ from .switch import DISCOVERY_SCHEMAS as SWITCH_SCHEMAS DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = { Platform.BINARY_SENSOR: BINARY_SENSOR_SCHEMAS, + Platform.CLIMATE: CLIMATE_SENSOR_SCHEMAS, Platform.COVER: COVER_SCHEMAS, Platform.LIGHT: LIGHT_SCHEMAS, Platform.LOCK: LOCK_SCHEMAS, diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index 56c51d144d8..e1fb4464b83 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -77,6 +77,7 @@ DISCOVERY_SCHEMAS = [ device_types.ColorDimmerSwitch, device_types.DimmerSwitch, device_types.OnOffLightSwitch, + device_types.Thermostat, ), ), ] diff --git a/tests/components/matter/fixtures/nodes/thermostat.json b/tests/components/matter/fixtures/nodes/thermostat.json new file mode 100644 index 00000000000..85ac42e5429 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/thermostat.json @@ -0,0 +1,370 @@ +{ + "node_id": 4, + "date_commissioned": "2023-06-28T16:26:35.525058", + "last_interview": "2023-06-28T16:26:35.525060", + "interview_version": 4, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "deviceType": 22, + "revision": 1 + } + ], + "0/29/1": [29, 31, 40, 42, 48, 49, 50, 51, 54, 60, 62, 63, 64], + "0/29/2": [41], + "0/29/3": [1], + "0/29/65532": 0, + "0/29/65533": 1, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/31/0": [ + { + "privilege": 0, + "authMode": 0, + "subjects": null, + "targets": null, + "fabricIndex": 1 + }, + { + "privilege": 5, + "authMode": 2, + "subjects": [112233], + "targets": null, + "fabricIndex": 2 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 3, + "0/31/65532": 0, + "0/31/65533": 1, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 1, + "0/40/1": "LONGAN-LINK", + "0/40/2": 4895, + "0/40/3": "Longan link HVAC", + "0/40/4": 8192, + "0/40/5": "", + "0/40/6": "XX", + "0/40/7": 1, + "0/40/8": "1.0", + "0/40/9": 2, + "0/40/10": "v2.0", + "0/40/11": "20200101", + "0/40/12": "", + "0/40/14": "", + "0/40/15": "5a1fd2d040f23cf66e3a9d2a88e11f78", + "0/40/16": false, + "0/40/17": true, + "0/40/18": "3D06D025F9E026A0", + "0/40/19": { + "caseSessionsPerFabric": 3, + "subscriptionsPerFabric": 3 + }, + "0/40/65532": 0, + "0/40/65533": 1, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 17, 18, 19, 65528, + 65529, 65531, 65532, 65533 + ], + "0/42/0": [], + "0/42/1": true, + "0/42/2": 1, + "0/42/3": null, + "0/42/65532": 0, + "0/42/65533": 1, + "0/42/65528": [], + "0/42/65529": [0], + "0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/48/0": 0, + "0/48/1": { + "failSafeExpiryLengthSeconds": 60, + "maxCumulativeFailsafeSeconds": 900 + }, + "0/48/2": 0, + "0/48/3": 0, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 1, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/49/0": 1, + "0/49/1": [ + { + "networkID": "TE9OR0FOLUlPVA==", + "connected": true + } + ], + "0/49/2": 10, + "0/49/3": 30, + "0/49/4": true, + "0/49/5": 0, + "0/49/6": "TE9OR0FOLUlPVA==", + "0/49/7": null, + "0/49/65532": 1, + "0/49/65533": 1, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 2, 4, 6, 8], + "0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533], + "0/50/65532": 0, + "0/50/65533": 1, + "0/50/65528": [1], + "0/50/65529": [0], + "0/50/65531": [65528, 65529, 65531, 65532, 65533], + "0/51/0": [ + { + "name": "WIFI_STA_DEF", + "isOperational": true, + "offPremiseServicesReachableIPv4": null, + "offPremiseServicesReachableIPv6": null, + "hardwareAddress": "3FR1X7qs", + "IPv4Addresses": ["wKgI7g=="], + "IPv6Addresses": [ + "/oAAAAAAAADeVHX//l+6rA==", + "JA4DsgZ9jUDeVHX//l+6rA==", + "/UgvJAe/AADeVHX//l+6rA==" + ], + "type": 1 + } + ], + "0/51/1": 4, + "0/51/2": 30, + "0/51/3": 0, + "0/51/4": 0, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": true, + "0/51/65532": 0, + "0/51/65533": 1, + "0/51/65528": [], + "0/51/65529": [0], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ], + "0/54/0": "aHckDXAk", + "0/54/1": 0, + "0/54/2": 3, + "0/54/3": 1, + "0/54/4": -61, + "0/54/5": null, + "0/54/6": null, + "0/54/7": null, + "0/54/8": null, + "0/54/9": null, + "0/54/10": null, + "0/54/11": null, + "0/54/12": null, + "0/54/65532": 3, + "0/54/65533": 1, + "0/54/65528": [], + "0/54/65529": [0], + "0/54/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 65528, 65529, 65531, 65532, + 65533 + ], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 0, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/62/0": [ + { + "noc": "", + "icac": null, + "fabricIndex": 1 + }, + { + "noc": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVASQRBBgkBwEkCAEwCUEETaqdhs6MRkbh8fdh4EEImZaziiE6anaVp6Mu3P/zIJUB0fHUMxydKRTAC8bIn7vUhBCM47OYlYTkX0zFhoKYrzcKNQEoARgkAgE2AwQCBAEYMAQUrouBLuksQTkLrFhNVAbTHkNvMSEwBRTPlgMACvPdpqPOzuvR0OfPgfUcxBgwC0AcUInETXp/2gIFGDQF2+u+9WtYtvIfo6C3MhoOIV1SrRBZWYxY3CVjPGK7edTibQrVA4GccZKnHhNSBjxktrPiGA==", + "icac": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEE+rI5XQyifTZbZRK1Z2DOuXdQkmdUkWklTv+G1x4ZfbSupbUDo4l7i/iFdyu//uJThAw1GPEkWe6i98IFKCOQpzcKNQEpARgkAmAwBBTPlgMACvPdpqPOzuvR0OfPgfUcxDAFFJQo6UEBWTLtZVYFZwRBgn+qstpTGDALQK3jYiaxwnYJMwTBQlcVNrGxPtuVTZrp5foZtQCp/JEX2ZWqVxKypilx0ES/CfMHZ0Lllv9QsLs8xV/HNLidllkY", + "fabricIndex": 2 + } + ], + "0/62/1": [ + { + "rootPublicKey": "BAP9BJt5aQ9N98ClPTdNxpMZ1/Vh8r9usw6C8Ygi79AImsJq4UjAaYad0UI9Lh0OmRA9sWE2aSPbHjf409i/970=", + "vendorID": 4996, + "fabricID": 1, + "nodeID": 1425709672, + "label": "", + "fabricIndex": 1 + }, + { + "rootPublicKey": "BJXfyipMp+Jx4pkoTnvYoAYODis4xJktKdQXu8MSpBLIwII58BD0KkIG9NmuHcp0xUQKzqlfyB/bkAanevO73ZI=", + "vendorID": 65521, + "fabricID": 1, + "nodeID": 4, + "label": "", + "fabricIndex": 2 + } + ], + "0/62/2": 5, + "0/62/3": 2, + "0/62/4": [ + "FTABAQAkAgE3AycULlZRw4lgwKgkFQEYJgT1e7grJgV1r5ktNwYnFC5WUcOJYMCoJBUBGCQHASQIATAJQQQD/QSbeWkPTffApT03TcaTGdf1YfK/brMOgvGIIu/QCJrCauFIwGmGndFCPS4dDpkQPbFhNmkj2x43+NPYv/e9Nwo1ASkBGCQCYDAEFCqZHzimE2c+jPoEuJoM1rQaAPFRMAUUKpkfOKYTZz6M+gS4mgzWtBoA8VEYMAtANu49PfywV8aJmtxNYZa7SJXGlK1EciiF6vhZsoqdDCwx1VQX8FdyVunw0H3ljzbvucU6o8aY6HwBsPJKCQVHzhg=", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEld/KKkyn4nHimShOe9igBg4OKzjEmS0p1Be7wxKkEsjAgjnwEPQqQgb02a4dynTFRArOqV/IH9uQBqd687vdkjcKNQEpARgkAmAwBBSUKOlBAVky7WVWBWcEQYJ/qrLaUzAFFJQo6UEBWTLtZVYFZwRBgn+qstpTGDALQCNU8W3im+pmCBR5A4e15ByjPq2msE05NI9eeFI6BO0p/whhaBSGtjI7Tb1onNNu9AH6AQoji8XDDa7Nj/1w9KoY" + ], + "0/62/5": 2, + "0/62/65532": 0, + "0/62/65533": 1, + "0/62/65528": [1, 3, 5, 8], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "0/63/0": [], + "0/63/1": [], + "0/63/2": 3, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 1, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/64/0": [ + { + "label": "room", + "value": "bedroom 2" + }, + { + "label": "orientation", + "value": "North" + }, + { + "label": "floor", + "value": "2" + }, + { + "label": "direction", + "value": "up" + } + ], + "0/64/65532": 0, + "0/64/65533": 1, + "0/64/65528": [], + "0/64/65529": [], + "0/64/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/3/0": 0, + "1/3/1": 4, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/4/0": 128, + "1/4/65532": 1, + "1/4/65533": 4, + "1/4/65528": [0, 1, 2, 3], + "1/4/65529": [0, 1, 2, 3, 4, 5], + "1/4/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/6/0": true, + "1/6/65532": 0, + "1/6/65533": 4, + "1/6/65528": [], + "1/6/65529": [0, 1, 2], + "1/6/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/29/0": [ + { + "deviceType": 769, + "revision": 1 + } + ], + "1/29/1": [3, 4, 6, 29, 30, 64, 513, 514, 516], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 1, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/30/0": [], + "1/30/65532": 0, + "1/30/65533": 1, + "1/30/65528": [], + "1/30/65529": [], + "1/30/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/64/0": [ + { + "label": "room", + "value": "bedroom 2" + }, + { + "label": "orientation", + "value": "North" + }, + { + "label": "floor", + "value": "2" + }, + { + "label": "direction", + "value": "up" + } + ], + "1/64/65532": 0, + "1/64/65533": 1, + "1/64/65528": [], + "1/64/65529": [], + "1/64/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/513/0": 2830, + "1/513/3": null, + "1/513/4": null, + "1/513/5": null, + "1/513/6": null, + "1/513/9": 0, + "1/513/17": null, + "1/513/18": null, + "1/513/21": 1600, + "1/513/22": 3000, + "1/513/23": 1600, + "1/513/24": 3000, + "1/513/25": 5, + "1/513/27": 4, + "1/513/28": 3, + "1/513/30": 0, + "1/513/65532": 35, + "1/513/65533": 5, + "1/513/65528": [], + "1/513/65529": [0], + "1/513/65531": [ + 0, 3, 4, 5, 6, 9, 17, 18, 21, 22, 23, 24, 25, 27, 28, 30, 65528, 65529, + 65531, 65532, 65533 + ], + "1/514/0": 0, + "1/514/1": 2, + "1/514/2": 0, + "1/514/3": 0, + "1/514/65532": 0, + "1/514/65533": 1, + "1/514/65528": [], + "1/514/65529": [], + "1/514/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/516/0": 0, + "1/516/1": 0, + "1/516/65532": 0, + "1/516/65533": 1, + "1/516/65528": [], + "1/516/65529": [], + "1/516/65531": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "attribute_subscriptions": [ + [1, 513, 17], + [1, 6, 0], + [1, 513, 0], + [1, 513, 28], + [1, 513, 65532], + [1, 513, 18], + [1, 513, 30], + [1, 513, 27] + ] +} diff --git a/tests/components/matter/test_climate.py b/tests/components/matter/test_climate.py new file mode 100644 index 00000000000..ec8453b5c56 --- /dev/null +++ b/tests/components/matter/test_climate.py @@ -0,0 +1,399 @@ +"""Test Matter locks.""" +from unittest.mock import MagicMock, call + +from chip.clusters import Objects as clusters +from matter_server.client.models.node import MatterNode +from matter_server.common.helpers.util import create_attribute_path_from_attribute +import pytest + +from homeassistant.components.climate import ( + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVACAction, + HVACMode, +) +from homeassistant.components.climate.const import ( + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_OFF, +) +from homeassistant.core import HomeAssistant + +from .common import ( + set_node_attribute, + setup_integration_with_node_fixture, + trigger_subscription_callback, +) + + +@pytest.fixture(name="thermostat") +async def thermostat_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a thermostat node.""" + return await setup_integration_with_node_fixture(hass, "thermostat", matter_client) + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_thermostat( + hass: HomeAssistant, + matter_client: MagicMock, + thermostat: MatterNode, +) -> None: + """Test thermostat.""" + # test default temp range + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["min_temp"] == 7 + assert state.attributes["max_temp"] == 35 + + # test set temperature when target temp is None + assert state.attributes["temperature"] is None + assert state.state == HVAC_MODE_COOL + with pytest.raises( + ValueError, match="Current target_temperature should not be None" + ): + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "temperature": 22.5, + }, + blocking=True, + ) + with pytest.raises(ValueError, match="Temperature must be provided"): + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "target_temp_low": 18, + "target_temp_high": 26, + }, + blocking=True, + ) + + # change system mode to heat_cool + set_node_attribute(thermostat, 1, 513, 28, 1) + await trigger_subscription_callback(hass, matter_client) + with pytest.raises( + ValueError, + match="current target_temperature_low and target_temperature_high should not be None", + ): + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.state == HVAC_MODE_HEAT_COOL + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "target_temp_low": 18, + "target_temp_high": 26, + }, + blocking=True, + ) + + # initial state + set_node_attribute(thermostat, 1, 513, 3, 1600) + set_node_attribute(thermostat, 1, 513, 4, 3000) + set_node_attribute(thermostat, 1, 513, 5, 1600) + set_node_attribute(thermostat, 1, 513, 6, 3000) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["min_temp"] == 16 + assert state.attributes["max_temp"] == 30 + assert state.attributes["hvac_modes"] == [ + HVACMode.OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + ] + + # test system mode update from device + set_node_attribute(thermostat, 1, 513, 28, 0) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.state == HVAC_MODE_OFF + + set_node_attribute(thermostat, 1, 513, 28, 7) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.state == HVAC_MODE_FAN_ONLY + + set_node_attribute(thermostat, 1, 513, 28, 8) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.state == HVAC_MODE_DRY + + # test running state update from device + set_node_attribute(thermostat, 1, 513, 41, 1) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.HEATING + + set_node_attribute(thermostat, 1, 513, 41, 8) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.HEATING + + set_node_attribute(thermostat, 1, 513, 41, 2) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.COOLING + + set_node_attribute(thermostat, 1, 513, 41, 16) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.COOLING + + set_node_attribute(thermostat, 1, 513, 41, 4) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.FAN + + set_node_attribute(thermostat, 1, 513, 41, 32) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.FAN + + set_node_attribute(thermostat, 1, 513, 41, 64) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.FAN + + set_node_attribute(thermostat, 1, 513, 41, 66) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.OFF + + # change system mode to heat + set_node_attribute(thermostat, 1, 513, 28, 4) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.state == HVAC_MODE_HEAT + + # change occupied heating setpoint to 20 + set_node_attribute(thermostat, 1, 513, 18, 2000) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["temperature"] == 20 + + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "temperature": 25, + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=thermostat.node_id, + endpoint_id=1, + command=clusters.Thermostat.Commands.SetpointRaiseLower( + clusters.Thermostat.Enums.SetpointAdjustMode.kHeat, + 50, + ), + ) + matter_client.send_device_command.reset_mock() + + # change system mode to cool + set_node_attribute(thermostat, 1, 513, 28, 3) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.state == HVAC_MODE_COOL + + # change occupied cooling setpoint to 18 + set_node_attribute(thermostat, 1, 513, 17, 1800) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["temperature"] == 18 + + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "temperature": 16, + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=thermostat.node_id, + endpoint_id=1, + command=clusters.Thermostat.Commands.SetpointRaiseLower( + clusters.Thermostat.Enums.SetpointAdjustMode.kCool, -20 + ), + ) + matter_client.send_device_command.reset_mock() + + # change system mode to heat_cool + set_node_attribute(thermostat, 1, 513, 28, 1) + await trigger_subscription_callback(hass, matter_client) + with pytest.raises( + ValueError, match="temperature_low and temperature_high must be provided" + ): + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "temperature": 18, + }, + blocking=True, + ) + + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.state == HVAC_MODE_HEAT_COOL + + # change occupied cooling setpoint to 18 + set_node_attribute(thermostat, 1, 513, 17, 2500) + await trigger_subscription_callback(hass, matter_client) + # change occupied heating setpoint to 18 + set_node_attribute(thermostat, 1, 513, 18, 1700) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["target_temp_low"] == 17 + assert state.attributes["target_temp_high"] == 25 + + # change target_temp_low to 18 + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "target_temp_low": 18, + "target_temp_high": 25, + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=thermostat.node_id, + endpoint_id=1, + command=clusters.Thermostat.Commands.SetpointRaiseLower( + clusters.Thermostat.Enums.SetpointAdjustMode.kHeat, 10 + ), + ) + matter_client.send_device_command.reset_mock() + set_node_attribute(thermostat, 1, 513, 18, 1800) + await trigger_subscription_callback(hass, matter_client) + + # change target_temp_high to 26 + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "target_temp_low": 18, + "target_temp_high": 26, + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=thermostat.node_id, + endpoint_id=1, + command=clusters.Thermostat.Commands.SetpointRaiseLower( + clusters.Thermostat.Enums.SetpointAdjustMode.kCool, 10 + ), + ) + matter_client.send_device_command.reset_mock() + set_node_attribute(thermostat, 1, 513, 17, 2600) + await trigger_subscription_callback(hass, matter_client) + + await hass.services.async_call( + "climate", + "set_hvac_mode", + { + "entity_id": "climate.longan_link_hvac", + "hvac_mode": HVAC_MODE_HEAT, + }, + blocking=True, + ) + + assert matter_client.write_attribute.call_count == 1 + assert matter_client.write_attribute.call_args == call( + node_id=thermostat.node_id, + attribute_path=create_attribute_path_from_attribute( + endpoint_id=1, + attribute=clusters.Thermostat.Attributes.SystemMode, + ), + value=4, + ) + matter_client.send_device_command.reset_mock() + + with pytest.raises(ValueError, match="Unsupported hvac mode dry in Matter"): + await hass.services.async_call( + "climate", + "set_hvac_mode", + { + "entity_id": "climate.longan_link_hvac", + "hvac_mode": HVACMode.DRY, + }, + blocking=True, + ) + + # change target_temp and hvac_mode in the same call + matter_client.send_device_command.reset_mock() + matter_client.write_attribute.reset_mock() + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "temperature": 22, + "hvac_mode": HVACMode.COOL, + }, + blocking=True, + ) + assert matter_client.write_attribute.call_count == 1 + assert matter_client.write_attribute.call_args == call( + node_id=thermostat.node_id, + attribute_path=create_attribute_path_from_attribute( + endpoint_id=1, + attribute=clusters.Thermostat.Attributes.SystemMode, + ), + value=3, + ) + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=thermostat.node_id, + endpoint_id=1, + command=clusters.Thermostat.Commands.SetpointRaiseLower( + clusters.Thermostat.Enums.SetpointAdjustMode.kCool, -40 + ), + ) From 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 d1661dce0fa..f55dc9344ab 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -18,10 +18,7 @@ from homeassistant.components.climate import ( ) 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 @@ class NoboZone(ClimateEntity): 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 3bb1fa373a5..c5536bad6ea 100644 --- a/homeassistant/components/nobo_hub/sensor.py +++ b/homeassistant/components/nobo_hub/sensor.py @@ -10,12 +10,8 @@ from homeassistant.components.sensor import ( ) 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 @@ class NoboTemperatureSensor(SensorEntity): 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 c4578c7d14d..06cfa065c54 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 f139124e961..4629f6a2a3b 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 b0bfe18614e..d237303e7c9 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -30,6 +30,7 @@ from homeassistant.helpers import ( 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 @@ class NukiEntity(CoordinatorEntity[NukiCoordinator], Generic[_NukiDeviceT]): 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 35955a6b189..3f4069752f2 100644 --- a/homeassistant/components/discovergy/sensor.py +++ b/homeassistant/components/discovergy/sensor.py @@ -11,16 +11,13 @@ from homeassistant.components.sensor import ( ) 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 @@ class DiscovergySensor(CoordinatorEntity[DiscovergyUpdateCoordinator], SensorEnt 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 f3008d9f1c1..1ef87e84933 100644 --- a/homeassistant/components/slimproto/manifest.json +++ b/homeassistant/components/slimproto/manifest.json @@ -6,5 +6,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/slimproto", "iot_class": "local_push", - "requirements": ["aioslimproto==2.1.1"] + "requirements": ["aioslimproto==2.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 81e71a831a2..a1d6eade299 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 007cb5fdea7..9f12920bbc1 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 969583f050a..3b48e96a351 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 a1d6eade299..c87482c2fef 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 9f12920bbc1..ee2ae9f67c5 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 e28dcbd0661..ef8d8eb884f 100644 --- a/homeassistant/components/opower/sensor.py +++ b/homeassistant/components/opower/sensor.py @@ -15,6 +15,7 @@ from homeassistant.components.sensor import ( 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 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = ( 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 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = ( 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 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = ( 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 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = ( 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 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = ( 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 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = ( 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 @@ class OpowerSensor(SensorEntity, CoordinatorEntity[OpowerCoordinator]): ) -> 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 79d8bf80fee..037983eb6ff 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 17c6896593b..0ee910f84f4 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 7f6a847f52e..6a45a0dcc56 100644 --- a/tests/components/opower/test_config_flow.py +++ b/tests/components/opower/test_config_flow.py @@ -8,6 +8,7 @@ import pytest 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 b9d0e63ecba..d2c196dbfdd 100644 --- a/homeassistant/components/growatt_server/strings.json +++ b/homeassistant/components/growatt_server/strings.json @@ -83,7 +83,7 @@ "name": "Intelligent Power Management temperature" }, "inverter_temperature": { - "name": "Energytoday" + "name": "Inverter temperature" }, "mix_statement_of_charge": { "name": "Statement of charge" From 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 e4ecbd9d866..f4d1e51b171 100644 --- a/homeassistant/components/twentemilieu/calendar.py +++ b/homeassistant/components/twentemilieu/calendar.py @@ -32,6 +32,7 @@ class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEntity): _attr_has_entity_name = True _attr_icon = "mdi:delete-empty" + _attr_name = None def __init__( self, From 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 9615404a9a6..284b8d6b00a 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -35,6 +35,7 @@ class VerisureAlarm( _attr_code_format = CodeFormat.NUMBER _attr_has_entity_name = True + _attr_name = None _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 62e9bdf6cf8..6c3dcd81295 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -32,6 +32,7 @@ class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], Switch """Representation of a Verisure smartplug.""" _attr_has_entity_name = True + _attr_name = None def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str From 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 4d40bb3356b..c331f45bc49 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -32,7 +32,7 @@ from .const import CONF_UTILITY, DOMAIN _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 ef8d8eb884f..36f88a36e8a 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 f70af18c3e1..ff58d566a34 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 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), 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 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), 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 6f011523a2a..4777cc94fbf 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 49721568ff2..24a06d7ee71 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 2c06621d079..cfacadce528 100644 --- a/homeassistant/components/growatt_server/sensor_types/inverter.py +++ b/homeassistant/components/growatt_server/sensor_types/inverter.py @@ -161,7 +161,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( ), GrowattSensorEntityDescription( key="inverter_temperature", - translation_key="inverter_energy_today", + translation_key="inverter_temperature", api_key="temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, From 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 a2961a55d78..62ff1d63ca0 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -153,14 +153,6 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): 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 7e46dc0d0bd..00d27035464 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 4a1edd4096e..f0875bc15d7 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230630.0"] + "requirements": ["home-assistant-frontend==20230703.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 47bd964c002..3c861d8a389 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 c87482c2fef..df524f01ae2 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 ee2ae9f67c5..0aa356165bf 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 8f5e6b95c39..8fc16926e56 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 df524f01ae2..8a19ee9dd76 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 0aa356165bf..e5feb50105c 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 f9fdc252537..2b75c1114ce 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 c733a96ca9d..ea00de33390 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 00d27035464..232362ce96f 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1,12 +1,14 @@ """Tests for WebSocket API commands.""" from copy import deepcopy import datetime -from unittest.mock import ANY, patch +from unittest.mock import ANY, AsyncMock, Mock, patch from async_timeout import timeout import pytest import voluptuous as vol +from homeassistant import config_entries, loader +from homeassistant.components.device_automation import toggle_entity from homeassistant.components.websocket_api import const from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, @@ -17,13 +19,20 @@ from homeassistant.components.websocket_api.const import FEATURE_COALESCE_MESSAG from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity +from homeassistant.helpers import device_registry as dr, entity from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.loader import async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_setup_component from homeassistant.util.json import json_loads -from tests.common import MockEntity, MockEntityPlatform, MockUser, async_mock_service +from tests.common import ( + MockConfigEntry, + MockEntity, + MockEntityPlatform, + MockUser, + async_mock_service, + mock_platform, +) from tests.typing import ( ClientSessionGenerator, WebSocketGenerator, @@ -40,6 +49,25 @@ STATE_KEY_SHORT_NAMES = { STATE_KEY_LONG_NAMES = {v: k for k, v in STATE_KEY_SHORT_NAMES.items()} +@pytest.fixture +def fake_integration(hass: HomeAssistant): + """Set up a mock integration with device automation support.""" + DOMAIN = "fake_integration" + + hass.config.components.add(DOMAIN) + + mock_platform( + hass, + f"{DOMAIN}.device_action", + Mock( + ACTION_SCHEMA=toggle_entity.ACTION_SCHEMA.extend( + {vol.Required("domain"): DOMAIN} + ), + spec=["ACTION_SCHEMA"], + ), + ) + + def _apply_entities_changes(state_dict: dict, change_dict: dict) -> None: """Apply a diff set to a dict. @@ -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 beb084d8c1c..93b199b1db5 100644 --- a/homeassistant/helpers/httpx_client.py +++ b/homeassistant/helpers/httpx_client.py @@ -19,8 +19,13 @@ from homeassistant.util.ssl import ( 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 a52b869b830..9e27f6efb3e 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -10,9 +10,8 @@ from enum import Enum 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 @@ class ConfigEntry: 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 @@ class ConfigEntry: 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 @@ class ConfigEntries: 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 86f61f0ed87..b22ac98b0dc 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -422,7 +422,7 @@ def _list_events_dict_factory( """Convert CalendarEvent dataclass items to dictionary of attributes.""" return { name: value - for name, value in obj + for name, value in _event_dict_factory(obj).items() if name in LIST_EVENT_FIELDS and value is not None } diff --git a/tests/components/calendar/conftest.py b/tests/components/calendar/conftest.py index 4d6b5adfde7..5d506d67c6f 100644 --- a/tests/components/calendar/conftest.py +++ b/tests/components/calendar/conftest.py @@ -9,3 +9,11 @@ from homeassistant.setup import async_setup_component async def setup_homeassistant(hass: HomeAssistant): """Set up the homeassistant integration.""" await async_setup_component(hass, "homeassistant", {}) + + +@pytest.fixture +def set_time_zone(hass: HomeAssistant) -> None: + """Set the time zone for the tests.""" + # Set our timezone to CST/Regina so we can check calculations + # This keeps UTC-6 all year round + hass.config.set_time_zone("America/Regina") diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 9fdc76abe03..463e075d169 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -4,8 +4,9 @@ from __future__ import annotations from datetime import timedelta from http import HTTPStatus from typing import Any -from unittest.mock import ANY, patch +from unittest.mock import patch +from freezegun import freeze_time import pytest import voluptuous as vol @@ -386,8 +387,14 @@ async def test_create_event_service_invalid_params( ) -async def test_list_events_service(hass: HomeAssistant) -> None: - """Test listing events from the service call using exlplicit start and end time.""" +@freeze_time("2023-06-22 10:30:00+00:00") +async def test_list_events_service(hass: HomeAssistant, set_time_zone: None) -> None: + """Test listing events from the service call using exlplicit start and end time. + + This test uses a fixed date/time so that it can deterministically test the + string output values. + """ + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) await hass.async_block_till_done() @@ -408,8 +415,8 @@ async def test_list_events_service(hass: HomeAssistant) -> None: assert response == { "events": [ { - "start": ANY, - "end": ANY, + "start": "2023-06-22T05:00:00-06:00", + "end": "2023-06-22T06:00:00-06:00", "summary": "Future Event", "description": "Future Description", "location": "Future Location", diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index 05c7d95d8ad..45dd9d6afe1 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -121,14 +121,6 @@ class FakeSchedule: await self.fire_time(dt_util.utcnow()) -@pytest.fixture -def set_time_zone(hass: HomeAssistant) -> None: - """Set the time zone for the tests.""" - # Set our timezone to CST/Regina so we can check calculations - # This keeps UTC-6 all year round - hass.config.set_time_zone("America/Regina") - - @pytest.fixture def fake_schedule( hass: HomeAssistant, freezer: FrozenDateTimeFactory From 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 b22ac98b0dc..3286dd152e8 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -264,8 +264,8 @@ SERVICE_LIST_EVENTS_SCHEMA: Final = vol.All( cv.has_at_most_one_key(EVENT_END_DATETIME, EVENT_DURATION), cv.make_entity_service_schema( { - vol.Optional(EVENT_START_DATETIME): datetime.datetime, - vol.Optional(EVENT_END_DATETIME): datetime.datetime, + vol.Optional(EVENT_START_DATETIME): cv.datetime, + vol.Optional(EVENT_END_DATETIME): cv.datetime, vol.Optional(EVENT_DURATION): vol.All( cv.time_period, cv.positive_timedelta ), diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 73b45a55640..92dbf8d47b8 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -67,6 +67,14 @@ class DemoCalendar(CalendarEntity): end_date: datetime.datetime, ) -> list[CalendarEvent]: """Return calendar events within a datetime range.""" + if start_date.tzinfo is None: + start_date = start_date.replace( + tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) + if end_date.tzinfo is None: + end_date = end_date.replace( + tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) assert start_date < end_date if self._event.start_datetime_local >= end_date: return [] From 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 1e69af66eec..e9f9ee99ff6 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -19,7 +19,7 @@ ATTR_PROTECTION_WINDOW_STARTING_UV = "start_uv" 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 44bde8341a0..90eefac594a 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -49,67 +49,67 @@ UV_LEVEL_LOW = "Low" 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 9542cb8b1a7..4aa29d11fcf 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 f48191e471a..9bfb01e5a88 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==48"], + "requirements": ["aiounifi==49"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 8a19ee9dd76..da1672698c7 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 e5feb50105c..d8ca0291a84 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 f192dd44300..21a878fa187 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Literal from homeassistant.components.sensor import ( - DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -39,7 +38,7 @@ from .const import ( 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 @@ SENSORS_SMARTMETER: tuple[SensorEntityDescription, ...] = ( ), 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_SMARTMETER: tuple[SensorEntityDescription, ...] = ( 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_PHASES: tuple[SensorEntityDescription, ...] = ( 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_SETTINGS: tuple[SensorEntityDescription, ...] = ( 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 0c745554e9d..781ca109235 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 14ff3b1e519..f84df458d4b 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 0f82918d82a..a8907ba3b68 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -131,7 +131,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 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 fbbd14aaf4e..916da5d24fb 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 8ec91acf965..317209886bd 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -39,7 +39,6 @@ SUPPORT_IROBOT = ( | 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 9121811af3c..ca24212a96c 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 cfd62c9deaf..4a54b900be1 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -64,7 +64,6 @@ EXPECTED_FEATURES = ( | 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 0567c551d98..a4520435161 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -110,6 +110,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class Proximity(Entity): """Representation of a Proximity.""" + # This entity is legacy and does not have a platform. + # We can't fix this easily without breaking changes. + _no_platform_reported = True + def __init__( self, hass: HomeAssistant, From 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 62ff1d63ca0..a35fb4640cc 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -86,7 +86,9 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): 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 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): 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 e27ff57f03f..b428018cd9e 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -726,6 +726,10 @@ class DeviceTracker: class Device(RestoreEntity): """Base class for a tracked device.""" + # This entity is legacy and does not have a platform. + # We can't fix this easily without breaking changes. + _no_platform_reported = True + host_name: str | None = None location_name: str | None = None gps: GPSType | None = None From 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 df5bf968ae1..75ad26665c3 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -79,6 +79,10 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._username = entry_data[CONF_USERNAME] self._password = entry_data[CONF_PASSWORD] self._reauth = True + self.context["title_placeholders"]["ip_address"] = entry_data[CONF_HOST] + self.context["title_placeholders"]["hostname"] = self.context[ + "title_placeholders" + ]["name"] return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( From 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 73ac4d6fbc4..5879c44db83 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 add04c236e7..60db0322717 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 3b4c0bf2dab..7a2e371024f 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 4c33d1f27ef..eed54ef58c3 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 b06794c9b32..c1eff29e3be 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 41405780124..a82284c24af 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 f208e3e4035..7dc89ddbaf3 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 f711ceaf74a..72a83850f93 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 fb3559de91a..b00c4200836 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 534ff1dc9ec..15e41043f5a 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 dfcb503182c..15c89498bc7 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 9905a8af74b..af26ed13b38 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 385d60a3886..fef185687db 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -64,7 +64,6 @@ DEFAULT_SERVICES = ( VacuumEntityFeature.START | VacuumEntityFeature.STOP | VacuumEntityFeature.RETURN_HOME - | VacuumEntityFeature.STATUS | VacuumEntityFeature.BATTERY | VacuumEntityFeature.CLEAN_SPOT ) @@ -199,7 +198,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): 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 203f5d55b95..dd15399f670 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 891264d4290..4ead41e86e9 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -16,7 +16,24 @@ "_touch-able._tcp.local.", "_appletv-v2._tcp.local.", "_hscp._tcp.local.", - "_airplay._tcp.local.", + { + "type": "_airplay._tcp.local.", + "properties": { + "model": "appletv*" + } + }, + { + "type": "_airplay._tcp.local.", + "properties": { + "model": "audioaccessory*" + } + }, + { + "type": "_airplay._tcp.local.", + "properties": { + "am": "airport*" + } + }, { "type": "_raop._tcp.local.", "properties": { diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index ccf35e384bb..6b5676c4a25 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -251,6 +251,21 @@ ZEROCONF = { "_airplay._tcp.local.": [ { "domain": "apple_tv", + "properties": { + "model": "appletv*", + }, + }, + { + "domain": "apple_tv", + "properties": { + "model": "audioaccessory*", + }, + }, + { + "domain": "apple_tv", + "properties": { + "am": "airport*", + }, }, { "domain": "samsungtv", From 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 a35fb4640cc..3c6ede66c69 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -150,6 +150,14 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): 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 | 78 +++- tests/components/matter/test_door_lock.py | 9 - tests/components/matter/test_sensor.py | 29 ++ 8 files changed, 455 insertions(+), 120 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 7c94c07c8cd..aabfc12eefb 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import ( 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 @@ DISCOVERY_SCHEMAS = [ 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 027dcda65a7..5021ed7fa0d 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( LIGHT_LUX, PERCENTAGE, + EntityCategory, Platform, UnitOfPressure, UnitOfTemperature, @@ -127,6 +128,7 @@ DISCOVERY_SCHEMAS = [ 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 e4af252fccb..6a14148585a 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -5,12 +5,15 @@ import asyncio 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 909f7be2ebe..00000000000 --- 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 00000000000..b0eacfb621c --- /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 d7982e1d5ae..4dbb3b27b9c 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 @@ from .common import ( ) -@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,22 +36,23 @@ 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") - assert state - assert state.state == "off" - - set_node_attribute(contact_sensor_node, 1, 69, 0, False) - await trigger_subscription_callback( - hass, matter_client, data=(contact_sensor_node.node_id, "1/69/0", False) - ) - - 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 == "on" + set_node_attribute(eve_contact_sensor_node, 1, 69, 0, True) + await trigger_subscription_callback( + hass, matter_client, data=(eve_contact_sensor_node.node_id, "1/69/0", True) + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == "off" + @pytest.fixture(name="occupancy_sensor_node") async def occupancy_sensor_node_fixture( @@ -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 003bfa3cf39..3eba65dc8ab 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_door_lock.py @@ -16,19 +16,10 @@ from homeassistant.core import HomeAssistant 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 a2e97e188f6..2650f2b1a6f 100644 --- a/tests/components/matter/test_sensor.py +++ b/tests/components/matter/test_sensor.py @@ -4,7 +4,9 @@ from unittest.mock import MagicMock 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 cfc44029e23..52d5aca070a 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -1,19 +1,4 @@ # Describes the format for available fan services -set_speed: - name: Set speed - description: Set fan speed. - target: - entity: - domain: fan - fields: - speed: - name: Speed - description: Speed setting. - required: true - example: "low" - selector: - text: - set_preset_mode: name: Set preset mode description: Set preset mode for a fan device. @@ -53,12 +38,6 @@ turn_on: entity: domain: fan fields: - speed: - name: Speed - description: Speed setting. - example: "high" - selector: - text: percentage: name: Percentage description: Percentage speed setting. From 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 5f6152b7bc7..4fc89bc918b 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -8,10 +8,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PORT, - EVENT_HOMEASSISTANT_STARTED, Platform, ) -from homeassistant.core import CoreState, HomeAssistant +from homeassistant.core import HomeAssistant +from homeassistant.helpers.start import async_at_started from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DEFAULT_PORT, DOMAIN @@ -38,19 +38,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=f"{host}:{port}") - async def async_finish_startup(_): + async def _async_finish_startup(_): await coordinator.async_refresh() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - if hass.state == CoreState.running: - await async_finish_startup(None) - else: - entry.async_on_unload( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, async_finish_startup - ) - ) - + async_at_started(hass, _async_finish_startup) return True From 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 052318b6b10..29b550463d0 100644 --- a/homeassistant/components/airzone_cloud/binary_sensor.py +++ b/homeassistant/components/airzone_cloud/binary_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations 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 b2c9ee173b7..37357bf59da 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 4eab870297b..80c0b4ae027 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -4,6 +4,7 @@ from typing import Any 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 3286dd152e8..d56b2b0ddfa 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -793,7 +793,9 @@ async def async_list_events_service( end = start + service_call.data[EVENT_DURATION] else: end = service_call.data[EVENT_END_DATETIME] - calendar_event_list = await calendar.async_get_events(calendar.hass, start, end) + calendar_event_list = await calendar.async_get_events( + calendar.hass, dt_util.as_local(start), dt_util.as_local(end) + ) return { "events": [ dataclasses.asdict(event, dict_factory=_list_events_dict_factory) diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 92dbf8d47b8..73b45a55640 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -67,14 +67,6 @@ class DemoCalendar(CalendarEntity): end_date: datetime.datetime, ) -> list[CalendarEvent]: """Return calendar events within a datetime range.""" - if start_date.tzinfo is None: - start_date = start_date.replace( - tzinfo=dt_util.get_time_zone(hass.config.time_zone) - ) - if end_date.tzinfo is None: - end_date = end_date.replace( - tzinfo=dt_util.get_time_zone(hass.config.time_zone) - ) assert start_date < end_date if self._event.start_datetime_local >= end_date: return [] diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 463e075d169..e0fbbf0cdeb 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -388,7 +388,17 @@ async def test_create_event_service_invalid_params( @freeze_time("2023-06-22 10:30:00+00:00") -async def test_list_events_service(hass: HomeAssistant, set_time_zone: None) -> None: +@pytest.mark.parametrize( + ("start_time", "end_time"), + [ + ("2023-06-22T04:30:00-06:00", "2023-06-22T06:30:00-06:00"), + ("2023-06-22T04:30:00", "2023-06-22T06:30:00"), + ("2023-06-22T10:30:00Z", "2023-06-22T12:30:00Z"), + ], +) +async def test_list_events_service( + hass: HomeAssistant, set_time_zone: None, start_time: str, end_time: str +) -> None: """Test listing events from the service call using exlplicit start and end time. This test uses a fixed date/time so that it can deterministically test the @@ -398,16 +408,13 @@ async def test_list_events_service(hass: HomeAssistant, set_time_zone: None) -> await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) await hass.async_block_till_done() - start = dt_util.now() - end = start + timedelta(days=1) - response = await hass.services.async_call( DOMAIN, SERVICE_LIST_EVENTS, { "entity_id": "calendar.calendar_1", - "start_date_time": start, - "end_date_time": end, + "start_date_time": start_time, + "end_date_time": end_time, }, blocking=True, return_response=True, From 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 81307c47bba..d25dab4234e 100644 --- a/homeassistant/components/delijn/manifest.json +++ b/homeassistant/components/delijn/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/delijn", "iot_class": "cloud_polling", "loggers": ["pydelijn"], - "requirements": ["pydelijn==1.0.0"] + "requirements": ["pydelijn==1.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index da1672698c7..a91ce36593f 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 f49c92e5438..c6eb36ee88f 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 314e8ffa092..9964bfe148c 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 9efe01cf962..9f464fd4147 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -34,6 +34,7 @@ ALLOW_NAME_TRANSLATION = { "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 669241b05aa..639f47272d9 100644 --- a/homeassistant/components/esphome/alarm_control_panel.py +++ b/homeassistant/components/esphome/alarm_control_panel.py @@ -33,6 +33,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import ( EsphomeEntity, + esphome_state_property, platform_async_setup_entry, ) from .enum_mapper import EsphomeEnumMapper @@ -111,6 +112,7 @@ class EsphomeAlarmControlPanel( self._attr_code_arm_required = bool(static_info.requires_code_to_arm) @property + @esphome_state_property def state(self) -> str | None: """Return the state of the device.""" return _ESPHOME_ACP_STATE_TO_HASS_STATE.from_esphome(self._state.state) diff --git a/tests/components/esphome/test_alarm_control_panel.py b/tests/components/esphome/test_alarm_control_panel.py index ddca7bf60ac..90d7bde5215 100644 --- a/tests/components/esphome/test_alarm_control_panel.py +++ b/tests/components/esphome/test_alarm_control_panel.py @@ -24,6 +24,7 @@ from homeassistant.components.esphome.alarm_control_panel import EspHomeACPFeatu from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant @@ -209,3 +210,38 @@ async def test_generic_alarm_control_panel_no_code( [call(1, AlarmControlPanelCommand.DISARM, None)] ) mock_client.alarm_control_panel_command.reset_mock() + + +async def test_generic_alarm_control_panel_missing_state( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic alarm_control_panel entity that is missing state.""" + entity_info = [ + AlarmControlPanelInfo( + object_id="myalarm_control_panel", + key=1, + name="my alarm_control_panel", + unique_id="my_alarm_control_panel", + supported_features=EspHomeACPFeatures.ARM_AWAY + | EspHomeACPFeatures.ARM_CUSTOM_BYPASS + | EspHomeACPFeatures.ARM_HOME + | EspHomeACPFeatures.ARM_NIGHT + | EspHomeACPFeatures.ARM_VACATION + | EspHomeACPFeatures.TRIGGER, + requires_code=False, + requires_code_to_arm=False, + ) + ] + states = [] + user_service = [] + await mock_generic_device_entry( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("alarm_control_panel.test_my_alarm_control_panel") + assert state is not None + assert state.state == STATE_UNKNOWN From 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 c1d22157a31..fd7a3081f7a 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -17,6 +17,17 @@ from .typing import TemplateVarsType 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 72a7f8de946..3ff7612d47e 100644 --- a/homeassistant/components/zamg/manifest.json +++ b/homeassistant/components/zamg/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zamg", "iot_class": "cloud_polling", - "requirements": ["zamg==0.2.2"] + "requirements": ["zamg==0.2.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index a91ce36593f..bbda184c561 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 d8ca0291a84..e3e39ea557d 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 3c861d8a389..eee176be186 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 0bbbd97c926..d36b3f61d9d 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -157,7 +157,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 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 a30aed22322..f01f8188bc0 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1693,7 +1693,7 @@ class Service: 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 @@ class ServiceCall: 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 @@ class ServiceRegistry: 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 @@ class ServiceRegistry: 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 @@ class ServiceRegistry: 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 @@ class ServiceRegistry: ) 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 f0875bc15d7..9f53aef8165 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230703.0"] + "requirements": ["home-assistant-frontend==20230705.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index eee176be186..39d241bd55d 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 bbda184c561..5e99d5774ce 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 e3e39ea557d..dccc608c1aa 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 72a83850f93..1cd95914808 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 5f66338ecc1..804c0826578 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -16,6 +16,7 @@ from homeassistant.components.vacuum import ( ) 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 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): """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 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): 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 80fbd4092c0..080893f1d95 100644 --- a/tests/components/roborock/test_vacuum.py +++ b/tests/components/roborock/test_vacuum.py @@ -20,7 +20,7 @@ from homeassistant.components.vacuum import ( ) 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 5023393dc5e..d4a026cfefc 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -553,10 +553,10 @@ class Recorder(threading.Thread): 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 f01f8188bc0..252abdb28d4 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1410,6 +1410,8 @@ class State: 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 0bb315365b5..4e9a0261ec2 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -56,6 +56,10 @@ from homeassistant.components.recorder.services import ( 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.common import ( 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 56e931b4345..fa0a14b8fbb 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 cd0b5a2ab88..0c3f0e4469a 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 75402c71325..f2092abef63 100644 --- a/.coveragerc +++ b/.coveragerc @@ -182,7 +182,6 @@ omit = homeassistant/components/crownstone/listeners.py homeassistant/components/cups/sensor.py homeassistant/components/currencylayer/sensor.py - homeassistant/components/daikin/__init__.py homeassistant/components/daikin/climate.py homeassistant/components/daikin/sensor.py homeassistant/components/daikin/switch.py diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 481a072bdb3..b0097f607d5 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -15,8 +15,9 @@ from homeassistant.const import ( CONF_UUID, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -52,6 +53,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not daikin_api: return False + await async_migrate_unique_id(hass, entry, daikin_api) + hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api}) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -67,7 +70,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def daikin_api_setup(hass, host, key, uuid, password): +async def daikin_api_setup(hass: HomeAssistant, host, key, uuid, password): """Create a Daikin instance only once.""" session = async_get_clientsession(hass) @@ -127,3 +130,82 @@ class DaikinApi: name=info.get("name"), sw_version=info.get("ver", "").replace("_", "."), ) + + +async def async_migrate_unique_id( + hass: HomeAssistant, config_entry: ConfigEntry, api: DaikinApi +) -> None: + """Migrate old entry.""" + dev_reg = dr.async_get(hass) + old_unique_id = config_entry.unique_id + new_unique_id = api.device.mac + new_name = api.device.values["name"] + + @callback + def _update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None: + """Update unique ID of entity entry.""" + return update_unique_id(entity_entry, new_unique_id) + + if new_unique_id == old_unique_id: + return + + # Migrate devices + for device_entry in dr.async_entries_for_config_entry( + dev_reg, config_entry.entry_id + ): + for connection in device_entry.connections: + if connection[1] == old_unique_id: + new_connections = { + (CONNECTION_NETWORK_MAC, dr.format_mac(new_unique_id)) + } + + _LOGGER.debug( + "Migrating device %s connections to %s", + device_entry.name, + new_connections, + ) + dev_reg.async_update_device( + device_entry.id, + merge_connections=new_connections, + ) + + if device_entry.name is None: + _LOGGER.debug( + "Migrating device name to %s", + new_name, + ) + dev_reg.async_update_device( + device_entry.id, + name=new_name, + ) + + # Migrate entities + await er.async_migrate_entries(hass, config_entry.entry_id, _update_unique_id) + + new_data = {**config_entry.data, KEY_MAC: dr.format_mac(new_unique_id)} + + hass.config_entries.async_update_entry( + config_entry, unique_id=new_unique_id, data=new_data + ) + + +@callback +def update_unique_id( + entity_entry: er.RegistryEntry, unique_id: str +) -> dict[str, str] | None: + """Update unique ID of entity entry.""" + if entity_entry.unique_id.startswith(unique_id): + # Already correct, nothing to do + return None + + unique_id_parts = entity_entry.unique_id.split("-") + unique_id_parts[0] = unique_id + entity_new_unique_id = "-".join(unique_id_parts) + + _LOGGER.debug( + "Migrating entity %s from %s to new id %s", + entity_entry.entity_id, + entity_entry.unique_id, + entity_new_unique_id, + ) + return {"new_unique_id": entity_new_unique_id} diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 6f90b0cf5ef..c6334dfaeca 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "loggers": ["pydaikin"], "quality_scale": "platinum", - "requirements": ["pydaikin==2.9.0"], + "requirements": ["pydaikin==2.10.5"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index 37b3ec45c4c..847f030fae5 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -42,7 +42,7 @@ async def async_setup_entry( [ DaikinZoneSwitch(daikin_api, zone_id) for zone_id, zone in enumerate(zones) - if zone != ("-", "0") + if zone[0] != "-" ] ) if daikin_api.device.support_advanced_modes: diff --git a/requirements_all.txt b/requirements_all.txt index 5e99d5774ce..5f32008df46 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 dccc608c1aa..3585613fe0c 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 00000000000..8145a7a1e99 --- /dev/null +++ b/tests/components/daikin/test_init.py @@ -0,0 +1,128 @@ +"""Define tests for the Daikin init.""" +import asyncio +from unittest.mock import AsyncMock, PropertyMock, patch + +from aiohttp import ClientConnectionError +import pytest + +from homeassistant.components.daikin import update_unique_id +from homeassistant.components.daikin.const import DOMAIN, KEY_MAC +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .test_config_flow import HOST, MAC + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_daikin(): + """Mock pydaikin.""" + + async def mock_daikin_factory(*args, **kwargs): + """Mock the init function in pydaikin.""" + return Appliance + + with patch("homeassistant.components.daikin.Appliance") as Appliance: + Appliance.factory.side_effect = mock_daikin_factory + type(Appliance).update_status = AsyncMock() + type(Appliance).inside_temperature = PropertyMock(return_value=22) + type(Appliance).target_temperature = PropertyMock(return_value=22) + type(Appliance).zones = PropertyMock(return_value=[("Zone 1", "0", 0)]) + type(Appliance).fan_rate = PropertyMock(return_value=[]) + type(Appliance).swing_modes = PropertyMock(return_value=[]) + yield Appliance + + +DATA = { + "ver": "1_1_8", + "name": "DaikinAP00000", + "mac": MAC, + "model": "NOTSUPPORT", +} + + +INVALID_DATA = {**DATA, "name": None, "mac": HOST} + + +async def test_unique_id_migrate(hass: HomeAssistant, mock_daikin) -> None: + """Test unique id migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=HOST, + title=None, + data={CONF_HOST: HOST, KEY_MAC: HOST}, + ) + config_entry.add_to_hass(hass) + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + type(mock_daikin).mac = PropertyMock(return_value=HOST) + type(mock_daikin).values = PropertyMock(return_value=INVALID_DATA) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.unique_id == HOST + + assert device_registry.async_get_device({}, {(KEY_MAC, HOST)}).name is None + + entity = entity_registry.async_get("climate.daikin_127_0_0_1") + assert entity.unique_id == HOST + assert update_unique_id(entity, MAC) is not None + + assert entity_registry.async_get("switch.none_zone_1").unique_id.startswith(HOST) + + type(mock_daikin).mac = PropertyMock(return_value=MAC) + type(mock_daikin).values = PropertyMock(return_value=DATA) + + assert config_entry.unique_id != MAC + + assert await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.unique_id == MAC + + assert ( + device_registry.async_get_device({}, {(KEY_MAC, MAC)}).name == "DaikinAP00000" + ) + + entity = entity_registry.async_get("climate.daikin_127_0_0_1") + assert entity.unique_id == MAC + assert update_unique_id(entity, MAC) is None + + assert entity_registry.async_get("switch.none_zone_1").unique_id.startswith(MAC) + + +async def test_client_connection_error(hass: HomeAssistant, mock_daikin) -> None: + """Test unique id migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MAC, + data={CONF_HOST: HOST, KEY_MAC: MAC}, + ) + config_entry.add_to_hass(hass) + + mock_daikin.factory.side_effect = ClientConnectionError + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_timeout_error(hass: HomeAssistant, mock_daikin) -> None: + """Test unique id migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MAC, + data={CONF_HOST: HOST, KEY_MAC: MAC}, + ) + config_entry.add_to_hass(hass) + + mock_daikin.factory.side_effect = asyncio.TimeoutError + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_RETRY From 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 f2092abef63..5ba8575979d 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 a7c81543a94..3391d02a829 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -14,6 +14,7 @@ from aioesphomeapi import ( APIVersion, BinarySensorInfo, CameraInfo, + CameraState, ClimateInfo, CoverInfo, DeviceInfo, @@ -339,8 +340,9 @@ class RuntimeEntryData: 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 00000000000..f856a9dd15c --- /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 c6c2d212e2d..22d89b42253 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 00000000000..aabd07366b4 --- /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 767cb94de48..5a7c7863b4e 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -20,6 +20,7 @@ PLATFORMS = [ 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 5622da48e67..4a9c22847ae 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -72,6 +72,7 @@ class FreeboxRouter: 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 @@ class FreeboxRouter: 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 @@ class FreeboxRouter: 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 7bf1cbfe7a4..b950d44508d 100644 --- a/tests/components/freebox/conftest.py +++ b/tests/components/freebox/conftest.py @@ -11,6 +11,7 @@ from .const import ( 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 96fe96c19c5..7028366d02b 100644 --- a/tests/components/freebox/const.py +++ b/tests/components/freebox/const.py @@ -93,75 +93,177 @@ DATA_STORAGE_GET_DISKS = [ { "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 00000000000..08ecfca3794 --- /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 | 66 ++++---- tests/components/vesync/test_init.py | 5 +- tests/components/zha/conftest.py | 6 +- 5 files changed, 77 insertions(+), 156 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 252abdb28d4..60485a678b0 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1726,6 +1726,8 @@ class ServiceCall: 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 849f9e38a68..f471e6f862c 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 test_execute_times_out( # 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 test_execute_times_out( }, 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 f50d4486b39..539a8c61414 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 @@ from homeassistant.components.light import ( 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 @@ from homeassistant.components.light import ( 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.components.light import ( 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() + service_call_events = async_capture_events(hass, EVENT_CALL_SERVICE) - 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() + 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() - 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 - ) + 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 0f77c9cbf35..c643e2bda19 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 271108496b2..e3a12703640 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -332,8 +332,8 @@ def zha_device_mock( @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 c5f7049e54f..e4d9902346c 100644 --- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -138,6 +138,17 @@ class MultiprotocolAddonManager(AddonManager): 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 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): ) -> 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 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): } ) 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 60501397557..06221fc7b97 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 415df2092a1..047130e787c 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 c1069a7e755..617e61336a5 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 83702adcc3a..a956214c098 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 @@ from tests.common import ( ) 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 08ecfca3794..ec504a514ad 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 9f53aef8165..07c5585833d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230705.0"] + "requirements": ["home-assistant-frontend==20230705.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 39d241bd55d..93cdc1eb3d7 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 5f32008df46..5b100318336 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 3585613fe0c..70b6da6bb71 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 | 364 +++++++------------- tests/components/logentries/test_init.py | 46 +-- tests/components/prometheus/test_init.py | 50 +-- tests/components/statsd/test_init.py | 50 ++- 8 files changed, 217 insertions(+), 411 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 60485a678b0..dbc8769bb6f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -973,6 +973,8 @@ _FilterableJobType = tuple[ 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 c42d532b800..76956874e73 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 521ac732e5b..0c6ce300d01 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -8,7 +8,11 @@ from homeassistant.components.ffmpeg import ( 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 @@ class MockFFmpegDev(ffmpeg.FFmpegBase): 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 38c3da79524..0a1d4741268 100644 --- a/tests/components/google_pubsub/test_init.py +++ b/tests/components/google_pubsub/test_init.py @@ -8,8 +8,7 @@ import pytest 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 683c69807b2..a1234b7a470 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) + 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) + 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 @@ async def test_event_listener_backlog_full( 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 0101356e3ed..98b171c813f 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 @@ from homeassistant.setup import async_setup_component 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 d9231732941..8b0acb9c5b0 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -44,7 +44,6 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONTENT_TYPE_TEXT_PLAIN, DEGREE, - EVENT_STATE_CHANGED, PERCENTAGE, STATE_CLOSED, STATE_CLOSING, @@ -59,7 +58,7 @@ from homeassistant.const import ( 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 2b94d8c0790..1b48b6195e5 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 82c212a99a5..f38508ec09c 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -9,8 +9,6 @@ from zwave_js_server.const.command_class.thermostat import ( 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.dispatcher import async_dispatcher_connect 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 @@ from .helpers import get_value_of_zwave_value 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 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): # 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 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): 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 3da63419a4b..606dda30b24 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -35,6 +35,7 @@ CLIMATE_DANFOSS_LC13_ENTITY = "climate.living_connect_z_thermostat" 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 68484111802..0eb4ec775f9 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 00000000000..b5afa1131a8 --- /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 d3f38aaa307..753c107c2ee 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -18,6 +18,7 @@ from homeassistant.components.climate import ( 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.const import ( 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 1cd95914808..e595b7abff4 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 2d31a883e4b..fb9f906527c 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -44,7 +44,7 @@ }, "abort": { "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", - "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "no_devices_found": "No supported SwitchBot devices found in range; If the device is in range, ensure the scanner has active scanning enabled, as SwitchBot devices cannot be discovered with passive scans. Active scans can be disabled once the device is configured. If you need clarification on whether the device is in-range, download the diagnostics for the integration that provides your Bluetooth adapter or proxy and check if the MAC address of the SwitchBot device is present.", "unknown": "[%key:common::config_flow::error::unknown%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "switchbot_unsupported_type": "Unsupported Switchbot Type." From 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 f1bf7deb502..ba922a30b84 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -6,7 +6,15 @@ from typing import Any 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 @@ class EcovacsVacuum(VacuumEntity): """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 - _LOGGER.debug("Vacuum initialized: %s", self.name) + 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.""" @@ -79,9 +85,9 @@ class EcovacsVacuum(VacuumEntity): 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 @@ class EcovacsVacuum(VacuumEntity): self.schedule_update_ha_state() @property - def unique_id(self) -> str: - """Return an unique ID.""" - return self.device.vacuum.get("did") + def state(self) -> str | None: + """Return the state of the vacuum cleaner.""" + if self.error is not None: + return STATE_ERROR - @property - def is_on(self) -> bool: - """Return true if vacuum is currently cleaning.""" - return self.device.is_cleaning + if self.device.is_cleaning: + return STATE_CLEANING - @property - def is_charging(self) -> bool: - """Return true if vacuum is currently charging.""" - return self.device.is_charging + if self.device.is_charging: + return STATE_DOCKED - @property - def status(self) -> str | None: - """Return the status of the vacuum cleaner.""" - return self.device.vacuum_status + if self.device.vacuum_status == sucks.CLEAN_MODE_STOP: + return STATE_IDLE - def return_to_base(self, **kwargs: Any) -> None: - """Set the vacuum cleaner to return to the dock.""" + if self.device.vacuum_status == sucks.CHARGE_MODE_RETURNING: + return STATE_RETURNING - self.device.run(sucks.Charge()) - - @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 @@ class EcovacsVacuum(VacuumEntity): 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 @@ class EcovacsVacuum(VacuumEntity): 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 @@ class EcovacsVacuum(VacuumEntity): ) -> 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 26ca5dedd34..36946b81c0c 100644 --- a/homeassistant/components/qnap/strings.json +++ b/homeassistant/components/qnap/strings.json @@ -19,5 +19,11 @@ "invalid_auth": "Bad authentication", "unknown": "Unknown error" } + }, + "issues": { + "deprecated_yaml": { + "title": "The QNAP YAML configuration is being removed", + "description": "Configuring QNAP using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the QNAP YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } From 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 2216d060f29..a44cfb3ce13 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/rainbird", "iot_class": "local_polling", "loggers": ["pyrainbird"], - "requirements": ["pyrainbird==2.0.0"] + "requirements": ["pyrainbird==2.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5b100318336..3fe51080f28 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 70b6da6bb71..c33cf3f4c99 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 af69882bba5..1f4d6aa3152 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 247b91d1b06..a3bb28e7a8b 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -292,6 +292,8 @@ async def async_setup_entry( class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): """Representation of a generic Xiaomi device.""" + _attr_name = None + def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" super().__init__(device, entry, unique_id, coordinator) diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 82ede87848e..0438b606efd 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -118,6 +118,7 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): _attr_device_class = HumidifierDeviceClass.HUMIDIFIER _attr_supported_features = HumidifierEntityFeature.MODES + _attr_name = None def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" From 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 facdb6752d3..02919baa8f1 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -128,7 +128,7 @@ class MatterLight(MatterEntity, LightEntity): renormalize( brightness, (0, 255), - (level_control.minLevel, level_control.maxLevel), + (level_control.minLevel or 1, level_control.maxLevel or 254), ) ) @@ -220,7 +220,7 @@ class MatterLight(MatterEntity, LightEntity): return round( renormalize( level_control.currentLevel, - (level_control.minLevel or 0, level_control.maxLevel or 254), + (level_control.minLevel or 1, level_control.maxLevel or 254), (0, 255), ) ) From 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 237fc2888ab..80291c73e61 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 55560d3bf8c..a1a75ef8260 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -72,6 +72,7 @@ class NukiDeviceEntity(NukiEntity[_NukiDeviceT], LockEntity): _attr_has_entity_name = True _attr_supported_features = LockEntityFeature.OPEN _attr_translation_key = "nuki_lock" + _attr_name = None @property def unique_id(self) -> str | None: From 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 a2cd2d5fc41..afa40f27afd 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -511,6 +511,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "rqbj": ( TuyaSensorEntityDescription( key=DPCode.GAS_SENSOR_VALUE, + name=None, icon="mdi:gas-cylinder", state_class=SensorStateClass.MEASUREMENT, ), @@ -633,6 +634,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "ylcg": ( TuyaSensorEntityDescription( key=DPCode.PRESSURE_VALUE, + name=None, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), From 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 2210f44bf7a..a0844fe6cdb 100644 --- a/homeassistant/components/flick_electric/sensor.py +++ b/homeassistant/components/flick_electric/sensor.py @@ -34,6 +34,7 @@ class FlickPricingSensor(SensorEntity): _attr_attribution = "Data provided by Flick Electric" _attr_native_unit_of_measurement = f"{CURRENCY_CENT}/{UnitOfEnergy.KILO_WATT_HOUR}" + _attr_has_entity_name = True _attr_translation_key = "power_price" _attributes: dict[str, Any] = {} From 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 715a960de5d..a1dc22ea4a1 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 291a1744d20..a4a9bc5d2b0 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 641d3b8ae4d..c7c6585e002 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -27,8 +27,10 @@ from homeassistant.util.dt import utcnow from .const import DEFAULT_NAME, DOMAIN, PLAYER_EVENT STATE_MAPPING = { - PlayerState.IDLE: MediaPlayerState.IDLE, + PlayerState.STOPPED: MediaPlayerState.IDLE, PlayerState.PLAYING: MediaPlayerState.PLAYING, + PlayerState.BUFFER_READY: MediaPlayerState.PLAYING, + PlayerState.BUFFERING: MediaPlayerState.PLAYING, PlayerState.PAUSED: MediaPlayerState.PAUSED, } From 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 f8d41db0e11..8530aa3b04c 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -608,7 +608,7 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): variables=service.data, context=service.context, wait=True ) if service.return_response: - return response + return response or {} return None async def async_added_to_hass(self) -> None: diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 199c3e08942..cc41b6c404c 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -26,7 +26,7 @@ from homeassistant.core import ( callback, split_entity_id, ) -from homeassistant.exceptions import HomeAssistantError, ServiceNotFound +from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import entity_registry as er, template from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import ( @@ -1625,7 +1625,7 @@ async def test_responses(hass: HomeAssistant, response: Any) -> None: ) -async def test_responses_error(hass: HomeAssistant) -> None: +async def test_responses_no_response(hass: HomeAssistant) -> None: """Test response variable not set.""" mock_restore_cache(hass, ()) assert await async_setup_component( @@ -1645,10 +1645,13 @@ async def test_responses_error(hass: HomeAssistant) -> None: }, ) - with pytest.raises(HomeAssistantError): - assert await hass.services.async_call( + # Validate we can call it with return_response + assert ( + await hass.services.async_call( DOMAIN, "test", {"greeting": "world"}, blocking=True, return_response=True ) + == {} + ) # Validate we can also call it without return_response assert ( await hass.services.async_call( From 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 3d22e2538a3..e87eb15b954 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -772,9 +772,10 @@ class Entity(ABC): ): return name + device_name = device_entry.name_by_user or device_entry.name if self.use_device_name: - return device_entry.name_by_user or device_entry.name - return f"{device_entry.name_by_user or device_entry.name} {name}" + return device_name + return f"{device_name} {name}" if device_name else name @callback def _async_write_ha_state(self) -> None: diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 85a7932aef8..7de6f70e793 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import Context, HomeAssistant, HomeAssistantError from homeassistant.helpers import device_registry as dr, entity, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity -from homeassistant.helpers.typing import UNDEFINED +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from tests.common import ( MockConfigEntry, @@ -989,12 +989,20 @@ async def _test_friendly_name( @pytest.mark.parametrize( - ("has_entity_name", "entity_name", "expected_friendly_name", "warn_implicit_name"), ( - (False, "Entity Blu", "Entity Blu", False), - (False, None, None, False), - (True, "Entity Blu", "Device Bla Entity Blu", False), - (True, None, "Device Bla", False), + "has_entity_name", + "entity_name", + "device_name", + "expected_friendly_name", + "warn_implicit_name", + ), + ( + (False, "Entity Blu", "Device Bla", "Entity Blu", False), + (False, None, "Device Bla", None, False), + (True, "Entity Blu", "Device Bla", "Device Bla Entity Blu", False), + (True, None, "Device Bla", "Device Bla", False), + (True, "Entity Blu", UNDEFINED, "Entity Blu", False), + (True, "Entity Blu", None, "Mock Title Entity Blu", False), ), ) async def test_friendly_name_attr( @@ -1002,6 +1010,7 @@ async def test_friendly_name_attr( caplog: pytest.LogCaptureFixture, has_entity_name: bool, entity_name: str | None, + device_name: str | None | UndefinedType, expected_friendly_name: str | None, warn_implicit_name: bool, ) -> None: @@ -1012,7 +1021,7 @@ async def test_friendly_name_attr( device_info={ "identifiers": {("hue", "1234")}, "connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")}, - "name": "Device Bla", + "name": device_name, }, ) ent._attr_has_entity_name = has_entity_name From 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 a1dc22ea4a1..40bb9650630 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 a4a9bc5d2b0..bc7a93f0f19 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 cf6feb44fbd..42549fb20d9 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -130,14 +130,11 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): @property def _is_netusb(self): - return ( - self.coordinator.data.netusb_input - == self.coordinator.data.zones[self._zone_id].input - ) + return self.coordinator.data.netusb_input == self.source_id @property def _is_tuner(self): - return self.coordinator.data.zones[self._zone_id].input == "tuner" + return self.source_id == "tuner" @property def media_content_id(self): @@ -516,10 +513,15 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): self._zone_id, self.reverse_source_mapping.get(source, source) ) + @property + def source_id(self): + """ID of the current input source.""" + return self.coordinator.data.zones[self._zone_id].input + @property def source(self): """Name of the current input source.""" - return self.source_mapping.get(self.coordinator.data.zones[self._zone_id].input) + return self.source_mapping.get(self.source_id) @property def source_list(self): @@ -597,7 +599,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): return ( self.coordinator.data.group_role == "client" and self.coordinator.data.group_id != NULL_GROUP - and self.source == ATTR_MC_LINK + and self.source_id == ATTR_MC_LINK ) @property @@ -606,7 +608,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): If the media player is not part of a group, False is returned. """ - return self.is_network_client or self.source == ATTR_MAIN_SYNC + return self.is_network_client or self.source_id == ATTR_MAIN_SYNC def get_all_mc_entities(self) -> list[MusicCastMediaPlayer]: """Return all media player entities of the musiccast system.""" @@ -639,11 +641,11 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): and self.coordinator.data.group_id == group_server.coordinator.data.group_id and self.ip_address != group_server.ip_address - and self.source == ATTR_MC_LINK + and self.source_id == ATTR_MC_LINK ) or ( self.ip_address == group_server.ip_address - and self.source == ATTR_MAIN_SYNC + and self.source_id == ATTR_MAIN_SYNC ) ) @@ -859,8 +861,12 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): """ _LOGGER.debug("%s client leave called", self.entity_id) if not force and ( - self.source == ATTR_MAIN_SYNC - or [entity for entity in self.other_zones if entity.source == ATTR_MC_LINK] + self.source_id == ATTR_MAIN_SYNC + or [ + entity + for entity in self.other_zones + if entity.source_id == ATTR_MC_LINK + ] ): await self.coordinator.musiccast.zone_unjoin(self._zone_id) else: From 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 5607cabffea..8a81648b580 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -3,6 +3,7 @@ import asyncio import copy import logging import os +import re import voluptuous as vol from zhaquirks import setup as setup_quirks @@ -85,19 +86,34 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True +def _clean_serial_port_path(path: str) -> str: + """Clean the serial port path, applying corrections where necessary.""" + + if path.startswith("socket://"): + path = path.strip() + + # Removes extraneous brackets from IP addresses (they don't parse in CPython 3.11.4) + if re.match(r"^socket://\[\d+\.\d+\.\d+\.\d+\]:\d+$", path): + path = path.replace("[", "").replace("]", "") + + return path + + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up ZHA. Will automatically load components to support devices found on the network. """ - # Strip whitespace around `socket://` URIs, this is no longer accepted by zigpy - # This will be removed in 2023.7.0 + # Remove brackets around IP addresses, this no longer works in CPython 3.11.4 + # This will be removed in 2023.11.0 path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] + cleaned_path = _clean_serial_port_path(path) data = copy.deepcopy(dict(config_entry.data)) - if path.startswith("socket://") and path != path.strip(): - data[CONF_DEVICE][CONF_DEVICE_PATH] = path.strip() + if path != cleaned_path: + _LOGGER.debug("Cleaned serial port path %r -> %r", path, cleaned_path) + data[CONF_DEVICE][CONF_DEVICE_PATH] = cleaned_path hass.config_entries.async_update_entry(config_entry, data=data) zha_data = hass.data.setdefault(DATA_ZHA, {}) diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index 23a76de4c25..24ee63fb3d5 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -114,19 +114,27 @@ async def test_config_depreciation(hass: HomeAssistant, zha_config) -> None: @pytest.mark.parametrize( ("path", "cleaned_path"), [ + # No corrections ("/dev/path1", "/dev/path1"), + ("/dev/path1[asd]", "/dev/path1[asd]"), ("/dev/path1 ", "/dev/path1 "), + ("socket://1.2.3.4:5678", "socket://1.2.3.4:5678"), + # Brackets around URI + ("socket://[1.2.3.4]:5678", "socket://1.2.3.4:5678"), + # Spaces ("socket://dev/path1 ", "socket://dev/path1"), + # Both + ("socket://[1.2.3.4]:5678 ", "socket://1.2.3.4:5678"), ], ) @patch("homeassistant.components.zha.setup_quirks", Mock(return_value=True)) @patch( "homeassistant.components.zha.websocket_api.async_load_api", Mock(return_value=True) ) -async def test_setup_with_v3_spaces_in_uri( +async def test_setup_with_v3_cleaning_uri( hass: HomeAssistant, path: str, cleaned_path: str ) -> None: - """Test migration of config entry from v3 with spaces after `socket://` URI.""" + """Test migration of config entry from v3, applying corrections to the port path.""" config_entry_v3 = MockConfigEntry( domain=DOMAIN, data={ From 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 024f04b0dc9..6606352ffc8 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -452,9 +452,11 @@ class SmartThingsEntity(Entity): 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 0b697786b18..b6f2159ae13 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 02f6af46655..fe917504dcd 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 715f26beaa7..f8c21166fe1 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 120a90fb2f4..aef9ce319e7 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 713b156fc4f..0e01910c84a 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 6c01bc2b6c4..0d237cec132 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 01745878bf0..cc7b67145c1 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 81bb8579cfd..f90395f0064 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 b61469f6b2a..84d2998e992 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -16,50 +16,50 @@ from .const import DATA_UPDATED, DOMAIN 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 a6571ae7bf0..94d4202ea8c 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 7d584c7c1a8..9f67665d66c 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 @@ SENSORS: tuple[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 @@ SENSORS: tuple[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 a76b4a001e6..3545f62d667 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 2881bf28d8f..eb0b9634e83 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 1da879cb02b..35083c4b089 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -122,6 +122,7 @@ class SwitchBotBlindTiltEntity(SwitchbotEntity, CoverEntity, RestoreEntity): | CoverEntityFeature.STOP_TILT | CoverEntityFeature.SET_TILT_POSITION ) + _attr_name = None _attr_translation_key = "cover" CLOSED_UP_THRESHOLD = 80 CLOSED_DOWN_THRESHOLD = 20 From 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 ed7d825afff..7f5d01f9897 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 9edf6e61751..e8a35ba66f1 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -8,6 +8,8 @@ from homeassistant.components.weather import ( 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 @@ from .const import ( 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 @@ class NWSWeather(WeatherEntity): 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 @@ class NWSWeather(WeatherEntity): 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 3fe51080f28..e214740acec 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 c33cf3f4c99..583c7c96996 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 2048db2a2c3..106b80998ac 100644 --- a/tests/components/nws/const.py +++ b/tests/components/nws/const.py @@ -3,6 +3,8 @@ from homeassistant.components.nws.const import CONF_STATION 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 @@ DEFAULT_OBSERVATION = { "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 @@ DEFAULT_FORECAST = [ "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 @@ EXPECTED_FORECAST_IMPERIAL = { 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 @@ EXPECTED_FORECAST_METRIC = { 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 ce268796639..06d2c2006d8 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -7,6 +7,7 @@ import pytest 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 @@ import homeassistant.util.dt as dt_util 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 23370f8a20c..160f529c285 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 @@ SENSOR_DESCRIPTIONS = [ ), 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 @@ SENSOR_DESCRIPTIONS = [ ), 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 @@ SENSOR_DESCRIPTIONS = [ ), 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 @@ SENSOR_DESCRIPTIONS = [ ), 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 @@ SENSOR_DESCRIPTIONS = [ ), 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 @@ SENSOR_DESCRIPTIONS = [ ), 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 @@ SENSOR_DESCRIPTIONS = [ ), 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 @@ SENSOR_DESCRIPTIONS = [ ), 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 @@ SENSOR_DESCRIPTIONS = [ ), 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 @@ SENSOR_DESCRIPTIONS = [ ), PurpleAirSensorEntityDescription( key="pressure", - name="Pressure", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=UnitOfPressure.MBAR, state_class=SensorStateClass.MEASUREMENT, @@ -144,7 +139,7 @@ SENSOR_DESCRIPTIONS = [ ), 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 @@ SENSOR_DESCRIPTIONS = [ ), PurpleAirSensorEntityDescription( key="temperature", - name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, state_class=SensorStateClass.MEASUREMENT, @@ -162,7 +156,7 @@ SENSOR_DESCRIPTIONS = [ ), 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 @@ SENSOR_DESCRIPTIONS = [ 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 836496d0ca8..5e7c61c1820 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 ee5be0e4617..139a17f5181 100644 --- a/homeassistant/components/rainbird/binary_sensor.py +++ b/homeassistant/components/rainbird/binary_sensor.py @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) 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 ac1ea961870..febb960d652 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 de74943baf9..f5cf2390095 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 @@ _LOGGER = logging.getLogger(__name__) 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 3b5ae332dbd..a98baead976 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 ceca9c71c36..3e2a3115e29 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 816f2a3b969..cfa2c4d2684 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 e9923b1a052..049a5f15c45 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 5ba8575979d..e69683288a2 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 1248c75612f..e6c69e0751e 100644 --- a/homeassistant/components/loqed/__init__.py +++ b/homeassistant/components/loqed/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession 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 00000000000..ee4fa7ecd74 --- /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 6f3316b283f..3d31194f5a6 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 da7009a5744..be57237afdc 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 6513c359a7c..d87a3c07679 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 @@ from tests.test_util.aiohttp import AiohttpClientMocker 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 @@ GOOGLE_TOKEN_URI = "https://oauth2.googleapis.com/token" 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 3613a640f1a..60f35a93d1a 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 @@ SENSOR_TYPES = ( ), 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 @@ SENSOR_TYPES = ( ), 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 4469fd59801..7e68f960fca 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 f38508ec09c..cb027f32e0a 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -9,6 +9,7 @@ from zwave_js_server.const.command_class.thermostat import ( 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 @@ THERMOSTAT_MODES = [ 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 83d86745d90..ef2d7196f04 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -87,7 +87,6 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( device_class=BinarySensorDeviceClass.PLUG, on_key="plugStatus", on_value=PlugState.PLUGGED.value, - translation_key="plugged_in", ), RenaultBinarySensorEntityDescription( key="charging", @@ -95,7 +94,6 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( 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 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( 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 90ad70521df..050c5a930f6 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -165,7 +165,6 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( 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 066b49abcc0..7cf016187be 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 2aceb5e7489..4b2a7dfc72b 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -151,7 +151,7 @@ MOCK_VEHICLES = { }, { 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 @@ MOCK_VEHICLES = { }, { 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 @@ MOCK_VEHICLES = { }, { 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 dc10dd839f0..9625810bedb 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 72f9201b7a4..b4e2f105b3b 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 4025bbd4cc4..6b01cab2bec 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 b9c9b19a93c..ee70420fec0 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 006959d1b4c..a92a5a0f38a 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 b28f741c381..898953c18ac 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 f67097516b4..0722ec1c5e6 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 5879c44db83..8034799a6d0 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 663df02a824..2f61bd95083 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 f2d2e5ef8e1..110a4cabb92 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 54e4f922053..c15b9723c97 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 b69068d3d64..b16d6da6df5 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 0a41f9c7a99..edb26c3622e 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 7a2e371024f..f06bf7ccd59 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 f89497b5ef9..935e38d33d9 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 b77bf5e6900..da4b5217b86 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 eed54ef58c3..2c63a543119 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 9af54311129..46db471305c 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 18a92494242..f0d2787b658 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 53441d365b4..9080b940b2a 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 d3dbbc678b0..c4c1f81109d 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 c3dde16a99f..60d8843c151 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 2bb6c82a8c1..a7934ba4209 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 d8f55dbe4e7..034f1ab315b 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 9cbcf718d73..e8d92a30e2e 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 518b8605aa7..b69e0acf65e 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 e0db3ba4e47..a27a60bba4f 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 b0784279667..6344b5a847a 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 53eca9c7f91..26ccd731828 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 66a74edf8f9..55d167ae253 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import ( CALLBACK_TYPE, + DOMAIN as HOMEASSISTANT_DOMAIN, CoreState, HomeAssistant, ServiceCall, @@ -216,16 +217,27 @@ class EntityPlatform: 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 4da9c25ca10..c4cf0593aae 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 df4f4d1c643..711c333c5ff 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 7e09c3c8147..16c0426d87f 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 0b5640fe3a4..5df34652f2b 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 53b2478490d..79653e1c9bc 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -60,7 +60,6 @@ class ServiceDetails(NamedTuple): SERVICE_HANDLERS = { SERVICE_ENIGMA2: ServiceDetails("media_player", "enigma2"), "yamaha": ServiceDetails("media_player", "yamaha"), - "openhome": ServiceDetails("media_player", "openhome"), "bluesound": ServiceDetails("media_player", "bluesound"), } @@ -87,6 +86,7 @@ MIGRATED_SERVICE_HANDLERS = [ SERVICE_MOBILE_APP, SERVICE_NETGEAR, SERVICE_OCTOPRINT, + "openhome", "philips_hue", SERVICE_SAMSUNG_PRINTER, "sonos", From 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 13a04515143..9d895f35eb7 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 e262665dd63..2c324ca7093 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 840802a12b7..cf24ec5115c 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 f9c73443d00..318798fdc5f 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -21,7 +21,6 @@ from homeassistant.core import HomeAssistant from .const import ( COLLECTION_TYPE_MOVIES, COLLECTION_TYPE_MUSIC, - COLLECTION_TYPE_TVSHOWS, DOMAIN, ITEM_KEY_COLLECTION_TYPE, ITEM_KEY_ID, @@ -155,10 +154,7 @@ class JellyfinSource(MediaSource): 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 423e4ad3950..671c9881ae0 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 00000000000..b748b125e4a --- /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 00000000000..e557018a89e --- /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 00000000000..95e59d33820 --- /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 00000000000..bb57ef451a2 --- /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 00000000000..49f30434eac --- /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 00000000000..31b2fe76558 --- /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 90ad63a39e4..c58074d999f 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 00000000000..9d8d2a8231a --- /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 00000000000..1a3c262440d --- /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 00000000000..47eaddd4cfc --- /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 00000000000..78706456b9b --- /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 00000000000..0ae91d7badd --- /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 00000000000..b8fb80042f3 --- /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 00000000000..dc070d78352 --- /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 00000000000..3209ccfb2c4 --- /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 00000000000..879680ec591 --- /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 00000000000..e9297549387 --- /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 00000000000..75e87e1a05b --- /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 00000000000..02509f13196 --- /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 00000000000..b3beaa1d758 --- /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 00000000000..63a0fd9deaf --- /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 00000000000..0817352edae --- /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 00000000000..5d97447808a --- /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 00000000000..6d629f245a0 --- /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 542be0736c7..56e352bd71f 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 00000000000..5f8871e6242 --- /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 c8c3e9b12ba..5911c453c95 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 160f529c285..fffceffa343 100644 --- a/homeassistant/components/purpleair/sensor.py +++ b/homeassistant/components/purpleair/sensor.py @@ -168,6 +168,7 @@ SENSOR_DESCRIPTIONS = [ # 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 128c5c3a493..16166278565 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 c7c6585e002..9bd9f7668c8 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -90,6 +90,7 @@ class SlimProtoPlayer(MediaPlayerEntity): | MediaPlayerEntityFeature.BROWSE_MEDIA ) _attr_device_class = MediaPlayerDeviceClass.SPEAKER + _attr_name = None def __init__(self, slimserver: SlimServer, player: SlimClient) -> None: """Initialize MediaPlayer entity.""" From 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 4883734f47e..5989fb1cfe3 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -28,11 +28,11 @@ SENSOR_TYPE_NEXT_PICKUP = "next_pickup" 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 a350b9880fc..20aa5982f0d 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 b17d81f9174..5d880370818 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 160cd0e8634..64f7dafc1b7 100644 --- a/homeassistant/components/mystrom/__init__.py +++ b/homeassistant/components/mystrom/__init__.py @@ -22,6 +22,24 @@ PLATFORMS_BULB = [Platform.LIGHT] _LOGGER = logging.getLogger(__name__) +async def _async_get_device_state( + device: MyStromSwitch | MyStromBulb, ip_address: str +) -> None: + try: + await device.get_state() + except MyStromConnectionError as err: + _LOGGER.error("No route to myStrom plug: %s", ip_address) + raise ConfigEntryNotReady() from err + + +def _get_mystrom_bulb(host: str, mac: str) -> MyStromBulb: + return MyStromBulb(host, mac) + + +def _get_mystrom_switch(host: str) -> MyStromSwitch: + return MyStromSwitch(host) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up myStrom from a config entry.""" host = entry.data[CONF_HOST] @@ -34,12 +52,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_type = info["type"] if device_type in [101, 106, 107]: - device = MyStromSwitch(host) + device = _get_mystrom_switch(host) platforms = PLATFORMS_SWITCH - elif device_type == 102: + await _async_get_device_state(device, info["ip"]) + elif device_type in [102, 105]: mac = info["mac"] - device = MyStromBulb(host, mac) + device = _get_mystrom_bulb(host, mac) platforms = PLATFORMS_BULB + await _async_get_device_state(device, info["ip"]) if device.bulb_type not in ["rgblamp", "strip"]: _LOGGER.error( "Device %s (%s) is not a myStrom bulb nor myStrom LED Strip", @@ -51,12 +71,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Unsupported myStrom device type: %s", device_type) return False - try: - await device.get_state() - except MyStromConnectionError as err: - _LOGGER.error("No route to myStrom plug: %s", info["ip"]) - raise ConfigEntryNotReady() from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = MyStromData( device=device, info=info, @@ -69,10 +83,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" device_type = hass.data[DOMAIN][entry.entry_id].info["type"] + platforms = [] if device_type in [101, 106, 107]: - platforms = PLATFORMS_SWITCH - elif device_type == 102: - platforms = PLATFORMS_BULB + platforms.extend(PLATFORMS_SWITCH) + elif device_type in [102, 105]: + platforms.extend(PLATFORMS_BULB) if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms): hass.data[DOMAIN].pop(entry.entry_id) diff --git a/tests/components/mystrom/__init__.py b/tests/components/mystrom/__init__.py index f0cc6224191..8b3e2f8535f 100644 --- a/tests/components/mystrom/__init__.py +++ b/tests/components/mystrom/__init__.py @@ -1 +1,172 @@ """Tests for the myStrom integration.""" +from typing import Any, Optional + + +def get_default_device_response(device_type: int) -> dict[str, Any]: + """Return default device response.""" + return { + "version": "2.59.32", + "mac": "6001940376EB", + "type": device_type, + "ssid": "personal", + "ip": "192.168.0.23", + "mask": "255.255.255.0", + "gw": "192.168.0.1", + "dns": "192.168.0.1", + "static": False, + "connected": True, + "signal": 94, + } + + +def get_default_bulb_state() -> dict[str, Any]: + """Get default bulb state.""" + return { + "type": "rgblamp", + "battery": False, + "reachable": True, + "meshroot": True, + "on": False, + "color": "46;18;100", + "mode": "hsv", + "ramp": 10, + "power": 0.45, + "fw_version": "2.58.0", + } + + +def get_default_switch_state() -> dict[str, Any]: + """Get default switch state.""" + return { + "power": 1.69, + "Ws": 0.81, + "relay": True, + "temperature": 24.87, + "version": "2.59.32", + "mac": "6001940376EB", + "ssid": "personal", + "ip": "192.168.0.23", + "mask": "255.255.255.0", + "gw": "192.168.0.1", + "dns": "192.168.0.1", + "static": False, + "connected": True, + "signal": 94, + } + + +class MyStromDeviceMock: + """Base device mock.""" + + def __init__(self, state: dict[str, Any]) -> None: + """Initialize device mock.""" + self._requested_state = False + self._state = state + + async def get_state(self) -> None: + """Set if state is requested.""" + self._requested_state = True + + +class MyStromBulbMock(MyStromDeviceMock): + """MyStrom Bulb mock.""" + + def __init__(self, mac: str, state: dict[str, Any]) -> None: + """Initialize bulb mock.""" + super().__init__(state) + self.mac = mac + + @property + def firmware(self) -> Optional[str]: + """Return current firmware.""" + if not self._requested_state: + return None + return self._state["fw_version"] + + @property + def consumption(self) -> Optional[float]: + """Return current firmware.""" + if not self._requested_state: + return None + return self._state["power"] + + @property + def color(self) -> Optional[str]: + """Return current color settings.""" + if not self._requested_state: + return None + return self._state["color"] + + @property + def mode(self) -> Optional[str]: + """Return current mode.""" + if not self._requested_state: + return None + return self._state["mode"] + + @property + def transition_time(self) -> Optional[int]: + """Return current transition time (ramp).""" + if not self._requested_state: + return None + return self._state["ramp"] + + @property + def bulb_type(self) -> Optional[str]: + """Return the type of the bulb.""" + if not self._requested_state: + return None + return self._state["type"] + + @property + def state(self) -> Optional[bool]: + """Return the current state of the bulb.""" + if not self._requested_state: + return None + return self._state["on"] + + +class MyStromSwitchMock(MyStromDeviceMock): + """MyStrom Switch mock.""" + + @property + def relay(self) -> Optional[bool]: + """Return the relay state.""" + if not self._requested_state: + return None + return self._state["on"] + + @property + def consumption(self) -> Optional[float]: + """Return the current power consumption in mWh.""" + if not self._requested_state: + return None + return self._state["power"] + + @property + def consumedWs(self) -> Optional[float]: + """The average of energy consumed per second since last report call.""" + if not self._requested_state: + return None + return self._state["Ws"] + + @property + def firmware(self) -> Optional[str]: + """Return the current firmware.""" + if not self._requested_state: + return None + return self._state["version"] + + @property + def mac(self) -> Optional[str]: + """Return the MAC address.""" + if not self._requested_state: + return None + return self._state["mac"] + + @property + def temperature(self) -> Optional[float]: + """Return the current temperature in celsius.""" + if not self._requested_state: + return None + return self._state["temperature"] diff --git a/tests/components/mystrom/test_init.py b/tests/components/mystrom/test_init.py index 01b52d2cb94..281d7af9947 100644 --- a/tests/components/mystrom/test_init.py +++ b/tests/components/mystrom/test_init.py @@ -2,11 +2,19 @@ from unittest.mock import AsyncMock, PropertyMock, patch from pymystrom.exceptions import MyStromConnectionError +import pytest from homeassistant.components.mystrom.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from . import ( + MyStromBulbMock, + MyStromSwitchMock, + get_default_bulb_state, + get_default_device_response, + get_default_switch_state, +) from .conftest import DEVICE_MAC from tests.common import MockConfigEntry @@ -16,30 +24,21 @@ async def init_integration( hass: HomeAssistant, config_entry: MockConfigEntry, device_type: int, - bulb_type: str = "strip", ) -> None: """Inititialize integration for testing.""" with patch( "pymystrom.get_device_info", - side_effect=AsyncMock(return_value={"type": device_type, "mac": DEVICE_MAC}), - ), patch("pymystrom.switch.MyStromSwitch.get_state", return_value={}), patch( - "pymystrom.bulb.MyStromBulb.get_state", return_value={} + side_effect=AsyncMock(return_value=get_default_device_response(device_type)), ), patch( - "pymystrom.bulb.MyStromBulb.bulb_type", bulb_type + "homeassistant.components.mystrom._get_mystrom_bulb", + return_value=MyStromBulbMock("6001940376EB", get_default_bulb_state()), ), patch( - "pymystrom.switch.MyStromSwitch.mac", - new_callable=PropertyMock, - return_value=DEVICE_MAC, - ), patch( - "pymystrom.bulb.MyStromBulb.mac", - new_callable=PropertyMock, - return_value=DEVICE_MAC, + "homeassistant.components.mystrom._get_mystrom_switch", + return_value=MyStromSwitchMock(get_default_switch_state()), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED - async def test_init_switch_and_unload( hass: HomeAssistant, config_entry: MockConfigEntry @@ -56,12 +55,35 @@ async def test_init_switch_and_unload( assert not hass.data.get(DOMAIN) -async def test_init_bulb(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: +@pytest.mark.parametrize( + ("device_type", "platform", "entry_state", "entity_state_none"), + [ + (101, "switch", ConfigEntryState.LOADED, False), + (102, "light", ConfigEntryState.LOADED, False), + (103, "button", ConfigEntryState.SETUP_ERROR, True), + (104, "button", ConfigEntryState.SETUP_ERROR, True), + (105, "light", ConfigEntryState.LOADED, False), + (106, "switch", ConfigEntryState.LOADED, False), + (107, "switch", ConfigEntryState.LOADED, False), + (110, "sensor", ConfigEntryState.SETUP_ERROR, True), + (113, "switch", ConfigEntryState.SETUP_ERROR, True), + (118, "button", ConfigEntryState.SETUP_ERROR, True), + (120, "switch", ConfigEntryState.SETUP_ERROR, True), + ], +) +async def test_init_bulb( + hass: HomeAssistant, + config_entry: MockConfigEntry, + device_type: int, + platform: str, + entry_state: ConfigEntryState, + entity_state_none: bool, +) -> None: """Test the initialization of a myStrom bulb.""" - await init_integration(hass, config_entry, 102) - state = hass.states.get("light.mystrom_device") - assert state is not None - assert config_entry.state is ConfigEntryState.LOADED + await init_integration(hass, config_entry, device_type) + state = hass.states.get(f"{platform}.mystrom_device") + assert (state is None) == entity_state_none + assert config_entry.state is entry_state async def test_init_of_unknown_bulb( @@ -120,7 +142,7 @@ async def test_init_cannot_connect_because_of_get_state( """Test error handling for failing get_state.""" with patch( "pymystrom.get_device_info", - side_effect=AsyncMock(return_value={"type": 101, "mac": DEVICE_MAC}), + side_effect=AsyncMock(return_value=get_default_device_response(101)), ), patch( "pymystrom.switch.MyStromSwitch.get_state", side_effect=MyStromConnectionError() ), patch( @@ -129,4 +151,4 @@ async def test_init_cannot_connect_because_of_get_state( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state == ConfigEntryState.SETUP_RETRY From 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 a811894a0c2..f603cf0ccd7 100644 --- a/homeassistant/components/rainmachine/update.py +++ b/homeassistant/components/rainmachine/update.py @@ -62,6 +62,7 @@ class RainMachineUpdateEntity(RainMachineEntity, UpdateEntity): """Define a RainMachine update entity.""" _attr_device_class = UpdateDeviceClass.FIRMWARE + _attr_name = None _attr_supported_features = ( UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS From 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 47680714d19..4a71789423f 100644 --- a/homeassistant/components/upb/light.py +++ b/homeassistant/components/upb/light.py @@ -49,9 +49,10 @@ async def async_setup_entry( class UpbLight(UpbAttachedEntity, LightEntity): - """Representation of an UPB Light.""" + """Representation of a UPB Light.""" _attr_has_entity_name = True + _attr_name = None def __init__(self, element, unique_id, upb): """Initialize an UpbLight.""" diff --git a/homeassistant/components/upb/scene.py b/homeassistant/components/upb/scene.py index fe6f07199c4..d1272b7a1f6 100644 --- a/homeassistant/components/upb/scene.py +++ b/homeassistant/components/upb/scene.py @@ -47,7 +47,7 @@ async def async_setup_entry( class UpbLink(UpbEntity, Scene): - """Representation of an UPB Link.""" + """Representation of a UPB Link.""" def __init__(self, element, unique_id, upb): """Initialize the base of all UPB devices.""" From 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 7ec1bf40c66..5d1419db8b2 100644 --- a/homeassistant/components/pi_hole/binary_sensor.py +++ b/homeassistant/components/pi_hole/binary_sensor.py @@ -8,7 +8,6 @@ from typing import Any 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 336c1cbc7ef..c3d0852e17a 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 e214740acec..daf6db5cfda 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 583c7c96996..b2cf1c3df1f 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 60db0322717..3794b27cc0e 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 00000000000..ce907a3368d --- /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 6f6639cecb4..db21a690984 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 aef33a6f8bf..c7ebf8a0a3b 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 9ac7453093c..ee7abb3e979 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 f8d578e1ca4..fc50e8141a1 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 6305f68efd9..f0a607f2da7 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 8fc16926e56..1acf0f1154e 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 daf6db5cfda..d038879b629 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 b2cf1c3df1f..531ce982f08 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 33650cfc2fe..7f93db67c4c 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 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), 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 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), 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 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), 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 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), 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 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), 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 d4ed17c72e9..82829094957 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 f482deb4ef4..2a5bc93f601 100644 --- a/homeassistant/components/rainmachine/select.py +++ b/homeassistant/components/rainmachine/select.py @@ -59,7 +59,7 @@ TYPE_FREEZE_PROTECTION_TEMPERATURE = "freeze_protection_temperature" 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 22943d73fcb..6333dcc82f4 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 @@ SENSOR_DESCRIPTIONS = ( ), 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 @@ SENSOR_DESCRIPTIONS = ( ), 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 @@ SENSOR_DESCRIPTIONS = ( ), 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 @@ SENSOR_DESCRIPTIONS = ( ), 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 @@ SENSOR_DESCRIPTIONS = ( ), 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 @@ SENSOR_DESCRIPTIONS = ( ), 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 @@ SENSOR_DESCRIPTIONS = ( ), 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 9991fd31e03..884d05359a6 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 60db5085951..e6ed92d04dc 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -161,14 +161,14 @@ TYPE_RESTRICTIONS_HOT_DAYS_EXTRA_WATERING = "hot_days_extra_watering" 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 f603cf0ccd7..372319ba9a0 100644 --- a/homeassistant/components/rainmachine/update.py +++ b/homeassistant/components/rainmachine/update.py @@ -44,7 +44,7 @@ UPDATE_STATE_MAP = { UPDATE_DESCRIPTION = RainMachineEntityDescription( key="update", - name="Firmware", + translation_key="firmware", api_category=DATA_MACHINE_FIRMWARE_UPDATE_STATUS, ) @@ -52,7 +52,7 @@ UPDATE_DESCRIPTION = RainMachineEntityDescription( 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 bff02df5c6c..d53d6a0890f 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 9964bfe148c..e8271f2cade 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 700757c6d58..b681678b098 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 @@ SENSORS: tuple[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 @@ SENSORS: tuple[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 @@ SENSORS: tuple[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 @@ SENSORS: tuple[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 @@ SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( ), PVOutputSensorEntityDescription( key="temperature", - name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -94,7 +93,6 @@ SENSORS: tuple[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 12f30b773d5..06d98971053 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 15e41043f5a..0cab59de291 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 3c6ede66c69..b332be7de2d 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -15,6 +15,7 @@ from homeassistant.components.vacuum import ( 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 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): 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 9e97c2f080f..0ecf0e7b697 100644 --- a/homeassistant/components/yalexs_ble/lock.py +++ b/homeassistant/components/yalexs_ble/lock.py @@ -29,6 +29,7 @@ class YaleXSBLELock(YALEXSBLEEntity, LockEntity): """A yale xs ble lock.""" _attr_has_entity_name = True + _attr_name = None @callback def _async_update_state( From 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 80675d9dec8..2c1a3ecee11 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 b6e7adc9631..2909d6c1b9b 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 c8fa05b2730..f559b09a1ff 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 cb11c64f16a..237a2d97668 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -25,12 +25,12 @@ BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { ), "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 @@ BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { ), "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 b5b34bf54ec..e9e434bc51c 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -67,7 +67,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { ), "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 fb9f906527c..c00f2fe79e4 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 a0ae9d925a8..b1df158a260 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 8b8e9b8a427..c07a083ac52 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 93cdc1eb3d7..d4cdafd7ec1 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 d038879b629..8d949afdbbd 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 531ce982f08..470191cba22 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 dbc8769bb6f..82ea7228157 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -16,7 +16,6 @@ from collections.abc import ( ) import concurrent.futures from contextlib import suppress -from contextvars import ContextVar import datetime import enum import functools @@ -155,8 +154,6 @@ MAX_EXPECTED_ENTITY_IDS = 16384 _LOGGER = logging.getLogger(__name__) -_cv_hass: ContextVar[HomeAssistant] = ContextVar("hass") - @functools.lru_cache(MAX_EXPECTED_ENTITY_IDS) def split_entity_id(entity_id: str) -> tuple[str, str]: @@ -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 cea8a866f5c..e8f1e58615c 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -93,7 +93,7 @@ from homeassistant.core import ( split_entity_id, valid_entity_id, ) -from homeassistant.exceptions import TemplateError +from homeassistant.exceptions import HomeAssistantError, TemplateError from homeassistant.generated import currencies from homeassistant.generated.countries import COUNTRIES from homeassistant.generated.languages import LANGUAGES @@ -609,7 +609,7 @@ def template(value: Any | None) -> template_helper.Template: raise vol.Invalid("template value should be a string") hass: HomeAssistant | None = None - with contextlib.suppress(LookupError): + with contextlib.suppress(HomeAssistantError): hass = async_get_hass() template_value = template_helper.Template(str(value), hass) @@ -631,7 +631,7 @@ def dynamic_template(value: Any | None) -> template_helper.Template: raise vol.Invalid("template value does not contain a dynamic template") hass: HomeAssistant | None = None - with contextlib.suppress(LookupError): + with contextlib.suppress(HomeAssistantError): hass = async_get_hass() template_value = template_helper.Template(str(value), hass) @@ -1098,7 +1098,7 @@ def _no_yaml_config_schema( # pylint: disable-next=import-outside-toplevel from .issue_registry import IssueSeverity, async_create_issue - with contextlib.suppress(LookupError): + with contextlib.suppress(HomeAssistantError): hass = async_get_hass() async_create_issue( hass, diff --git a/tests/conftest.py b/tests/conftest.py index 56014d7a556..922e42c7a7e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -490,17 +490,7 @@ def hass_fixture_setup() -> list[bool]: @pytest.fixture -def hass(_hass: HomeAssistant) -> HomeAssistant: - """Fixture to provide a test instance of Home Assistant.""" - # This wraps the async _hass fixture inside a sync fixture, to ensure - # the `hass` context variable is set in the execution context in which - # the test itself is executed - ha._cv_hass.set(_hass) - return _hass - - -@pytest.fixture -async def _hass( +async def hass( hass_fixture_setup: list[bool], event_loop: asyncio.AbstractEventLoop, load_registries: bool, diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 458774b748c..5ea6df42349 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -12,6 +12,7 @@ import voluptuous as vol import homeassistant from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( config_validation as cv, issue_registry as ir, @@ -383,7 +384,7 @@ def test_service() -> None: schema("homeassistant.turn_on") -def test_service_schema() -> None: +def test_service_schema(hass: HomeAssistant) -> None: """Test service_schema validation.""" options = ( {}, @@ -1550,10 +1551,10 @@ def test_config_entry_only_schema_cant_find_module() -> None: def test_config_entry_only_schema_no_hass( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: - """Test if the the hass context var is not set in our context.""" + """Test if the the hass context is not set in our context.""" with patch( "homeassistant.helpers.config_validation.async_get_hass", - side_effect=LookupError, + side_effect=HomeAssistantError, ): cv.config_entry_only_config_schema("test_domain")( {"test_domain": {"foo": "bar"}} diff --git a/tests/test_core.py b/tests/test_core.py index 8b63eab7b42..7e0766c8ac5 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -9,10 +9,12 @@ import gc import logging import os from tempfile import TemporaryDirectory +import threading import time from typing import Any from unittest.mock import MagicMock, Mock, PropertyMock, patch +import async_timeout import pytest import voluptuous as vol @@ -40,6 +42,7 @@ from homeassistant.core import ( ServiceResponse, State, SupportsResponse, + callback, ) from homeassistant.exceptions import ( HomeAssistantError, @@ -202,6 +205,184 @@ def test_async_run_hass_job_delegates_non_async() -> None: assert len(hass.async_add_hass_job.mock_calls) == 1 +async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: + """Test calling async_get_hass via different paths. + + The test asserts async_get_hass can be called from: + - Coroutines and callbacks + - Callbacks scheduled from callbacks, coroutines and threads + - Coroutines scheduled from callbacks, coroutines and threads + + The test also asserts async_get_hass can not be called from threads + other than the event loop. + """ + task_finished = asyncio.Event() + + def can_call_async_get_hass() -> bool: + """Test if it's possible to call async_get_hass.""" + try: + if ha.async_get_hass() is hass: + return True + raise Exception + except HomeAssistantError: + return False + + raise Exception + + # Test scheduling a coroutine which calls async_get_hass via hass.async_create_task + async def _async_create_task() -> None: + task_finished.set() + assert can_call_async_get_hass() + + hass.async_create_task(_async_create_task(), "create_task") + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a callback which calls async_get_hass via hass.async_add_job + @callback + def _add_job() -> None: + assert can_call_async_get_hass() + task_finished.set() + + hass.async_add_job(_add_job) + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a callback which calls async_get_hass from a callback + @callback + def _schedule_callback_from_callback() -> None: + @callback + def _callback(): + assert can_call_async_get_hass() + task_finished.set() + + # Test the scheduled callback itself can call async_get_hass + assert can_call_async_get_hass() + hass.async_add_job(_callback) + + _schedule_callback_from_callback() + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a coroutine which calls async_get_hass from a callback + @callback + def _schedule_coroutine_from_callback() -> None: + async def _coroutine(): + assert can_call_async_get_hass() + task_finished.set() + + # Test the scheduled callback itself can call async_get_hass + assert can_call_async_get_hass() + hass.async_add_job(_coroutine()) + + _schedule_coroutine_from_callback() + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a callback which calls async_get_hass from a coroutine + async def _schedule_callback_from_coroutine() -> None: + @callback + def _callback(): + assert can_call_async_get_hass() + task_finished.set() + + # Test the coroutine itself can call async_get_hass + assert can_call_async_get_hass() + hass.async_add_job(_callback) + + await _schedule_callback_from_coroutine() + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a coroutine which calls async_get_hass from a coroutine + async def _schedule_callback_from_coroutine() -> None: + async def _coroutine(): + assert can_call_async_get_hass() + task_finished.set() + + # Test the coroutine itself can call async_get_hass + assert can_call_async_get_hass() + await hass.async_create_task(_coroutine()) + + await _schedule_callback_from_coroutine() + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a callback which calls async_get_hass from an executor + def _async_add_executor_job_add_job() -> None: + @callback + def _async_add_job(): + assert can_call_async_get_hass() + task_finished.set() + + # Test the executor itself can not call async_get_hass + assert not can_call_async_get_hass() + hass.add_job(_async_add_job) + + await hass.async_add_executor_job(_async_add_executor_job_add_job) + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a coroutine which calls async_get_hass from an executor + def _async_add_executor_job_create_task() -> None: + async def _async_create_task() -> None: + assert can_call_async_get_hass() + task_finished.set() + + # Test the executor itself can not call async_get_hass + assert not can_call_async_get_hass() + hass.create_task(_async_create_task()) + + await hass.async_add_executor_job(_async_add_executor_job_create_task) + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a callback which calls async_get_hass from a worker thread + class MyJobAddJob(threading.Thread): + @callback + def _my_threaded_job_add_job(self) -> None: + assert can_call_async_get_hass() + task_finished.set() + + def run(self) -> None: + # Test the worker thread itself can not call async_get_hass + assert not can_call_async_get_hass() + hass.add_job(self._my_threaded_job_add_job) + + my_job_add_job = MyJobAddJob() + my_job_add_job.start() + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + my_job_add_job.join() + + # Test scheduling a coroutine which calls async_get_hass from a worker thread + class MyJobCreateTask(threading.Thread): + async def _my_threaded_job_create_task(self) -> None: + assert can_call_async_get_hass() + task_finished.set() + + def run(self) -> None: + # Test the worker thread itself can not call async_get_hass + assert not can_call_async_get_hass() + hass.create_task(self._my_threaded_job_create_task()) + + my_job_create_task = MyJobCreateTask() + my_job_create_task.start() + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + my_job_create_task.join() + + async def test_stage_shutdown(hass: HomeAssistant) -> None: """Simulate a shutdown, test calling stuff.""" test_stop = async_capture_events(hass, EVENT_HOMEASSISTANT_STOP) From 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 d7af3b66688..bac4e63e288 100644 --- a/homeassistant/components/buienradar/strings.json +++ b/homeassistant/components/buienradar/strings.json @@ -301,55 +301,55 @@ "name": "Condition 1d", "state": { "clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]", - "cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]", - "snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]", - "lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]" + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "condition_2d": { "name": "Condition 2d", "state": { "clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]", - "cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]", - "snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]", - "lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]" + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "condition_3d": { "name": "Condition 3d", "state": { "clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]", - "cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]", - "snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]", - "lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]" + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "condition_4d": { "name": "Condition 4d", "state": { "clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]", - "cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]", - "snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]", - "lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]" + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "condition_5d": { "name": "Condition 5d", "state": { "clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]", - "cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]", - "snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]", - "lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]" + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "conditioncode_1d": { @@ -371,76 +371,76 @@ "name": "Detailed condition 1d", "state": { "clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]", - "partlycloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy%]", + "partlycloudy": "[%key:component::weather::entity_component::_::state::partlycloudy%]", "partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]", "partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]", "partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]", - "cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]", + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", "light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]", "light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]", "partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]", "partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]", "partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]", - "snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]", - "snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]", - "lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]" + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "conditiondetailed_2d": { "name": "Detailed condition 2d", "state": { "clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]", - "partlycloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy%]", + "partlycloudy": "[%key:component::weather::entity_component::_::state::partlycloudy%]", "partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]", "partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]", "partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]", - "cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]", + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", "light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]", "light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]", "partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]", "partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]", "partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]", - "snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]", - "snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]", - "lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]" + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "conditiondetailed_3d": { "name": "Detailed condition 3d", "state": { "clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]", - "partlycloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy%]", + "partlycloudy": "[%key:component::weather::entity_component::_::state::partlycloudy%]", "partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]", "partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]", "partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]", - "cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]", + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", "light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]", "light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]", "partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]", "partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]", "partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]", - "snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]", - "snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]", - "lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]" + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "conditiondetailed_4d": { "name": "Detailed condition 4d", "state": { "clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]", - "partlycloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy%]", + "partlycloudy": "[%key:component::weather::entity_component::_::state::partlycloudy%]", "partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]", "partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]", "partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]", - "cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]", + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", "light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]", "light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]", "partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]", @@ -455,21 +455,21 @@ "name": "Detailed condition 5d", "state": { "clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]", - "partlycloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy%]", + "partlycloudy": "[%key:component::weather::entity_component::_::state::partlycloudy%]", "partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]", "partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]", "partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]", - "cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]", + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", "light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]", "light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]", "partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]", "partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]", "partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]", - "snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]", - "snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]", - "lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]" + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "conditionexact_1d": { From 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 4913d76c0c9..b895be83f2e 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -127,6 +127,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) + _attr_name = None def __init__(self, simplisafe: SimpliSafe, system: SystemType) -> None: """Initialize the SimpliSafe alarm.""" diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index d31dc5da282..34c0ea5ea95 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -111,7 +111,6 @@ class BatteryBinarySensor(SimpliSafeEntity, BinarySensorEntity): """Initialize.""" super().__init__(simplisafe, system, device=device) - self._attr_name = "Battery" self._attr_unique_id = f"{super().unique_id}-battery" self._device: DeviceV3 diff --git a/homeassistant/components/simplisafe/button.py b/homeassistant/components/simplisafe/button.py index d8da8bc7592..bd60c040f56 100644 --- a/homeassistant/components/simplisafe/button.py +++ b/homeassistant/components/simplisafe/button.py @@ -44,7 +44,7 @@ async def _async_clear_notifications(system: System) -> None: BUTTON_DESCRIPTIONS = ( SimpliSafeButtonDescription( key=BUTTON_KIND_CLEAR_NOTIFICATIONS, - name="Clear notifications", + translation_key=BUTTON_KIND_CLEAR_NOTIFICATIONS, push_action=_async_clear_notifications, ), ) diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 618c21566f7..4f230442f85 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -29,5 +29,12 @@ } } } + }, + "entity": { + "button": { + "clear_notifications": { + "name": "Clear notifications" + } + } } } From 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 88bcdd4987b..f1bfc7de876 100644 --- a/homeassistant/components/goalzero/manifest.json +++ b/homeassistant/components/goalzero/manifest.json @@ -16,5 +16,5 @@ "iot_class": "local_polling", "loggers": ["goalzero"], "quality_scale": "silver", - "requirements": ["goalzero==0.2.1"] + "requirements": ["goalzero==0.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8d949afdbbd..977bfcb31fc 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 470191cba22..3a7dbbf3c0c 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 3c7ed643841..95527257ea9 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 61ec45a98f9..9001824d678 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -72,7 +72,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( 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 47fbb29915b..90b1489803a 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 b54509ea5e3..d14a6bcb390 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 9a56f5d91eb..04dcef05202 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 977bfcb31fc..f57bf13ac7d 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 3a7dbbf3c0c..9a2602d6b7e 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 74369507229..2ea19443aa4 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 e69683288a2..0d44a63633a 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 fedb2edd899..fb13e86dd1d 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 731743e48c8..ecd49718559 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -27,9 +27,10 @@ from homeassistant.core import callback 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 a53bb2db8ed..f0e3972f197 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 00000000000..b87d3ac3899 --- /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 ffd87691b38..1dcdc559de7 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -19,14 +19,14 @@ import pytest 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_generic_device_entry( 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 662816a53d8..f5b7795a57b 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -18,15 +18,15 @@ import pytest 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 c1df7c024cd..a77fd9b0087 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 84bafc9fd84..d3d47a40d66 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 dd0daf1c455..53ae72e375e 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -15,7 +15,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send @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 08750d06dd0..322e057ec15 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -8,7 +8,6 @@ from aioesphomeapi import VoiceAssistantEventType 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 test_pipeline_events( ) 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 @@ async def test_no_speech( 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 b22e2996f7e..277aa10075e 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -514,8 +514,8 @@ class Camera(Entity): @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 3a562296a50..bb19d2e1655 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -9,7 +9,7 @@ }, "iot_class": "local_push", "loggers": ["pywemo"], - "requirements": ["pywemo==0.9.1"], + "requirements": ["pywemo==1.1.0"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index f57bf13ac7d..cc0922a0337 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 9a2602d6b7e..e6a3baf8136 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 b6901e1b81b..9d9eef49b36 100644 --- a/homeassistant/components/amberelectric/__init__.py +++ b/homeassistant/components/amberelectric/__init__.py @@ -4,9 +4,10 @@ from amberelectric import Configuration 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 32cec180dbc..fb95cd1c41e 100644 --- a/tests/components/amberelectric/test_binary_sensor.py +++ b/tests/components/amberelectric/test_binary_sensor.py @@ -11,11 +11,11 @@ from dateutil import parser 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 7a35b2c1c7e..286345dba10 100644 --- a/tests/components/amberelectric/test_sensor.py +++ b/tests/components/amberelectric/test_sensor.py @@ -7,11 +7,11 @@ from amberelectric.model.range import Range 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 36626c2324e..d0a65799807 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 7a504a0db84..d18226e5af9 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -30,7 +30,7 @@ from .entity import PlugwiseEntity 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="temperature", - name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, @@ -62,7 +61,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="illuminance", - name="Illuminance", native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, @@ -347,7 +344,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), 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 afc921f1101..e1b5b5c4053 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 2480353c2d3..34e1d89b8b4 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -85,6 +85,13 @@ NO_STORE = re.compile( # 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 @@ class HassIOView(HomeAssistantView): ) 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 @@ class HassIOView(HomeAssistantView): 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 fc92e9309a0..2a9d9b73978 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -17,11 +17,32 @@ from yarl import URL 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 @@ class HassIOIngress(HomeAssistantView): 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 @@ class HassIOIngress(HomeAssistantView): 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 06b7523614c..6df946ad2cf 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 69438dc17f1..5668f941c6e 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -82,6 +82,7 @@ SUPPORTED_FEATURES = ( | 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 15be22ddfbf..fa43ac76ea6 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -51,6 +51,7 @@ from homeassistant.helpers.event import async_track_state_change_event 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 @@ class MediaPlayerGroup(MediaPlayerEntity): 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 @@ class MediaPlayerGroup(MediaPlayerEntity): 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 @@ class MediaPlayerGroup(MediaPlayerEntity): | 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 9ad33caf073..3b6f5bcdd2f 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -52,6 +52,7 @@ BASE_SUPPORTED_FEATURES = ( | 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 1cc90aa4904..f96d2a012c8 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 5a513e4f3a0..536d229dbda 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 526ddd2bcc7..c519d237100 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 d3fae39bc4d..d57ba8ba49d 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 4549a7f5fec..3524c0f1e88 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 f55dc9344ab..00667c43fdb 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 @@ class NoboZone(ClimateEntity): 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 c5536bad6ea..9cc957ec1df 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 @@ class NoboTemperatureSensor(SensorEntity): 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 a86f95d6187..0bce3251d5a 100644 --- a/homeassistant/components/airzone_cloud/diagnostics.py +++ b/homeassistant/components/airzone_cloud/diagnostics.py @@ -7,6 +7,7 @@ from typing import Any from aioairzone_cloud.const import ( API_CITY, API_GROUP_ID, + API_GROUPS, API_LOCATION_ID, API_OLD_ID, API_PIN, @@ -29,7 +30,6 @@ from .coordinator import AirzoneUpdateCoordinator 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 8602dfa14cf..289565f0473 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 cc0922a0337..a4a4cf09bbd 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 e6a3baf8136..1a79cab57c4 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 730ac27325a..6c8ae366518 100644 --- a/tests/components/airzone_cloud/test_diagnostics.py +++ b/tests/components/airzone_cloud/test_diagnostics.py @@ -5,9 +5,11 @@ from unittest.mock import patch 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 @@ RAW_DATA_MOCK = { 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 80c0b4ae027..a8cb539bb1d 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -16,6 +16,7 @@ from aioairzone_cloud.const import ( API_DISCONNECTION_DATE, API_ERRORS, API_FAH, + API_GROUP_ID, API_GROUPS, API_HUMIDITY, API_INSTALLATION_ID, @@ -61,6 +62,7 @@ CONFIG = { GET_INSTALLATION_MOCK = { API_GROUPS: [ { + API_GROUP_ID: "grp1", API_NAME: "Group", API_DEVICES: [ { @@ -94,6 +96,7 @@ GET_INSTALLATION_MOCK = { ], }, { + 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 0bf3952c4ed..c3022b87eb7 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 5fddd2d78fe..0ce8af4f3a6 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 e8271f2cade..84a670d3759 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 9f464fd4147..5f233b4dec8 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -31,6 +31,7 @@ ALLOW_NAME_TRANSLATION = { "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 f95c2660164..c5705c34076 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -148,7 +148,9 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): 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 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): 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 e6753b39dce..fcebc7638c6 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 84a670d3759..c7f842748c2 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 52d5aca070a..db3bea9cad3 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 9c1b748c9ac..d498f0a2c14 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 740d107d625..992c58cf5f6 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 2f8e20464f3..8ab42c6d3e9 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 84c0f769c7c..abd4d2e623e 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 @@ ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema( # 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 c518ad227a7..fd2dba4b084 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 | 62 +++++++++++++--------- tests/components/esphome/test_camera.py | 3 -- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 94a9b03b90c..f3fb8b867d8 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 @@ class EsphomeCamera(Camera, EsphomeEntity[CameraInfo, CameraState]): """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.""" - return await camera.async_get_still_stream( - request, self._async_camera_stream_image, camera.DEFAULT_CONTENT_TYPE, 0.0 + stream_request = partial( + self._async_request_image, self._client.request_image_stream + ) + return await camera.async_get_still_stream( + 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 f856a9dd15c..94ff4c6e7a8 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 2ea19443aa4..baae4698b1e 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 8bfda778c79..42f9fdb05d5 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 a4a4cf09bbd..c724b59ba74 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 1a79cab57c4..b0a6df91055 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 e57267a1658..08f45b94789 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 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionBinarySensorEntityDescription, ...] = ( 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 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionBinarySensorEntityDescription, ...] = ( 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 @@ DEVICE_SENSOR_TYPES: tuple[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 1406d9d26c7..b47023f3ec4 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 8b46e3e7941..3696f618fd7 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -49,6 +49,8 @@ def async_handle_api_call( class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]): """Representation of a Sensibo Base Entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: SensiboDataUpdateCoordinator, @@ -68,8 +70,6 @@ class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]): class SensiboDeviceBaseEntity(SensiboBaseEntity): """Representation of a Sensibo Device.""" - _attr_has_entity_name = True - def __init__( self, coordinator: SensiboDataUpdateCoordinator, @@ -93,8 +93,6 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity): 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 c39026265c7..94765a17a4d 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 @@ DEVICE_NUMBER_TYPES = ( ), 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 29ebdc89261..cda8a972ede 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -41,7 +41,6 @@ DEVICE_SELECT_TYPES = ( 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 @@ DEVICE_SELECT_TYPES = ( 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 69d6a8cb78b..7208902456e 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 @@ FILTER_LAST_RESET_DESCRIPTION = 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 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( 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 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( 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 @@ PURE_SENSOR_TYPES: tuple[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 @@ PURE_SENSOR_TYPES: tuple[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 @@ DEVICE_SENSOR_TYPES: tuple[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 @@ DEVICE_SENSOR_TYPES: tuple[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 @@ ELEMENT_SENSOR_TYPES: tuple[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 @@ ELEMENT_SENSOR_TYPES: tuple[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 @@ ELEMENT_SENSOR_TYPES: tuple[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 b00c4200836..2379e2c2b38 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 ee9c946268f..20167ddd184 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 @@ DEVICE_SWITCH_TYPES: tuple[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 @@ DEVICE_SWITCH_TYPES: tuple[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 4cfb6058740..46b9b860ca6 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 bb190908847..99bcfac8c9b 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 64f7dafc1b7..972db00e476 100644 --- a/homeassistant/components/mystrom/__init__.py +++ b/homeassistant/components/mystrom/__init__.py @@ -50,6 +50,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("No route to myStrom plug: %s", host) raise ConfigEntryNotReady() from err + info.setdefault("type", 101) + device_type = info["type"] if device_type in [101, 106, 107]: device = _get_mystrom_switch(host) diff --git a/tests/components/mystrom/__init__.py b/tests/components/mystrom/__init__.py index 8b3e2f8535f..21f6bd7a549 100644 --- a/tests/components/mystrom/__init__.py +++ b/tests/components/mystrom/__init__.py @@ -2,12 +2,11 @@ from typing import Any, Optional -def get_default_device_response(device_type: int) -> dict[str, Any]: +def get_default_device_response(device_type: int | None) -> dict[str, Any]: """Return default device response.""" - return { + response = { "version": "2.59.32", "mac": "6001940376EB", - "type": device_type, "ssid": "personal", "ip": "192.168.0.23", "mask": "255.255.255.0", @@ -17,6 +16,9 @@ def get_default_device_response(device_type: int) -> dict[str, Any]: "connected": True, "signal": 94, } + if device_type is not None: + response["type"] = device_type + return response def get_default_bulb_state() -> dict[str, Any]: diff --git a/tests/components/mystrom/test_init.py b/tests/components/mystrom/test_init.py index 281d7af9947..80011b47915 100644 --- a/tests/components/mystrom/test_init.py +++ b/tests/components/mystrom/test_init.py @@ -44,7 +44,7 @@ async def test_init_switch_and_unload( hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test the initialization of a myStrom switch.""" - await init_integration(hass, config_entry, 101) + await init_integration(hass, config_entry, 106) state = hass.states.get("switch.mystrom_device") assert state is not None assert config_entry.state is ConfigEntryState.LOADED @@ -58,7 +58,7 @@ async def test_init_switch_and_unload( @pytest.mark.parametrize( ("device_type", "platform", "entry_state", "entity_state_none"), [ - (101, "switch", ConfigEntryState.LOADED, False), + (None, "switch", ConfigEntryState.LOADED, False), (102, "light", ConfigEntryState.LOADED, False), (103, "button", ConfigEntryState.SETUP_ERROR, True), (104, "button", ConfigEntryState.SETUP_ERROR, True), From 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 b190e6151ed..4e65fdfc26d 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 f14129cf156..13b48644ffd 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 52c89f3f34b..bc7c7d97430 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 134ce00ef70..ea02bfedefb 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 48c57fb5d03..4f139785cd3 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 88a2a6c9b0f..b38bc93567d 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 a19760ad989..2176aa0c91e 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 e9b2e9e2e9c..ed8638d8419 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 2fdf15a4a10..33080a9c1a2 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 672bd899962..36d0a67fded 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 d4cdafd7ec1..b38bd073eaf 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 c724b59ba74..e990e899435 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 b0a6df91055..819899f96cf 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 6b49307d439..fa55b352f61 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -19,12 +19,11 @@ from .entity import SkybellEntity 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 b9aba0e82ac..1e510687a02 100644 --- a/homeassistant/components/skybell/camera.py +++ b/homeassistant/components/skybell/camera.py @@ -17,8 +17,14 @@ from .coordinator import SkybellDataUpdateCoordinator 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 311122c28e7..70fe01fdb5e 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 4658f0f99c0..130196a990d 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 @@ SENSOR_TYPES: tuple[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 @@ SENSOR_TYPES: tuple[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 @@ SENSOR_TYPES: tuple[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 4289c3ed3c3..28a66df2d02 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 b3cb8c53032..f67cca41ac9 100644 --- a/homeassistant/components/skybell/switch.py +++ b/homeassistant/components/skybell/switch.py @@ -14,15 +14,15 @@ from .entity import SkybellEntity 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 799949a462a..7ced3487269 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -43,6 +43,7 @@ class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity): AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) + _attr_name = None def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None: """Initialize the Yale Alarm Device.""" diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index fde08d08fbd..397a9cc8db1 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -40,6 +40,8 @@ async def async_setup_entry( class YaleDoorlock(YaleEntity, LockEntity): """Representation of a Yale doorlock.""" + _attr_name = None + def __init__( self, coordinator: YaleDataUpdateCoordinator, data: dict, code_format: int ) -> None: From 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 f9f9365eb8e..fbd2625961b 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 51560161faa..0814958ad27 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 7153dac1bcb..15e04ffca93 100644 --- a/homeassistant/components/workday/config_flow.py +++ b/homeassistant/components/workday/config_flow.py @@ -3,7 +3,8 @@ from __future__ import annotations 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 e018eaa588e..698ef17902f 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 e990e899435..70285de2588 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 819899f96cf..f712da5d6ed 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 a6c526a6a7f..bd0a6d30369 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -12,7 +12,17 @@ import voluptuous as vol 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 @@ from .util import resolve_db_url _LOGGER = logging.getLogger(__name__) +NONE_SENTINEL = "none" + OPTIONS_SCHEMA: vol.Schema = vol.Schema( { vol.Optional( @@ -39,6 +51,34 @@ OPTIONS_SCHEMA: vol.Schema = vol.Schema( 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 @@ class SQLConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): 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 2a8ea80580b..96fc4bc943a 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 6888652cb4c..74c165e9d20 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 9927a9734cd..a1417cd38df 100644 --- a/tests/components/sql/__init__.py +++ b/tests/components/sql/__init__.py @@ -27,6 +27,8 @@ ENTRY_CONFIG = { 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 8958454ac62..915394863ea 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -7,6 +7,8 @@ from sqlalchemy.exc import SQLAlchemyError 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 a6aa18c9294..0fe0e881c95 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 2b3006eeb3b..86c7f8343df 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 | 12 ++++++------ tests/components/sensibo/test_sensor.py | 8 ++++---- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index f99792f7dc1..f90b887d04c 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 70285de2588..3197a1c60b3 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 f712da5d6ed..5b7464db638 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 b2108d3e6f4..4e856d396c1 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": [ - "cool", - "heat", - "dry", "heat_cool", + "cool", + "dry", "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 003c2f27903..24dbdef1fe3 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 91f4940a4e5..b38c1d3829b 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -20,5 +20,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/bthome", "iot_class": "local_push", - "requirements": ["bthome-ble==2.12.0"] + "requirements": ["bthome-ble==2.12.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3197a1c60b3..4e4e439217d 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 5b7464db638..cabbc0768c2 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 00000000000..703ad671799 --- /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 00000000000..f68197f9fe5 --- /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 29bd9b4f6a9..9e86fc8b36e 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -163,11 +163,17 @@ class KNXCover(KnxEntity, CoverEntity): 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 4ee9bd04eee..2d2b72e9015 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 | 15 ++--- requirements_test_pre_commit.txt | 2 +- tests/util/test_timeout.py | 62 ++++++++++---------- 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c662c6754f4..f85f8583a04 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 8fea717e6bb..ce07741c37f 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -210,9 +210,10 @@ class AmcrestChecker(ApiWrapper): 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 ec8431366ab..e2614af6a3e 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 + 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 + 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 eff26bcfe82..4047daf73cf 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 e89c6cd3f02..f301cd3c634 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 @@ async def test_simple_zone_timeout_freeze_inside_executor_job( 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 @@ async def test_mix_global_timeout_freeze_and_zone_freeze_inside_executor_job( 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 @@ async def test_mix_global_timeout_freeze_and_zone_freeze_other_zone_inside_execu 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 eaa1acc11bf..0a9b0254f91 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 6c8910cd7f9..05b53acba5f 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -597,24 +597,34 @@ DHCP: list[dict[str, str | bool]] = [ }, { "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 @@ DHCP: list[dict[str, str | bool]] = [ "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 baae4698b1e..e6c805a64c1 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 d2c01bbd4f3..ab7207f0ac4 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 @@ class RingBinarySensor(RingEntityMixin, BinarySensorEntity): 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 e99fabfab2f..0b3f1509b18 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 @@ class RingCam(RingEntityMixin, Camera): 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 16aa86511be..5fc438c2390 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 143c333f600..2604e557b79 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 @@ class RingLight(RingEntityMixin, LightEntity): 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 3d198ce7573..fbaeb8a4b5b 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -13,7 +13,6 @@ from homeassistant.config_entries import ConfigEntry 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 @@ class RingSensor(RingEntityMixin, SensorEntity): """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 @@ class RingSensor(RingEntityMixin, SensorEntity): 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 @@ SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( ), RingSensorEntityDescription( key="last_ding", - name="Last Ding", + translation_key="last_ding", category=["doorbots", "authorized_doorbots"], icon="mdi:history", kind="ding", @@ -229,7 +213,7 @@ SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( ), 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 626444a9dcf..7f1b147471d 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 c5b448ad68b..43209a5a6a3 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 9a3c80114e9..43bd303577a 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -52,11 +52,6 @@ class BaseRingSwitch(RingEntityMixin, SwitchEntity): 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 @@ class BaseRingSwitch(RingEntityMixin, SwitchEntity): 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 a2eb72c2711..7607f9fa5db 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 a33b9a0d732..468b4f0d0ec 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 8398995462f..818fd338ffb 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations 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 @@ SENSOR_DESCRIPTIONS = [ 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 e595b7abff4..70ed98a6d5f 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 daa904d482a..f9f3d327d29 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -14,7 +14,7 @@ from tests.common import MockConfigEntry 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 cd84bec11b2..5b0bc4d4c63 100644 --- a/homeassistant/components/stookwijzer/sensor.py +++ b/homeassistant/components/stookwijzer/sensor.py @@ -33,6 +33,7 @@ class StookwijzerSensor(SensorEntity): _attr_attribution = "Data provided by stookwijzer.nu" _attr_device_class = SensorDeviceClass.ENUM _attr_has_entity_name = True + _attr_name = None _attr_translation_key = "stookwijzer" def __init__(self, client: Stookwijzer, entry: ConfigEntry) -> None: From 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 d44d66bbd47..a5ccb78baed 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 09515dfd4c8..740716db78e 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 68f14c64a69..887f0ba0491 100644 --- a/tests/components/speedtestdotnet/test_sensor.py +++ b/tests/components/speedtestdotnet/test_sensor.py @@ -3,8 +3,6 @@ from unittest.mock import MagicMock 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 e8c237114b5..c30098f254b 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -74,7 +74,7 @@ from .const import ( ) 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 @@ from .schema import ( 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 @@ class KNXModule: 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 a9f5341fbfd..bdc480851c3 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -53,7 +53,7 @@ CONF_KNX_DEFAULT_RATE_LIMIT: Final = 0 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 09307794066..87c1a8b6052 100644 --- a/homeassistant/components/knx/telegrams.py +++ b/homeassistant/components/knx/telegrams.py @@ -3,8 +3,7 @@ from __future__ import annotations 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 import Telegram 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 @@ class Telegrams: """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 @@ class Telegrams: ) 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 @@ class Telegrams: 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 ca804176ee9..5463892a3ef 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 785ff9d8317..a5d3d0f3263 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 00000000000..964b9ea2a11 --- /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 22d1c5042f5..87614460096 100644 --- a/homeassistant/components/starlink/binary_sensor.py +++ b/homeassistant/components/starlink/binary_sensor.py @@ -60,64 +60,63 @@ class StarlinkBinarySensorEntity(StarlinkEntity, BinarySensorEntity): 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 43e276332c8..2df9d9b033b 100644 --- a/homeassistant/components/starlink/button.py +++ b/homeassistant/components/starlink/button.py @@ -58,7 +58,6 @@ class StarlinkButtonEntity(StarlinkEntity, ButtonEntity): 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 a1cc60da79e..efcf92600b8 100644 --- a/homeassistant/components/starlink/sensor.py +++ b/homeassistant/components/starlink/sensor.py @@ -68,7 +68,7 @@ class StarlinkSensorEntity(StarlinkEntity, SensorEntity): 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 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( ), StarlinkSensorEntityDescription( key="azimuth", - name="Azimuth", + translation_key="azimuth", icon="mdi:compass", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -88,7 +88,7 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( ), StarlinkSensorEntityDescription( key="elevation", - name="Elevation", + translation_key="elevation", icon="mdi:compass", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -99,7 +99,7 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( ), 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 dddbada730d..48f84ea7baf 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 daa7b45b305..31932fe9854 100644 --- a/homeassistant/components/starlink/switch.py +++ b/homeassistant/components/starlink/switch.py @@ -69,7 +69,7 @@ class StarlinkSwitchEntity(StarlinkEntity, SwitchEntity): 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 07a545bd5d7..b6c538bf9fb 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 3936435f84e..b6a70e32865 100644 --- a/homeassistant/components/bluetooth/advertisement_tracker.py +++ b/homeassistant/components/bluetooth/advertisement_tracker.py @@ -18,6 +18,8 @@ TRACKER_BUFFERING_WOBBLE_SECONDS = 5 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 d1fcb115180..ce778e0309b 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 3aedd6f2deb..55d995dd63c 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 0edab3ce77b..12bdba66d75 100644 --- a/tests/components/bluetooth/test_usage.py +++ b/tests/components/bluetooth/test_usage.py @@ -15,7 +15,7 @@ from homeassistant.components.bluetooth.wrappers import ( ) 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 a644797c1be..be2e5b99159 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 72029acc2f1..9d998e739f0 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 00000000000..a3ec6952c6c --- /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 2cbd20a7437..c3e1625d623 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 @@ from tests.typing import ClientSessionGenerator 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 59b8b4b070e..7cdf6767362 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 4e4e439217d..ce569e14c1b 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 cabbc0768c2..480302b7e7e 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 1c4b9afb08d..def44d382ce 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 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { ), "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 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { ), "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 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { ), "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 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { ), "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 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { ), "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 b8537e11442..5b17f3283e8 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 | 54 ++++--- tests/components/ipp/test_sensor.py | 86 +++++------ 9 files changed, 290 insertions(+), 242 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 feda6554210..f66630b2a69 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 @@ MOCK_ZEROCONF_IPPS_SERVICE_INFO = zeroconf.ZeroconfServiceInfo( 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 00000000000..de3f1e0e73c --- /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 32060b4df86..f502c30068c 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 - - -async def test_unload_config_entry( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test the IPP configuration entry unloading.""" - entry = await init_integration(hass, aioclient_mock) - - assert hass.data[DOMAIN] - assert entry.entry_id in hass.data[DOMAIN] - assert entry.state is ConfigEntryState.LOADED - - await hass.config_entries.async_unload(entry.entry_id) + 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 entry.entry_id not in hass.data[DOMAIN] - assert entry.state is ConfigEntryState.NOT_LOADED + assert mock_request.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_ipp: AsyncMock, +) -> None: + """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 mock_config_entry.entry_id in hass.data[DOMAIN] + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + 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 diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index f8dd94ffc72..ebebd18bc72 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 unittest.mock import AsyncMock -from homeassistant.components.ipp.const import DOMAIN -from homeassistant.components.sensor import ( - ATTR_OPTIONS as SENSOR_ATTR_OPTIONS, - DOMAIN as SENSOR_DOMAIN, -) +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 e8de285138e..455619182ab 100644 --- a/homeassistant/components/bluetooth/base_scanner.py +++ b/homeassistant/components/bluetooth/base_scanner.py @@ -303,7 +303,19 @@ class BaseHaRemoteScanner(BaseHaScanner): ) -> 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 @@ class BaseHaRemoteScanner(BaseHaScanner): 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 | 32 ++++++--- tests/components/esphome/test_config_flow.py | 66 +++++++++++++++++++ 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index ecd49718559..9ed7ad7123d 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -55,6 +55,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): 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 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = {"name": self._name} async def _async_try_fetch_device_info(self) -> FlowResult: - error = await self.fetch_device_info() + """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 error == ERROR_REQUIRES_ENCRYPTION_KEY: + 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 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): 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 f5b7795a57b..28d411be939 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 14a81f2c665..2af0cb30f1e 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -2,10 +2,12 @@ from __future__ import annotations from pyrainbird.async_client import AsyncRainbirdClient, AsyncRainbirdController +from pyrainbird.exceptions import RainbirdApiException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_SERIAL_NUMBER @@ -29,11 +31,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_PASSWORD], ) ) + try: + model_info = await controller.get_model_and_version() + except RainbirdApiException as err: + raise ConfigEntryNotReady from err coordinator = RainbirdUpdateCoordinator( hass, name=entry.title, controller=controller, serial_number=entry.data[CONF_SERIAL_NUMBER], + model_info=model_info, ) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/rainbird/coordinator.py b/homeassistant/components/rainbird/coordinator.py index b503e72d3a6..d76ac78f7e9 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -9,6 +9,7 @@ from typing import TypeVar import async_timeout from pyrainbird.async_client import AsyncRainbirdController, RainbirdApiException +from pyrainbird.data import ModelAndVersion from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo @@ -42,6 +43,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): name: str, controller: AsyncRainbirdController, serial_number: str, + model_info: ModelAndVersion, ) -> None: """Initialize ZoneStateUpdateCoordinator.""" super().__init__( @@ -54,6 +56,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): self._controller = controller self._serial_number = serial_number self._zones: set[int] | None = None + self._model_info = model_info @property def controller(self) -> AsyncRainbirdController: @@ -72,6 +75,8 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): name=f"{MANUFACTURER} Controller", identifiers={(DOMAIN, self._serial_number)}, manufacturer=MANUFACTURER, + model=self._model_info.model_name, + sw_version=f"{self._model_info.major}.{self._model_info.minor}", ) async def _async_update_data(self) -> RainbirdDeviceState: diff --git a/tests/components/rainbird/conftest.py b/tests/components/rainbird/conftest.py index 22f238ce553..21ad5230581 100644 --- a/tests/components/rainbird/conftest.py +++ b/tests/components/rainbird/conftest.py @@ -35,6 +35,8 @@ SERIAL_NUMBER = 0x12635436566 # Get serial number Command 0x85. Serial is 0x12635436566 SERIAL_RESPONSE = "850000012635436566" +# Model and version command 0x82 +MODEL_AND_VERSION_RESPONSE = "820006090C" # Get available stations command 0x83 AVAILABLE_STATIONS_RESPONSE = "83017F000000" # Mask for 7 zones EMPTY_STATIONS_RESPONSE = "830000000000" @@ -183,7 +185,13 @@ def mock_api_responses( These are returned in the order they are requested by the update coordinator. """ - return [stations_response, zone_state_response, rain_response, rain_delay_response] + return [ + MODEL_AND_VERSION_RESPONSE, + stations_response, + zone_state_response, + rain_response, + rain_delay_response, + ] @pytest.fixture(name="responses") diff --git a/tests/components/rainbird/test_number.py b/tests/components/rainbird/test_number.py index 4bf214c50f7..2ecdfcc537f 100644 --- a/tests/components/rainbird/test_number.py +++ b/tests/components/rainbird/test_number.py @@ -70,6 +70,8 @@ async def test_set_value( device = device_registry.async_get_device({(DOMAIN, SERIAL_NUMBER)}) assert device assert device.name == "Rain Bird Controller" + assert device.model == "ST8x-WiFi" + assert device.sw_version == "9.12" aioclient_mock.mock_calls.clear() responses.append(mock_response(ACK_ECHO)) From 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 1c5d25dfb3d..473e08f5c80 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 b38bd073eaf..16b25353183 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 ce569e14c1b..48538b013e0 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 480302b7e7e..8b3e3a26165 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 8a5f53d52de..f025013cc2b 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 @@ class LogEntry: 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 @@ class DedupStore(OrderedDict): 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 0d44a63633a..2e10d1be257 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 092e8ea08d6..2645139f702 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -46,23 +46,22 @@ from homeassistant.helpers import ( 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 @@ class SignalUpdateCallback: 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 7ae3e0db943..3f8c99d7658 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 .camera_sdm import async_setup_sdm_entry -from .const import DATA_SDM -from .legacy.camera import async_setup_legacy_entry +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_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 3eceb448fa4..00000000000 --- 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 372909d00c2..307bd201b4d 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 ca975ed055d..00000000000 --- 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 d20057f4e28..381cc36449d 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -9,15 +9,10 @@ some overrides to custom steps inserted in the middle of the flow. """ 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 @@ from google_nest_sdm.structure import InfoTrait, Structure 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 @@ DEVICE_ACCESS_CONSOLE_EDIT_URL = ( _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 @@ class NestFlowHandler( # 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 @@ class NestFlowHandler( 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 @@ class NestFlowHandler( 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 @@ class NestFlowHandler( 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 @@ class NestFlowHandler( 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 @@ class NestFlowHandler( 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 @@ class NestFlowHandler( 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 88d046fb62b..00000000000 --- 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 5c412b86dbd..00000000000 --- 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 e74f23aeaf6..00000000000 --- 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 323633e0ee3..00000000000 --- 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 664606b9edc..00000000000 --- 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 a091469cd81..00000000000 --- 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 3c397f3d1f4..00000000000 --- 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 dbb30ceb52a..54bc44a09b3 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 a9073aec80d..aa170710eb6 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 a74d0f3a54b..00000000000 --- 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 a452d015a2b..b9069db8e48 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 48538b013e0..7a2b1497ab7 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 8b3e3a26165..c482150e9ce 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 897961d9f98..00000000000 --- 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 530e3695d11..408e4e0d963 100644 --- a/tests/components/nest/test_diagnostics.py +++ b/tests/components/nest/test_diagnostics.py @@ -9,8 +9,6 @@ from homeassistant.config_entries import ConfigEntryState 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 db560e44e83..ecfe412bdbf 100644 --- a/tests/components/nest/test_init_sdm.py +++ b/tests/components/nest/test_init.py @@ -22,15 +22,20 @@ import pytest 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 f27382d0345..00000000000 --- 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 6ba704e6c3e..00000000000 --- 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 18bf782eaf2..209dece71ab 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 f85f8583a04..6f9a24d6db0 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 4047daf73cf..f1dde9ca022 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 1acf0f1154e..63bd2ffc081 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 7a2b1497ab7..415685b2f5a 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 c482150e9ce..6dc2fdacfe2 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 f559b09a1ff..68602e34d3e 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -600,7 +600,7 @@ class FastUrlDispatcher(UrlDispatcher): 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 @@ class FastUrlDispatcher(UrlDispatcher): 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 2645139f702..5f2a0b0bffd 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 b9069db8e48..86650bbbe9a 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 ee70420fec0..abe7efee3ed 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 5f233b4dec8..56609e57fd9 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 331a1bc151a..368a3eb6e98 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 9570c4a4628..ecc561f0355 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 71fc7d848ea..75dca4ed840 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 c03b5a3f841..b110e53ee64 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 abe7efee3ed..185482d62e3 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 b16d6da6df5..0f3b88fd7f2 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 f06bf7ccd59..7512b2abec7 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 6315fffb193..8097eb6336b 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 935e38d33d9..6219ade3e58 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 2c63a543119..67c92d7ce07 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 5fdf580c6aa..617d813007c 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 f0d2787b658..bf8a669af50 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 a7934ba4209..70cd45f4d21 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 6344b5a847a..b0a625d0016 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 c4cf0593aae..51a5636092a 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 5668f941c6e..686a9dbbde9 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -82,6 +82,7 @@ SUPPORTED_FEATURES = ( | 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 fa43ac76ea6..b271e57cb8a 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -50,6 +50,7 @@ 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 +KEY_ANNOUNCE = "announce" KEY_CLEAR_PLAYLIST = "clear_playlist" KEY_ENQUEUE = "enqueue" KEY_ON_OFF = "on_off" @@ -116,6 +117,7 @@ class MediaPlayerGroup(MediaPlayerEntity): 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 @@ class MediaPlayerGroup(MediaPlayerEntity): 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 @@ class MediaPlayerGroup(MediaPlayerEntity): | 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 f96d2a012c8..9ad7b983c7f 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 536d229dbda..21807262742 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 c519d237100..08f2b08f4df 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 3524c0f1e88..2a1a2a05e4e 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 15c136f17c3..2cfbc537dbb 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -23,6 +23,7 @@ from homeassistant.const import ( 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 @@ async def platform_async_setup_entry( 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 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): 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 90d7bde5215..5a99f403394 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 231bd51c0a3..209ea344328 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 c0e7db14998..f33026800e7 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 94ff4c6e7a8..f9a25d6b5f2 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 test_camera_single_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 test_camera_single_image_unavailable_during_request( 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 test_camera_stream( 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 test_camera_stream_with_disconnection( 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 59072dc2e59..7e00fd22a1c 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 59eadb3cfd9..b190d287198 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 39bfec852e7..1a7d62f886b 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 4f8f3918a1b..99f4bbc86a9 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 a0998898e75..99058ad3ed4 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 6e6461d34b1..83312c85934 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 bcef78e9345..ca97d9abeba 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 8157c5f5c3d..cf3ee4876a8 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 8d17276c304..528483d4290 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 8f4eb0f9513..9a1863c3c90 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 39e01a7d07c..cd60eb70edd 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 cf82836cbec..0dc4d19ba36 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -77,19 +77,19 @@ DEFAULT_NAME = "Vacuum cleaner robot" 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 fbcc97445c8..4d0d6b4b12c 100644 --- a/homeassistant/components/vacuum/reproduce_state.py +++ b/homeassistant/components/vacuum/reproduce_state.py @@ -37,8 +37,8 @@ VALID_STATES_STATE = { 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 bc003c6e27b..4f977436055 100644 --- a/tests/components/demo/test_vacuum.py +++ b/tests/components/demo/test_vacuum.py @@ -37,6 +37,7 @@ from homeassistant.const import ( 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 0297f4216c4..9a71c747e65 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -32,6 +32,7 @@ from homeassistant.components.vacuum import ( ) 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 dd15399f670..38baf591094 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -29,6 +29,7 @@ from homeassistant.components.vacuum import ( ) 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 3a911a68416..e6850728450 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -12,6 +12,7 @@ from homeassistant.components.vacuum import ( ) 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 5512bcd1176..add558ff48b 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -13,7 +13,7 @@ from homeassistant.components.sensor import ( ) 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 8d9677a0af4..f10120f7884 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 f44d736b426..62bb4af7930 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -22,7 +22,7 @@ from homeassistant.components.sensor import ( ) 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 c5c954a9f8e..60e53f90dbd 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 946d4ac653d..8438307c698 100644 --- a/homeassistant/components/dynalite/config_flow.py +++ b/homeassistant/components/dynalite/config_flow.py @@ -7,6 +7,7 @@ import voluptuous as vol 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 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # 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 1d78108f909..8ad7deacd92 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 b922d98f25e..c0192a0037d 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -17,7 +17,7 @@ from homeassistant.const import ( 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 e50369d6e74..1a2409b1cd2 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 edb26c3622e..89da615cf31 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 b4776b19c50..c51868394de 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -9,7 +9,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 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 f9156bed658..fe9a4b6453f 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 14badde17d2..8c4998fa45e 100644 --- a/homeassistant/components/mystrom/light.py +++ b/homeassistant/components/mystrom/light.py @@ -18,7 +18,7 @@ from homeassistant.components.light import ( ) 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 259501e1486..a485a58f5a6 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 8e89bb5f151..7180862758c 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -10,7 +10,7 @@ import voluptuous as vol 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 15a634cf7a9..0d5dc160a11 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -23,7 +23,7 @@ from homeassistant.const import ( 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 24d1885a917..66c9430911e 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 6d214b63e2e..c26b72f9295 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -28,7 +28,7 @@ from homeassistant.const import ( 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 36946b81c0c..26ca5dedd34 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 096e3829bc7..9dadae2e3e2 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( ) 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 0087b70d820..766bca63495 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 0814958ad27..c80608ab1c2 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import ( ) 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 fcebc7638c6..6ea8348812d 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 892bcac5bf9..81d5b5bdc21 100644 --- a/homeassistant/components/zodiac/__init__.py +++ b/homeassistant/components/zodiac/__init__.py @@ -3,7 +3,7 @@ import voluptuous as vol 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 8cf0e22237e..f8ae42d30a8 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 d0bd335decc..f337c7c3e74 100644 --- a/tests/components/dynalite/test_config_flow.py +++ b/tests/components/dynalite/test_config_flow.py @@ -6,7 +6,7 @@ import pytest 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 b02c673f698..366c193f8fe 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 @@ SENSOR_TYPES: tuple[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 @@ SENSOR_TYPES: tuple[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 @@ SENSOR_TYPES: tuple[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 86ce87c92e4..3d84e4480b4 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 4ea6ff48dc1..c0643858f42 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 60a332446ce..6150a657c1a 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -313,7 +313,8 @@ class EzvizCamera(EzvizEntity, Camera): 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 5711aff2a4a..92ff8c6fa05 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 0dc4d19ba36..2399e5d9b3b 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -429,12 +429,6 @@ class VacuumEntity(_BaseVacuum, ToggleEntity): """ 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 @@ class StateVacuumEntity(_BaseVacuum): 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 4f977436055..711d0217f2d 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 26c8d745b27..c517f1aeaaf 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 bdeef15971e..a2b648d9eb3 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 e87eb15b954..55dc69540fd 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 @@ class Entity(ABC): 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 @@ class Entity(ABC): 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 self.registry_entry.disabled: + if device_id := registry_entry.device_id: + self.device_entry = dr.async_get(self.hass).async_get(device_id) + + 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 @@ class Entity(ABC): 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 55d167ae253..da3c76c73f8 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -738,6 +738,8 @@ class EntityPlatform: ) 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 818460bc13d..1210fb6403e 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 d53d6a0890f..162daddd412 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 3d374015acb..9ceb91de9ba 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 c26b72f9295..febd4b61ebb 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -95,26 +95,31 @@ _CPU_MON_COND: tuple[SensorEntityDescription, ...] = ( 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 @@ _MEMORY_MON_COND: tuple[SensorEntityDescription, ...] = ( 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 @@ _NETWORK_MON_COND: tuple[SensorEntityDescription, ...] = ( 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 @@ _VOLUME_MON_COND: tuple[SensorEntityDescription, ...] = ( 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 @@ _VOLUME_MON_COND: tuple[SensorEntityDescription, ...] = ( 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 @@ class QNAPNetworkSensor(QNAPSensor): 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 @@ class QNAPNetworkSensor(QNAPSensor): 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 @@ class QNAPVolumeSensor(QNAPSensor): """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 @@ class QNAPVolumeSensor(QNAPSensor): 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 00000000000..241313965c4 --- /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 0511544224a..dc7842e5fbd 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,75 +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.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_neighboring_wifi_networks( - 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 neighboring_wifi_networks 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}_neighboring_wifi_networks" + 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} Neighboring Wifi networks" - ) - 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.device.async_get_wifi_neighbor_access_points = 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) @@ -112,52 +89,7 @@ async def test_update_neighboring_wifi_networks( # 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.usefixtures("entity_registry_enabled_by_default") -async def test_update_connected_plc_devices( - hass: HomeAssistant, mock_device: MockDevice, entity_registry: er.EntityRegistry -) -> None: - """Test state change of a connected_plc_devices sensor device.""" - entry = configure_integration(hass) - device_name = entry.title.replace(" ", "_").lower() - state_key = f"{DOMAIN}.{device_name}_connected_plc_devices" - 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 - ) - - # 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) - 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) + 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 f8ae42d30a8..b4eacef7435 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 c7f842748c2..d1f68271c55 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 56609e57fd9..e53b311b43e 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -40,6 +40,7 @@ ALLOW_NAME_TRANSLATION = { "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 da3c76c73f8..f97e509f486 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -30,7 +30,6 @@ from homeassistant.core import ( from homeassistant.exceptions import ( HomeAssistantError, PlatformNotReady, - RequiredParameterMissing, ) from homeassistant.generated import languages from homeassistant.setup import async_start_setup @@ -43,14 +42,13 @@ from . import ( 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 @@ PLATFORM_NOT_READY_RETRIES = 10 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 @@ class EntityPlatform: 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 @@ class EntityPlatform: 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 @@ class EntityPlatform: 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 @@ class EntityPlatform: 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 @@ class EntityPlatform: 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 50fca4581b3..1fbaf48de4b 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 382a168bc44..3714e58479b 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 ef48a5988a3..4883f79b349 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 711c333c5ff..9673e1dc73a 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1169,57 +1169,6 @@ async def test_device_info_not_overrides(hass: HomeAssistant) -> None: 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,85 @@ async def test_translated_device_class_name_influences_entity_id( @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" + _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=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(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" @@ -1879,9 +1885,9 @@ async def test_device_name_defaulting_config_entry( ) 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 + 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 54419d5ae3f..8d52115d49b 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 d445c12e4da..c3c305ab07e 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 415685b2f5a..567b8a96d5c 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 6dc2fdacfe2..0e6a806518e 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 8523ded1fff..f34eae3cf1f 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 @@ SENSOR_TYPES: tuple[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 @@ SENSOR_TYPES: tuple[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 @@ SENSOR_TYPES: tuple[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 3680fae6d8c..9ff1b077f33 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 209dece71ab..154ffff78a3 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 21807262742..97605886036 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 e09cabb74fc..6d9f2747847 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 2986bbb170e..bc9b8722c58 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 ea2e2dd2c4c..007b15d2403 100644 --- a/homeassistant/components/hydrawise/coordinator.py +++ b/homeassistant/components/hydrawise/coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations 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 fc88c08b27a..48c9cdcf042 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 d1334143375..9214b9daeac 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 00089bb8774..dbd2c08b28e 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations 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 567b8a96d5c..44d3f4834f8 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 4b376941344..cb4abc9b385 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -30,8 +30,6 @@ TRACKERS_WITH_BUILTIN_BATTERY = ("TRNJA4", "TRAXL1") 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 038461494d6..e9739819734 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 def321d928f..712f8eda75a 100644 --- a/homeassistant/components/tractive/entity.py +++ b/homeassistant/components/tractive/entity.py @@ -11,6 +11,8 @@ from .const import DOMAIN 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 9c0f8f307ed..24439b489c8 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 7ae480d4f98..6d8274df253 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 70ed98a6d5f..63ebd31b34c 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 0cab59de291..f4443e89f76 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 c1eff29e3be..3423b2cd470 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 068bc183ec4..3a2586bdfd7 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.components import vacuum 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 @@ from .schema_state import ( 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 6cab62cdb5d..18cda0b137d 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 9a71c747e65..8034d42ecbe 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 02883b5cd85..848950169d8 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -20,6 +20,7 @@ from .discovery import MQTTDiscoveryPayload 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 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdate): 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 f35af9fb037..14074ce1135 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -7,7 +7,6 @@ import re 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 eee1d006137..08aa53aec7a 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 f8c7b55f7ce..c18e24d1a70 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 @@ DEFAULT_TAG_SCAN_JSON = ( @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 8034d42ecbe..6b1a74f256d 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 38baf591094..b22fb96aa13 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.const import CONF_COMMAND_TOPIC, CONF_STATE_T 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 e8f1e58615c..90aa499af4b 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1127,7 +1127,11 @@ def _no_yaml_config_schema( 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 d0a88bf8249..2a9e2225e9f 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==2.6.5"], + "requirements": ["aiohomekit==2.6.7"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 44d3f4834f8..e5013b24ce6 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 0e6a806518e..13586bf8f26 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 3aa5faa527b..7a6e2486c71 100644 --- a/homeassistant/components/reolink/button.py +++ b/homeassistant/components/reolink/button.py @@ -7,7 +7,11 @@ from typing import Any 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 homeassistant.helpers.entity_platform import AddEntitiesCallback 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 @@ BUTTON_ENTITIES = ( ), ) +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 @@ class ReolinkButtonEntity(ReolinkChannelCoordinatorEntity, ButtonEntity): 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 9e97f053e07..910288d6920 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 test_updating_to_often( ) 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 test_updating_manually( ) 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 test_updating_manually( 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 ac0a33fc7a9..d4114f9bbbd 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -326,16 +326,18 @@ async def test_updating_to_often( ) 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 test_updating_manually( 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 test_updating_manually( 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 b837f580862..af7bf3222a1 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -575,16 +575,18 @@ async def test_updating_to_often( ) 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 test_updating_manually( ) 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 test_updating_manually( 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 e5331fbe7dd..12a037f0dd1 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -684,16 +684,18 @@ async def test_updating_to_often( ) 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 test_updating_manually( 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 test_updating_manually( 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 | 83 +++-- homeassistant/helpers/translation.py | 2 +- script/hassfest/services.py | 72 ++++- script/hassfest/translations.py | 14 + tests/helpers/test_service.py | 45 ++- 7 files changed, 483 insertions(+), 119 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index d1221dd1210..1ba204e5eda 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 6219ade3e58..a4a46d2ca94 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 40bb9650630..1a418a68fd1 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -50,6 +50,7 @@ from . import ( 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 + 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, {} - ) + 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 96ce9b618c2..79ac3a0c5b7 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 a0c629567fa..e0e771ee11d 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 @@ FIELD_SCHEMA = vol.Schema( 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 e53b311b43e..597b8e1ae1f 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 bc7a93f0f19..a99f303f6c9 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 4e433119a2a..6f925fe090d 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 09592594659..6dcfe14a03a 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 e0e771ee11d..2f8e20939db 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 038ded07e8a..83c599bc65d 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 baab687e64a..0cf6db4ae81 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.29.2"] + "requirements": ["python-roborock==0.30.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index e5013b24ce6..35aa527997c 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 13586bf8f26..4e3791da4a4 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 cb6484c5e3e..ca9f63bbd1c 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -13,13 +13,10 @@ 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 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 @@ from .util import ( 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 4405bd21310..caec5b8a288 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 26b336208fe..81fdcde236a 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 @@ class FritzBoxTools( """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 @@ class FritzBoxTools( 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 @@ class FritzBoxTools( 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 @@ class FritzBoxTools( ) 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 @@ class FritzBoxTools( 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 @@ class FritzBoxTools( ) -> 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 @@ class FritzBoxTools( 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 66f4cf2b879..acb135d01bb 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -6,7 +6,12 @@ from fritzconnection.core.processor import Service 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 @@ class FritzHostMock(FritzHosts): """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 7a89aab1af1..c19327fbf5e 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -52,27 +52,8 @@ MOCK_FB_SERVICES: dict[str, dict] = { }, }, "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_MESH_DATA = { ], } +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 2399e5d9b3b..8285e1d76d1 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 @@ from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API 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 @@ from homeassistant.helpers.entity import ( 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 a27a60bba4f..93ef1e8584c 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 00000000000..eaa39bceaec --- /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 5b9b443b65e..72a29182169 100644 --- a/homeassistant/components/ridwell/manifest.json +++ b/homeassistant/components/ridwell/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aioridwell"], - "requirements": ["aioridwell==2023.01.0"] + "requirements": ["aioridwell==2023.07.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 35aa527997c..9b66ea6dbe5 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 4e3791da4a4..6eca0fd720d 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 a3ec6952c6c..b1cda16fb4d 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 c3e1625d623..bc35b7fdd57 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 | 34 ++++++++++++------ tests/components/command_line/test_sensor.py | 29 ++++++++------- tests/components/command_line/test_switch.py | 36 +++++++++++++------ 4 files changed, 83 insertions(+), 45 deletions(-) diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index 910288d6920..50971219f48 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 test_updating_to_often( "command": "echo 1", "payload_on": "1", "payload_off": "0", - "scan_interval": 0.1, + "scan_interval": 10, } } ] @@ -241,24 +244,26 @@ async def test_updating_to_often( 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 d4114f9bbbd..64fa2a60719 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 test_updating_to_often( "command_state": "echo 1", "value_template": "{{ value }}", "name": "Test", - "scan_interval": 0.1, + "scan_interval": 10, } } ] @@ -331,20 +334,31 @@ async def test_updating_to_often( "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)) + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=11)) await hass.async_block_till_done() + assert called + called.clear() + 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 af7bf3222a1..a0f8f2cdf84 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 test_updating_to_often( "sensor": { "name": "Test", "command": "echo 1", - "scan_interval": 0.1, + "scan_interval": 10, } } ] @@ -576,24 +578,27 @@ async def test_updating_to_often( 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 12a037f0dd1..09e8c47d708 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 test_updating_to_often( "command_on": "echo 2", "command_off": "echo 3", "name": "Test", - "scan_interval": 0.1, + "scan_interval": 10, } } ] @@ -689,20 +692,31 @@ async def test_updating_to_often( "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)) + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=11)) await hass.async_block_till_done() + assert called + called.clear() + 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 1a418a68fd1..946340ea69c 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 2f8e20939db..b3f59ab66a3 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -46,13 +46,18 @@ FIELD_SCHEMA = vol.Schema( } ) -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 e4d41fb0cb8..9e8201bc1b5 100644 --- a/homeassistant/components/sfr_box/binary_sensor.py +++ b/homeassistant/components/sfr_box/binary_sensor.py @@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import ( 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 @@ class SFRBoxBinarySensor( 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 f6741da1398..ab987944acc 100644 --- a/homeassistant/components/sfr_box/button.py +++ b/homeassistant/components/sfr_box/button.py @@ -19,6 +19,7 @@ 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 import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -100,7 +101,9 @@ class SFRBoxButton(ButtonEntity): 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 19512f43821..fa754bbe62f 100644 --- a/homeassistant/components/sfr_box/sensor.py +++ b/homeassistant/components/sfr_box/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( 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 @@ class SFRBoxSensor(CoordinatorEntity[SFRDataUpdateCoordinator[_T]], SensorEntity 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 6cd4c19c638..4cc5cacbde3 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -254,7 +254,9 @@ class BlockSleepingClimate( @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 9ed7922fa19..a22b8a993f1 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -39,6 +39,7 @@ from homeassistant.core import HomeAssistant, callback 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 @@ class TraccarEntity(TrackerEntity, RestoreEntity): 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 48760a8bfc0..4b2d2955832 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ) 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 @@ class VenstarEntity(CoordinatorEntity[VenstarDataUpdateCoordinator]): 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 d9182bb9905..debf1f4ea0d 100644 --- a/homeassistant/components/vulcan/calendar.py +++ b/homeassistant/components/vulcan/calendar.py @@ -16,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry 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 @@ class VulcanCalendarEntity(CalendarEntity): 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 249774519d0..b28f06eb97d 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -42,6 +42,7 @@ from homeassistant.const import ( 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 @@ class XiaomiGatewayIlluminanceSensor(SensorEntity): """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 f7a653e1779..02f3f584003 100644 --- a/homeassistant/components/demo/button.py +++ b/homeassistant/components/demo/button.py @@ -5,6 +5,7 @@ from homeassistant.components import persistent_notification 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 @@ class DemoButton(ButtonEntity): """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 340a4b306cb..407860526ae 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -14,6 +14,7 @@ from homeassistant.components.climate import ( 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 @@ class DemoClimate(ClimateEntity): 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 5334da23125..b0f51740b04 100644 --- a/homeassistant/components/elmax/common.py +++ b/homeassistant/components/elmax/common.py @@ -21,6 +21,7 @@ from elmax_api.model.panel import PanelEntry, PanelStatus 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 @@ class ElmaxEntity(CoordinatorEntity[ElmaxCoordinator]): 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 860e5bf832e..ed54315de90 100644 --- a/homeassistant/components/kmtronic/switch.py +++ b/homeassistant/components/kmtronic/switch.py @@ -5,6 +5,7 @@ import urllib.parse 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 @@ class KMtronicSwitch(CoordinatorEntity, SwitchEntity): 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 2e1185fd692..72b66bc5cf1 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -276,16 +276,16 @@ class LcnEntity(Entity): 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 64abf6e54c4..6d20f29905d 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 16ea12a5d96..fa531410e33 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -60,9 +60,9 @@ class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]): 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 9aca0850b05..c841e3b0e36 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -527,6 +527,6 @@ class OverkizHomeKitSetupCodeSensor(OverkizEntity, SensorEntity): # 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 69b3d5db6f7..00f0e0f518b 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.7.1"] + "requirements": ["reolink-aio==0.7.3"] } diff --git a/homeassistant/components/reolink/select.py b/homeassistant/components/reolink/select.py index 6303bc58131..2ae3442278e 100644 --- a/homeassistant/components/reolink/select.py +++ b/homeassistant/components/reolink/select.py @@ -49,7 +49,7 @@ SELECT_ENTITIES = ( icon="mdi:spotlight-beam", entity_category=EntityCategory.CONFIG, translation_key="floodlight_mode", - get_options=[mode.name for mode in SpotlightModeEnum], + get_options=lambda api, ch: api.whiteled_mode_list(ch), supported=lambda api, ch: api.supported(ch, "floodLight"), value=lambda api, ch: SpotlightModeEnum(api.whiteled_mode(ch)).name, method=lambda api, ch, name: api.set_whiteled(ch, mode=name), diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 7dc89ddbaf3..53f2e57b97b 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 9b66ea6dbe5..3aaab1edf2e 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 6eca0fd720d..bddb261e3eb 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 63bd2ffc081..764b12cedc2 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 3aaab1edf2e..17d45dcb24a 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 bddb261e3eb..746b5cf643e 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 1b6c5e3045a..c668430914f 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 17d45dcb24a..e95d879d72f 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 746b5cf643e..e954b5a447c 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 293822987c3..7694a85b8ed 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -25,10 +25,10 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.101", "zigpy-deconz==0.21.0", - "zigpy==0.56.1", + "zigpy==0.56.2", "zigpy-xbee==0.18.1", "zigpy-zigate==0.11.0", - "zigpy-znp==0.11.2" + "zigpy-znp==0.11.3" ], "usb": [ { diff --git a/requirements_all.txt b/requirements_all.txt index e95d879d72f..f6d8a03cbe8 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 e954b5a447c..cd72a6df01b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2023,10 +2023,10 @@ zigpy-xbee==0.18.1 zigpy-zigate==0.11.0 # homeassistant.components.zha -zigpy-znp==0.11.2 +zigpy-znp==0.11.3 # homeassistant.components.zha -zigpy==0.56.1 +zigpy==0.56.2 # homeassistant.components.zwave_js zwave-js-server-python==0.49.0 diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index 7346f1e5bcb..2df6c2be5db 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -1,10 +1,11 @@ """Test zha siren.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import ANY, call, patch import pytest from zigpy.const import SIG_EP_PROFILE import zigpy.profiles.zha as zha +import zigpy.zcl import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.security as security import zigpy.zcl.foundation as zcl_f @@ -85,48 +86,76 @@ async def test_siren(hass: HomeAssistant, siren) -> None: # turn on from HA with patch( - "zigpy.zcl.Cluster.request", + "zigpy.device.Device.request", return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), + ), patch( + "zigpy.zcl.Cluster.request", + side_effect=zigpy.zcl.Cluster.request, + autospec=True, ): # turn on via UI await hass.services.async_call( SIREN_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True ) - assert len(cluster.request.mock_calls) == 1 - assert cluster.request.call_args[0][0] is False - assert cluster.request.call_args[0][1] == 0 - assert cluster.request.call_args[0][3] == 50 # bitmask for default args - assert cluster.request.call_args[0][4] == 5 # duration in seconds - assert cluster.request.call_args[0][5] == 0 - assert cluster.request.call_args[0][6] == 2 + assert cluster.request.mock_calls == [ + call( + cluster, + False, + 0, + ANY, + 50, # bitmask for default args + 5, # duration in seconds + 0, + 2, + manufacturer=None, + expect_reply=True, + tsn=None, + ) + ] # test that the state has changed to on assert hass.states.get(entity_id).state == STATE_ON # turn off from HA with patch( - "zigpy.zcl.Cluster.request", + "zigpy.device.Device.request", return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]), + ), patch( + "zigpy.zcl.Cluster.request", + side_effect=zigpy.zcl.Cluster.request, + autospec=True, ): # turn off via UI await hass.services.async_call( SIREN_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True ) - assert len(cluster.request.mock_calls) == 1 - assert cluster.request.call_args[0][0] is False - assert cluster.request.call_args[0][1] == 0 - assert cluster.request.call_args[0][3] == 2 # bitmask for default args - assert cluster.request.call_args[0][4] == 5 # duration in seconds - assert cluster.request.call_args[0][5] == 0 - assert cluster.request.call_args[0][6] == 2 + assert cluster.request.mock_calls == [ + call( + cluster, + False, + 0, + ANY, + 2, # bitmask for default args + 5, # duration in seconds + 0, + 2, + manufacturer=None, + expect_reply=True, + tsn=None, + ) + ] # test that the state has changed to off assert hass.states.get(entity_id).state == STATE_OFF # turn on from HA with patch( - "zigpy.zcl.Cluster.request", + "zigpy.device.Device.request", return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), + ), patch( + "zigpy.zcl.Cluster.request", + side_effect=zigpy.zcl.Cluster.request, + autospec=True, ): # turn on via UI await hass.services.async_call( @@ -140,14 +169,21 @@ async def test_siren(hass: HomeAssistant, siren) -> None: }, blocking=True, ) - assert len(cluster.request.mock_calls) == 1 - assert cluster.request.call_args[0][0] is False - assert cluster.request.call_args[0][1] == 0 - assert cluster.request.call_args[0][3] == 97 # bitmask for passed args - assert cluster.request.call_args[0][4] == 10 # duration in seconds - assert cluster.request.call_args[0][5] == 0 - assert cluster.request.call_args[0][6] == 2 - + assert cluster.request.mock_calls == [ + call( + cluster, + False, + 0, + ANY, + 97, # bitmask for passed args + 10, # duration in seconds + 0, + 2, + manufacturer=None, + expect_reply=True, + tsn=None, + ) + ] # test that the state has changed to on assert hass.states.get(entity_id).state == STATE_ON From 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 df191afb859..0c85705a2a6 100644 --- a/homeassistant/components/escea/climate.py +++ b/homeassistant/components/escea/climate.py @@ -76,6 +76,7 @@ class ControllerEntity(ClimateEntity): _attr_fan_modes = list(_HA_FAN_TO_ESCEA) _attr_has_entity_name = True + _attr_name = None _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] _attr_icon = ICON _attr_precision = PRECISION_WHOLE From 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 9ed7ad7123d..5011439c778 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -35,7 +35,7 @@ from .const import ( DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, DOMAIN, ) -from .dashboard import async_get_dashboard, async_set_dashboard_info +from .dashboard import async_get_or_create_dashboard_manager, async_set_dashboard_info ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key" ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk" @@ -406,7 +406,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): """ if ( self._device_name is None - or (dashboard := async_get_dashboard(self.hass)) is None + or (manager := await async_get_or_create_dashboard_manager(self.hass)) + is None + or (dashboard := manager.async_get()) is None ): return False diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index 35e9cf74555..c9d74f0b30c 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -143,7 +143,14 @@ class ESPHomeDashboardManager: @callback def async_get_dashboard(hass: HomeAssistant) -> ESPHomeDashboard | None: - """Get an instance of the dashboard if set.""" + """Get an instance of the dashboard if set. + + This is only safe to call after `async_setup` has been completed. + + It should not be called from the config flow because there is a race + where manager can be an asyncio.Event instead of the actual manager + because the singleton decorator is not yet done. + """ manager: ESPHomeDashboardManager | None = hass.data.get(KEY_DASHBOARD_MANAGER) return manager.async_get() if manager else None diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 28d411be939..fc37e1e51ee 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 5d0ea67f31d..75a2644791e 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -42,6 +42,7 @@ class BlinkSyncModule(AlarmControlPanelEntity): _attr_icon = ICON _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY _attr_name = None + _attr_has_entity_name = True def __init__(self, data, name, sync): """Initialize the alarm control panel.""" diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index c7daf0ec1e1..1487c6a7b42 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -58,13 +58,14 @@ async def async_setup_entry( class BlinkBinarySensor(BinarySensorEntity): """Representation of a Blink binary sensor.""" + _attr_has_entity_name = True + def __init__( self, data, camera, description: BinarySensorEntityDescription ) -> None: """Initialize the sensor.""" self.data = data self.entity_description = description - self._attr_name = f"{DOMAIN} {camera} {description.name}" self._camera = data.cameras[camera] self._attr_unique_id = f"{self._camera.serial}-{description.key}" self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index e74555f8db9..9740e427e9c 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -38,6 +38,7 @@ async def async_setup_entry( class BlinkCamera(Camera): """An implementation of a Blink Camera.""" + _attr_has_entity_name = True _attr_name = None def __init__(self, data, name, camera): From 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 f587ef2e54c..ec8bd818d38 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -25,7 +25,6 @@ from homeassistant.exceptions import HomeAssistantError 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 dc3e6f4c17d..ec2ad8d77cc 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 00a8a6122db..5a6a0bf6998 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 4aa29d11fcf..2534622975c 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 fc50e8141a1..b7be12233df 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 b9fa9bc57b8..f68ebd9c8c6 100644 --- a/tests/components/unifiprotect/test_repairs.py +++ b/tests/components/unifiprotect/test_repairs.py @@ -5,7 +5,7 @@ from copy import copy 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 @@ from homeassistant.components.repairs.websocket_api import ( 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 3cfe79ef5fb..eb83fe490e7 100644 --- a/homeassistant/components/ukraine_alarm/binary_sensor.py +++ b/homeassistant/components/ukraine_alarm/binary_sensor.py @@ -30,36 +30,36 @@ from .const import ( 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 @@ class UkraineAlarmSensor( 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 6831d66adb3..73a2657065e 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 16b25353183..b82a7315648 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 8256ce2d060..f7467972773 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 31e5812dadf..210bd8a0bfc 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 ab987944acc..13a1563034f 100644 --- a/homeassistant/components/sfr_box/button.py +++ b/homeassistant/components/sfr_box/button.py @@ -67,7 +67,6 @@ BUTTON_TYPES: tuple[SFRBoxButtonEntityDescription, ...] = ( 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 fa754bbe62f..c01d298daff 100644 --- a/homeassistant/components/sfr_box/sensor.py +++ b/homeassistant/components/sfr_box/sensor.py @@ -180,7 +180,6 @@ SYSTEM_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[SystemInfo], ...] = ( 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 @@ SYSTEM_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[SystemInfo], ...] = ( 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 cf74e9eb656..3fc9691cc12 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 dc6ccc1f25d..f362cfc146f 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 2390ba625eb..171a5803ada 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 906d9aee629..a69d2a4c382 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 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( ), 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 068132dea41..62e923a766d 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 7f2c2cf5e06..fe09667ca4a 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -482,6 +482,13 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): 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 ee4f170e8e6..59d5090b711 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 0680aa7455d..a7fd27a051f 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -21,22 +21,21 @@ from .data import EagleDataCoordinator 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 b32f38302f4..58c7f6bd795 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 96b9e0a85dc..5e76a81932a 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 0acd655363f..f480839388c 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 317209886bd..5dbd1e986f3 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 @@ class IRobotEntity(Entity): 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 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): """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 c0092922783..dd74a023ff1 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 be2e5b99159..206e8c5bae0 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 92f5b442d9e..5384b86cb98 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -26,7 +26,7 @@ import homeassistant.util.yaml.loader as yaml_loader # 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 f6d8a03cbe8..27884a263f0 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 cd72a6df01b..5210e0e335b 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 e6c805a64c1..11920917a59 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 18cda0b137d..7c73e579112 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -419,9 +419,9 @@ class MqttVacuum(MqttEntity, VacuumEntity): ) 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 fef185687db..ee06131af02 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -259,8 +259,8 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): 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 0f3811bef6f..2f25a934e7f 100644 --- a/homeassistant/components/juicenet/entity.py +++ b/homeassistant/components/juicenet/entity.py @@ -14,6 +14,8 @@ from .const import DOMAIN 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 45be1dd9004..e78f6189baf 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 @@ class JuiceNetNumber(JuiceNetDevice, NumberEntity): 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 fdc40211d77..5f71e066b9c 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -29,40 +29,36 @@ 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="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 @@ class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity): """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 bc4a66e72d4..0e3732c66d2 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 576c66c0841..7c373eeeb24 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 c1dc891805a..c983a105c93 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 338795624cd..f7c12523b2c 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 431c73616ce..c983a105c93 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 00000000000..078e5b35980 --- /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 a05a9e3415d..c983a105c93 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 0ce8af4f3a6..d23bb605c7b 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 ef6745bd36f..c983a105c93 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 00000000000..f1525b2516d --- /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 f254295ea20..c983a105c93 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 00000000000..cb4601f2a09 --- /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 cca67d92144..c983a105c93 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 c76a6faf2f5..464d01b90b4 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 265c6049563..c983a105c93 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 8a8915541d8..10a982535f2 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 b34dd5e83da..c983a105c93 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 4c22e5ecead..aea07cc3ff2 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 8c2c8f8464a..c983a105c93 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 00000000000..6b2a04a85df --- /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 1d29e08dccf..c983a105c93 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 00000000000..1715f019f27 --- /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 e0af28bf3a6..c983a105c93 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 00000000000..b440d76ebc2 --- /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 2ce77132a53..c983a105c93 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 00000000000..b2f3b5efffa --- /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 2e10d1be257..912c472de3e 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 923df261d84..2de87659919 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -30,6 +30,7 @@ PLATFORMS = [ 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 81fbda63fef..dac02b91315 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -432,6 +432,15 @@ class ReolinkHost: 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 00000000000..42758dc9929 --- /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 53f2e57b97b..c0c2094eeb9 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 2014c4c5221..dcbb955d739 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 46db471305c..e954a55b280 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 024bb927508..55ac9f2bfeb 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 0722ec1c5e6..ac061194d5c 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 d001c57ef5c..900aa39dd6e 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 00000000000..6ad3416b1b9 --- /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 843cc123c69..f9d4e73a4e5 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 b974007707e..c0c32d48794 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 5e4c2a157de..f38dc4ed866 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 bde73e82b37..95ce968a67f 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 53c514bb587..e2d5c60ada2 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 00000000000..fd34973a21d --- /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 6bd3bf815d6..cb93ef568fc 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 76ecb174f6d..39681201766 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 62e339dbda8..2950d5162dd 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 00000000000..602138e82f5 --- /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 206b32cb526..6256cfaac1e 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 127fbb69b33..77167b8294b 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 9d50eae07e6..91a6000e683 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 33b33749048..585db4b1fa3 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 e5532ae82f9..bf72d18b259 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 c51c25a2f61..2b55f7bebb6 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 b79a333101b..cdcaf0e2c04 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 00000000000..816511bf05e --- /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 4482f50f3e2..41f7dbfea8f 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 e7d06a9f624..9eb3d14a225 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 3242a9cedb4..e1d842f5bc4 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 4d948b2f4d1..16192d5d595 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 7b3096c25e4..00425c93eb6 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 00000000000..93c0e6ef23d --- /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 3d51ba2f7bb..95f4d33f91f 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 61c9a21af37..6c07d1fea55 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 7c04cc00f39..7ab69a82124 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 00000000000..f41c34a7449 --- /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 3150403dbf1..91b8669505b 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 00000000000..bf22845d054 --- /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 6be18eaa1ef..bda0bc5835f 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 e923ded939e..9cbd895683c 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 dd3ddd095cc..c2192911eea 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 00000000000..fafd5fb96b0 --- /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 f0fbcf4a8d7..e2e23ad40a2 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 719465e98ca..4de0f85851f 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 5aa2f1ebda7..73ac6675ccf 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 00000000000..0eceed8a8e0 --- /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 f9465e788d8..e800a3a3eee 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 89bc67feeed..080be414b5c 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 be278a59059..2fd0b0db815 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 00000000000..df720586631 --- /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 f4cec426860..c983a105c93 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 dab4a77a6ec..e249ad877d5 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 835d39c9d2e..643fc223083 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 6dcfe14a03a..0446b244787 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 c864684226f..453b3af46bd 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 00000000000..b03a57a51dc --- /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 9084728a216..d08312852b3 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 45a19b0466d..448a221b2ca 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 ee35732e311..9c53ff9994a 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 1c85efc9ff4..a4e07e33a6a 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 6a354bc3a63..f2261072ddd 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 00000000000..0ceabd7abe8 --- /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 cecb3804227..54d06db5627 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 00000000000..49a7388add2 --- /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 6c8b5af8199..485afa44a03 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 00000000000..d560b760e47 --- /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 d34335ca1d3..97c5d9c2486 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 8ad7deacd92..512e00237d9 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 b87d3ac3899..026d0315238 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -378,6 +378,12 @@ class ESPHomeManager: 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 1dcdc559de7..f4b3bfa3ec7 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -154,6 +154,7 @@ class MockESPHomeDevice: 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 @@ class MockESPHomeDevice: """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 @@ async def _mock_generic_device_entry( """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_esphome_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 1a7d62f886b..e268d065e21 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -11,7 +11,7 @@ from aioesphomeapi import ( 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 33e114c87f5..405bb735b66 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 8034799a6d0..bfe0f490cda 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 62d0988d770..6b3afdca335 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 6f925fe090d..cfeafa856d2 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 22d89b42253..08ccbcf0b5a 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 c15b9723c97..44c43219b82 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 620bd351806..6309bafcfb9 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 861a2acc1f1..2e630cfb4de 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 245368f9d5b..2f4d2c6fafe 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 a92a5a0f38a..39456cdf427 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 1f4d6aa3152..712d6ad8823 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 898953c18ac..81334c12379 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 1b676ea6be9..b54d35d4221 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 a3cf7fe0457..aba2e770bc9 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 a09b4498035..300ea37f805 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 3794b27cc0e..2dfb3465d68 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 dc356bec226..6615e947f28 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 00000000000..4097be02393 --- /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 aba57989119..a184f422725 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 647ea55e311..05ae600d4b7 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 39b960a6f7c..b191187bb0a 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 21accc53a06..bd2b4f11b9d 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 05d341a7041..2037633ff71 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 8a2f20f209f..e6b16215793 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 1f130416363..1f3bb8ffebb 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 d1871c7536c..5ef15827eb9 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 09f95f16a44..4293b313f5c 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 d30124ddf5a..eb9ec24dad0 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 b15a3b94e01..6751a3ecc56 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 00000000000..a539c890169 --- /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 37add1bc202..94038aab408 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 9716153958b..4e3780322e9 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 52428dd5e1e..a16395ad6c0 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 00000000000..d8214c3aa8b --- /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 9733d7418a3..7d1cda2fa63 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 92ff8c6fa05..5355fcc377c 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 3f968cf385a..0438338f55e 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 00000000000..776644c7cfa --- /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 75963557a03..002b28b4e4d 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 00000000000..b1e03681c96 --- /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 1fdde46e55c..35c11ee678f 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 00000000000..9aaff2d1e93 --- /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 a074ebafe99..ce4abacb64c 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 fadfc304fce..627f562be7e 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 5d880370818..73f479825da 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 51edd207e95..7617d56d512 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 a7e5394802b..ad46ec130d0 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 14aa88b7952..35964ee4546 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 7b2a4059434..8ba6f278bfa 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 53a5fd59de3..5c4143b4562 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 95527257ea9..b9828280aa2 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 fcaa56424f1..dd845fc2a1b 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 1f75e4a0347..7784996da9b 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 c10b6162859..2ecac4a5742 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 52aa8863872..d62a1e72d45 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 bee6c0f0e29..267100eaad6 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 6613bb6a329..83d31439666 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 69055d6bbc6..cff9b572cc6 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 164445e375f..48d17dfdcf7 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 5a6a0bf6998..e5cd35703f3 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 f4382decb0f..5fc0b11f4c2 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 00000000000..3f977fc941e --- /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 10df6c564b4..cb855a953a6 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 a73ade9311c..9a06fb45ad2 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 69c802d90aa..c3c4bc640bf 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 3c9cd6043df..2271d4201f6 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 9b5171d1483..f2ce72397d4 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 00000000000..03d4c5728a5 --- /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 1abf8bd5dea..b401c01f3a3 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 d2cc1bcfec9..9c881e6324f 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 0b7295bd7bf..8af2d12d0e9 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 00000000000..0cdffd5d508 --- /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 f470076ee7f..f13cd646388 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 a1bce80d7ad..bef65e28880 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 e27e29dfc6f..13078495b43 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 00000000000..b1008336992 --- /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 ad5cff4a5ff..14e2196eb83 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 5f4cec1336e..caeea189c0e 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 39e430ab165..b40797bc165 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 00000000000..21902ad1825 --- /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 07acf0a72df..8dafa911ada 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 00000000000..ad07a4d7565 --- /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 ce3c29f39b5..07150f530be 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 fc30709960b..397d7267bc0 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 93275fd2a1d..7f3039509ba 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 008c182f41b..4ecf4cfee45 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 d37d6bb5be8..7b18979ed0e 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 47c0867187e..0e0a32bfb24 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 2970124c000..c5a11db8a6f 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 f92fa11cd77..fdf73cd8cf8 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 cbfff7808ee..5ec782d7bf3 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 20848ccff08..6136ac94e99 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 ad320285d5b..b02d5e36805 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 00000000000..ec4e39a6128 --- /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 24b7290668f..5f68bd6a1f2 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 86650bbbe9a..b6941f51392 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 e61e893e199..726d6867d2d 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 617d813007c..05d0e716ef4 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 bed9647a1b7..05cf8cc3c97 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 00000000000..9c4c67bddf7 --- /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 0deb5225cd3..ede1f311acf 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 c9bc84243da..f3d343ffda3 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 901e70de414..d4948072667 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 00000000000..4dae6cb898b --- /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 c43f081dbf7..2002ab8614a 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 4629f6a2a3b..68ab508141b 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 a5c49f6d6a6..da5c0638a4f 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 00000000000..11f94e7a72c --- /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 46439b761e1..0131bb6ae3a 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 fc7d8508a12..5a96d2f8951 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 d7e7068e84c..8803d2788bf 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 00000000000..70a3767c889 --- /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 94ba0d2982e..c82ea7ebbbf 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 2bbb927fd27..454644be244 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 9d753b2fe77..9cf3a1fc4c1 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 8e989f1dfa0..cabab347264 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 0fa95145287..7ccba4fb497 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 00000000000..b13fb997b7f --- /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 77ef501f9d8..d68624e0763 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 a80a059481d..d23fe1c0924 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 1b5da9f0d4f..9c8d8921b12 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 eb12811722b..1ed271931c3 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 9af2cb48291..e7afe71bb31 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 f0e0d93231c..0fd107609d1 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 6dc052043bf..b877ae88b0a 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 00000000000..4cd819859a3 --- /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 1f7e523e685..c983a105c93 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 00000000000..2bd9229b607 --- /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 782a4d17c18..5ed655b7d78 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 f08b6f59862..9cba83653fd 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 3bd6d7636ac..311325fa404 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 394c46563cd..ee6f215e59b 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 0db63cb7adf..e02eb2e60e5 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 a6c7fcc4a76..9b9ac45fc85 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 f1f20506edb..0a93f87a249 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 9518af77dbc..644b2d61216 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 e9f860f1a62..613c6cbc9e2 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 00000000000..9898a8ad866 --- /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 11920917a59..2834ea59672 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 63aa2f2f916..ea153be11cf 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/python_script", "loggers": ["RestrictedPython"], "quality_scale": "internal", - "requirements": ["RestrictedPython==6.0"] + "requirements": ["RestrictedPython==6.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 27884a263f0..0cb289d748f 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 5210e0e335b..04065df19c1 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 edb879c784a..0dad311f899 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 00000000000..6f37bcce85e --- /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 67463a22172..6a6a8bf5cf6 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 697b0bce2db..3d776193432 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 34f89ec279b..11226966b0a 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 a98baead976..9f4d0c2e34d 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 9aa2bb7f50a..2f799afd028 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 884d05359a6..783c876fe62 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 1458075fbd5..5e94b2bf7d4 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 00000000000..15ca4c36da8 --- /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 5911c453c95..2dc99833d5f 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 7cf016187be..e0b8cb0cdf0 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 9ba509b63f6..c983a105c93 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 00000000000..afbab8d8040 --- /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 8e233bc7aac..1b06a142a59 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 00000000000..2c8eb584ca8 --- /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 43695554ed0..00640a2ff59 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 7e68f960fca..6c49fb38d6c 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 c648f02139b..91b8669505b 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 43209a5a6a3..b300e335b19 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 16fd51ea95b..4a28db94fa4 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 04c504def03..3510a43c604 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 9d9d02f0efc..1de3e14bbc9 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 ce5827e2c6c..f67779e9eaa 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 4936a499764..e800a3a3eee 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 00000000000..12b372d0ce2 --- /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 2221eed169f..f1eea1c9469 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 501e0d33faf..2989ee5d00b 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 439d020a432..8e4a82a1079 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 b0958d31727..79b633e28b6 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 fbd2625961b..7f8252af820 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 2379e2c2b38..6946b21761c 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 250912f49cd..402a6c24aeb 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 5b8197177a0..598a2bddfff 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 8aeefcf7846..de4d8fbe534 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 4f230442f85..4be806ebbbd 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 d9890dba35a..65bd4afb8b7 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 25528b8a374..c130feaa620 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 c4380a4fc62..c983a105c93 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 00000000000..3c72a1a50d1 --- /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 f80b22dba7e..aa1a26c3537 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 766bca63495..242bf62ab04 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 df3a46281c8..522e1b5b348 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 00000000000..d6c9f4d53f6 --- /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 f795edf213a..ca9f4883a69 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 2f957f87072..878341f23bc 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 93485ce4788..26da134acdd 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 62bff00c786..a4df830f1fe 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 9d61c20f7cb..f6df83ef6ed 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 75c1b850146..c5b5136e970 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 82709053496..10ae15a3cb9 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 7ebcd4c5285..616a4fc5a11 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 4c2d34ba88b..90f9bf2d769 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 4ae8d69bacd..13fe16aa28c 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 4c3e4d360e8..1d7041f0eb5 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 10e99f93814..292ae55da1f 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 b54c2cf15eb..7504a911123 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 00000000000..56b35ab1044 --- /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 58be48f9d18..bc760d2469e 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 abde396ba75..2ce3c3835a6 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 3c3919f5d01..1d42c8fc102 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 f7a539fe0e6..6e1ec9643a7 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 a7c3df5903e..1dcb15fa482 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 ad8f0f41ae7..4c4080a8394 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 245d45fc800..32baeec11c1 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 92903b1d2ae..24ed1aaf568 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 d33235ffba4..78d6e87f218 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 209bce9078a..e4b2b40637c 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 0a9b0254f91..58136005053 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 05b53acba5f..63a0bb43d2a 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -765,6 +765,11 @@ DHCP: list[dict[str, str | bool]] = [ "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 ea374a88b8f..48d3df5c4f9 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 00000000000..8395bc7503a --- /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 1985857d128..c3297dd8902 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 c623effe22b..985edb05645 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 58305798cf9..59f38ca77a0 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 b218f758985..66fa656ebfe 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 b6c538bf9fb..044a46784ef 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 0449a900c29..a287104e7ad 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 9ca73fac0a3..40170fd54e9 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 eed62ab0499..9fc6573b112 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 75a9b9156c1..dcf79ebc215 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 63fb48542c9..0944c91fd83 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 e1cf03ba4ee..0b3bd6435e4 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 15c89498bc7..578d2a96ff8 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 8d25d5925c1..705f2996a3c 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 00000000000..0896f43b1b5 --- /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 d7850b34607..ccfd46ef680 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 0ecbd134b6a..18b762057a7 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 74ab0cf5945..30e6672957d 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 00000000000..1e2e41d2741 --- /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 211ae4cd1ff..0f66798f864 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 3decfe3cd0c..70ff38b10be 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 bbdd82768f5..c983a105c93 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 00000000000..9e09a3904cd --- /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 31876bd542d..cdb50d55943 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 00000000000..8104fdd285e --- /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 68caa44a699..74eeae22b23 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 217de09a534..e21f0d2ca82 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 3cab4d2bf67..9593b6bb6a4 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 00000000000..1ed092e5cf6 --- /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 d01cf32994b..1b75dd4957a 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 60d5ed3312c..620a7f51113 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 0e8f8f8e217..3ab4faf0c30 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 346ea7ef403..922962c9866 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 16166278565..1850df9a060 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 afc595a3adc..b5279804d0a 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 34a88528411..2d61bda442f 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 e2c144d5423..c3fdcc8f1f4 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 c6a4de3072a..fd69b8eb708 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 02b64f3c50e..6afae5ffe7b 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 9f9031d6543..6998f540471 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 b7be12233df..fd2287e08be 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 af8eb81d9b0..cf415705d72 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 9b2cc0a1b12..b5b6dea93d5 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 4252f796199..918c51cee39 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 1eeacbae800..09b9dd09540 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 15ce6c88b55..e6bd3edad11 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 cada5a7febd..b33cef0026a 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 32cda00f708..e3ecc3556f0 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 6eb44d8cb0c..bef853001a1 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 46aee795890..7aee1694061 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 00000000000..6a7e8c6e1ec --- /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 2a4e2a008be..ccfc7726bc6 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 85b3f4015b5..335daa68ee8 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 da264ea3b5d..52ee0382dbe 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 8359691effe..08cbdf943f6 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 1fc1e61b6ee..b4df8a1bb0e 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 d54956f3e10..0700d5d6f0e 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 7a2ea859b7d..2f5da4659f0 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 665e03b531a..314f6f8b4e5 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 e8c0a0711dc..9ee430a655b 100644 --- a/homeassistant/components/nut/diagnostics.py +++ b/homeassistant/components/nut/diagnostics.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant 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 00000000000..f91269f5196 --- /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 a8c843ab55a..ee3d9150870 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 e8d92a30e2e..1b7c53b1a8a 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 7d4cd14b19e..48b45a0811f 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 00000000000..1c1092ee256 --- /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 e7eeef75947..f715679dff8 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 5c9b6424473..7fa1569992f 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 fe5ef51c2ce..321eae3b2e9 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 00000000000..cb01a0febf5 --- /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 fc2a3ad264f..f8853ec93ea 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 66a2b975b5e..e9e2b7d4c09 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 76ef40fa3aa..9ce1c41f27a 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 2f76806dfd3..db242479783 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 7524ba50fb5..169352d1bac 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 602301758f8..b2cba19031e 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 7415ac626a9..c8f2414a87b 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 ec2ad8d77cc..f416adac027 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 e60e2238088..a7ef39eb529 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 3fe73d84667..8d2fb38517d 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 fd53912397a..be2a3178a8b 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 62de202372b..8e2b435483f 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 7ad8b36473f..e4102c44208 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 00000000000..6efc9ec4272 --- /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 320ed297873..8dc222d65ba 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 09ada292afd..635fe08cccc 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 d0de9645c6a..96066246230 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 3435517aec7..495c5dad1cc 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 06a646dd481..0738b58595a 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 79455783edf..41eedbe83a8 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 a982e9ccf8d..de271db0ad9 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 74af388df85..83177345159 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 28b6577cdf9..52907966688 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 00000000000..14f723694fc --- /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 ebb83a0845f..9e831339787 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 3e3c967f972..6a20c5f8a54 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 f6b76e67cd7..929eb5a2dc1 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 00000000000..fa69025c43c --- /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 711064b435e..9d0cf5d91e6 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 0eb68c959ac..6f85187cfeb 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 b06c3934152..a9ea57d7828 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 a44eea0fe33..54895b6e3b2 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 1a28044dcb5..7b6717eec6d 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 dc6f2b5f52b..15e783c0d90 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 33f66070bfb..5da203d8a80 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 70cd45f4d21..ae5a3165cd9 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 7ce1210f809..aebf5630205 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 110a4cabb92..9e88d3b5676 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 b5cce19e88b..fb6f798e9bd 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 3b97559018c..503d7a2ca9e 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 3f688628032..c6722dad10b 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 00000000000..10ebcc68f64 --- /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 1d3c0e8a8a9..6fc3d81f196 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 b9624f16a31..e4f1b3fcd4f 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 2fe27769c3f..899fee357fd 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 89da615cf31..5a02cd19665 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 60b54735493..33eb1e88ed3 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 f9c212f946c..fa8fc2d2da8 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 202b4a98aa9..acd98b10255 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 c92838ea322..f4011860c78 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 a3b372f219e..b42109ee649 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 b0a625d0016..a03e93cde41 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 db3bea9cad3..8bd329ac8fe 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 0f3b88fd7f2..d3a06edbee1 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 6186bc6dccb..c983a105c93 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 00000000000..3222a0f1bdf --- /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 3299245fbc0..af04eac5f33 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 21cebe46f26..ac06e125b0c 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 bb981dbc69c..c983a105c93 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 00000000000..efd77d225f7 --- /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 ff59a7efe63..c72187b2ffe 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 dc5eb30df51..3d5ae9b6a61 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 912c472de3e..e10a23e9c31 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 16c0426d87f..01f486e2704 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 00000000000..05ac16381d1 --- /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 00000000000..3e981675057 --- /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 00000000000..7de4c15b5fa --- /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 00000000000..fa7639dece0 --- /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 00000000000..cdc43a802c9 --- /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 00000000000..165e336bbec --- /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 00000000000..e3fcc8978c7 --- /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 24215a8a0c4..64fae252975 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -83,6 +83,12 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ ], "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 d8ffa25b765..6d9a132a29a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -155,6 +155,7 @@ FLOWS = { "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 d1f68271c55..21f7acd59e3 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 0cb289d748f..d33f01b34be 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 04065df19c1..f35021c3e00 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 00000000000..6a064409e9e --- /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 00000000000..f09a274742f --- /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 00000000000..fde70b60a01 --- /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 00000000000..0f0e297c4d7 --- /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 8ab42c6d3e9..9f9e37941e2 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 2f61bd95083..5ed02a84e0d 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 154ffff78a3..4c2f612bcbc 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 60d8843c151..171a853f74c 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 046ea237560..c335d962600 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 00000000000..6b8ddb46c49 --- /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 97605886036..7338747b545 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 67c92d7ce07..10148f99fef 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 c20d1171bb2..d7d2a5b32e8 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 00000000000..aedaec42035 --- /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 946340ea69c..ab0b4ea32e9 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -173,7 +173,7 @@ _SERVICE_SCHEMA = vol.Schema( 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 a99f303f6c9..674d2e1af4c 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 c3022b87eb7..f7a3854b6b3 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 6b01cab2bec..deaab6d75ee 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 07507035c57..4960cf9fb82 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 3423b2cd470..61d2b40314b 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 ddeae448f8a..5ffbc2a49ae 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 385dc74a0ab..9bc7750790f 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 9c02284d4f8..550aecad56b 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 179d62b463c..e52a0882eb1 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 33f6c8ca31d..1e1727abea8 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 00000000000..3ee45a4f464 --- /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 164c917c793..a58dfb4b8ce 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 a93ba4a7476..3f3e3df78c7 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 ba0fdb89712..b0cc4f11639 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 00000000000..be8535daec6 --- /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 b84fcdd73ef..7ce44f9edae 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 821f8889978..542df60f13f 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 5cecbb68a9f..f1a8fe5c8e5 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 7d1e8f1d476..3906dcb89fe 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 8e5e8cd91f8..daa1749a34c 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 00000000000..140ab6ea949 --- /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 cf364edcf21..9c5e5083794 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 00000000000..7307caa6bb3 --- /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 07f02959c39..b236f8eb80e 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 00000000000..1b744cb7a71 --- /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 c611577eb26..2be5c07c804 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 fd8e1f4825d..57e7fc68582 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 0ad497a30a2..813bf758eb0 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 cdd61379567..9a17fed506c 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 cf6cdfc240d..76ed0aca22d 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 8097eb6336b..f7ee375f990 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 22a52e61b63..94f445f1ec1 100644 --- a/homeassistant/components/led_ble/light.py +++ b/homeassistant/components/led_ble/light.py @@ -43,6 +43,7 @@ class LEDBLEEntity(CoordinatorEntity, LightEntity): _attr_supported_color_modes = {ColorMode.RGB, ColorMode.WHITE} _attr_has_entity_name = True + _attr_name = None _attr_supported_features = LightEntityFeature.EFFECT def __init__( From 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 abd4d2e623e..c996fcaf524 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -453,6 +453,27 @@ class ColorTempSelector(Selector[ColorTempSelectorConfig]): 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 fd2dba4b084..09cf79116a0 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 9b16dbd2713..036af10150a 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 b69e0acf65e..1d238d3dd51 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 f9fc5999da6..7cf6d8e4027 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 87f8407d93c..64718308325 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 d294d61fd4d..9de0368ba35 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 d8e1e133f55..9288de04f2c 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 899ead91cb5..7c57fcff272 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 cfd616fd5e7..b51d04926f5 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 51b1d6b00c1..386f0096a5f 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 0c3a4b0b0d2..f657508a4c4 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 41164a7ccf5..e5de48a1262 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 11ed2f8bf10..020544c5d4e 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 8b8828eaa92..92279e58a54 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 f0dead7a1dd..68970933346 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 cf19e15d7ae..6cb5c1352c6 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 d713c395b67..a4dc6d929f5 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 f099cede9f2..b74dcc2a494 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 7af67f10e25..a55f13b27c4 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 d498f0a2c14..75e34cf5049 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 7512b2abec7..d3cf946f5bf 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 c517f1aeaaf..aab35b42077 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 93ef1e8584c..3bdf650ddd3 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 a2b648d9eb3..0d8ef63bfc3 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 bf8a669af50..14331c5cded 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 992c58cf5f6..c80517d1fe1 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 da4b5217b86..9e20b0cad2b 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 e10a23e9c31..703523ed364 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 9386a407acb..9aeba56360e 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -37,6 +37,7 @@ PLATFORMS_BY_TYPE: dict[str, list] = { 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 6150a657c1a..01e8425c13b 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -290,6 +290,16 @@ class EzvizCamera(EzvizEntity, Camera): 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 00000000000..0f6a52ef578 --- /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 11412c1fc70..075fe6bd6d1 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -24,7 +24,10 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { 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 5355fcc377c..aec1f892b1f 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 dbe8ac3f1ab..bed677ebd30 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 911862a4221..35efbdf3cbe 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -53,6 +53,7 @@ NEED_RESET_ERRORS = [ "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 b82a7315648..8ae3ba06985 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 d33f01b34be..26d4911ddda 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 f35021c3e00..864938446e8 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 8fb55936fc9..dc6d4c6815a 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 9080b940b2a..d058ff6e6f2 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 99e0bcca4d4..03b176eaab3 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 00000000000..2f0208ef8b5 --- /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 0f9ae61ba4c..9ab3bb6bce3 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 00000000000..ed1ca79fe07 --- /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 fdb1a1af014..e5ac921cc77 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 192823cef65..7b49eaf4186 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 00dd0ecafd2..b8461037b8b 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 034f1ab315b..e6b3d99ced4 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 9311acf2ba9..8d053e3af58 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 02027a84d8f..cff7b265c37 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 132dae6e745..027653a4a6f 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 cbdc9cf8477..efc71df7adc 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 597dd8ddb53..d14c562bd76 100644 --- a/homeassistant/components/fritz/image.py +++ b/homeassistant/components/fritz/image.py @@ -30,9 +30,6 @@ async def async_setup_entry( avm_wrapper.fritz_guest_wifi.get_info ) - if not guest_wifi_info.get("NewEnable"): - return - async_add_entities( [ FritzGuestWifiQRImage( diff --git a/tests/components/fritz/snapshots/test_image.ambr b/tests/components/fritz/snapshots/test_image.ambr index b64d8601a8a..452aab2a887 100644 --- a/tests/components/fritz/snapshots/test_image.ambr +++ b/tests/components/fritz/snapshots/test_image.ambr @@ -8,6 +8,9 @@ # name: test_image_entity[fc_data0] b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x94\x00\x00\x00\x94\x01\x00\x00\x00\x00]G=y\x00\x00\x00\xf5IDATx\xda\xedVQ\x0eC!\x0c"\xbb@\xef\x7fKn\xe0\x00\xfd\xdb\xcf6\xf9|\xc6\xc4\xc6\x0f\xd2\x02\xadb},\xe2\xb9\xfb\xe5\x0e\xc0(\x18\xf2\x84/|\xaeo\xef\x847\xda\x14\x1af\x1c\xde\xe3\x19(X\tKxN\xb2\x87\x17j9\x1d None: - """Test image entities.""" - - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) - entry.add_to_hass(hass) - - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED - - images = hass.states.async_all(IMAGE_DOMAIN) - assert len(images) == 0 From 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 06221fc7b97..45e85f5a474 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 047130e787c..9bc1a49125b 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 617e61336a5..644a3c04553 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 c9d74f0b30c..4cbb9cbe847 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -93,13 +93,6 @@ class ESPHomeDashboardManager: hass, addon_slug, url, async_get_clientsession(hass) ) await dashboard.async_request_refresh() - if not cur_dashboard and not dashboard.last_update_success: - # If there was no previous dashboard and the new one is not available, - # we skip setup and wait for discovery. - _LOGGER.error( - "Dashboard unavailable; skipping setup: %s", dashboard.last_exception - ) - return self._current_dashboard = dashboard diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index d16bf7c4d00..d8732ea0453 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -58,7 +58,9 @@ async def test_setup_dashboard_fails( assert mock_config_entry.state == ConfigEntryState.LOADED assert mock_get_devices.call_count == 1 - assert dashboard.STORAGE_KEY not in hass_storage + # The dashboard addon might recover later so we still + # allow it to be set up. + assert dashboard.STORAGE_KEY in hass_storage async def test_setup_dashboard_fails_when_already_setup( From 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 82ea7228157..54f44d0998c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1770,7 +1770,7 @@ class ServiceRegistry: the context. Will return NONE if the service does not exist as there is other error handling when calling the service if it does not exist. """ - if not (handler := self._services[domain][service]): + if not (handler := self._services[domain.lower()][service.lower()]): return SupportsResponse.NONE return handler.supports_response diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index ab0b4ea32e9..16a79b3ae12 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 674d2e1af4c..d41b55c0b48 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 39681201766..de8bde6897e 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 16192d5d595..f8c1b2ede72 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 cfeafa856d2..31bd812a947 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 f7c12523b2c..9ebccedc88d 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 9cbd895683c..04be198d149 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 39456cdf427..f552e9ae12b 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 ac061194d5c..90b053ec087 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 bfe0f490cda..c517bfd7a20 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 df720586631..f56a4e514b7 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 e249ad877d5..9fc0de2ab28 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 5ed02a84e0d..979835fcfd2 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 b03a57a51dc..74334a15f4a 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 448a221b2ca..632fe832aa8 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 d3a06edbee1..674dcc2b92e 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 9aaff2d1e93..66c1f19de5b 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 078e5b35980..461eed9aefa 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 d23bb605c7b..a1519fa0f48 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 f1525b2516d..8834892b7ab 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 db242479783..83537c6b1de 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 7b49eaf4186..bbf521b06e3 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 fa8fc2d2da8..e954c0cccf6 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 cb4601f2a09..ea1c94b6ec3 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 5a02cd19665..57cb5c3eb56 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 83177345159..f57536263ca 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 54895b6e3b2..2c3f493e2c8 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 d3cf946f5bf..19a9a8eab77 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 9288de04f2c..a2087f1247a 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 f657508a4c4..e4a2b6349b7 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 020544c5d4e..8a2351ebad4 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 68970933346..faa47c979a1 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 a4dc6d929f5..49eab33848c 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 3f3e3df78c7..3ba996adff7 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 efd77d225f7..74ddd45c1af 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 140ab6ea949..ed8594a1068 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 9a17fed506c..56ff9018530 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 cff9b572cc6..dfbc0b4e384 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 a4a46d2ca94..5398d38ca5d 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 e5cd35703f3..fe9cc3b528a 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 9e20b0cad2b..d041d6ac61a 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 10148f99fef..bcf594a2675 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 464d01b90b4..ce18a4d153f 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 ad07a4d7565..c9cf755ad13 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 61d2b40314b..ae47b33774d 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 68ab508141b..11c19bbee3f 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 5a96d2f8951..7a3c438d11f 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 10a982535f2..27c41df6b4e 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 1ed271931c3..b76b61f1903 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 2bd9229b607..5b5c5da46bc 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 ee6f215e59b..7b9f6789c79 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 9898a8ad866..ccf1b33c767 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 a55f13b27c4..17539387a29 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 14331c5cded..e3df487a57b 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 afbab8d8040..d2b15461c9e 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 2989ee5d00b..5711656ef69 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 f4011860c78..3bfea1b09e7 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 aea07cc3ff2..a40c5814d36 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 e4f1b3fcd4f..f2d5997ae9d 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 171a853f74c..90725da9e8f 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 3c72a1a50d1..b711c2f2009 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 242bf62ab04..0d51c7543f1 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 6b2a04a85df..6d7bda36fae 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 ae5a3165cd9..b50709ed76f 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 9e09a3904cd..34a98f908dc 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 3222a0f1bdf..fce7129353e 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 e21f0d2ca82..c52f2627253 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 3d84e4480b4..d98d60f4643 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 6c67d881153..0089f6db8fc 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 1715f019f27..6af231bb4c5 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 b440d76ebc2..a265a7c204c 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 3bdf650ddd3..9822c2fa821 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 c8db962215f..3de74cbbf4c 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 6ea8348812d..4aaf241536f 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 0896f43b1b5..ddfee94aa04 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 efc71df7adc..50eadfc6667 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 b2f3b5efffa..a17059c5eab 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 51a5636092a..871e1b4ecbc 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 6f85187cfeb..50c57e6db3e 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 6f51b9df744..2ac69c3a22d 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -14,6 +14,7 @@ from homeassistant.components.update import ( ) 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 @@ from .entry_data import RuntimeEntryData KEY_UPDATE_LOCK = "esphome_update_lock" +_LOGGER = logging.getLogger(__name__) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -109,14 +113,10 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): 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 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): """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 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): ) -> 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 f4b3bfa3ec7..e809089da11 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -15,6 +15,7 @@ from aioesphomeapi import ( 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_generic_device_entry( "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 53ae72e375e..bd38f4d3302 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 703523ed364..6b2870e8488 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 05ac16381d1..2954a5fe377 100644 --- a/homeassistant/components/gardena_bluetooth/__init__.py +++ b/homeassistant/components/gardena_bluetooth/__init__.py @@ -20,7 +20,7 @@ import homeassistant.util.dt as dt_util 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 00000000000..367e2f727bc --- /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 165e336bbec..c7a6e9637df 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 77ae80b62ff..fc7b3c03abe 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -224,7 +224,7 @@ async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: 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 05e2f8bd9fb..e3d59ff43f7 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 0bcb209a760..37b4577e5df 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 59de2ae630c..5f64e38c4a3 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 @@ SENSOR_DESCRIPTIONS = ( ), AirVisualProMeasurementDescription( key="battery_level", - name="Battery", device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, @@ -60,7 +58,6 @@ SENSOR_DESCRIPTIONS = ( ), 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 @@ SENSOR_DESCRIPTIONS = ( ), 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 @@ SENSOR_DESCRIPTIONS = ( ), 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 @@ SENSOR_DESCRIPTIONS = ( ), AirVisualProMeasurementDescription( key="temperature", - name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -107,7 +101,6 @@ SENSOR_DESCRIPTIONS = ( ), 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 8daea2cdd46..e4bc1664fd9 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -167,18 +167,14 @@ class ImageEntity(Entity): """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 @@ class ImageEntity(Entity): 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 c0c32d48794..4b98b69eb19 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 95ce968a67f..e34a7c88229 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 602138e82f5..a7ccdd48202 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 7ec58ccd8e5..33ee8bbe4c9 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 0ba99c0984a..397a41bf24b 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 585db4b1fa3..d7ac882bb82 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 5235a8bf325..ccdc2374142 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 9eb3d14a225..7949c066916 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 b14246a392d..b7762732303 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 e5948a54a8d..8730ffe01d5 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 90e4f094ee6..276844a8806 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 8dfd203c84b..ad8ebaa016e 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 cb322320675..5143b519d27 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 6c07d1fea55..85556bbcd5a 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 cae88ef24c1..4b168126251 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 04be198d149..4c7c224bc44 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 aacaf81465b..30ad296554c 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 3ee3fe7609f..641b1dbadf3 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 fafd5fb96b0..9083ba93795 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 bac4e63e288..f254f7602f8 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 4de0f85851f..ce622e48aae 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 05ea76f3179..78274b0586c 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 f56a4e514b7..3dc02f56030 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 a64e1be7fcf..e82c6a0db12 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 bcd818effb0..204f43768c7 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 632fe832aa8..e32ab875c28 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 2dfb3465d68..d9b89608072 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 7a4ee9d4fc3..ef36d46d8b9 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 84f05b88384..eeae9aa2e2f 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 07c8fa8bdb5..1cd67d3b021 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 d646f20f7a1..48f347a0908 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 713cc84efd4..d402e27287c 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 49a7388add2..c81b9f0ea39 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 5724ad643fe..7dc44e47a98 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 512e00237d9..468cdebf0b1 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 05ae600d4b7..fc43fc3000e 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 bd2b4f11b9d..b2fb73cc020 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 5ef15827eb9..c854307dd92 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 e8cdbe23a5c..4bc705adfbe 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 a2aff2a4207..97da526185f 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 2ec1fe1bc41..2bbbb229949 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 d8214c3aa8b..aa38ee170a5 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 776644c7cfa..1869673b643 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 7617d56d512..d1d812cb210 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 a7bc0190f5f..7e8c32017ce 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 dd845fc2a1b..7cbb10a236b 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 2ecac4a5742..d61e8a7b7a8 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 c7a6e9637df..0a9677b1f92 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 1c1092ee256..ac057f5c639 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 7fa1569992f..b3594f31510 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 2df5398222c..2b1b41a2c28 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 83537c6b1de..2bd70750ff9 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 bbf521b06e3..1c656b46b9e 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 d2c196dbfdd..f507387e628 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 f416adac027..59630e87932 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 8d2fb38517d..8dacb0e6321 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 8e2b435483f..9ae22090d7f 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 e954c0cccf6..c45d455631b 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 6efc9ec4272..22715907a99 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 635fe08cccc..7bd362cf3d7 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 2c031dc0a02..124aa070595 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 495c5dad1cc..e2a3e9dc7e1 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 41eedbe83a8..091f0c18232 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 57cb5c3eb56..791b1a21929 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 7420ef7f3f9..e47ae0fca84 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 14f723694fc..48ebbe5d345 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 6a20c5f8a54..3795508d75d 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 50c57e6db3e..41826dc6ae7 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 2c3f493e2c8..aef5dba1986 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 41a16408783..ec26e423e06 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 8f9c06f53fb..a9ec58f12ad 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 9bc7750790f..96db11d4656 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 e52a0882eb1..5ba0812697f 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 3ee45a4f464..af2152a88bb 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 3ba996adff7..37cdd5c0343 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 3a3940ffc2c..74c2b3ee440 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 be8535daec6..4c6c68b9573 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 fa7dd9b6bf8..f3ea929c9ec 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 542df60f13f..b39bad14d45 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 3906dcb89fe..707d7d71d34 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 11e2f66f91e..1f85c20fc72 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 30c22a8ca0e..0cebfd4bf5c 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 7307caa6bb3..e5ffff68162 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 57e7fc68582..ab2d4ad9440 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 56ff9018530..1ff008653d4 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 cd08638c775..e1a6863a199 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 ac06e125b0c..21d2bdc84bd 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 fe9a4b6453f..006fd5ebcc7 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 267100eaad6..e441832926b 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 dfbc0b4e384..9d155ae32ae 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 fe9cc3b528a..8436d24902c 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 7e214df2592..a4d9138d88e 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 10ebcc68f64..aad9c122d23 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 9a06fb45ad2..4f641238a49 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 64718308325..d0e456f142b 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 bc546321da3..b5ec175d1c9 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 3d5ae9b6a61..61f1ca9180a 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 9c881e6324f..a714d1af00f 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 3ff8d4308a3..944f2b32fab 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 b1008336992..4357276a650 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 ce18a4d153f..e73fac97bb7 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 21902ad1825..75b8375adb1 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 73e6a150a09..0e1e71fd82c 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 c9cf755ad13..61694074d79 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 bb6ac1879da..2e18ba3654f 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 397d7267bc0..defe412e96d 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 4ecf4cfee45..003531518dc 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 ae47b33774d..f314ddd47d3 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 7e0ff2c99d6..30fe5f46d6b 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 b6941f51392..2c2def6b7a3 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 05d0e716ef4..e9125f33016 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 9c4c67bddf7..1fd10282991 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 a863b9596b1..6fa421e0855 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 23a1fb8dfa6..e145f5ea8ca 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 4dae6cb898b..d733e39a0fc 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 11f94e7a72c..b3d03815278 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 70a3767c889..2cf18248ab8 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 2a7bd307ff8..f58731a2377 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 d23fe1c0924..a5b8395b56b 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 12d5c3e21f6..a29a8952434 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 a82284c24af..c4daf32499a 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 6b8ddb46c49..5f256233149 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 7b9f6789c79..b9aae585d9f 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 53f5f0153fe..aa992b4874f 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 5e7c61c1820..ff505010713 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 26ca5dedd34..64b3f22293a 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 6f37bcce85e..de61d38ffea 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 3d776193432..2132cab8682 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 9f4d0c2e34d..6046189ddc4 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 783c876fe62..fc48ebce4eb 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 17539387a29..24f0d806edd 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 15ca4c36da8..5590691e245 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 e0b8cb0cdf0..0b0c3d87822 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 6c49fb38d6c..85ddf559cf5 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 63ebd31b34c..3b3e6221895 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 939c30766e2..e52ab554473 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 5711656ef69..a8e146eeb27 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 79b633e28b6..4894bc6437d 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 3fc9691cc12..7ea18304164 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 265184e6227..eeb2c3d3224 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 598a2bddfff..ddac4713fac 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 4be806ebbbd..99216035080 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 c130feaa620..974e5fb7d37 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 6bf8cbcc166..c005c241d79 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 d6c9f4d53f6..724e1a86477 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 878341f23bc..bc1e68db02f 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 a4df830f1fe..d6874f94f95 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 c5b5136e970..7ce1d727b17 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 616a4fc5a11..7af95aab38c 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 caec5b8a288..ec2721aba8b 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 13fe16aa28c..87881e3414b 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 292ae55da1f..4d2c497dc8b 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 48f84ea7baf..aa89d87b6be 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 a3cd4879c6a..8827df6a08a 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 2ce3c3835a6..8474d391141 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 6e1ec9643a7..2d297cc829e 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 24ed1aaf568..f7ae9c9f238 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 e4b2b40637c..c3e1f949152 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 b68359a5176..dea370f45b3 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 8104fdd285e..eeca235ab44 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 8bfd9fb96b1..832f3b4f899 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 da53f79b697..504823c4d16 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 41e40525081..bd4a48df915 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 b5279804d0a..6daa5c9cb1a 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 c3fdcc8f1f4..97741bd65bb 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 f4443e89f76..ccb7d878a49 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 6afae5ffe7b..e441d4695ed 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 fd2287e08be..73ac6e08c17 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 b5b6dea93d5..7e4590d35a2 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 45d0c7de1c8..ea052f0b45a 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 8fccc3cb9e9..588dc3ebf5c 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 09b9dd09540..f38989b536e 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 b33cef0026a..42efaeb0538 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 bef853001a1..948c079444d 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 4e51177910c..3bfb58f8104 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 335daa68ee8..f715529b36b 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 314f6f8b4e5..0ff64eeda53 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 bb9e1d4d848..b2b270e3422 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 985edb05645..a5e7b73e59e 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 aff89019e4c..94dc9aa219f 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 2efa3c9a1c3..656219f13bb 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 3de74cbbf4c..b1c332984a1 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 4aaf241536f..a217a7a36b1 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 0944c91fd83..a77b78c5a09 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 578d2a96ff8..a9588855818 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 5928013e098..ec0c5d0702a 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 ddfee94aa04..ecb69d9fc38 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 af26ed13b38..c4f28fc750b 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 18b762057a7..03a93bd9a5b 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 1ecc2bc4db8..7f369e9909b 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 f0a607f2da7..a92e7aa605e 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 50eadfc6667..1e44191a762 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 1e2e41d2741..34e8b845472 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 37b4577e5df..3b86cbdd5a4 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 63add194d08..0c5a1d30976 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 00000000000..86812318218 --- /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 a318c7c08bc..3bfaa279e93 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 9839fefd9d5..0c8c8a2a30f 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 a1264acc9ff..1e0cbfe0f11 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 b4dc01d03aa..0c5e6f4139b 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 9f133c0b0ca..ae22fb7b7ef 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -75,11 +75,11 @@ class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): "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 78274b0586c..01c5673d4b1 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 21f7acd59e3..4dcde6d883f 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 60507d5a8c6..6ac4e622736 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -345,7 +345,6 @@ class UniFiController: 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 e3efaef915b..d0f387a3c6c 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 95197dcbb49..3c101dff9cc 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -413,9 +413,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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 eddb2633bea..50877756d52 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -37,7 +37,6 @@ from .const import ( CONF_CALLBACK_URL_OVERRIDE, CONF_LISTEN_PORT, CONF_POLL_AVAILABILITY, - DOMAIN, LOGGER as _LOGGER, MEDIA_METADATA_DIDL, MEDIA_TYPE_MAP, @@ -381,7 +380,6 @@ class DlnaDmrEntity(MediaPlayerEntity): 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 e07e0b6cfcb..f8413e8f620 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.const import ( 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 452de577ce0..18e6197360a 100644 --- a/homeassistant/components/knx/device.py +++ b/homeassistant/components/knx/device.py @@ -25,7 +25,7 @@ class KNXInterfaceDevice: _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 ca4e799f16b..31d0ff09467 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 4822b2d2704..67b4e1c9299 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 26d4911ddda..103bcb889ef 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 864938446e8..114e164481c 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 85b8b22043a..f52bdca4b86 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 de3e2f9d3ea..3cf92a8adcc 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 87d8cf398fb..69e1161a65c 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -80,7 +80,9 @@ class BroadlinkDevice: """ 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 5cea4ca3b15..bcac6ac1e1d 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 286929c5345..05edfbad91d 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -365,7 +365,7 @@ class ScannerEntity(BaseTrackerEntity): 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 4a9f6c2b163..9f9ec48f347 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -196,7 +196,7 @@ class WatcherBase(ABC): 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 213fabc911b..2b56a9f6cbb 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 8c7f86700e7..9227b7da617 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 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): 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 7aff107d8c5..c50b70245e3 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -220,7 +220,9 @@ class ControllerManager: # 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 d58086e59ec..007f8895bf0 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -128,7 +128,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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 f450c38527a..988adbd87a7 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -203,7 +203,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """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 bc3ce49cb6b..6fed4bc16d1 100644 --- a/homeassistant/components/hue/v2/device.py +++ b/homeassistant/components/hue/v2/device.py @@ -58,7 +58,7 @@ async def async_setup_devices(bridge: "HueBridge"): @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 5878f01889b..ef01b2e4693 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -147,7 +147,9 @@ class HueBaseEntity(Entity): # 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 e0296bcb434..b8521a80af7 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -44,7 +44,7 @@ async def async_setup_hue_events(bridge: "HueBridge"): 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 @@ async def async_setup_hue_events(bridge: "HueBridge"): 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 2e9af4ad9e6..537b4b8f860 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -200,7 +200,9 @@ class IBeaconCoordinator: 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 bffda965456..d48d87fa347 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 af4e5700805..4a7f30506b2 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -422,7 +422,7 @@ class KodiEntity(MediaPlayerEntity): 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 72b66bc5cf1..7ef7eb73673 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 776ad116f4a..e190b25eded 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 6d20f29905d..da2c03745fa 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -142,7 +142,7 @@ async def _async_migrate_unique_ids( 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 8e76706b7fd..52b8e905b4b 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -80,7 +80,7 @@ class MatterAdapter: 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 5f2a0b0bffd..e85073061c2 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -152,7 +152,9 @@ class SignalUpdateCallback: 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 e269b76fcc4..891365655de 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 d9478a99316..ba2faaeaae5 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -244,7 +244,7 @@ class NestEventMediaStore(EventMediaStore): 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 12798c164f8..ff6783ecaa3 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -70,7 +70,7 @@ class NetatmoBase(Entity): 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 ad228f08a4b..258f14056ca 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -339,9 +339,13 @@ class NotionEntity(CoordinatorEntity[DataUpdateCoordinator[NotionData]]): 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 c1df94f5f83..842fe4298cf 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -171,7 +171,7 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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 b27051b1492..7c9cab5f181 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 2e345905d50..babdbc573bd 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 69959453a78..8f08aab8d30 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 17fc6f3cc4d..dec1b35d346 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 2123ee74f1b..7d4331f0d40 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 49caf30b010..f01cdddb1db 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -223,8 +223,7 @@ async def async_setup_trigger( 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 b490b4c724c..70cedd9dd3d 100644 --- a/homeassistant/components/tasmota/discovery.py +++ b/homeassistant/components/tasmota/discovery.py @@ -302,7 +302,7 @@ async def async_start( # noqa: C901 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 242c2179a05..996490282d5 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 88c500f18fd..3e4410fa41a 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -178,7 +178,7 @@ class ProtectData: 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 359e4c6831a..3cb119837d7 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -100,7 +100,7 @@ class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator[list[UptimeRobotMon 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 8c1dd9b2197..7ff351893b1 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -254,7 +254,7 @@ class DriverEvents: 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 @@ class ControllerEvents: 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 @@ class ControllerEvents: 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 @@ class NodeEvents: ) 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 @@ class NodeEvents: """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 @@ class NodeEvents: "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 @@ class NodeEvents: 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 867405530ab..5fc7da68e99 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -2347,7 +2347,7 @@ def _get_node_statistics_dict( """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 33cb59d8505..edc10d4a16e 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -232,7 +232,7 @@ async def async_attach_trigger( 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 52ecc0a7742..c44a0c6336a 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -179,7 +179,7 @@ async def async_attach_trigger( 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 1740820d0ba..86cebe81180 100644 --- a/homeassistant/components/zwave_me/__init__.py +++ b/homeassistant/components/zwave_me/__init__.py @@ -96,7 +96,7 @@ class ZWaveMeController: """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 7df01fc8fd2..79b4eac68d5 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -272,13 +272,14 @@ class DeviceRegistryItems(UserDict[str, _EntryTypeT]): 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 @@ class DeviceRegistry: @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 @@ class DeviceRegistry: 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 @@ class DeviceRegistry: 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 @@ class DeviceRegistry: 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 bb9c4d45a32..29e6f9a8f31 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 bcbc0fc9cde..b97911262ef 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 00048e09577..5665f7529d5 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 f1802ce51aa..e00350b7627 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 35edfb977a9..93bad2db295 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 348894346bb..85169e80394 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 81718fe277c..f8e26289691 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 8145a7a1e99..a6a58b4fb39 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 c931fed78e2..dbd4cef0139 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 a77c6159927..2740b638316 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 723fd0d6332..e82d6615923 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 2216e4df737..171112c9097 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 8a3605782a2..5efa5ca96f7 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 ae7c39ed4ba..41a550b3c50 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 b29e6499fec..af54b1c2793 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 159b495e0f8..b5acf3e496a 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 ae208194d2a..c9f75e6b594 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 ebd4a028f8c..b1b30bda669 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 2603f0bf93a..287af75c9cd 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 576cf16044e..00cc0057d7c 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 9580430621b..a069ae0807b 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 c429c8b46a8..1784ba83446 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 test_updates_from_players_changed_new_ids( # 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 9c7736e2b8e..ead1f83cb94 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 112c138a843..109f4205901 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 19e1b738aed..0c27e0a3648 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -325,9 +325,7 @@ async def assert_devices_and_entities_created( # 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 aea91c06e88..3be150f0269 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 26c323617d2..ab400c53ee4 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 f7f08188036..d5ac8406f24 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 f83ed9c7e78..a6234f34593 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 667c73a20ac..6c4cc4e512e 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 49338c72c5d..dcdd86f0902 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 2437c7c1351..2e3aafb4984 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 cf52263e69d..a6bcdf63950 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 637aeec1b0b..47287fbd1d2 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 2a217bebd5f..5d6961e57c3 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 f64af98c9b5..70a5a89a3ae 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 62ed847bf28..8ed309f61df 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 ce1dc19319a..4faf48e2118 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 6972bee35d0..5f5c5f7854e 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 ea07834976b..5494e69d9e9 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 03c39a4b542..f0fe4f1faba 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 cd1cc7280c6..cfd714725c4 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 3793902258d..ddce53bfca0 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 9f3b7565332..485c2774f7b 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 81a86f1c61f..fb103384874 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 14074ce1135..62b87bdb791 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 08aa53aec7a..9432f231301 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 8f2aa754bac..d5483cf3a74 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 c18e24d1a70..55eac636edb 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 f659568c674..a35b10afa9c 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 408e4e0d963..191253a2a9a 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 2b25694de6c..6c827e76163 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 c7cbaf4d131..ebafd313ff4 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 503ba23e052..b72ac7e3a79 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 0bd4c538cf6..6b602c8c4d1 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 2ecdfcc537f..1335a1595d3 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 8dd0a1bf154..8c47410ce40 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 31148d4551a..76ea88b4b45 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 d0ba320c1c6..58d51eca537 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 d53ef6a7a02..087a6840c59 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 02e9ec87630..a253810c4c8 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 56756aa87fb..e49817469b4 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 a223bcd8f74..ee74dbbedc8 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 4a54b900be1..34b49f5d581 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 b6f2159ae13..d6fe0bd40fc 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 fe917504dcd..ce875190efb 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 f8c21166fe1..bdf3cc901a7 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 aef9ce319e7..ccf4b50fa1b 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 0e01910c84a..d2d0a133859 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 0d237cec132..58111087848 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 cc7b67145c1..ab163360778 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 f90395f0064..437acb04f56 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 435a5ac6f5a..e3f473e01c6 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 a40917cfc3c..0a98f746c4c 100644 --- a/tests/components/steamist/test_init.py +++ b/tests/components/steamist/test_init.py @@ -105,7 +105,7 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( 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 4744d6c2ccf..703dd2a1893 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 880f4ed0e75..ffff4b1b8b0 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 @@ 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 @@ -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 74014c91102..9a3f4f91ec7 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 b19e8e51103..09467b893e0 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 test_device_remove_non_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 @@ -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 278c2549b45..f66c82dc2ed 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 ce0a08f18ff..0a1a727abcf 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 c421a08ccf8..189dff49839 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 fec1bf7a04a..c027b57acf8 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 85454959cf4..eba850e61e9 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 e0ef37c1b75..0e258d0e1c7 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 02df1b0e32e..bd3babdc383 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 d938512981f..46cdff180e9 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 85e012c5bfb..22f62cb977a 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 5ec555d88df..0bb06ea723b 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 3d20749baac..44495cf0e15 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 c6a0f7a845d..ebdf2112435 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 ccb65c1d8fa..b5d4149a526 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 11213d9c375..f7aacec36ac 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 8209564579c..fd091b2bfe7 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 aa5ec77b798..4454e38e0d8 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 @@ async def test_device_diagnostics_secret_value( 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 a33ee75661c..3ec1f113b3e 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 a8671edbe64..54638358fe7 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 eae9d6f5416..501ad13cbaa 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 dca28929b3b..d5496255add 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 e183bd4c380..7df5859f502 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 9673e1dc73a..e07c3cb4753 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1108,7 +1108,7 @@ async def test_device_info_called(hass: HomeAssistant) -> None: 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 test_device_info_not_overrides(hass: HomeAssistant) -> None: 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 test_device_info_homeassistant_url( 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 test_device_info_change_to_no_url( 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 test_device_name_defaulting_config_entry( 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 34eee944114..b43dabbba80 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -84,6 +84,7 @@ class ValloxFanEntity(ValloxEntity, FanEntity): """Representation of the fan.""" _attr_has_entity_name = True + _attr_name = None _attr_supported_features = FanEntityFeature.PRESET_MODE | FanEntityFeature.SET_SPEED def __init__( From 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 660e4c770b0..b3c36ed39d2 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 103bcb889ef..76ab11172bb 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 114e164481c..37388bfca95 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 a4da77fe50b..019ff7b7863 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 3e4410fa41a..73d05f1be1d 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -227,7 +227,7 @@ class ProtectData: 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 15bd17554ad..79ee483dd8d 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 @@ class ProtectNVREntity(ProtectDeviceEntity): 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 cfa90664f36..95353e84754 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 76ab11172bb..59a058352f2 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 37388bfca95..d3163bc5fd2 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 4f3fcbf9c87..479c84d238c 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/vallox", "iot_class": "local_polling", "loggers": ["vallox_websocket_api"], - "requirements": ["vallox-websocket-api==3.2.1"] + "requirements": ["vallox-websocket-api==3.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 59a058352f2..a1d3f619fd7 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 d3163bc5fd2..b166ed3ae48 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 bed677ebd30..f4c690dcffc 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 764b12cedc2..4f208ed0115 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 6eaf2885d89..a161c3ecde1 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 cdc270f2e99..51acbe8c7d9 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 8ae3ba06985..6b3e1d8506d 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 a1d3f619fd7..8742fd97c77 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 b166ed3ae48..f9237251eba 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 fc91f8eb72e..08e4fb007b7 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 8742fd97c77..aa80f7676ca 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 f9237251eba..0a7fe75fe45 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 2fc32129d1f..aacda108398 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -78,10 +78,6 @@ class DomainData: """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 3b48e96a351..e7ebb7b546b 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 aa80f7676ca..7497521708a 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 0a7fe75fe45..681c17952e4 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 d452ab8764a..00b9883f261 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 @@ class ESPHomeClient(BaseBleakClient): 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 @@ class ESPHomeClient(BaseBleakClient): 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 @@ class ESPHomeClient(BaseBleakClient): 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 0f827d60736..f9c68e2b1f0 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1001,13 +1001,14 @@ class MediaPlayerEntity(Entity): 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 6b2870e8488..52350a498d9 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 9aeba56360e..4355d3d2595 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -35,6 +35,7 @@ PLATFORMS_BY_TYPE: dict[str, list] = { 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 00000000000..a5dbdea1d6f --- /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 075fe6bd6d1..1a8bfba21fb 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -33,7 +33,6 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { 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 aec1f892b1f..1b244182059 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 35562877bab..178859ecbe7 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -10,6 +10,8 @@ from typing_extensions import Self 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 @@ class StrEnum(str, Enum): 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 407860526ae..bfc2cd1a2e7 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 83fea57a9fa..8fc90efaabc 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -37,6 +37,7 @@ from homeassistant.const import ( 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 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): 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 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): 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 2ee32ca9dbc..47e3e765b72 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 368a3eb6e98..89618685873 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 dd1f3d061a9..1d77ac8f130 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 ccb7d878a49..e7896f5da86 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 bfc96eabfdf..2946c8c3743 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -191,21 +191,6 @@ class MaxLengthExceeded(HomeAssistantError): 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 79b4eac68d5..a59313ed886 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -6,13 +6,14 @@ from collections.abc import Coroutine, ValuesView 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 @@ if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntry from . import entity_registry + from .entity import DeviceInfo _LOGGER = logging.getLogger(__name__) @@ -60,6 +62,39 @@ DISABLED_CONFIG_ENTRY = DeviceEntryDisabler.CONFIG_ENTRY.value 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 @@ class DeviceRegistry: *, 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 @@ class DeviceRegistry: 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 @@ class DeviceRegistry: 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 @@ class DeviceRegistry: 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 f97e509f486..067d6430c9f 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -7,7 +7,6 @@ from contextvars import ContextVar 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 .issue_registry import IssueSeverity, async_create_issue 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 @@ PLATFORM_NOT_READY_RETRIES = 10 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 @@ class EntityPlatform: 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 @@ class EntityPlatform: 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 7df5859f502..3e59b08cfa8 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -8,7 +8,7 @@ import pytest 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 7de6f70e793..0d9ee76ac62 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -972,6 +972,7 @@ async def _test_friendly_name( 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 e07c3cb4753..1f7e579ea95 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1830,6 +1830,7 @@ async def test_device_name_defaulting_config_entry( 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 8676365212a..33064d21097 100644 --- a/homeassistant/components/wyoming/__init__.py +++ b/homeassistant/components/wyoming/__init__.py @@ -7,11 +7,16 @@ from homeassistant.config_entries import ConfigEntry 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 26443cc11eb..fd73a6bd047 100644 --- a/homeassistant/components/wyoming/const.py +++ b/homeassistant/components/wyoming/const.py @@ -5,3 +5,6 @@ DOMAIN = "wyoming" 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 9ad8092bb8c..7fbf3542e13 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 0fc7bf5e6c4..6510fd8c761 100644 --- a/homeassistant/components/wyoming/tts.py +++ b/homeassistant/components/wyoming/tts.py @@ -6,14 +6,14 @@ import wave 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 @@ class WyomingTtsProvider(tts.TextToSpeechEntity): 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 @@ class WyomingTtsProvider(tts.TextToSpeechEntity): @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 @@ class WyomingTtsProvider(tts.TextToSpeechEntity): 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 7497521708a..74c409bc8c1 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 681c17952e4..24f5987227f 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 d48b908f26b..3d12d41ce5e 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_INFO = Info( 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 eb0b33c3276..1cb5a6cb874 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 8767660ca08..51a684bc4fd 100644 --- a/tests/components/wyoming/test_tts.py +++ b/tests/components/wyoming/test_tts.py @@ -8,7 +8,7 @@ import wave 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 e635b1f7021..54b65c17e60 100644 --- a/homeassistant/components/devolo_home_network/manifest.json +++ b/homeassistant/components/devolo_home_network/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["devolo_plc_api"], "quality_scale": "platinum", - "requirements": ["devolo-plc-api==1.3.1"], + "requirements": ["devolo-plc-api==1.3.2"], "zeroconf": [ { "type": "_dvl-deviceapi._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 74c409bc8c1..b9bea4e24e2 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 24f5987227f..a11459908c0 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 972db00e476..3166c05db19 100644 --- a/homeassistant/components/mystrom/__init__.py +++ b/homeassistant/components/mystrom/__init__.py @@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: info.setdefault("type", 101) device_type = info["type"] - if device_type in [101, 106, 107]: + if device_type in [101, 106, 107, 120]: device = _get_mystrom_switch(host) platforms = PLATFORMS_SWITCH await _async_get_device_state(device, info["ip"]) @@ -86,7 +86,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" device_type = hass.data[DOMAIN][entry.entry_id].info["type"] platforms = [] - if device_type in [101, 106, 107]: + if device_type in [101, 106, 107, 120]: platforms.extend(PLATFORMS_SWITCH) elif device_type in [102, 105]: platforms.extend(PLATFORMS_BULB) diff --git a/tests/components/mystrom/test_init.py b/tests/components/mystrom/test_init.py index 80011b47915..4100a270e0a 100644 --- a/tests/components/mystrom/test_init.py +++ b/tests/components/mystrom/test_init.py @@ -68,7 +68,7 @@ async def test_init_switch_and_unload( (110, "sensor", ConfigEntryState.SETUP_ERROR, True), (113, "switch", ConfigEntryState.SETUP_ERROR, True), (118, "button", ConfigEntryState.SETUP_ERROR, True), - (120, "switch", ConfigEntryState.SETUP_ERROR, True), + (120, "switch", ConfigEntryState.LOADED, False), ], ) async def test_init_bulb( From 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 0f6a52ef578..ef1dd785392 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 1b244182059..f3e76c67480 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 7934220cf91..b8551682f1f 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -8,7 +8,7 @@ from mullvad_api import MullvadAPI 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_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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 f39f05dd430..2ccf754bbbd 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 5b6ef78133f..ad045dbb54c 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 04069d42d7d..3914e0c52c1 100644 --- a/homeassistant/components/imap/__init__.py +++ b/homeassistant/components/imap/__init__.py @@ -14,7 +14,7 @@ from homeassistant.exceptions import ( 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 00be545fb67..4c4a2e2a35c 100644 --- a/homeassistant/components/imap/config_flow.py +++ b/homeassistant/components/imap/config_flow.py @@ -33,6 +33,7 @@ from homeassistant.util.ssl import SSLCipherList 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 @@ OPTIONS_SCHEMA_ADVANCED = { 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 2e36dd41e16..fd3da28971e 100644 --- a/homeassistant/components/imap/const.py +++ b/homeassistant/components/imap/const.py @@ -11,6 +11,7 @@ CONF_CHARSET: Final = "charset" 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 6fad8895931..62579d61f5a 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 fb4347b08a7..efb505cda77 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 ff949423614..31b42b50781 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 @@ from tests.common import MockConfigEntry, async_capture_events, async_fire_time_ @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 4e3d052e6ef..6be1822f90f 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -63,7 +63,10 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): self._attr_native_min_value = static_info.min_value self._attr_native_max_value = static_info.max_value self._attr_native_step = static_info.step - self._attr_native_unit_of_measurement = static_info.unit_of_measurement + # protobuf doesn't support nullable strings so we need to check + # if the string is empty + if unit_of_measurement := static_info.unit_of_measurement: + self._attr_native_unit_of_measurement = unit_of_measurement if mode := static_info.mode: self._attr_mode = NUMBER_MODES.from_esphome(mode) else: diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 3185a5eb536..2e658389e03 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -76,7 +76,10 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): super()._on_static_info_update(static_info) static_info = self._static_info self._attr_force_update = static_info.force_update - self._attr_native_unit_of_measurement = static_info.unit_of_measurement + # protobuf doesn't support nullable strings so we need to check + # if the string is empty + if unit_of_measurement := static_info.unit_of_measurement: + self._attr_native_unit_of_measurement = unit_of_measurement self._attr_device_class = try_parse_enum( SensorDeviceClass, static_info.device_class ) diff --git a/tests/components/esphome/test_number.py b/tests/components/esphome/test_number.py index cf3ee4876a8..dc90d1c1098 100644 --- a/tests/components/esphome/test_number.py +++ b/tests/components/esphome/test_number.py @@ -15,7 +15,7 @@ from homeassistant.components.number import ( DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -89,3 +89,36 @@ async def test_generic_number_nan( state = hass.states.get("number.test_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 9a1863c3c90..6c034e674ee 100644 --- a/tests/components/esphome/test_sensor.py +++ b/tests/components/esphome/test_sensor.py @@ -13,7 +13,7 @@ from aioesphomeapi import ( ) from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass -from homeassistant.const import ATTR_ICON, STATE_UNKNOWN +from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -275,3 +275,30 @@ async def test_generic_text_sensor( state = hass.states.get("sensor.test_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 6025af19a60..4a41e572c1a 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 8a9b8e9af70..375ed58035b 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -110,7 +110,7 @@ class SonosLevelEntity(SonosEntity, NumberEntity): """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 dab70466c85..ca3cc89d750 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 7ce1d727b17..fb10167f1d0 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 1201ed96490..c551d4a00d3 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -67,18 +67,6 @@ POLL_REQUIRED = ( 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_setup_entry( ) _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 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity): 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 2d7a9322aeb..40b0c2d21c6 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 c6be200e2b2..27a259f4441 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -119,9 +119,6 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): @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 f4fbe4a498f..bd800ea04dd 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -9,6 +9,7 @@ from typing import Any, Concatenate, ParamSpec, TypeVar 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 @@ def adb_decorator( 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 @@ class ADBDevice(MediaPlayerEntity): 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 @@ class ADBDevice(MediaPlayerEntity): # 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 @@ class ADBDevice(MediaPlayerEntity): 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 @@ class ADBDevice(MediaPlayerEntity): 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 @@ class ADBDevice(MediaPlayerEntity): 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 @@ class ADBDevice(MediaPlayerEntity): _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 @@ class ADBDevice(MediaPlayerEntity): 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 @@ class AndroidTVDevice(ADBDevice): 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 @@ class FireTVDevice(ADBDevice): 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 5ebd95ccacd..f0fca5aae90 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -23,7 +23,7 @@ PROPS_DEV_MAC = "ether ab:cd:ef:gh:ij:kl brd" 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 @@ class AdbDeviceTcpAsyncFake: 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 @@ class ClientAsyncFakeSuccess: 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 @@ class ClientAsyncFakeFail: 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 59c7ce751ac..c7083626e15 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 e448fe066c4..4bc9bb24835 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -42,11 +42,12 @@ from async_upnp_client.utils import CaseInsensitiveDict from homeassistant import config_entries from homeassistant.components import network from homeassistant.const import ( + EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL, __version__ as current_version, ) -from homeassistant.core import HomeAssistant, callback as core_callback +from homeassistant.core import Event, HomeAssistant, callback as core_callback from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import config_validation as cv, discovery_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -728,15 +729,18 @@ class Server: async def async_start(self) -> None: """Start the server.""" - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) - await self._async_start_upnp_servers() + bus = self.hass.bus + bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) + bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, self._async_start_upnp_servers + ) async def _async_get_instance_udn(self) -> str: """Get Unique Device Name for this instance.""" instance_id = await async_get_instance_id(self.hass) return f"uuid:{instance_id[0:8]}-{instance_id[8:12]}-{instance_id[12:16]}-{instance_id[16:20]}-{instance_id[20:32]}".upper() - async def _async_start_upnp_servers(self) -> None: + async def _async_start_upnp_servers(self, event: Event) -> None: """Start the UPnP/SSDP servers.""" # Update UDN with our instance UDN. udn = await self._async_get_instance_udn() diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index a80b9f48798..ed5241a42ad 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -742,6 +742,8 @@ async def test_bind_failure_skips_adapter( SsdpListener.async_start = _async_start UpnpServer.async_start = _async_start await init_ssdp_component(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() assert "Failed to setup listener for" in caplog.text From 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 e92e80a9a68..d03073dcfd3 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/onvif", "iot_class": "local_push", "loggers": ["onvif", "wsdiscovery", "zeep"], - "requirements": ["onvif-zeep-async==3.1.9", "WSDiscovery==2.0.0"] + "requirements": ["onvif-zeep-async==3.1.12", "WSDiscovery==2.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index b9bea4e24e2..1b99449ff9d 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 a11459908c0..f8736dc37ea 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 a524d8ea519..358cbbf5c83 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -387,8 +387,12 @@ class ONVIFDevice: "WSPullPointSupport" ) LOGGER.debug("%s: WSPullPointSupport: %s", self.name, pull_point_support) + # Even if the camera claims it does not support PullPoint, try anyway + # since at least some AXIS and Bosch models do. The reverse is also + # true where some cameras claim they support PullPoint but don't so + # the only way to know is to try. return await self.events.async_start( - pull_point_support is not False, + True, self.config_entry.options.get( CONF_ENABLE_WEBHOOKS, DEFAULT_ENABLE_WEBHOOKS ), From 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 440ca5e6c9f..c1d35c94b4f 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 34e1d89b8b4..0e18a009323 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -177,7 +177,8 @@ class HassIOView(HomeAssistantView): ) 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 2a9d9b73978..4a612de7f87 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -20,6 +20,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession 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 @@ class HassIOIngress(HomeAssistantView): 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 @@ class HassIOIngress(HomeAssistantView): 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 6df946ad2cf..3eda10b1514 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 b0097f607d5..3ef9c0aba62 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -139,7 +139,7 @@ async def async_migrate_unique_id( dev_reg = dr.async_get(hass) old_unique_id = config_entry.unique_id new_unique_id = api.device.mac - new_name = api.device.values["name"] + new_name = api.device.values.get("name") @callback def _update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None: From 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 a44cfb3ce13..f1f1aed044b 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/rainbird", "iot_class": "local_polling", "loggers": ["pyrainbird"], - "requirements": ["pyrainbird==2.1.0"] + "requirements": ["pyrainbird==2.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1b99449ff9d..416ae7f3a49 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 f8736dc37ea..ee4c49ac945 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 b937e7f2e0b..314db187b6a 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -768,7 +768,7 @@ class HKDevice: 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 6171e9406a0..5a687020eb6 100644 --- a/homeassistant/components/homekit_controller/entity.py +++ b/homeassistant/components/homekit_controller/entity.py @@ -11,6 +11,7 @@ from aiohomekit.model.characteristics import ( ) 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 @@ class HomeKitEntity(Entity): 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 @@ class HomeKitEntity(Entity): """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 @@ class HomeKitEntity(Entity): 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 3b6f5bcdd2f..c111a23bf06 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 @@ class HeosMediaPlayer(MediaPlayerEntity): """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 b8a1ba64423..ce5ec4191c5 100644 --- a/homeassistant/components/tomorrowio/__init__.py +++ b/homeassistant/components/tomorrowio/__init__.py @@ -72,6 +72,8 @@ from .const import ( 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 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator[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 4b1e2487da8..51d8d5f31cc 100644 --- a/homeassistant/components/tomorrowio/const.py +++ b/homeassistant/components/tomorrowio/const.py @@ -115,3 +115,5 @@ TMRW_ATTR_PRESSURE_SURFACE_LEVEL = "pressureSurfaceLevel" 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 046dc79f2c6..6f75679f124 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -11,6 +11,7 @@ from pytomorrowio.const import ( PollenIndex, PrecipitationType, PrimaryPollutantType, + UVDescription, ) from homeassistant.components.sensor import ( @@ -64,6 +65,8 @@ from .const import ( 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 @@ SENSOR_TYPES = ( 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 1057477b0ac..c795dbfdbaf 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 d92ac401f92..86b84ec3ca6 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -224,6 +224,7 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): 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 ed5fb0982a0..0ca4f348956 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 487b3a4adb8..77335769383 100644 --- a/tests/components/tomorrowio/test_sensor.py +++ b/tests/components/tomorrowio/test_sensor.py @@ -60,6 +60,9 @@ CLOUD_COVER = "cloud_cover" 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 @@ V4_FIELDS = [ 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 f1f1aed044b..986e89783d7 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/rainbird", "iot_class": "local_polling", "loggers": ["pyrainbird"], - "requirements": ["pyrainbird==2.1.1"] + "requirements": ["pyrainbird==3.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 416ae7f3a49..96ff74ee91a 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 ee4c49ac945..59e8e4b66e4 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 89618685873..8fd01ada0e9 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 6b3e1d8506d..31dcba97924 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 f7467972773..317a68d36bc 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 210bd8a0bfc..cb78783559b 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 4f208ed0115..e5448fb395d 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 96ff74ee91a..47f1514f13d 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 59e8e4b66e4..0cb2d4a6a7d 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 237a2d97668..7169f01b38f 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -41,7 +41,6 @@ BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { ), "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 e9e434bc51c..a408bcb58bc 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -46,7 +46,6 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { ), "battery": SensorEntityDescription( key="battery", - translation_key="battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, @@ -60,7 +59,6 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { ), "humidity": SensorEntityDescription( key="humidity", - translation_key="humidity", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.HUMIDITY, @@ -74,7 +72,6 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { ), "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 c00f2fe79e4..8eab1ec6f1a 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 c2727654525..36c3ba27463 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 1b1e51db035..ced1094981f 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 02c4e7ce923..43dc4b4151d 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 67702ba5455..2af191f97bc 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 1f42c5dce48..d095ac81955 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 5815f931029..f50533e339a 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 @@ SENSOR_ENTITIES = [ ), 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 @@ SENSOR_ENTITIES = [ ), 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 @@ SENSOR_ENTITIES = [ ), 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 @@ SENSOR_ENTITIES = [ ), 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 @@ SENSOR_ENTITIES = [ ), 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 @@ SENSOR_ENTITIES = [ ), 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 @@ SENSOR_ENTITIES = [ ), 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 @@ SENSOR_ENTITIES = [ ), 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 a714d1af00f..6c1214f76c6 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 7097237bc5d..327d371769b 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 d7083715394..839d546588c 100644 --- a/homeassistant/components/evil_genius_labs/__init__.py +++ b/homeassistant/components/evil_genius_labs/__init__.py @@ -103,6 +103,8 @@ class EvilGeniusUpdateCoordinator(DataUpdateCoordinator[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 41fbcfa9b48..a915619b1b8 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 @@ class EvilGeniusLight(EvilGeniusEntity, LightEntity): ] 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 6514f2c5396..a728efdf05a 100644 --- a/homeassistant/components/bthome/config_flow.py +++ b/homeassistant/components/bthome/config_flow.py @@ -80,7 +80,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN): 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 b38c1d3829b..418c7b8e3e3 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 47f1514f13d..962e3fb3cdc 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 0cb2d4a6a7d..7c86965e902 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 ad5d5b45cbb..ee983148fd4 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 367e2f727bc..50cc209e268 100644 --- a/homeassistant/components/gardena_bluetooth/number.py +++ b/homeassistant/components/gardena_bluetooth/number.py @@ -16,7 +16,7 @@ from homeassistant.components.number import ( 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 @@ DESCRIPTIONS = ( 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 @@ DESCRIPTIONS = ( 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 @@ DESCRIPTIONS = ( 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 @@ DESCRIPTIONS = ( 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 a551c6e3b9e..8af04c11c18 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 typing import Any 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 ba43386b821..ebb0cc35c20 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 415531214c5..47681308b53 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 cb01a0febf5..8ef77f8d8c3 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 d16d406999e..d3c5665b945 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 af390a005a7..a8034588ed1 100644 --- a/homeassistant/components/select/__init__.py +++ b/homeassistant/components/select/__init__.py @@ -144,9 +144,10 @@ class SelectEntity(Entity): @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 @@ class SelectEntity(Entity): 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 | 15 +------ .../components/unifiprotect/select.py | 8 ++-- .../components/unifiprotect/sensor.py | 44 ++++--------------- 10 files changed, 73 insertions(+), 111 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index fe4399c4c6d..668fe479e1f 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 8c620402e77..3306743b707 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -183,7 +183,8 @@ class ProtectButton(ProtectDeviceEntity, ButtonEntity): 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 019ff7b7863..481d51ec529 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -151,23 +151,25 @@ class ProtectCamera(ProtectDeviceEntity, Camera): 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 @@ class ProtectCamera(ProtectDeviceEntity, Camera): @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 79ee483dd8d..fa85a0629cb 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -297,10 +297,12 @@ class ProtectNVREntity(ProtectDeviceEntity): @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 @@ class EventEntityMixin(ProtectDeviceEntity): 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 500b4b4703e..38ce73828c2 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 4fa9ebf4001..791a5e958ea 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -73,18 +73,19 @@ class ProtectLock(ProtectDeviceEntity, LockEntity): @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 @@ class ProtectLock(ProtectDeviceEntity, LockEntity): ): 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 4704c42762e..c3f4e58e247 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -98,21 +98,22 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): @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 8c688231628..375784d0323 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -3,7 +3,6 @@ from __future__ import annotations 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 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]): 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 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]): 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 753563023f4..26a03fb7967 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -356,15 +356,15 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity): @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 dec6f10a57f..d842b13b015 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 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity): 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 62579d61f5a..c332e3e8edb 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 b382d97a2ae..1f6c9961c51 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 30ad296554c..8f8e728cb9d 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 a184b91c05e..367eb6cb296 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 @@ class DuneHDPlayerEntity(MediaPlayerEntity): 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 @@ class DuneHDPlayerEntity(MediaPlayerEntity): 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 0446b244787..53c87349836 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 f3e76c67480..94a73fc16cd 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 fa85a0629cb..a8a4c78465d 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -3,7 +3,7 @@ from __future__ import annotations 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 @@ class ProtectDeviceEntity(Entity): @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 b146651b6e6..5bbbb0e895d 100644 --- a/homeassistant/components/airvisual_pro/__init__.py +++ b/homeassistant/components/airvisual_pro/__init__.py @@ -60,6 +60,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """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 5f64e38c4a3..69fbd1a128a 100644 --- a/homeassistant/components/airvisual_pro/sensor.py +++ b/homeassistant/components/airvisual_pro/sensor.py @@ -30,7 +30,9 @@ from .const import DOMAIN 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 @@ SENSOR_DESCRIPTIONS = ( 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 @@ SENSOR_DESCRIPTIONS = ( 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 @@ SENSOR_DESCRIPTIONS = ( 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 @@ class AirVisualProSensor(AirVisualProEntity, SensorEntity): 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 f06f120885e..04801c8fa0e 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 7f953946b69..5141782e574 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 a5dbdea1d6f..9bc65f12355 100644 --- a/homeassistant/components/ezviz/image.py +++ b/homeassistant/components/ezviz/image.py @@ -21,7 +21,6 @@ from .coordinator import EzvizDataUpdateCoordinator from .entity import EzvizEntity _LOGGER = logging.getLogger(__name__) -GET_IMAGE_TIMEOUT = 10 IMAGE_TYPE = ImageEntityDescription( key="last_motion_image", @@ -52,7 +51,7 @@ class EzvizLastMotion(EzvizEntity, ImageEntity): 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 d0831f2e90e..9f8e8bfb6f8 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -57,7 +57,7 @@ class AuthPhase: 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 a554001970b..f598906661c 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -51,7 +51,7 @@ class ActiveConnection: 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 728405b5d96..fcaa13ff8de 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -95,7 +95,7 @@ class WebSocketHandler: # 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 @@ class WebSocketHandler: 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 @@ class WebSocketHandler: 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 @@ class WebSocketHandler: 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 90e244aaf06..62417b0873a 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -144,6 +144,16 @@ WEBHOOK_PAYLOAD_SCHEMA = vol.Any( ), ) +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 2477e849666..e8303c12c10 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -453,7 +453,7 @@ class SensorEntity(Entity): 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 bf7f173e647..b13a861fa79 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -373,7 +373,7 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): 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 9e27f6efb3e..825064e5410 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -338,7 +338,7 @@ class ConfigEntry: # 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 @@ class ConfigEntry: 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 @@ class ConfigEntry: 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 @@ class ConfigEntry: 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 @@ class ConfigEntry: 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 @@ class ConfigEntry: 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 @@ class ConfigEntry: # 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 @@ class ConfigEntry: 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 @@ class ConfigEntry: # 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 @@ class ConfigEntry: "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 @@ class ConfigEntry: ) @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 96fc718c67d..351b39f61e7 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -24,10 +24,14 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send 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 @@ from .const import ( TRACKER_ACTIVITY_STATUS_UPDATED, TRACKER_HARDWARE_STATUS_UPDATED, TRACKER_POSITION_UPDATED, + TRACKER_WELLNESS_STATUS_UPDATED, ) PLATFORMS = [ @@ -202,6 +207,9 @@ class TractiveClient: 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 @@ class TractiveClient: 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 a87e22c505d..81936ae5d80 100644 --- a/homeassistant/components/tractive/const.py +++ b/homeassistant/components/tractive/const.py @@ -6,11 +6,15 @@ DOMAIN = "tractive" 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 @@ TRACKABLES = "trackables" 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 24439b489c8..8f56d1a2e9c 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -22,8 +23,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback 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 @@ from .const import ( 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 @@ class TractiveActivitySensor(TractiveSensor): 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 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( 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 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( 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 d5aee51ed61..44b0a497881 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 9822c2fa821..73e50af5caa 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 cb4abc9b385..d7968f15bf8 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -24,8 +24,6 @@ from .const import ( ) 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 1e44191a762..9731fb0c2d1 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 52e33da54ed..5a61e306991 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 @@ class MonopriceZone(MediaPlayerEntity): 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 fb9bac987bb..3e865bd4c0c 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 @@ class LaundrifyPowerPlug( """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 @@ class LaundrifyPowerPlug( 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 52350a498d9..d160efb776c 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 2954a5fe377..98869019d29 100644 --- a/homeassistant/components/gardena_bluetooth/__init__.py +++ b/homeassistant/components/gardena_bluetooth/__init__.py @@ -20,7 +20,7 @@ import homeassistant.util.dt as dt_util 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 fa7639dece0..997c78d0f00 100644 --- a/homeassistant/components/gardena_bluetooth/coordinator.py +++ b/homeassistant/components/gardena_bluetooth/coordinator.py @@ -80,7 +80,7 @@ class Coordinator(DataUpdateCoordinator[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 00000000000..0c8558419e2 --- /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 0a9677b1f92..3548412e04f 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 c6763e6d9f6..91f19dbdd08 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -62,7 +62,10 @@ async def async_setup_entry( 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 @@ class KulerskyLight(LightEntity): "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 @@ class KulerskyLight(LightEntity): 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 fa9b1fd48dd..f39c92519e4 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 31dcba97924..fca6e2fcb25 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 317a68d36bc..5ed5ad53224 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 cb78783559b..8de97cb6156 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 6a4e3d191eb..1981a56e211 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 b0542aa588a..1a2580cfc61 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 17155912e48..fe01058fda7 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 34043da012e..a9b184cc936 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 2bbbb229949..e38e8e1a2c4 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 325a852c6d8..95e164f1276 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 962e3fb3cdc..1e6d17c93a7 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 7c86965e902..289d847cd1d 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 98a088620ea..0185c727f67 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 1e6d17c93a7..a7582d464f7 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 289d847cd1d..f8875c60adb 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 612885e9aba..48a2e6964c7 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 6238496ed09..c6fd5bdd830 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 16a79b3ae12..5470a94896d 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 69252a7c508..c5681e4a278 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 8b84a0a9344..00c06a6acc1 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 0a41df17c8d..b4554f1a4e6 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 27dca72fd96..34b918cd3ed 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 6c0ac74296a..505d1d463e8 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 403d2f2993d..a072c7638a1 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 7892d98c45a..7a709e0cc2e 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 89d78dd8fa1..ed5dd81339e 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 6161b793610..be1a05947cc 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 86733d83f15..660de3ff6b6 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 b13a861fa79..c3cd21e6b2d 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -13,7 +13,7 @@ from typing import Any from aioimaplib import AUTH, IMAP4_SSL, NONAUTH, SELECTED, AioImapException import async_timeout -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, @@ -54,6 +54,7 @@ _LOGGER = logging.getLogger(__name__) BACKOFF_TIME = 10 EVENT_IMAP = "imap_content" +MAX_ERRORS = 3 MAX_EVENT_DATA_BYTES = 32168 @@ -174,6 +175,7 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): ) -> None: """Initiate imap client.""" self.imap_client = imap_client + self.auth_errors: int = 0 self._last_message_id: str | None = None self.custom_event_template = None _custom_event_template = entry.data.get(CONF_CUSTOM_EVENT_DATA_TEMPLATE) @@ -315,7 +317,9 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator): async def _async_update_data(self) -> int | None: """Update the number of unread emails.""" try: - return await self._async_fetch_number_of_messages() + messages = await self._async_fetch_number_of_messages() + self.auth_errors = 0 + return messages except ( AioImapException, UpdateFailed, @@ -330,8 +334,15 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator): self.async_set_update_error(ex) raise ConfigEntryError("Selected mailbox folder is invalid.") from ex except InvalidAuth as ex: - _LOGGER.warning("Username or password incorrect, starting reauthentication") await self._cleanup() + self.auth_errors += 1 + if self.auth_errors <= MAX_ERRORS: + _LOGGER.warning("Authentication failed, retrying") + else: + _LOGGER.warning( + "Username or password incorrect, starting reauthentication" + ) + self.config_entry.async_start_reauth(self.hass) self.async_set_update_error(ex) raise ConfigEntryAuthFailed() from ex @@ -359,27 +370,28 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): async def _async_wait_push_loop(self) -> None: """Wait for data push from server.""" + cleanup = False while True: try: number_of_messages = await self._async_fetch_number_of_messages() except InvalidAuth as ex: + self.auth_errors += 1 await self._cleanup() - _LOGGER.warning( - "Username or password incorrect, starting reauthentication" - ) - self.config_entry.async_start_reauth(self.hass) + if self.auth_errors <= MAX_ERRORS: + _LOGGER.warning("Authentication failed, retrying") + else: + _LOGGER.warning( + "Username or password incorrect, starting reauthentication" + ) + self.config_entry.async_start_reauth(self.hass) self.async_set_update_error(ex) await asyncio.sleep(BACKOFF_TIME) except InvalidFolder as ex: _LOGGER.warning("Selected mailbox folder is invalid") await self._cleanup() - self.config_entry._async_set_state( # 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 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): await asyncio.sleep(BACKOFF_TIME) continue else: + self.auth_errors = 0 self.async_set_updated_data(number_of_messages) try: idle: asyncio.Future = await self.imap_client.idle_start() @@ -398,6 +411,10 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): async with async_timeout.timeout(10): await idle + # From python 3.11 asyncio.TimeoutError is an alias of TimeoutError + except asyncio.CancelledError as ex: + cleanup = True + raise asyncio.CancelledError from ex except (AioImapException, asyncio.TimeoutError): _LOGGER.debug( "Lost %s (will attempt to reconnect after %s s)", @@ -406,6 +423,9 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): ) await self._cleanup() await asyncio.sleep(BACKOFF_TIME) + finally: + if cleanup: + await self._cleanup() async def shutdown(self, *_: Any) -> None: """Close resources.""" diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index 31b42b50781..b9512da0278 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 c4d764f4c71..f8dcb4f4a9c 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -3,20 +3,28 @@ from __future__ import annotations 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 @@ from .coordinator import ( FroniusPowerFlowUpdateCoordinator, FroniusStorageUpdateCoordinator, ) +from .sensor import InverterSensor _LOGGER: Final = logging.getLogger(__name__) PLATFORMS: Final = [Platform.SENSOR] @@ -67,6 +76,7 @@ class FroniusSolarNet: 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 @@ class FroniusSolarNet: # _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 @@ class FroniusSolarNet: ) ) + # 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 @@ class FroniusSolarNet: ) 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 @@ class FroniusSolarNet: 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 de3e0cc9563..042773472c5 100644 --- a/homeassistant/components/fronius/const.py +++ b/homeassistant/components/fronius/const.py @@ -8,6 +8,7 @@ DOMAIN: Final = "fronius" 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 4e706db032f..d701d0d1860 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 bd70604398d..4d11291508b 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 00000000000..28b2077691c --- /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 00000000000..844fcff89e4 --- /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 00000000000..e65784e7971 --- /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 00000000000..150ea901a0c --- /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 00000000000..e65784e7971 --- /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 00000000000..0ebeb823def --- /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 00000000000..30de1a1fa98 --- /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 00000000000..e77b751db3b --- /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 00000000000..a8ae2fc6d86 --- /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 00000000000..1da28803195 --- /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 00000000000..0967ef424bc --- /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 00000000000..5b2676c3a3f --- /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 0e8b405da44..d46c60c3cb3 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 e7f7e617df9..73a5998ea92 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 a7582d464f7..81d11692c20 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 f0c2cf8a691..2ce4be9a7d9 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 b650c59a5de..00000000000 --- 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 4775ba6caa3..00000000000 --- 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 4dcde6d883f..9fa12a84383 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 d225400c7cc..3c9d92094f7 100644 --- a/homeassistant/components/mutesync/binary_sensor.py +++ b/homeassistant/components/mutesync/binary_sensor.py @@ -9,10 +9,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback 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 9b18620acf8..2a3cca666ee 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 aa692153215..b2bc66b60c0 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 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): 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 d3920d3f0e4..1d074bba9c2 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -36,6 +36,7 @@ class StookalertBinarySensor(BinarySensorEntity): _attr_attribution = "Data provided by rivm.nl" _attr_device_class = BinarySensorDeviceClass.SAFETY _attr_has_entity_name = True + _attr_name = None def __init__(self, client: stookalert.stookalert, entry: ConfigEntry) -> None: """Initialize a Stookalert device.""" From 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 55ac33d198f..8ecc8a0e8c4 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -21,6 +21,7 @@ from homeassistant.const import ( 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 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): ), ) + @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 00000000000..c2645974f49 --- /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 8df88ff923a..9795b2303f1 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -18,13 +18,12 @@ from homeassistant.components.light import ( 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 @@ class PhilipsTVLightEntity( 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 bdd55bb2dad..6ee70b03d54 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -18,13 +18,12 @@ from homeassistant.components.media_player import ( 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 @@ class PhilipsTVMediaPlayer( 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 0aa61979eb2..55e84e88599 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -13,13 +13,12 @@ from homeassistant.components.remote import ( ) 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 @@ class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteE 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 b66fd3296d9..a950e606566 100644 --- a/homeassistant/components/philips_js/switch.py +++ b/homeassistant/components/philips_js/switch.py @@ -6,12 +6,11 @@ from typing import Any 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 @@ class PhilipsTVScreenSwitch( 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 @@ class PhilipsTVScreenSwitch( 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 @@ class PhilipsTVAmbilightHueSwitch( 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 9795b2303f1..75f43039de8 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 @@ class PhilipsTVLightEntity(PhilipsJsEntity, LightEntity): 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 55e84e88599..c5b24089809 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 @@ class PhilipsTVRemote(PhilipsJsEntity, RemoteEntity): """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 302e1b9accf..a260d42feda 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 a950e606566..29cfa10a230 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 @@ class PhilipsTVScreenSwitch(PhilipsJsEntity, SwitchEntity): 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 @@ class PhilipsTVScreenSwitch(PhilipsJsEntity, SwitchEntity): class PhilipsTVAmbilightHueSwitch(PhilipsJsEntity, SwitchEntity): """A Philips TV Ambi+Hue switch.""" + _attr_translation_key = "ambilight_hue" + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -81,7 +84,6 @@ class PhilipsTVAmbilightHueSwitch(PhilipsJsEntity, SwitchEntity): 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 9f26200e9ae..ac0dd0c919c 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 @@ class PlumLight(LightEntity): """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 @@ class PlumLight(LightEntity): 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 a1fc632c2fd..18185619f6b 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -121,7 +121,10 @@ class OwnTracksEntity(TrackerEntity, RestoreEntity): @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 2c71eac0193..f5ea14e8f4e 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 203d17a5dc2..7eb14548ada 100644 --- a/homeassistant/components/radiotherm/entity.py +++ b/homeassistant/components/radiotherm/entity.py @@ -14,6 +14,8 @@ from .data import RadioThermUpdate 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 21f53d72bfa..693811f59ab 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 2cf0602a3fa..3b71baffec6 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 c7aa8edc6c9..b936497cfc6 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 189296039aa..683308e081c 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -12,7 +12,6 @@ from motioneye_client.const import ( 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 @@ async def async_setup_entry( class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera): """motionEye mjpeg camera.""" - _name: str - def __init__( self, config_entry_id: str, @@ -203,7 +200,7 @@ class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera): 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 @@ class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera): # 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 c8b7679149c..4d0abb84d46 100644 --- a/homeassistant/components/motioneye/sensor.py +++ b/homeassistant/components/motioneye/sensor.py @@ -6,7 +6,7 @@ from types import MappingProxyType 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 @@ async def async_setup_entry( class MotionEyeActionSensor(MotionEyeEntity, SensorEntity): """motionEye action sensor camera.""" + _attr_translation_key = "actions" + def __init__( self, config_entry_id: str, @@ -69,12 +71,6 @@ class MotionEyeActionSensor(MotionEyeEntity, SensorEntity): ), ) - @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 fdf73cd8cf8..ea7901617cb 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 3be1c20981d..069c5edaad7 100644 --- a/homeassistant/components/motioneye/switch.py +++ b/homeassistant/components/motioneye/switch.py @@ -8,7 +8,6 @@ from motioneye_client.client import MotionEyeClient 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 @@ from .const import CONF_CLIENT, CONF_COORDINATOR, DOMAIN, TYPE_MOTIONEYE_SWITCH_ 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 @@ class MotionEyeSwitch(MotionEyeEntity, SwitchEntity): 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 18185619f6b..ba0beb40cf8 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -59,6 +59,9 @@ async def async_setup_entry( 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 @@ class OwnTracksEntity(TrackerEntity, RestoreEntity): """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 ab0a60c44ca..fba10a269f7 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant 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 d9b59b2d02c..7797167ea0b 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 46b21ebab32..367da49c7f6 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 e0244034865..f788ad21098 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -375,25 +375,25 @@ class UpdateEntity(RestoreEntity): 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 80b9d75303b..14728c05e53 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -70,6 +70,8 @@ def catch_vlc_errors( 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 @@ class VlcDevice(MediaPlayerEntity): ) -> 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 @@ class VlcDevice(MediaPlayerEntity): 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 f0684b6b01d..8e6ad545bd0 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 @@ class VeSyncBaseEntity(Entity): 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 @@ class VeSyncBaseEntity(Entity): """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 f89224aaba8..a3bf027c28f 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 9129d060cdc..e6cc979e808 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 bc0db04dd47..f3612c2d011 100644 --- a/homeassistant/components/vesync/sensor.py +++ b/homeassistant/components/vesync/sensor.py @@ -79,7 +79,7 @@ PM25_SUPPORTED = ["Core300S", "Core400S", "Core600S"] 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 @@ SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( ), 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 @@ class VeSyncSensorEntity(VeSyncBaseEntity, SensorEntity): """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 08cbdf943f6..9a54062a2b5 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 93cb5c67a5d..e6101b2ba51 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 2c12f9bc5f6..b9426f5ba1e 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 82a31b5fc14..428f066e6cc 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 1f7b0aa9baf..67940603d41 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 040e41747a2..1fc89722699 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 77f4011a532..cfe9d66a2ed 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 40ec754aa44..676e5b50f49 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -109,10 +109,8 @@ CONF_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" 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 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( ).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 @@ _DISCOVERY_SCHEMA_BASE = _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA 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 314db187b6a..6ef5917a0fb 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -22,11 +22,10 @@ from aiohomekit.model.services import Service, ServicesTypes 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 @@ class HKDevice: 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 @@ class HKDevice: 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 @@ class HKDevice: 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 @@ class HKDevice: 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 5a687020eb6..f6aadfac7ac 100644 --- a/homeassistant/components/homekit_controller/entity.py +++ b/homeassistant/components/homekit_controller/entity.py @@ -11,8 +11,6 @@ from aiohomekit.model.characteristics import ( ) 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 @@ class HomeKitEntity(Entity): """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 1474906cacb..a5889cd11a7 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -164,6 +164,14 @@ RPC_SENSORS: Final = { 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 2a80233aeb9..96e888d7509 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -189,6 +189,7 @@ MOCK_STATUS_RPC = { "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 207b73bf44b..c067f5dffc9 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 50cc209e268..5f7f870302b 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 0c8558419e2..d7cf914b9df 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 997c78d0f00..9f5dc3223b5 100644 --- a/homeassistant/components/gardena_bluetooth/coordinator.py +++ b/homeassistant/components/gardena_bluetooth/coordinator.py @@ -15,7 +15,7 @@ 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.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -119,3 +119,15 @@ class GardenaBluetoothEntity(CoordinatorEntity[Coordinator]): 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 5f7f870302b..ec7ae513a3e 100644 --- a/homeassistant/components/gardena_bluetooth/number.py +++ b/homeassistant/components/gardena_bluetooth/number.py @@ -21,7 +21,11 @@ from homeassistant.core import HomeAssistant 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 d7cf914b9df..eaa44d9d4fb 100644 --- a/homeassistant/components/gardena_bluetooth/sensor.py +++ b/homeassistant/components/gardena_bluetooth/sensor.py @@ -20,7 +20,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback 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 e3fcc8978c7..adb23c74c1d 100644 --- a/homeassistant/components/gardena_bluetooth/switch.py +++ b/homeassistant/components/gardena_bluetooth/switch.py @@ -51,10 +51,7 @@ class GardenaBluetoothValveSwitch(GardenaBluetoothEntity, SwitchEntity): 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 6a064409e9e..a5ea94088fd 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 @@ UNSUPPORTED_GROUP_SERVICE_INFO = BluetoothServiceInfo( 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 f09a274742f..a4d7170e945 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 00000000000..a3ecff80a46 --- /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 00000000000..a12cce06019 --- /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 00000000000..883f377c3a5 --- /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 00000000000..3ad7e6dce61 --- /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 00000000000..f1955905cce --- /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 00000000000..d7cdc205f50 --- /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 67c8412102d..a3fe046409b 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -130,6 +130,11 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): url = f"http://{config['host']}:{config['port']}" config_entry_data = {"url": url} + if self._async_in_progress(include_uninitialized=True): + # We currently don't handle multiple config entries, abort if hassio + # discovers multiple addons with otbr support + return self.async_abort(reason="single_instance_allowed") + if current_entries := self._async_current_entries(): for current_entry in current_entries: if current_entry.source != SOURCE_HASSIO: diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index b6cb0df78cd..da25edde045 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -23,6 +23,12 @@ HASSIO_DATA = hassio.HassioServiceInfo( slug="otbr", uuid="12345", ) +HASSIO_DATA_2 = hassio.HassioServiceInfo( + config={"host": "core-silabs-multiprotocol_2", "port": 8082}, + name="Silicon Labs Multiprotocol", + slug="other_addon", + uuid="23456", +) @pytest.fixture(name="addon_info") @@ -313,6 +319,80 @@ async def test_hassio_discovery_flow_sky_connect( assert config_entry.unique_id == HASSIO_DATA.uuid +async def test_hassio_discovery_flow_2x_addons( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info +) -> None: + """Test the hassio discovery flow when the user has 2 addons with otbr support.""" + url1 = "http://core-silabs-multiprotocol:8081" + url2 = "http://core-silabs-multiprotocol_2:8081" + aioclient_mock.get(f"{url1}/node/dataset/active", text="aa") + aioclient_mock.get(f"{url2}/node/dataset/active", text="bb") + + async def _addon_info(hass, slug): + await asyncio.sleep(0) + if slug == "otbr": + return { + "available": True, + "hostname": None, + "options": { + "device": ( + "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" + "9e2adbd75b8beb119fe564a0f320645d-if00-port0" + ) + }, + "state": None, + "update_available": False, + "version": None, + } + return { + "available": True, + "hostname": None, + "options": { + "device": ( + "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" + "9e2adbd75b8beb119fe564a0f320645d-if00-port1" + ) + }, + "state": None, + "update_available": False, + "version": None, + } + + addon_info.side_effect = _addon_info + + with patch( + "homeassistant.components.otbr.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + results = await asyncio.gather( + hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA + ), + hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA_2 + ), + ) + + expected_data = { + "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", + } + + assert results[0]["type"] == FlowResultType.CREATE_ENTRY + assert results[0]["title"] == "Home Assistant SkyConnect" + assert results[0]["data"] == expected_data + assert results[0]["options"] == {} + assert results[1]["type"] == FlowResultType.ABORT + assert results[1]["reason"] == "single_instance_allowed" + assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] + assert config_entry.data == expected_data + assert config_entry.options == {} + assert config_entry.title == "Home Assistant SkyConnect" + assert config_entry.unique_id == HASSIO_DATA.uuid + + async def test_hassio_discovery_flow_router_not_setup( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info ) -> None: From 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 8f8810b5f33..8685282acec 100644 --- a/homeassistant/components/otbr/__init__.py +++ b/homeassistant/components/otbr/__init__.py @@ -24,6 +24,9 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) 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 1f103884db2..e641f67dfaf 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 990c015244f..4ec99818b28 100644 --- a/tests/components/otbr/test_init.py +++ b/tests/components/otbr/test_init.py @@ -11,10 +11,12 @@ from homeassistant.components import otbr 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 a3fe046409b..f1219aaebf0 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 da25edde045..d67a9c0ff0a 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 d67a9c0ff0a..b7d42b661c9 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -380,7 +380,9 @@ async def test_hassio_discovery_flow_2x_addons( } 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 test_hassio_discovery_flow_2x_addons( 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 f1219aaebf0..35772c00a89 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 b7d42b661c9..deb8672b961 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 7fbf3542e13..810092094d1 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 81d11692c20..bd2903f501c 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 f8875c60adb..f91c9616339 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 8fd01ada0e9..da7d73c272d 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 fca6e2fcb25..03c472d106a 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 5ed5ad53224..6f1f3d9a351 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 8de97cb6156..8f55e68cd3c 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 | 60 +++++++++--- tests/components/alexa/test_smart_home.py | 101 ++++++++++++++++++++- 2 files changed, 146 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index eb23b09627e..c1b99b017e5 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) - - data = {ATTR_ENTITY_ID: entity.entity_id, ATTR_TEMPERATURE: target_temp} response = directive.response() + + 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]}, + } + ) + 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 a1f77a9b49b..a2dcdedd470 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 c728ea0a682..3feddacd4e5 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 bd2903f501c..6f1e2bcba66 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 f91c9616339..f4bb633b748 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 e5448fb395d..a8665d76656 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 6f1e2bcba66..16cb430d090 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 f4bb633b748..30513062d10 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 3ad7e6dce61..b09d2177c22 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 @@ from tests.common import MockConfigEntry, async_fire_time_changed 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 2a9e2225e9f..82d91863fad 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 16cb430d090..19ef9194307 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 30513062d10..655a165cf39 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 03c472d106a..927f0db3f01 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 6f1f3d9a351..4e608c36b97 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 8f55e68cd3c..84fa6a0cbb4 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 e6d1d035e3b..12ad3350e44 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -35,7 +35,7 @@ from homeassistant.const import ( 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 async_setup_entry( 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 b271e57cb8a..ad375630bea 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -44,11 +44,11 @@ from homeassistant.const import ( 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 @@ class MediaPlayerGroup(MediaPlayerEntity): } @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 d0064d07511..d1ea9695322 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -30,7 +30,6 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ( ConfigType, DiscoveryInfoType, - EventType, StateType, ) @@ -287,7 +286,7 @@ class MinMaxSensor(SensorEntity): @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 57df5d1ab0a..d55ffe0fd28 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -5,8 +5,7 @@ from collections.abc import Callable 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 @@ from .utils import get_rpc_entity_name @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 03df3da346b..a66b77ed94b 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -12,7 +12,7 @@ from aioshelly.rpc_device import RpcDevice, WsServer from homeassistant.components.http import HomeAssistantView from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -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 @@ from homeassistant.helpers.device_registry import ( 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 326d2f98259..5a76fd262a8 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -9,7 +9,6 @@ GPSType = tuple[float, float] 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 @@ UNDEFINED = UndefinedType._singleton # pylint: disable=protected-access # 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 a22b8a993f1..ad31f20e3cf 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -365,8 +365,11 @@ class TraccarScanner: 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 @@ class TraccarEntity(TrackerEntity, RestoreEntity): """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 @@ class TraccarEntity(TrackerEntity, RestoreEntity): 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 d160efb776c..6cf3f66d8af 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 98869019d29..c779d30b0fc 100644 --- a/homeassistant/components/gardena_bluetooth/__init__.py +++ b/homeassistant/components/gardena_bluetooth/__init__.py @@ -20,7 +20,12 @@ import homeassistant.util.dt as dt_util 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 00000000000..0285f7bdf82 --- /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 3548412e04f..5a3f77eafa4 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 00000000000..8a2600dcbb1 --- /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 00000000000..cda24f871e8 --- /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 | 59 ++++++++++++++----- .../components/transmission/strings.json | 22 +++++++ 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 3cee556044f..833c1910d4e 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"), - TransmissionTorrentsSensor( - tm_client, name, "Active torrents", "active_torrents" + 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, "Paused torrents", "paused_torrents" - ), - TransmissionTorrentsSensor(tm_client, name, "Total torrents", "total_torrents"), - TransmissionTorrentsSensor( - tm_client, name, "Completed torrents", "completed_torrents" + tm_client, + name, + "active_torrents", + "active_torrents", ), TransmissionTorrentsSensor( - tm_client, name, "Started torrents", "started_torrents" + tm_client, + name, + "paused_torrents", + "paused_torrents", + ), + TransmissionTorrentsSensor( + tm_client, + name, + "total_torrents", + "total_torrents", + ), + TransmissionTorrentsSensor( + tm_client, + name, + "completed_torrents", + "completed_torrents", + ), + TransmissionTorrentsSensor( + 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 97741bd65bb..aaab4d2e2d7 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 31d0ff09467..b8d77d5d82a 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 67b4e1c9299..8cac3fb81f7 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 19ef9194307..cdd695d4d74 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 655a165cf39..75b8fc80d06 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 | 122 +++++++++++++++--- 3 files changed, 125 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index af2fd61081c..d7641c34316 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 18a3ccef7d8..04db33fdff6 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.const.command_class.meter import CC_SPECIFIC_METER_TYPE 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 @@ ACTION_TYPES = { 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 @@ PING_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( 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 @@ REFRESH_VALUE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( 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_CONFIG_PARAMETER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( 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 @@ SET_VALUE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( } ) -ACTION_SCHEMA = vol.Any( +_ACTION_SCHEMA = vol.Any( CLEAR_LOCK_USERCODE_SCHEMA, PING_SCHEMA, REFRESH_VALUE_SCHEMA, @@ -141,6 +142,13 @@ ACTION_SCHEMA = vol.Any( ) +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 b5d4149a526..ce2b916b7a1 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.config_entries import ConfigEntry 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,14 +288,15 @@ async def test_actions( assert args[2] == 1 -async def test_actions_multiple_calls( +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 can be called multiple times and still work.""" + """Test actions.""" node = climate_radio_thermostat_ct100_plus driver = client.driver assert driver @@ -288,6 +304,9 @@ async def test_actions_multiple_calls( 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, @@ -302,7 +321,64 @@ async def test_actions_multiple_calls( "domain": DOMAIN, "type": "refresh_value", "device_id": device.id, - "entity_id": "climate.z_wave_thermostat", + "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({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.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 317f2619bef..4cce9290a68 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -66,8 +66,11 @@ async def async_setup_entry( 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 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): """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 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): @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 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): @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 9480fa3ce17..4bdabdf9c96 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -10,7 +10,6 @@ from typing import Any, Protocol, cast 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 @@ PLATFORMS = [ 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 299ab753cb9..36da7072626 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 9fa12a84383..1da6a6be9da 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 21d060f4ba7..dae63b4ead1 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -4,9 +4,8 @@ from __future__ import annotations 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.const import ( ) 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 1919586daa3..255fba0e7e7 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.const import ( ) 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 892116121a0..66cbbcbd67e 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -50,6 +50,9 @@ async def async_setup_entry( 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 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): """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 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): @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 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): @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 6e92fac3b72..99766ebfec9 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 @@ class HomeControlSwitchEntity(CoordinatorEntity, SwitchEntity): }, 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 afa40f27afd..96866b7cd67 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -854,25 +854,25 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 9cd94330812..f8670074c5c 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 @@ class SoundTouchMediaPlayer(MediaPlayerEntity): 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 f52a94d2c9a..0c49368001d 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 @@ class SENZClimate(CoordinatorEntity, ClimateEntity): """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 04b689ae917..62df3a12c2b 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 @@ class AFSAPIDevice(MediaPlayerEntity): 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 bd800ea04dd..8f5f3bdfe56 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 @@ class ADBDevice(MediaPlayerEntity): ) -> 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 3ebf04ab219..85c672e0f64 100644 --- a/homeassistant/components/dynalite/dynalitebase.py +++ b/homeassistant/components/dynalite/dynalitebase.py @@ -41,17 +41,15 @@ def async_setup_entry_base( 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 @@ class DynaliteBase(RestoreEntity, ABC): 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 3fb328ab7fb..9f916e5751f 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 @@ class BruntDevice( 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 b28f06eb97d..86c7905848a 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -293,7 +293,7 @@ SENSOR_TYPES = { ), 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 @@ SENSOR_TYPES = { ), 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 @@ SENSOR_TYPES = { ), 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 @@ SENSOR_TYPES = { ), 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 @@ SENSOR_TYPES = { ), 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 @@ SENSOR_TYPES = { ), 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 3fb6c8d2c48..de5093d1817 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -336,7 +336,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """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 34b61d89c48..e1e5f3d61bb 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -307,12 +307,7 @@ async def async_setup_entry_helper( 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 aeae184dc89..9f0a178ce87 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 32421f67fbb..8213baf33aa 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 18f1e28ece6..51f30b8a861 100644 --- a/homeassistant/components/yalexs_ble/entity.py +++ b/homeassistant/components/yalexs_ble/entity.py @@ -15,6 +15,7 @@ from .models import YaleXSBLEData 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 0ecf0e7b697..d457784a038 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 6304b791edd..9d702ff52eb 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 @@ SENSORS: tuple[YaleXSBLESensorEntityDescription, ...] = ( ), YaleXSBLESensorEntityDescription( key="battery_level", - name="Battery level", device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, @@ -67,7 +65,7 @@ SENSORS: tuple[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 bd96e07f6ba..c79830be3a9 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 326265b8b3f..58ba237ae68 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 @@ class WiLightDevice(Entity): 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 cd0a3cc21ac..aa50b79f139 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 3d0c6d0ff39..ba9a108f636 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 2509dc50737..b17eac36f09 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 @@ class WiLightLightOnOff(WiLightDevice, LightEntity): 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 a287104e7ad..ccba52d99e0 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 f2d74cce359..101162302ae 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 @@ class WiLightValveSwitch(WiLightDevice, SwitchEntity): 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 0145d6f0906..de2cced08b5 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -120,9 +120,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity): 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 @@ class SpotifyMediaPlayer(MediaPlayerEntity): ) 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 @@ class SpotifyMediaPlayer(MediaPlayerEntity): 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 8c04e591968..59315e9f576 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 @@ class Panel: 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 @@ class Panel: 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 @@ class Panel: "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 493a738c1ea..4f084d5900a 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 0dff261d864..b394d439654 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 81365273986..d84b4c812c7 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 79bc7e37ee3..bd8950163a9 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 2b658387ef5..d1c5d6cf8f8 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 de415035c76..37b16530b0d 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 @@ SENSORS: tuple[WhirlpoolSensorEntityDescription, ...] = ( ), WhirlpoolSensorEntityDescription( key="DispenseLevel", - name="Detergent Level", translation_key="whirlpool_tank", entity_registry_enabled_default=False, device_class=SensorDeviceClass.ENUM, @@ -131,7 +129,7 @@ SENSORS: tuple[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 @@ class WasherDryerClass(SensorEntity): 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 @@ class WasherDryerTimeClass(RestoreSensor): 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 94dc9aa219f..a24e42304d0 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 10c7f08484b..c11bb37294f 100644 --- a/homeassistant/components/script/config.py +++ b/homeassistant/components/script/config.py @@ -23,6 +23,10 @@ from homeassistant.const import ( 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 @@ _MINIMAL_SCRIPT_ENTITY_SCHEMA = vol.Schema( 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 @@ async def _async_validate_config_item( 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 34f807e3cc5..86ea2cf9e7f 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 cc41b6c404c..cddefc8d3dc 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 c996fcaf524..61ac81b0bca 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 09cf79116a0..10dc825372f 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 1d586198c5a..880d02cfeae 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 @@ class Volumio(MediaPlayerEntity): """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 @@ class Volumio(MediaPlayerEntity): 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 1b3839ce2d7..3eb4d72effd 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 0d41aec699b..bc5e15ba989 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 @@ class SongpalEntity(MediaPlayerEntity): 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 @@ class SongpalEntity(MediaPlayerEntity): 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 36d0a67fded..71952431b5a 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 cdd695d4d74..b1989ae25a0 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 0cc979a321f..36c3a5dbda5 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -86,7 +86,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 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 96ccaa47d84..fe685398c5d 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 5a32ca23332..41ecb751b86 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 @@ class ZerprocLight(LightEntity): "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 @@ class ZerprocLight(LightEntity): 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 2243edc48e4..ee9aa5531c8 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 @@ SENSOR_TYPES = ( ), 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 a4f52dc6aa3..0e2e23f244c 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 5b9dacb9db7..38ea7d46537 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -51,42 +51,35 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( 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 @@ class YoLinkBinarySensorEntity(YoLinkEntity, BinarySensorEntity): 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 e9d11fb77d0..6e4495ee0b9 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 @@ class YoLinkClimateEntity(YoLinkEntity, ClimateEntity): """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 1b22f76f177..0d1f1e590b4 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 @@ class YoLinkCoverEntity(YoLinkEntity, CoverEntity): """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 76ef1ecd534..09da5545d57 100644 --- a/homeassistant/components/yolink/entity.py +++ b/homeassistant/components/yolink/entity.py @@ -19,6 +19,8 @@ from .coordinator import YoLinkCoordinator 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 a7f52e801b2..248a42df60c 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 7565c66867a..3b0f68c175c 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 @@ class YoLinkLockEntity(YoLinkEntity, LockEntity): """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 75c4949859c..149bdc0adf8 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -126,7 +126,6 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( 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 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( 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 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( 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 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( 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 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( 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 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( ), 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 @@ class YoLinkSensorEntity(YoLinkEntity, SensorEntity): 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 ad51b912193..81c2b46a840 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 @@ class YoLinkSirenEntity(YoLinkEntity, SirenEntity): 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 de16e1a6e39..b1cd8d87a75 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 773477e6c3f..415c1e9584d 100644 --- a/homeassistant/components/yolink/switch.py +++ b/homeassistant/components/yolink/switch.py @@ -40,52 +40,52 @@ DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = ( 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 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity): 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 de2cced08b5..3c2f9ef729c 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -398,13 +398,24 @@ class SpotifyMediaPlayer(MediaPlayerEntity): ) 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 d2d0dba6773..5d7e29c1312 100644 --- a/homeassistant/components/smartthings/cover.py +++ b/homeassistant/components/smartthings/cover.py @@ -62,7 +62,11 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: # Must have one of the min_required if any(capability in capabilities for capability in min_required): # Return all capabilities supported/consumed - return min_required + [Capability.battery, Capability.switch_level] + return min_required + [ + Capability.battery, + Capability.switch_level, + Capability.window_shade_level, + ] return None @@ -74,12 +78,16 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): """Initialize the cover class.""" super().__init__(device) self._device_class = None + self._current_cover_position = None self._state = None self._state_attrs = None self._attr_supported_features = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE ) - if Capability.switch_level in device.capabilities: + if ( + Capability.switch_level in device.capabilities + or Capability.window_shade_level in device.capabilities + ): self._attr_supported_features |= CoverEntityFeature.SET_POSITION async def async_close_cover(self, **kwargs: Any) -> None: @@ -103,7 +111,12 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): if not self.supported_features & CoverEntityFeature.SET_POSITION: return # Do not set_status=True as device will report progress. - await self._device.set_level(kwargs[ATTR_POSITION], 0) + if Capability.window_shade_level in self._device.capabilities: + await self._device.set_window_shade_level( + kwargs[ATTR_POSITION], set_status=False + ) + else: + await self._device.set_level(kwargs[ATTR_POSITION], set_status=False) async def async_update(self) -> None: """Update the attrs of the cover.""" @@ -117,6 +130,11 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): self._device_class = CoverDeviceClass.GARAGE self._state = VALUE_TO_STATE.get(self._device.status.door) + if Capability.window_shade_level in self._device.capabilities: + self._current_cover_position = self._device.status.shade_level + elif Capability.switch_level in self._device.capabilities: + self._current_cover_position = self._device.status.level + self._state_attrs = {} battery = self._device.status.attributes[Attribute.battery].value if battery is not None: @@ -142,9 +160,7 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): @property def current_cover_position(self) -> int | None: """Return current position of cover.""" - if not self.supported_features & CoverEntityFeature.SET_POSITION: - return None - return self._device.status.level + return self._current_cover_position @property def device_class(self) -> CoverDeviceClass | None: diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 29eb681dc4d..89e5071051c 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -30,5 +30,5 @@ "documentation": "https://www.home-assistant.io/integrations/smartthings", "iot_class": "cloud_push", "loggers": ["httpsig", "pysmartapp", "pysmartthings"], - "requirements": ["pysmartapp==0.3.3", "pysmartthings==0.7.6"] + "requirements": ["pysmartapp==0.3.5", "pysmartthings==0.7.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index b1989ae25a0..9bf2a461a12 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 75b8fc80d06..1487828b246 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 bdf3cc901a7..bf781c71c4e 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 69fbd1a128a..188647b7338 100644 --- a/homeassistant/components/airvisual_pro/sensor.py +++ b/homeassistant/components/airvisual_pro/sensor.py @@ -86,16 +86,14 @@ SENSOR_DESCRIPTIONS = ( ), 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 04801c8fa0e..b5c68371fdf 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 de5093d1817..405eb86e6ec 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -5,7 +5,7 @@ import asyncio 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 @@ from .client import ( # noqa: F401 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 @@ CONFIG_ENTRY_CONFIG_KEYS = [ 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 async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # 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 ba2e0427ba7..ef2c771218a 100644 --- a/homeassistant/components/mqtt/config_integration.py +++ b/homeassistant/components/mqtt/config_integration.py @@ -52,7 +52,7 @@ from .const import ( 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 e1e5f3d61bb..314800f33f2 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -309,9 +309,16 @@ async def async_setup_entry_helper( 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 9f0a178ce87..fb11400a312 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 ee32b7131c4..d1b1d6b68b3 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 921f46703c2..d32754625f4 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 e99182323c8..fa16ef77817 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 8bb21f5eb51..5552457c213 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 4a6d1bf64d4..e717c04b317 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 c388ded6587..2eec5f8374b 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 c4181a3f885..803a0d74766 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 1c386b28703..0cc4d936841 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 9432f231301..3395dc0825f 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 6b1a74f256d..85e3bdd12b9 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 59d5090b711..08def9a923e 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 5a7bedd91e6..7ff4ccbab85 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 4727caca2cc..0583a1176b6 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 2b77a573bad..bf7e1529a4e 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 f882209139c..96d9cdcef64 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 4da60d44bb7..dfea7b3f915 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 583e65bc61c..f1903fa4c3c 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 d5483cf3a74..d6ab692af52 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 76a9cb9c6f6..7c448eba85e 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 b22fb96aa13..a24884941fc 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 b06cfa34442..4471cc7dc11 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 b96b82277b0..9e068a07824 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 8e2cdaf8eaa..9c881352f8c 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 942a2ec87d4..c4f798e05ec 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 c3b115de60a..6333139e540 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 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( ), WhoisSensorEntityDescription( key="registrar", - name="Registrar", + translation_key="registrar", icon="mdi:store", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -124,7 +124,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( ), 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 553293962cd..c28c079784d 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 d0bcff20b0e..464af13c7c8 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 4d6ad7148d2..d7230de0832 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -333,7 +333,7 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { ), 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 d1c9398bb24..0091fc098de 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 83fa590429f..0a740ec4347 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -6,7 +6,10 @@ from datetime import timedelta 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 @@ class TransportNSWSensor(SensorEntity): 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 @@ class PublicTransportData: 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 @@ class PublicTransportData: 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 181c5fdd1e4..f9ead2a3054 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 00b9883f261..f7c9da48883 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -4,6 +4,7 @@ from __future__ import annotations 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 2a562ab348a..0cc88baf32f 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 61ac81b0bca..c7087918cf0 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 10dc825372f..c1d5f76ea78 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 25b2df41478..06cb7958242 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -53,7 +53,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { 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 f4f827980bb..4430172e9a7 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -9,7 +9,7 @@ from homeassistant.components.number import ( 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 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "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 66fa656ebfe..9b112d9a388 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 4d82881e173..8a976b25c7a 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 d1d703f9875..abda8703e02 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 9bf2a461a12..eaa70a0fd62 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 1487828b246..bd6ceda67a9 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 96866b7cd67..9483443a19c 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -1017,6 +1017,22 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { 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 641b1dbadf3..e24c941c514 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 e05fce9df3c..42bcb9847f1 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 ed5882cfe74..4308e573859 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 @@ class NextcloudEntity(CoordinatorEntity[NextcloudDataUpdateCoordinator]): """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 bcb530ffd73..6c70421bf93 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 d06372bb28b..0814945bc07 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -17,7 +17,6 @@ SCAN_INTERVAL = timedelta(minutes=2) 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 24316006b5e..845e8dfe85a 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 9a54062a2b5..5ff0aa58722 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 b9426f5ba1e..c463db179eb 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 1fc89722699..06198bca145 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 427f88a1fb9..83aaac95393 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 c9cbeff90d5..a140de59017 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 @@ class CrownstoneEntity(CrownstoneBaseEntity, LightEntity): 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 632b2e29d57..dc8038862c6 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 e485940034f..d49a1ac387e 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 @@ class AgentCamera(MjpegCamera): 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 5a3e394b119..25667ea16c6 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -4,7 +4,7 @@ from __future__ import annotations 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 @@ from .entity import PlugwiseEntity @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 @@ NUMBER_TYPES = ( 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 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): ) -> 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 516ed319d01..cf8b40916f3 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 8c4998fa45e..d32a64dc1e6 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 7180862758c..54c1dc9ad5a 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 5a84ad5719c..c7154c19f15 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -37,6 +37,8 @@ def handle_error( class TradfriBaseEntity(CoordinatorEntity[TradfriDeviceDataUpdateCoordinator]): """Base Tradfri device.""" + _attr_has_entity_name = True + def __init__( self, device_coordinator: TradfriDeviceDataUpdateCoordinator, @@ -52,7 +54,6 @@ class TradfriBaseEntity(CoordinatorEntity[TradfriDeviceDataUpdateCoordinator]): 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 976a48906fc..c51918b4a4f 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 d6bb91a4979..a26dfa1d9a0 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 32160c6a130..df35301b373 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 3eb4d72effd..383eec8a8fb 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -24,7 +24,6 @@ from homeassistant.const import ( 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 @@ SENSOR_DESCRIPTIONS_BATTERY: tuple[TradfriSensorEntityDescription, ...] = ( 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 @@ SENSOR_DESCRIPTIONS_FAN: tuple[TradfriSensorEntityDescription, ...] = ( ), 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 @@ class TradfriSensor(TradfriBaseEntity, SensorEntity): 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 34d7e89929a..0a9a86bd23a 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 e0e2467ca4b..2f6f1996157 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 23391c8e875..d301638ec5d 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 2beffc7c894..f8ebd9db723 100644 --- a/homeassistant/components/iss/config_flow.py +++ b/homeassistant/components/iss/config_flow.py @@ -7,9 +7,7 @@ from homeassistant.const import CONF_SHOW_ON_MAP 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 3d240041b67..c3bdcf6fa32 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 fac23dfd9fa..32516ee99c9 100644 --- a/homeassistant/components/iss/sensor.py +++ b/homeassistant/components/iss/sensor.py @@ -8,6 +8,8 @@ from homeassistant.components.sensor import SensorEntity 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 homeassistant.helpers.update_coordinator import ( ) 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 6f8d00eb48c..473b9fa07d1 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -304,6 +304,8 @@ class ValloxServiceHandler: 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 2d40c43836d..05085c24424 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 b43dabbba80..2f420096c74 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 36145f85bc7..ce43ca9c3fb 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 @@ NUMBER_ENTITIES: tuple[ValloxNumberEntityDescription, ...] = ( ), 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 @@ NUMBER_ENTITIES: tuple[ValloxNumberEntityDescription, ...] = ( ), 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 a4f6563798d..ee0e1e43204 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 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ), ValloxSensorEntityDescription( key="humidity", - name="Humidity", metric_key="A_CYC_RH_VALUE", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, @@ -243,7 +241,7 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ), 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 42efaeb0538..acc6a31f158 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 7e8cb4e39c5..194659d40cd 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 4cc5cacbde3..2027cf73d25 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -210,7 +210,7 @@ class BlockSleepingClimate( """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 9f95ea5ac21..548428c444c 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -332,11 +332,6 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): ) 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 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): 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 06cb7958242..a392a338aba 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -318,7 +318,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { 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 2858bff098e..1b511f03eda 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 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( ), 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 7e8c32017ce..43e6fca4ada 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 816fb35fadb..8a5f7fa828f 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 @@ DISPATCH_CONFIG_UPDATED = "kraken_config_updated" 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 0250f17052b..4bbf232f84b 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 @@ from .const import ( 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 cfa31d56e80..0ad727faf2c 100644 --- a/homeassistant/components/sms/sensor.py +++ b/homeassistant/components/sms/sensor.py @@ -17,7 +17,6 @@ from .const import DOMAIN, GATEWAY, NETWORK_COORDINATOR, SIGNAL_COORDINATOR, SMS 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 c005c241d79..ae3324aa156 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 09576f07e6b..a929bd24b25 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -108,6 +108,8 @@ def soma_api_call(api_call): 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 @@ class SomaEntity(Entity): """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 @@ class SomaEntity(Entity): 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 144c793ac57..26487756a44 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 @@ class SomaTilt(SomaEntity, CoverEntity): 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 a53bcd26e83..6472f6934e0 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 261d1565160..2769d045c0b 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 @@ class SpiderThermostat(ClimateEntity): """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 259c0fa4974..5b326db1e45 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 @@ class SpiderPowerPlugEnergy(SensorEntity): """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 @@ class SpiderPowerPlugEnergy(SensorEntity): 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 @@ class SpiderPowerPlugPower(SensorEntity): """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 2e86f47dd2d..c8d67be36ae 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 607e4c5b84a..28bbf0fcc18 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 @@ class SpiderPowerPlug(SwitchEntity): """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 11dfdf67b35..a64f4312de1 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -61,14 +61,14 @@ SENSOR_TYPES: tuple[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 @@ SENSOR_TYPES: tuple[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 @@ SENSOR_TYPES: tuple[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 @@ SENSOR_TYPES: tuple[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 @@ SENSOR_TYPES: tuple[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 @@ SENSOR_TYPES: tuple[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 @@ SENSOR_TYPES: tuple[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 @@ SESSION_SENSOR_TYPES: tuple[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 @@ SESSION_SENSOR_TYPES: tuple[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 90c64a6a8d6..4278c6a3bec 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 dfb439133f6..2c2d0ca154b 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 2218ec2a6b4..e0a34460c8c 100644 --- a/homeassistant/components/tesla_wall_connector/binary_sensor.py +++ b/homeassistant/components/tesla_wall_connector/binary_sensor.py @@ -16,7 +16,6 @@ from . import ( 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 1f83e38030a..0322830890a 100644 --- a/homeassistant/components/tesla_wall_connector/sensor.py +++ b/homeassistant/components/tesla_wall_connector/sensor.py @@ -24,7 +24,6 @@ from . import ( 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 @@ WALL_CONNECTOR_SENSORS = [ ), 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 @@ WALL_CONNECTOR_SENSORS = [ ), 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 @@ WALL_CONNECTOR_SENSORS = [ ), 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 @@ WALL_CONNECTOR_SENSORS = [ ), 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 @@ WALL_CONNECTOR_SENSORS = [ ), 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 @@ WALL_CONNECTOR_SENSORS = [ ), 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 @@ WALL_CONNECTOR_SENSORS = [ ), 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 @@ WALL_CONNECTOR_SENSORS = [ ), 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 @@ WALL_CONNECTOR_SENSORS = [ ), 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 907209cdcca..982894eb17c 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 74c37e9d5ce..5e2e507e450 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -271,11 +271,6 @@ class PicnicSensor(SensorEntity, CoordinatorEntity): ) 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 abaefec4082..2552be7717a 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -19,31 +19,31 @@ from .const import DOMAIN, NAME 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 73998913f41..7c09cc605bd 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 3b291c9973d..a5b9b9c8a8d 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -5,9 +5,7 @@ from freezegun import freeze_time 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 b427967ded5..bef724392b7 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 .account import StarlineAccount, StarlineDevice 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 6dadfdbd3ea..ca8118d6b43 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 20e5eaed07e..7eee5e7a7f8 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 @@ class StarlineEntity(Entity): """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 4fb8457a779..f663c472a78 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 1acddb27721..4b787ae5212 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 .account import StarlineAccount, StarlineDevice 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 4d2c497dc8b..800fd3a65f3 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 412c08b9ff7..b254fa8133f 100644 --- a/homeassistant/components/starline/switch.py +++ b/homeassistant/components/starline/switch.py @@ -18,7 +18,6 @@ from .entity import StarlineEntity 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 @@ class StarlineSwitch(StarlineEntity, SwitchEntity): 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 6cf3f66d8af..163065b7c2a 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 01f486e2704..33ae9d167c2 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 ed1569e1af0..00000000000 --- 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 8a6e1843d86..00000000000 --- 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 764e03786f8..00000000000 --- 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 03427e88af9..00000000000 --- 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 1da6a6be9da..4e892a2d499 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 f8dcb4f4a9c..6202b945d97 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -15,12 +15,13 @@ 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.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 @@ from .coordinator import ( FroniusPowerFlowUpdateCoordinator, FroniusStorageUpdateCoordinator, ) -from .sensor import InverterSensor _LOGGER: Final = logging.getLogger(__name__) PLATFORMS: Final = [Platform.SENSOR] @@ -76,7 +76,6 @@ class FroniusSolarNet: 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 @@ class FroniusSolarNet: 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 042773472c5..b65864ee089 100644 --- a/homeassistant/components/fronius/const.py +++ b/homeassistant/components/fronius/const.py @@ -6,6 +6,7 @@ from homeassistant.helpers.entity import DeviceInfo 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 d701d0d1860..ff949af0cba 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -24,12 +24,13 @@ from homeassistant.const import ( 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 163065b7c2a..b9ff3c4ea0b 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 785956572af..00000000000 --- 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 2709c08ad78..00000000000 --- 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 a1646bed51c..00000000000 --- 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 1f9f031a3bb..00000000000 --- 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 4e892a2d499..99566340ccd 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 ec7ae513a3e..c425d17621d 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 b9ff3c4ea0b..4a5c843f357 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 adb23c74c1d..bc83e3ed5a9 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 a4d7170e945..a1d31c45807 100644 --- a/tests/components/gardena_bluetooth/conftest.py +++ b/tests/components/gardena_bluetooth/conftest.py @@ -63,7 +63,10 @@ def mock_client(enable_bluetooth: None, mock_read_char_raw: dict[str, Any]) -> N 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 mock_client(enable_bluetooth: None, mock_read_char_raw: dict[str, Any]) -> N "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 a12cce06019..0c464f7cbc1 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 883f377c3a5..5a23b6d7f50 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 00000000000..37dae0bff75 --- /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 f1955905cce..588b73aadbb 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 @@ from tests.common import MockConfigEntry [ 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 d7cdc205f50..e9fd452e6a2 100644 --- a/tests/components/gardena_bluetooth/test_sensor.py +++ b/tests/components/gardena_bluetooth/test_sensor.py @@ -26,6 +26,7 @@ from tests.common import MockConfigEntry [ 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 00000000000..c2571b7a588 --- /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 0cf6db4ae81..5f6aa63ce2f 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.30.0"] + "requirements": ["python-roborock==0.30.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index eaa70a0fd62..d1cd300ddbd 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 bd6ceda67a9..811e114a2f8 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 a8665d76656..a8324ed770d 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 d1cd300ddbd..16c673ee1f1 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 811e114a2f8..d2424fdac95 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 13f7ba36bcd..224d98d92bf 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 63a0bb43d2a..052edf09bec 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -185,6 +185,10 @@ DHCP: list[dict[str, str | bool]] = [ "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 16c673ee1f1..90f0f7aeb4a 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 d2424fdac95..255ce1ce7ec 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 33ae9d167c2..5198f12519c 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 79653e1c9bc..00000000000 --- 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 d6d3443f562..00000000000 --- 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 6d84a5ffd0a..75d4b0b9a00 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 90f0f7aeb4a..9065b918ba5 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 255ce1ce7ec..e9b238f1d17 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 9a7caec925b..4515f52d8a3 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -62,7 +62,6 @@ NO_IOT_CLASS = [ "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 b5744b42d6b..00000000000 --- 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 7a9fda82511..00000000000 --- 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 5b82b5dae72..30ecf16bb37 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -195,7 +195,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 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 99b9c9392d8..2eae3631187 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -3,7 +3,7 @@ from __future__ import annotations 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 @@ class ConversationResult: } -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 db2a8d9512e..4a294489c97 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -128,14 +128,6 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): 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 3d0fac63420..1154c7132d2 100644 --- a/homeassistant/components/google_generative_ai_conversation/__init__.py +++ b/homeassistant/components/google_generative_ai_conversation/__init__.py @@ -69,14 +69,6 @@ class GoogleGenerativeAIAgent(conversation.AbstractConversationAgent): 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 c1b569ce9e1..efa81c7b73c 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -66,11 +66,6 @@ class OpenAIAgent(conversation.AbstractConversationAgent): 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 df57c78c9aa..648f8f33811 100644 --- a/tests/components/conversation/__init__.py +++ b/tests/components/conversation/__init__.py @@ -24,11 +24,6 @@ class MockAgent(conversation.AbstractConversationAgent): 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 6ad9beb3362..f89af1dc201 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 99f264e4a3a..3cb64a9a441 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 5ec27a1768c..7276e7a0d9b 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -151,7 +151,7 @@ def find_moov(mp4_io: BufferedIOBase) -> int: while 1: mp4_io.seek(index) box_header = mp4_io.read(8) - if len(box_header) != 8: + if len(box_header) != 8 or box_header[0:4] == b"\x00\x00\x00\x00": raise HomeAssistantError("moov atom not found") if box_header[4:8] == b"moov": return index diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 0dc67c37403..e0152190d90 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -245,7 +245,7 @@ class FakePyAvBuffer: # Forward to appropriate FakeStream packet.stream.mux(packet) # Make new init/part data available to the worker - self.memory_file.write(b"\x00\x00\x00\x00moov") + self.memory_file.write(b"\x00\x00\x00\x08moov") def close(self): """Close the buffer.""" From 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 791b1a21929..5404ee4af64 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 128a36e3e14..dd394c84f91 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -6,15 +6,24 @@ from collections.abc import Callable, Mapping, Sequence 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 @@ class Store(Generic[_T]): # 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 76dfbdbeb46..81953c7d785 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 @@ from homeassistant.const import ( 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 @@ async def test_saving_load_round_trip(tmpdir: py.path.local) -> None: } 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 7a384656b66..b19f4b9dfee 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 @@ class RussoundRNETDevice(MediaPlayerEntity): 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 @@ class RussoundRNETDevice(MediaPlayerEntity): 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 6f75679f124..aba5b44f284 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -95,6 +95,10 @@ class TomorrowioSensorEntityDescription(SensorEntityDescription): "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 @@ SENSOR_TYPES = ( 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 @@ SENSOR_TYPES = ( 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 @@ SENSOR_TYPES = ( 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 @@ SENSOR_TYPES = ( 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 @@ SENSOR_TYPES = ( 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 @@ SENSOR_TYPES = ( 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 @@ SENSOR_TYPES = ( 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 c795dbfdbaf..a104570f5c8 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 c779d30b0fc..2390f5af561 100644 --- a/homeassistant/components/gardena_bluetooth/__init__.py +++ b/homeassistant/components/gardena_bluetooth/__init__.py @@ -22,6 +22,7 @@ from .coordinator import Coordinator, DeviceUnavailable 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 00000000000..cfaa4d72c2a --- /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 cdc43a802c9..0226460d4d8 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 5a3f77eafa4..1d9a281fdbc 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 9065b918ba5..74a6082c0bc 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 e9b238f1d17..3b968ee932e 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 00000000000..b9cdca0e03c --- /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 00000000000..e184a2ecce8 --- /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 06bbca3a4ab..3b631057529 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 e641f67dfaf..9f2fd4a4355 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 bb3b474519e..e7d5ac8980e 100644 --- a/tests/components/otbr/conftest.py +++ b/tests/components/otbr/conftest.py @@ -6,16 +6,34 @@ import pytest 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 4ec99818b28..49694cf5585 100644 --- a/tests/components/otbr/test_init.py +++ b/tests/components/otbr/test_init.py @@ -15,8 +15,8 @@ from homeassistant.setup import async_setup_component 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 8dd07db6f22..83416ae297d 100644 --- a/tests/components/otbr/test_silabs_multiprotocol.py +++ b/tests/components/otbr/test_silabs_multiprotocol.py @@ -31,7 +31,9 @@ DATASET_CH16_PENDING = ( ) -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 65bec9e8408..b5dd7aa62c4 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()) + 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() - await websocket_client.send_json_auto_id({"type": "otbr/info"}) - - 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 074685c69f9..77c5146cefa 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 @@ class EzvizSensor(EzvizBaseEntity, NumberEntity): 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 e7a0890033c..b7020367f74 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 8c27f2970a3..89faf6d80eb 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -137,6 +137,13 @@ SENSOR_TYPES: tuple[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 7709ba0a638..165cefc9240 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -6,6 +6,7 @@ from meteofrance_api.model.forecast import Forecast 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 @@ class MeteoFranceWeather( 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 @@ class MeteoFranceWeather( 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 ca5e14b6d7b..76bd89af3d5 100644 --- a/homeassistant/components/ecowitt/entity.py +++ b/homeassistant/components/ecowitt/entity.py @@ -25,7 +25,7 @@ class EcowittEntity(Entity): 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 a392a338aba..c57a37365ed 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -51,68 +51,64 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "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 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "cwwsq": ( TuyaBinarySensorEntityDescription( key=DPCode.FEED_STATE, - name="Feeding", + translation_key="feeding", icon="mdi:information", on_value="feeding", ), @@ -215,7 +211,6 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "ldcg": ( TuyaBinarySensorEntityDescription( key=DPCode.TEMPER_ALARM, - name="Tamper", device_class=BinarySensorDeviceClass.TAMPER, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -290,7 +285,6 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "wkf": ( TuyaBinarySensorEntityDescription( key=DPCode.WINDOW_STATE, - name="Window", device_class=BinarySensorDeviceClass.WINDOW, on_value="opened", ), @@ -328,21 +322,20 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { 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 64d405ee5ad..4c73b70c29a 100644 --- a/homeassistant/components/tuya/button.py +++ b/homeassistant/components/tuya/button.py @@ -22,31 +22,31 @@ BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = { "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 @@ BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = { "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 5bb9c794ca4..3505bbf9f22 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -44,7 +44,6 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { "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 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { ), 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 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { # 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 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { "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 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { "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 3ab4c3568c4..5c2663d251c 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -70,7 +70,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "clkg": ( TuyaLightEntityDescription( key=DPCode.SWITCH_BACKLIGHT, - name="Backlight", + translation_key="backlight", entity_category=EntityCategory.CONFIG, ), ), @@ -114,7 +114,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { # 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 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "kg": ( TuyaLightEntityDescription( key=DPCode.SWITCH_BACKLIGHT, - name="Backlight", + translation_key="backlight", entity_category=EntityCategory.CONFIG, ), ), @@ -184,7 +184,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "kj": ( TuyaLightEntityDescription( key=DPCode.LIGHT, - name="Backlight", + translation_key="backlight", entity_category=EntityCategory.CONFIG, ), ), @@ -193,7 +193,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "kt": ( TuyaLightEntityDescription( key=DPCode.LIGHT, - name="Backlight", + translation_key="backlight", entity_category=EntityCategory.CONFIG, ), ), @@ -226,7 +226,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "qn": ( TuyaLightEntityDescription( key=DPCode.LIGHT, - name="Backlight", + translation_key="backlight", entity_category=EntityCategory.CONFIG, ), ), @@ -249,21 +249,21 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "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 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "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 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "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 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { ), 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 4430172e9a7..5e7bdcc260a 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -27,7 +27,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "dgnbj": ( NumberEntityDescription( key=DPCode.ALARM_TIME, - name="Time", + translation_key="time", entity_category=EntityCategory.CONFIG, ), ), @@ -36,35 +36,35 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "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 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "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 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "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 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "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 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "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 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "sd": ( NumberEntityDescription( key=DPCode.VOLUME_SET, - name="Volume", + translation_key="volume", icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, ), @@ -169,7 +169,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "sgbj": ( NumberEntityDescription( key=DPCode.ALARM_TIME, - name="Time", + translation_key="time", entity_category=EntityCategory.CONFIG, ), ), @@ -178,7 +178,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "sp": ( NumberEntityDescription( key=DPCode.BASIC_DEVICE_VOLUME, - name="Volume", + translation_key="volume", icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, ), @@ -188,37 +188,37 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "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 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "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 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "zd": ( NumberEntityDescription( key=DPCode.SENSITIVITY, - name="Sensitivity", + translation_key="sensitivity", entity_category=EntityCategory.CONFIG, ), ), @@ -264,21 +264,21 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "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 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "fs": ( NumberEntityDescription( key=DPCode.TEMP, - name="Temperature", + translation_key="temperature", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer-lines", ), @@ -298,13 +298,13 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "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 dadc64f9846..90cf4266ae6 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 @@ class TuyaSceneEntity(Scene): 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 b84737f7360..3cc8c72f555 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -23,7 +23,7 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "dgnbj": ( SelectEntityDescription( key=DPCode.ALARM_VOLUME, - name="Volume", + translation_key="volume", entity_category=EntityCategory.CONFIG, ), ), @@ -32,23 +32,23 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "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 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "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 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "qn": ( SelectEntityDescription( key=DPCode.LEVEL, - name="Temperature level", + translation_key="temperature_level", icon="mdi:thermometer-lines", ), ), @@ -82,12 +80,12 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "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 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "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 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "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 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "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 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "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 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "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 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "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 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "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 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "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 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "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 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "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 9483443a19c..1c7c2ab781d 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 @@ BATTERY_SENSORS: tuple[TuyaSensorEntityDescription, ...] = ( ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "cwwsq": ( TuyaSensorEntityDescription( key=DPCode.FEED_REPORT, - name="Last amount", + translation_key="last_amount", icon="mdi:counter", state_class=SensorStateClass.MEASUREMENT, ), @@ -246,36 +247,36 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "jwbj": ( TuyaSensorEntityDescription( key=DPCode.CH4_SENSOR_VALUE, - name="Methane", + translation_key="methane", state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, @@ -335,21 +336,21 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "qn": ( TuyaSensorEntityDescription( key=DPCode.WORK_POWER, - name="Power", + translation_key="power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), @@ -528,19 +528,19 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), 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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "fs": ( TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), @@ -990,13 +989,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 e7896f5da86..db16015ba56 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" } }, - "fan_angle": { + "vertical_fan_angle": { + "name": "Vertical swing flap angle", + "state": { + "30": "30°", + "60": "60°", + "90": "90°" + } + }, + "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 a7245913e73..c99d6f3a0b2 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -29,12 +29,12 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { # 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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "qjdcz": ( SwitchEntityDescription( key=DPCode.SWITCH_1, - name="Switch", + translation_key="switch", ), ), # Heater @@ -403,13 +403,13 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "sgbj": ( SwitchEntityDescription( key=DPCode.MUFFLING, - name="Mute", + translation_key="mute", entity_category=EntityCategory.CONFIG, ), ), @@ -444,68 +444,68 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "szjqr": ( SwitchEntityDescription( key=DPCode.SWITCH, - name="Switch", + translation_key="switch", icon="mdi:cursor-pointer", ), ), @@ -522,27 +522,27 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "wsdcg": ( SwitchEntityDescription( key=DPCode.SWITCH, - name="Switch", + translation_key="switch", device_class=SwitchDeviceClass.OUTLET, ), ), @@ -587,7 +587,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "zndb": ( SwitchEntityDescription( key=DPCode.SWITCH, - name="Switch", + translation_key="switch", ), ), # Fan @@ -624,37 +624,37 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "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 da7d73c272d..08407e46c1c 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 067d6430c9f..b7dadcf0f67 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -569,7 +569,8 @@ class EntityPlatform: 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 1a8bfba21fb..9b19148bdb7 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -18,7 +18,6 @@ from .entity import EzvizEntity 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 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { ), "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 @@ class EzvizSensor(EzvizEntity, SensorEntity): """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 94a73fc16cd..909a9b5f9c0 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 c3d0852e17a..149e503d0f8 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 74a6082c0bc..706bf13144f 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 3b968ee932e..006c9084832 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 927f0db3f01..5c94730b5db 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 4e608c36b97..6c5f1addd5a 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 84fa6a0cbb4..e725201bb7b 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 dac02b91315..81fbda63fef 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -432,15 +432,6 @@ class ReolinkHost: 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 42758dc9929..af8d049dbc6 100644 --- a/homeassistant/components/reolink/sensor.py +++ b/homeassistant/components/reolink/sensor.py @@ -9,7 +9,6 @@ from decimal import Decimal 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 @@ class ReolinkHostSensorEntity(ReolinkHostCoordinatorEntity, SensorEntity): 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 c0c2094eeb9..7d8c3a213eb 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 2834ea59672..803d0cb90a0 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 6f9a24d6db0..9db1f2ae2e7 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 f1dde9ca022..28b51fcb447 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 8b24fc912f1..dab8353c773 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -22,7 +22,12 @@ from .const import ( DOMAIN, ) -REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) +REAUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -42,18 +47,12 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Confirm re-authentication with Honeywell.""" errors: dict[str, str] = {} - + assert self.entry is not None if user_input: - assert self.entry is not None - password = user_input[CONF_PASSWORD] - data = { - CONF_USERNAME: self.entry.data[CONF_USERNAME], - CONF_PASSWORD: password, - } - try: await self.is_valid( - username=data[CONF_USERNAME], password=data[CONF_PASSWORD] + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], ) except aiosomecomfort.AuthError: @@ -71,7 +70,7 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.entry, data={ **self.entry.data, - CONF_PASSWORD: password, + **user_input, }, ) await self.hass.config_entries.async_reload(self.entry.entry_id) @@ -79,7 +78,9 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="reauth_confirm", - data_schema=REAUTH_SCHEMA, + data_schema=self.add_suggested_values_to_schema( + REAUTH_SCHEMA, self.entry.data + ), errors=errors, ) diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index a416f030a05..25ffa0a6093 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -156,14 +156,14 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "new-password"}, + {CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"}, ) await hass.async_block_till_done() assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data == { - CONF_USERNAME: "test-username", + CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password", } @@ -200,7 +200,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, client: MagicMock) -> ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "new-password"}, + {CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"}, ) await hass.async_block_till_done() @@ -246,7 +246,7 @@ async def test_reauth_flow_connnection_error( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "new-password"}, + {CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"}, ) await hass.async_block_till_done() From 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 11c19bbee3f..19aeae989f4 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 3d85f984e9a..e5fd5626302 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 d2102b651b7..6aafb9f2685 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 16b07e91446..aa07a5248cf 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/honeywell", "iot_class": "cloud_polling", "loggers": ["somecomfort"], - "requirements": ["AIOSomecomfort==0.0.14"] + "requirements": ["AIOSomecomfort==0.0.15"] } diff --git a/requirements_all.txt b/requirements_all.txt index 706bf13144f..ab97d677678 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 006c9084832..5be91cabebf 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 6538bd345de..b465a6b7037 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( 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 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) 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 @@ class APIStatusView(HomeAssistantView): 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 f3d3d48fdd2..5394e273a4c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1101,6 +1101,7 @@ SERVER_PORT: Final = 8123 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 61da000fc07..5ba9d60996b 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -678,3 +678,19 @@ async def test_api_call_service_bad_data( "/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 3c2f9ef729c..952e6c606c2 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -398,7 +398,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): ) 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 @@ class SpotifyMediaPlayer(MediaPlayerEntity): 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 0c7369f15e7..03fdc7e37c1 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -244,7 +244,10 @@ class MatchRule: 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 057921f80a9..6f36ee624e9 100644 --- a/tests/components/zha/test_registries.py +++ b/tests/components/zha/test_registries.py @@ -18,7 +18,8 @@ if typing.TYPE_CHECKING: 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 cfaa4d72c2a..b984d3420ae 100644 --- a/homeassistant/components/gardena_bluetooth/button.py +++ b/homeassistant/components/gardena_bluetooth/button.py @@ -40,7 +40,7 @@ DESCRIPTIONS = ( 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 f4c690dcffc..cbe4bf9069c 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 5c94730b5db..b4126b1c261 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 ab97d677678..64653cdb3f0 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 5be91cabebf..b948846cdde 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 a8324ed770d..6a4c1a66334 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 64653cdb3f0..5d4d9648d71 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 b948846cdde..649c25c4d6b 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 825064e5410..eccac004b7e 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -692,8 +692,9 @@ class ConfigEntry: 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 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): """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 77c5146cefa..74d496ef6c1 100644 --- a/homeassistant/components/ezviz/number.py +++ b/homeassistant/components/ezviz/number.py @@ -97,7 +97,7 @@ class EzvizSensor(EzvizBaseEntity, NumberEntity): 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 322e057ec15..4188e375907 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 730f0f5e8f3..bab2b89009f 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 d5da2af629e..38456058d8a 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 0a740ec4347..520b1a5626b 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -8,7 +8,9 @@ import voluptuous as vol 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 f9ead2a3054..46aee182b53 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 a5ea94088fd..7de0780e129 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 @@ UNSUPPORTED_GROUP_SERVICE_INFO = BluetoothServiceInfo( 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 a1d31c45807..98ae41d195b 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 @@ from gardena_bluetooth.parse import Characteristic 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 mock_client(enable_bluetooth: None, mock_read_char_raw: dict[str, Any]) -> N 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 5a23b6d7f50..14135cb390c 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 cda24f871e8..d12f825b1a7 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 e184a2ecce8..52fa3d4b00e 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 588b73aadbb..3b04d0cc818 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 e9fd452e6a2..307a9467f00 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 c2571b7a588..40e8c148335 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 375784d0323..c250a021340 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Coroutine 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 @@ _LOGGER = logging.getLogger(__name__) 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 @@ class ProtectRequiredKeysMixin(EntityDescription, Generic[T]): 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 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]): """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 e0c56cfd5fc..3e2b5e1b19e 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -41,13 +41,13 @@ from .const import ( _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 69a95ea8a9c..73b22ddab9f 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 81739db4d11..84ef91bf5a8 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -24,6 +24,7 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS_MILLIWATT, EntityCategory, UnitOfElectricPotential, + UnitOfMass, UnitOfPressure, UnitOfTemperature, ) @@ -68,6 +69,28 @@ SENSOR_DESCRIPTIONS = { 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 64fae252975..aba97c8ea8c 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -504,6 +504,11 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ ], "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 5d4d9648d71..d57d0cc1c69 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 649c25c4d6b..bc665abf4c8 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 ea11feab9c2..879ab4f7bc4 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -105,6 +105,22 @@ HHCCJCY10_SERVICE_INFO = BluetoothServiceInfoBleak( 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 1d6344063b5..40d89a8214d 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -4,7 +4,12 @@ from homeassistant.components.xiaomi_ble.const import DOMAIN 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 4488e881938..a58169aa6e5 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.config_entries import ConfigEntry 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 @@ WEMO_MODEL_DISPATCH = { _LOGGER = logging.getLogger(__name__) +DispatchCallback = Callable[[DeviceCoordinator], Coroutine[Any, Any, None]] HostPortTuple = tuple[str, int | None] @@ -81,11 +83,26 @@ CONFIG_SCHEMA = vol.Schema( 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 @@ class WemoDispatcher: 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 @@ class WemoDispatcher: 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 ce7dfc2fa11..396a555e4f4 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 async_setup_entry( 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 1d2c2c9252d..aaa85455c56 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.components.fan import FanEntity, FanEntityFeature 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 @@ from homeassistant.util.percentage import ( 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 @@ SET_HUMIDITY_SCHEMA = { 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 async_setup_entry( """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 2767d44032c..fb01d117c08 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.components.light import ( 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 async_setup_entry( 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 00000000000..ee12ccbf846 --- /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 15e396cc660..2547dc0ad0d 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.components.sensor import ( 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 @@ ATTRIBUTE_SENSORS = ( 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 async_setup_entry( 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 6d5e6b678b4..508621ba415 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.components.switch import SwitchEntity 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 @@ MAKER_SWITCH_TOGGLE = "toggle" 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 async_setup_entry( """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 65431fb7657..abb8aa186c9 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -9,7 +9,7 @@ from typing import Literal 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.entity import DeviceInfo 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 @@ class DeviceCoordinator(DataUpdateCoordinator[None]): 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 @@ class DeviceCoordinator(DataUpdateCoordinator[None]): # 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 5fe798004da..6c4d28ecae7 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 0e9ba19af42..1d4271063f2 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 @@ async def test_discovery(hass: HomeAssistant, pywemo_registry) -> None: 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 bdcf08bb2f6..9299b1ed0b0 100644 --- a/homeassistant/components/androidtv_remote/__init__.py +++ b/homeassistant/components/androidtv_remote/__init__.py @@ -1,6 +1,7 @@ """The Android TV Remote integration.""" from __future__ import annotations +import asyncio import logging from androidtvremote2 import ( @@ -9,6 +10,7 @@ from androidtvremote2 import ( ConnectionClosed, InvalidAuth, ) +import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform @@ -43,11 +45,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api.add_is_available_updated_callback(is_available_updated) try: - await api.async_connect() + async with async_timeout.timeout(5.0): + await api.async_connect() except InvalidAuth as exc: # The Android TV is hard reset or the certificate and key files were deleted. raise ConfigEntryAuthFailed from exc - except (CannotConnect, ConnectionClosed) as exc: + except (CannotConnect, ConnectionClosed, asyncio.TimeoutError) as exc: # The Android TV is network unreachable. Raise exception and let Home Assistant retry # later. If device gets a new IP address the zeroconf flow will update the config. raise ConfigEntryNotReady from exc From 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 ccab196032f..0e5d9ead0f8 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 d57d0cc1c69..e8ddc96de20 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 77e95fa221d..3ed61d8fc3d 100644 --- a/homeassistant/components/ezviz/binary_sensor.py +++ b/homeassistant/components/ezviz/binary_sensor.py @@ -22,9 +22,13 @@ BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { 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 @@ class EzvizBinarySensor(EzvizEntity, BinarySensorEntity): """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 909a9b5f9c0..0245edc0e3e 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 | 157 ++++-------------- 4 files changed, 189 insertions(+), 127 deletions(-) create mode 100644 homeassistant/components/trafikverket_train/coordinator.py diff --git a/.coveragerc b/.coveragerc index 4a5c843f357..acd218a2d1b 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 8047cf2046d..dd35d058ed5 100644 --- a/homeassistant/components/trafikverket_train/__init__.py +++ b/homeassistant/components/trafikverket_train/__init__.py @@ -15,6 +15,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady 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 00000000000..fba6eb93dd9 --- /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 c0643858f42..f57850e51b8 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 @@ class TrainSensor(SensorEntity): 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 @@ class TrainSensor(SensorEntity): self._attr_unique_id = create_unique_id( from_station.name, to_station.name, departuretime, weekday ) + self._update_attr() - async def async_update(self) -> None: + @callback + def _handle_coordinator_update(self) -> None: + self._update_attr() + return super()._handle_coordinator_update() + + @callback + def _update_attr(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 + data = self.coordinator.data - self._attr_available = True + self._attr_native_value = data.departure_time - # 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, + 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, } - - if delay_in_minutes := state.get_delay_time(): - attributes[ATTR_DELAY_TIME] = delay_in_minutes.total_seconds() / 60 - - 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) - - if other_info := state.other_information: - attributes[ATTR_OTHER_INFORMATION] = ", ".join(other_info) - - if deviation := state.deviations: - attributes[ATTR_DEVIATIONS] = ", ".join(deviation) - - self._attr_extra_state_attributes = attributes 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 1a2580cfc61..849581b6f9f 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -31,6 +31,7 @@ from homeassistant.const import ( 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 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = { 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 fe01058fda7..2c6883e4a71 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 d9cf27c12aa..37c0b175faa 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -22,6 +22,7 @@ from homeassistant.components.number.const import ( ) 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 e7d73ec0f1c..52e234505c1 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 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity): """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 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity): """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 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity): 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 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): identifiers={(DOMAIN, self.unique_id)}, manufacturer="Airtouch", model="Airtouch 4", - name=self.name, + name=self._unit.GroupName, ) @property @@ -242,11 +242,6 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): """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 1c7c2ab781d..9f055a6262e 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -986,6 +986,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), ), # eMylo Smart WiFi IR Remote + # Air Conditioner Mate (Smart IR Socket) "wnykq": ( TuyaSensorEntityDescription( key=DPCode.VA_TEMPERATURE, @@ -999,6 +1000,30 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { 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 c99d6f3a0b2..676991fe167 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -573,6 +573,13 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { 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 20167ddd184..204ed622f13 100644 --- a/homeassistant/components/sensibo/switch.py +++ b/homeassistant/components/sensibo/switch.py @@ -151,9 +151,10 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity): @async_handle_api_call async def async_turn_on_timer(self, key: str, value: bool) -> bool: """Make service call to api for setting timer.""" + new_state = not self.device_data.device_on data = { "minutesFromNow": 60, - "acState": {**self.device_data.ac_states, "on": value}, + "acState": {**self.device_data.ac_states, "on": new_state}, } result = await self._client.async_set_timer(self._device_id, data) return bool(result.get("status") == "success") From 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 dd5480450e2..316e816fd6f 100644 --- a/homeassistant/components/sql/__init__.py +++ b/homeassistant/components/sql/__init__.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( 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.const import ( 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 @@ QUERY_SCHEMA = vol.Schema( 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 96fc4bc943a..cbdef90f623 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations 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.components.sensor import ( 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.device_registry import DeviceEntryType 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 _async_get_or_init_domain_data(hass: HomeAssistant) -> SQLData: 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 @@ class SQLSensor(SensorEntity): 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 @@ class SQLSensor(SensorEntity): 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 @@ class SQLSensor(SensorEntity): _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 42d578555ab..fcd98a77831 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -624,7 +624,7 @@ class ManualTriggerEntity(TriggerBaseEntity): 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 a1417cd38df..53356a85c4e 100644 --- a/tests/components/sql/__init__.py +++ b/tests/components/sql/__init__.py @@ -13,12 +13,14 @@ from homeassistant.components.sql.const import CONF_COLUMN_NAME, CONF_QUERY, DOM 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_NO_DB = { } } +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 0fe0e881c95..3d0e2768ade 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -13,7 +13,12 @@ from homeassistant.components.recorder import Recorder 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 homeassistant.util import dt as dt_util 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 . import ( 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 21a54d64728..ba69a76fbdd 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -181,7 +181,9 @@ async def async_check_ha_config_file( # noqa: C901 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 91ef17d526d..3b9b3cf6558 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -8,9 +8,10 @@ from homeassistant.helpers.check_config import ( 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 @@ bla: 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 36c3a5dbda5..8430d7284ee 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -9,10 +9,16 @@ import shlex 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_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) 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_setup(hass: HomeAssistant, config: ConfigType) -> bool: 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_setup(hass: HomeAssistant, config: ConfigType) -> bool: 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_setup(hass: HomeAssistant, config: ConfigType) -> bool: "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 fe685398c5d..ac594c811ed 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -10,6 +10,7 @@ import pytest 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 test_do_not_run_forever( "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 8f08aab8d30..e5e90bf19af 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry 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 @@ from .const import ( 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 7aa86af1e9a..e678f92c480 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 6d7b3496880..0d4a091b729 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -17,6 +17,7 @@ from awesomeversion import AwesomeVersion 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 @@ from .const import ( 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 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): 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 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): 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 eeb2c3d3224..7c3f6033d07 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 9039893999d..8536c3d72e6 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -13,6 +13,7 @@ from homeassistant.components.shelly.const import ( ATTR_GENERATION, DOMAIN, ENTRY_RELOAD_COOLDOWN, + MAX_PUSH_UPDATE_FAILURES, RPC_RECONNECT_INTERVAL, SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, @@ -24,15 +25,18 @@ from homeassistant.helpers.device_registry import ( 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 042eb94b195..27e2d2e5ad0 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 d41b55c0b48..7348e1bf3e2 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 e8ddc96de20..63ca182ce2c 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 d36b3f61d9d..96d8bd03a52 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -33,6 +33,7 @@ COMMENT_REQUIREMENTS = ( "face-recognition", "opencv-python-headless", "pybluez", + "pycocotools", "pycups", "python-eq3bt", "python-gammu", From 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 6a8131d2454..6c083b6a024 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -1098,7 +1098,11 @@ class Helpers: 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 ef2ecc7aa23..c59150266d9 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -61,6 +61,8 @@ ENTITY_DESCRIPTION_ALARM = SIAAlarmControlPanelEntityDescription( "OS": STATE_ALARM_DISARMED, "NC": STATE_ALARM_ARMED_NIGHT, "NL": STATE_ALARM_ARMED_NIGHT, + "NE": STATE_ALARM_ARMED_CUSTOM_BYPASS, + "NF": STATE_ALARM_ARMED_CUSTOM_BYPASS, "BR": PREVIOUS_STATE, "NP": PREVIOUS_STATE, "NO": PREVIOUS_STATE, From 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 6c8e8fc100b..50e8f89716b 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 aa351f7ccbd..fd03ed3044b 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.components.subaru.sensor import ( ) 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 .api_responses import ( 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 b4126b1c261..e118f7ae39b 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 6c5f1addd5a..6575d2f8fb3 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 e725201bb7b..d4445c95369 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 803d0cb90a0..02a8a04e1b9 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 01f77cb2d38..dd29d02d655 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["pymazda"], "quality_scale": "platinum", - "requirements": ["pymazda==0.3.9"] + "requirements": ["pymazda==0.3.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index 63ca182ce2c..3e67f654d6c 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 bc665abf4c8..73f0a230056 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 82d91863fad..9528ae568fd 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 3e67f654d6c..b69361458dc 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 73f0a230056..9280daf186c 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 5740abef789..b07e2d5880a 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -12,7 +12,6 @@ from zeroconf import ( 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 922e42c7a7e..6b4c35c4a37 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 b8d77d5d82a..0dbc4c8f7d6 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 8cac3fb81f7..3aefeea048a 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 b69361458dc..e0390171ebd 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 9280daf186c..4179c2fe8e1 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 02f3f584003..8b4a94a40af 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 @@ class DemoButton(ButtonEntity): 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 6a4c1a66334..6ecdf0fddbd 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 e0390171ebd..6fb3c6fdd7b 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 4179c2fe8e1..35c64169868 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 54f44d0998c..6ea03e85c43 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -764,7 +764,7 @@ class HomeAssistant: 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 @@ class HomeAssistant: "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 9a86bed7594..67ec232db9c 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 df225580dae..9d9f5d986a0 100644 --- a/homeassistant/util/timeout.py +++ b/homeassistant/util/timeout.py @@ -232,7 +232,7 @@ class _GlobalTaskContext: """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 @@ class _ZoneTaskContext: # 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 eccac004b7e..a1c09b8815f 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -687,7 +687,7 @@ class ConfigEntry: if self._on_unload is not None: while self._on_unload: if job := self._on_unload.pop()(): - self._tasks.add(hass.async_create_task(job)) + self.async_create_task(hass, job) if not self._tasks and not self._background_tasks: return From 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 74334a15f4a..2de92fc3827 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 b9aae585d9f..a14324a9082 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 c52f2627253..56cb46d26b4 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 314800f33f2..57ec933cd58 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 @@ class MqttAttributes(Entity): 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 @@ class MqttAttributes(Entity): 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 4134dd97148..d30080f4647 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 @@ class MqttSiren(MqttEntity, SirenEntity): 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 @@ class MqttSiren(MqttEntity, SirenEntity): 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 @@ class MqttSiren(MqttEntity, SirenEntity): 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 @@ class MqttSiren(MqttEntity, SirenEntity): """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 cfd714725c4..fd760044f3c 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 05edfbad91d..7d8d0791b4d 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -405,13 +405,13 @@ class ScannerEntity(BaseTrackerEntity): @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 d168835b394..9115fc5991b 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -123,7 +123,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): 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 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): 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 73b22ddab9f..683a5dab9dd 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 6fb3c6fdd7b..4efb95e7769 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 35c64169868..8bbd5666dee 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 4a8a9dec587..0575ac132d4 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.5.35"] + "requirements": ["env-canada==0.5.36"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4efb95e7769..497cbbf875c 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 8bbd5666dee..e4021b23e91 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 f4486ca5a19..25b77922878 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 f788ad21098..13ab6d38e8a 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations 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 @@ class UpdateEntity(RestoreEntity): 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 8dc04e7da86..7f0866ce62e 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -205,7 +205,6 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { key="process", name="Process", icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, mandatory_arg=True, ), "processor_use": SysMonitorSensorEntityDescription( From 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 00000000000..c5aac39156d --- /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 6bd99399952..f2c5274c4a7 100644 --- a/tests/components/youtube/test_sensor.py +++ b/tests/components/youtube/test_sensor.py @@ -4,6 +4,7 @@ from unittest.mock import patch 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 .conftest import TOKEN, ComponentSetup 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 2660a1a9d3a..ae5f1008820 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 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( ), DaikinSensorEntityDescription( key=ATTR_HUMIDITY, - name="Humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, @@ -79,7 +78,7 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( ), 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 7848949831b..93ee636c726 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 7e5d0df0259..93e23be5d8d 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -35,11 +35,11 @@ from .const import ( 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 02e51993198..3e74611f767 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 @@ DEFAULT_CACHEDB = ".logi_cache.pickle" 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 7d4697adb64..b27ba30128f 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -10,6 +10,7 @@ from homeassistant.const import ( ATTR_BATTERY_CHARGING, CONF_MONITORED_CONDITIONS, CONF_SENSORS, + PERCENTAGE, STATE_OFF, STATE_ON, ) @@ -20,11 +21,47 @@ from homeassistant.helpers.icon import icon_for_battery_level 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 3ed67389003..59a57a480c2 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 @@ DEFAULT_URLBASE = "" 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 1ab4b170e00..f534144d02c 100644 --- a/homeassistant/components/ombi/sensor.py +++ b/homeassistant/components/ombi/sensor.py @@ -11,13 +11,47 @@ from homeassistant.core import HomeAssistant 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 02a8a04e1b9..2db6d8fe3d4 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 00000000000..380033e450a --- /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 00000000000..6a41465ac92 --- /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 00000000000..4fe16c3a8b6 --- /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 acd218a2d1b..6b3270f85e9 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 4355d3d2595..59dfb7c269c 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -33,6 +33,7 @@ _LOGGER = logging.getLogger(__name__) 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 00000000000..32f9b38888f --- /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 d052a4b8216..c28d84552d6 100644 --- a/homeassistant/components/ezviz/const.py +++ b/homeassistant/components/ezviz/const.py @@ -6,8 +6,6 @@ MANUFACTURER = "EZVIZ" # 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 b1870654be0..5e9b1d50def 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 67ebca7aea7..9818e3d3197 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 5198f12519c..918ad4c2343 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 246c952e219..6d54255f8ed 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -30,6 +30,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ 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 8b4a94a40af..3c0498fefef 100644 --- a/homeassistant/components/demo/button.py +++ b/homeassistant/components/demo/button.py @@ -51,3 +51,4 @@ class DemoButton(ButtonEntity): 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 00000000000..e9d26d9f54d --- /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 d9b89608072..555760a5af9 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 00000000000..6eeab6a32bb --- /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 00000000000..cd6a8b96f7a --- /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 00000000000..2da0940012a --- /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 00000000000..759fd80bcf0 --- /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 00000000000..02f4da8ca08 --- /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 5394e273a4c..94fa194fa09 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 ab8b5a5df89..4c2d803a549 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 00000000000..e8236163d05 --- /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 00000000000..66cda6a088a --- /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 00000000000..133f7e173e3 --- /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 00000000000..9acb24f37cf --- /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 dbed1c8aa9e..06f91403057 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -89,7 +89,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( 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 50af9ef8a55..0d4b2c4a7b4 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -61,7 +61,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( 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 46ecc16d385..9b3b04a54f5 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -35,7 +35,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( 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 75ab25efcfa..166bfdd38cc 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -41,7 +41,7 @@ MQTT_CAMERA_ATTRIBUTES_BLOCKED = frozenset( 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 676e5b50f49..f29a114620a 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -296,7 +296,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( ): 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 @@ class MqttTemperatureControlEntity(MqttEntity, ABC): 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 0b435db0b7a..c11cf2dfb85 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -159,7 +159,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( 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 a9c4017593c..dd4eca9878a 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -61,7 +61,7 @@ PLATFORM_SCHEMA_MODERN_BASE = MQTT_BASE_SCHEMA.extend( { 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 f5e92d8ecf9..58189c3cb3e 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 392a112bcdb..aebb05c19f7 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -136,7 +136,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( 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 2764539770d..a21d45369f8 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 fe09667ca4a..2a726075bb0 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -190,7 +190,7 @@ PLATFORM_SCHEMA_MODERN_BASIC = ( 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 70992887ca7..8f710eb5ea6 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -132,7 +132,7 @@ _PLATFORM_SCHEMA_BASE = ( 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 063895d738c..98ee7648eeb 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -100,7 +100,7 @@ PLATFORM_SCHEMA_MODERN_TEMPLATE = ( 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 966cbc21105..cb586c06309 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -76,7 +76,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { 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 57ec933cd58..ec437f08d39 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -50,7 +50,12 @@ from homeassistant.helpers.event import ( 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 @@ class MqttEntity( 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 @@ class MqttEntity( 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 @@ class MqttEntity( 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 @@ class MqttEntity( 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 5986eab1207..971b44b43bf 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -87,7 +87,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( 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 f716e4fe46f..5e12f67a698 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -34,7 +34,7 @@ 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, 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 26e72af9192..df8cf024bd2 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -54,7 +54,7 @@ MQTT_SELECT_ATTRIBUTES_BLOCKED = frozenset( 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 e4b5f61bda0..ae94b0df0ce 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -78,7 +78,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend( 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 d30080f4647..328812a6e49 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -79,7 +79,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( 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 7f4f609f265..107b0b1cb10 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -49,7 +49,7 @@ CONF_STATE_OFF = "state_off" 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 01622c10a6d..13677b7f35b 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 930f4d22506..f6db0d3fd64 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -57,7 +57,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( 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 7c73e579112..516a7772c11 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -131,7 +131,7 @@ PLATFORM_SCHEMA_LEGACY_MODERN = ( ), 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 ee06131af02..5113e19f097 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -126,7 +126,7 @@ PLATFORM_SCHEMA_STATE_MODERN = ( 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 0f622d55b84..17e9430dba3 100644 --- a/homeassistant/components/mqtt/water_heater.py +++ b/homeassistant/components/mqtt/water_heater.py @@ -123,7 +123,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( ): 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 d1b1d6b68b3..e69839e6b16 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -55,6 +55,7 @@ from .test_common import ( 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 d32754625f4..28bf5f558cb 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -41,6 +41,7 @@ from .test_common import ( 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 fa16ef77817..481e98f0099 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -30,6 +30,7 @@ from .test_common import ( 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 fd760044f3c..9d580da073e 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 fb103384874..eb923ac2f07 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 62b87bdb791..d3b8a145df7 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 3395dc0825f..c0d7a94de5b 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 c7285f0fa5f..5a30a3a65de 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -5,8 +5,12 @@ from unittest.mock import patch 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 @@ async def test_availability_with_shared_state_topic( # 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 96d9cdcef64..dbdd373a659 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -48,6 +48,7 @@ from .test_common import ( 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 d6ab692af52..30eb0fd1939 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -53,6 +53,7 @@ from .test_common import ( 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 611d36882ee..9a72541bb50 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 c4a6a0ad777..45b5cbe9fba 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -40,6 +40,7 @@ from .const import ( 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_WIND_SPEED: Final = "wind_speed" 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 @@ class WeatherEntity(Entity): """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 @@ class WeatherEntity(Entity): 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 b995ce2b729..759021741ff 100644 --- a/homeassistant/components/weather/const.py +++ b/homeassistant/components/weather/const.py @@ -34,6 +34,7 @@ ATTR_WEATHER_WIND_SPEED = "wind_speed" 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 26ccd731828..21029c77284 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 5ed6a02f24b..53753ad4a72 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -13,6 +13,7 @@ from homeassistant.components.weather import ( 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 @@ from homeassistant.components.weather import ( 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 a5c49fb92c2..df6a43ad40c 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -19,6 +19,7 @@ from homeassistant.components.weather import ( 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 @@ class MockWeather(MockEntity, WeatherEntity): """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 @@ class MockWeatherMockForecast(MockWeather): 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 2db6d8fe3d4..c4e943bacea 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 2027cf73d25..04c211a98cb 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -20,6 +20,7 @@ from homeassistant.config_entries import ConfigEntry 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.helpers.update_coordinator import CoordinatorEntity 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 @@ class BlockSleepingClimate( 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 e678f92c480..608798976ba 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 7c3f6033d07..6ff48f5b85b 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 505d1d463e8..c806cb5e742 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -21,9 +21,11 @@ from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState 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 c4e943bacea..9dd7c75e22a 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 00000000000..7e511ed7d2c --- /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 cdedd0c9620..0616b774951 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -21,42 +21,21 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -CONF_ALTITUDE = "altitude" +from .const import ( + ATTR_ALTITUDE, + ATTR_CALLSIGN, + ATTR_ICAO24, + ATTR_SENSOR, + CONF_ALTITUDE, + DEFAULT_ALTITUDE, + DOMAIN, + EVENT_OPENSKY_ENTRY, + EVENT_OPENSKY_EXIT, +) -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 - -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 4b3721eed15..d90a9d28662 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyfibaro"], - "requirements": ["pyfibaro==0.7.1"] + "requirements": ["pyfibaro==0.7.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 497cbbf875c..f2345630dbb 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 e4021b23e91..21bdcf31527 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 9dd7c75e22a..f2901d88557 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 6eeab6a32bb..48bb2fd1726 100644 --- a/homeassistant/components/event/__init__.py +++ b/homeassistant/components/event/__init__.py @@ -177,8 +177,8 @@ class EventEntity(RestoreEntity): 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 0260a540f0c..3e9dfaad923 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -360,6 +360,14 @@ RPC_SENSORS: Final = { 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 @@ RPC_SENSORS: Final = { 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 @@ RPC_SENSORS: Final = { 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 | 465 ++++++++++-------- 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, 1347 insertions(+), 254 deletions(-) create mode 100644 homeassistant/components/kitchen_sink/weather.py diff --git a/.coveragerc b/.coveragerc index 6b3270f85e9..9e5541a07bc 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 6d54255f8ed..04eba5f0586 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -56,6 +56,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [ 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 e64d0bcc28d..887a9212335 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 @@ from homeassistant.components.weather import ( 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 @@ CONDITION_CLASSES: dict[str, list[str]] = { 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 @@ class DemoWeather(WeatherEntity): 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 @@ class DemoWeather(WeatherEntity): 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,13 @@ class DemoWeather(WeatherEntity): 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], @@ -196,7 +256,48 @@ class DemoWeather(WeatherEntity): templow=entry[3], precipitation_probability=entry[4], ) - reftime = reftime + timedelta(hours=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], + 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) return forecast_data diff --git a/homeassistant/components/kitchen_sink/__init__.py b/homeassistant/components/kitchen_sink/__init__.py index 7857e6b3149..a85221108f8 100644 --- a/homeassistant/components/kitchen_sink/__init__.py +++ b/homeassistant/components/kitchen_sink/__init__.py @@ -26,7 +26,12 @@ import homeassistant.util.dt as dt_util 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 00000000000..aba30013746 --- /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 45b5cbe9fba..c63db816711 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 @@ from homeassistant.const import ( 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.entity_component import EntityComponent 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 @@ from .const import ( DOMAIN, UNIT_CONVERSIONS, VALID_UNITS, + WeatherEntityFeature, ) from .websocket_api import async_setup as async_setup_ws_api @@ -72,6 +74,7 @@ ATTR_CONDITION_SUNNY = "sunny" 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 @@ class WeatherEntity(Entity): "visibility_unit", "_attr_precipitation_unit", "precipitation_unit", + "_attr_forecast", + "forecast", ) ): if _reported is False: @@ -291,8 +304,9 @@ class WeatherEntity(Entity): ) 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 @@ class WeatherEntity(Entity): @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,197 +785,197 @@ class WeatherEntity(Entity): 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) - - temperature = forecast_entry.pop( - ATTR_FORECAST_NATIVE_TEMP, forecast_entry.get(ATTR_FORECAST_TEMP) - ) - - from_temp_unit = ( - self.native_temperature_unit or self._default_temperature_unit - ) - to_temp_unit = self._temperature_unit - - 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 - ) - - 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, - ) - - forecast_entry[ATTR_FORECAST_APPARENT_TEMP] = round_temperature( - value_apparent_temp, precision - ) - - 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, - ) - - 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, - ) - ) 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_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, - ) - - 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, - ) - - 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, - ) - - 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, - ) - - 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 - ) - - forecast.append(forecast_entry) - - data[ATTR_FORECAST] = forecast + if self.forecast: + data[ATTR_FORECAST] = self._convert_forecast(self.forecast) return data + @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 + + from_temp_unit = self.native_temperature_unit or self._default_temperature_unit + to_temp_unit = self._temperature_unit + + for _forecast_entry in native_forecast_list: + forecast_entry: dict[str, Any] = dict(_forecast_entry) + + temperature = forecast_entry.pop( + ATTR_FORECAST_NATIVE_TEMP, forecast_entry.get(ATTR_FORECAST_TEMP) + ) + + 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 + ) + + 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, + ) + + forecast_entry[ATTR_FORECAST_APPARENT_TEMP] = round_temperature( + value_apparent_temp, precision + ) + + 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, + ) + + 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, + ) + ) 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_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, + ) + + 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, + ) + + 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, + ) + + 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, + ) + + 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) + + converted_forecast_list.append(forecast_entry) + + return converted_forecast_list + @property @final def state(self) -> str | None: @@ -998,3 +1027,53 @@ class WeatherEntity(Entity): ) ) 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 759021741ff..c6da2c28c71 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 @@ from homeassistant.util.unit_conversion import ( 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 51f129fc4a2..f2be4dfec6d 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 b2b789a084f..ced801a4d46 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.core import HomeAssistant 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 24df7abb1f3..91097dfae14 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 53753ad4a72..92643b616c9 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -34,6 +34,7 @@ from homeassistant.components.weather import ( ROUNDING_PRECISION, Forecast, WeatherEntity, + WeatherEntityFeature, round_temperature, ) from homeassistant.components.weather.const import ( @@ -54,6 +55,7 @@ from homeassistant.const import ( 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_conversion import ( ) 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 @@ class MockWeatherEntity(WeatherEntity): 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 @@ class MockWeatherEntityCompat(WeatherEntity): 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 5d7928124dd..2864abf58bb 100644 --- a/tests/components/weather/test_recorder.py +++ b/tests/components/weather/test_recorder.py @@ -5,7 +5,11 @@ from datetime import timedelta 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 homeassistant.util.unit_system import METRIC_SYSTEM 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 760acbb2bb0..4f5223c6f79 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 df6a43ad40c..e2d026ec840 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -4,9 +4,12 @@ Call init before using it in your tests to ensure clean test data. """ 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 @@ class MockWeatherCompat(MockEntity, WeatherEntity): 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 6ecdf0fddbd..1caf0e01efc 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 f2345630dbb..3fb2ff29f9a 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 21bdcf31527..b99ba4b372c 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 f2901d88557..3bc592e98eb 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 3bc592e98eb..fbe3fa1a66c 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 fbe3fa1a66c..7341c0da62e 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 7341c0da62e..855731be729 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 f7c9da48883..35e66ea7e47 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -4,7 +4,6 @@ from __future__ import annotations 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 import ( ) 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 @@ def verify_connected(func: _WrapFuncType) -> _WrapFuncType: 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 @@ class ESPHomeClient(BaseBleakClient): # 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 @@ class ESPHomeClient(BaseBleakClient): # 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 1caf0e01efc..33c43936544 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 3fb2ff29f9a..8f16fbeb22c 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 b99ba4b372c..3ff331e2a00 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 81fdcde236a..cdea8ebee54 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -6,6 +6,7 @@ from dataclasses import dataclass, field 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 @@ class FritzBoxTools( 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 c19327fbf5e..dc27e8aab96 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -29,7 +29,7 @@ MOCK_HOST = "fake_host" 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 f7e5980720d..760b5f32d0c 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 915a6bb6fd0..99ca7a3b6c5 100644 --- a/tests/components/fritz/test_update.py +++ b/tests/components/fritz/test_update.py @@ -9,7 +9,6 @@ from homeassistant.core import HomeAssistant 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 9e5541a07bc..a5397971d1f 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 aea65f9358e..4acd335c1b8 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.config_entries import ConfigEntry 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 00000000000..3ec29121382 --- /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 35e66ea7e47..ee629eed6f9 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -4,6 +4,8 @@ from __future__ import annotations 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 @@ import uuid 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.device import BLEDevice 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 @@ def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType: 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 @@ class ESPHomeClient(BaseBleakClient): 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 @@ class ESPHomeClient(BaseBleakClient): 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 @@ class ESPHomeClient(BaseBleakClient): 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 @@ class ESPHomeClient(BaseBleakClient): 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 @@ class ESPHomeClient(BaseBleakClient): 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 @@ class ESPHomeClient(BaseBleakClient): 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 @@ class ESPHomeClient(BaseBleakClient): 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 @@ class ESPHomeClient(BaseBleakClient): 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 @@ class ESPHomeClient(BaseBleakClient): @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 @@ class ESPHomeClient(BaseBleakClient): 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 00000000000..8d060151dbf --- /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 292d1921abf..a984d057c0c 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 aacda108398..3203964fdc1 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 @@ class DomainData: 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 3391d02a829..2d147d243f2 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -40,6 +40,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback 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 @@ class RuntimeEntryData: 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 026d0315238..4741eaaa6fb 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -390,7 +390,9 @@ class ESPHomeManager: 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 e118f7ae39b..c8f4bc835ce 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 96d8bd03a52..f3d0defac4d 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -107,7 +107,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 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 855731be729..e20e28b3d0a 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 2a350816685..e206521c3d9 100644 --- a/homeassistant/components/poolsense/binary_sensor.py +++ b/homeassistant/components/poolsense/binary_sensor.py @@ -17,12 +17,12 @@ from .const import DOMAIN 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 f8f91620321..fe3535b378f 100644 --- a/homeassistant/components/poolsense/sensor.py +++ b/homeassistant/components/poolsense/sensor.py @@ -22,55 +22,53 @@ from .const import DOMAIN 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 2ddf3ee77e8..9ec67e223a1 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 4560dcfda8c..b5d3fc79b39 100644 --- a/homeassistant/components/youtube/sensor.py +++ b/homeassistant/components/youtube/sensor.py @@ -15,6 +15,7 @@ from homeassistant.helpers.typing import StateType from . import YouTubeDataUpdateCoordinator from .const import ( ATTR_LATEST_VIDEO, + ATTR_PUBLISHED_AT, ATTR_SUBSCRIBER_COUNT, ATTR_THUMBNAIL, ATTR_TITLE, @@ -47,7 +48,8 @@ SENSOR_TYPES = [ 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 7f369e9909b..ccb7e9c506e 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 c5aac39156d..b643bdeb979 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 119d4796f57..8b8548ad1f9 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 00000000000..a28e085c104 --- /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 e46cf99ffdc..ab9358be1d3 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.core import HomeAssistant 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( +@pytest.mark.parametrize( + ("fixture"), + [ + ("not_found_user"), + ("first_time_user"), + ("default_user"), + ], +) +async def test_sensors( hass: HomeAssistant, setup_integration: ComponentSetup, config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + fixture: str, + request: pytest.FixtureRequest, ) -> None: - """Test update when user can't be fetched.""" - await setup_integration( - config_entry, - MockUser(thrown_error=WSError("network", "status", "User not found")), - ) + """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 == "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( - hass: HomeAssistant, - setup_integration: ComponentSetup, - config_entry: MockConfigEntry, - default_user: MockUser, -) -> None: - """Test update when playing a song.""" - await setup_integration(config_entry, default_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 d746e63ca52..1578359356d 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_CONFIG: Final[dict[str, str]] = { 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 11946c42173..6c93fbe35c1 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 import configurator 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 @@ from .const import ( 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 @@ _CONFIGURING: dict[str, str] = {} 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 ee629eed6f9..748035bedac 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -364,16 +364,18 @@ class ESPHomeClient(BaseBleakClient): 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 @@ class ESPHomeClient(BaseBleakClient): @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 @@ class ESPHomeClient(BaseBleakClient): 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 @@ class ESPHomeClient(BaseBleakClient): 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 @@ class ESPHomeClient(BaseBleakClient): 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 5e226dcead7..8b4cfcb61a4 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -35,48 +35,46 @@ from .const import DOMAIN 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 @@ class OndiloICO( 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 4e5f2330840..3843670bc50 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 a5397971d1f..4f3e82042f6 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 70a66251bdc..b09682fcaf9 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 f67ea58bfc4..8178e243279 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.config_entries import ConfigEntry 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 00000000000..383ab7116b6 --- /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 58974bcc326..9cc402e014c 100644 --- a/homeassistant/components/aseko_pool_live/entity.py +++ b/homeassistant/components/aseko_pool_live/entity.py @@ -4,8 +4,8 @@ from aioaseko import Unit 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 74051ef454f..09c4af31428 100644 --- a/homeassistant/components/aseko_pool_live/sensor.py +++ b/homeassistant/components/aseko_pool_live/sensor.py @@ -12,8 +12,8 @@ from homeassistant.config_entries import ConfigEntry 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 d9bd215d2a1..6cabe51fff5 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 @@ class IcloudTrackerEntity(TrackerEntity): """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 e7c982607cb..01aabc5871c 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 @@ class IcloudDeviceBatterySensor(SensorEntity): """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 d00fe793bf8..d7f30ce5c3b 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -121,12 +121,13 @@ class ModernFormsDataUpdateCoordinator(DataUpdateCoordinator[ModernFormsDeviceSt 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 @@ class ModernFormsDeviceEntity(CoordinatorEntity[ModernFormsDataUpdateCoordinator 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 f8e3f8bbcf8..b3361c3f143 100644 --- a/homeassistant/components/modern_forms/binary_sensor.py +++ b/homeassistant/components/modern_forms/binary_sensor.py @@ -40,14 +40,11 @@ class ModernFormsBinarySensor(ModernFormsDeviceEntity, BinarySensorEntity): *, 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 @@ class ModernFormsLightSleepTimerActive(ModernFormsBinarySensor): 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 @@ class ModernFormsFanSleepTimerActive(ModernFormsBinarySensor): 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 8bd8665dc3b..9d5a3c32235 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 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity): 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 55569054ac4..013d6a17d6d 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 @@ class ModernFormsLightEntity(ModernFormsDeviceEntity, LightEntity): 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 6d2ef5b6dab..efd659f3ae0 100644 --- a/homeassistant/components/modern_forms/sensor.py +++ b/homeassistant/components/modern_forms/sensor.py @@ -43,21 +43,20 @@ class ModernFormsSensor(ModernFormsDeviceEntity, SensorEntity): *, 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 @@ class ModernFormsLightTimerRemainingTimeSensor(ModernFormsSensor): 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 @@ class ModernFormsLightTimerRemainingTimeSensor(ModernFormsSensor): 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 @@ class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): 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 defe412e96d..dd47ef721af 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 90d5d13d649..18d8caccbd6 100644 --- a/homeassistant/components/modern_forms/switch.py +++ b/homeassistant/components/modern_forms/switch.py @@ -39,21 +39,20 @@ class ModernFormsSwitch(ModernFormsDeviceEntity, SwitchEntity): *, 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 @@ class ModernFormsAwaySwitch(ModernFormsSwitch): 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 @@ class ModernFormsAwaySwitch(ModernFormsSwitch): 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 @@ class ModernFormsAdaptiveLearningSwitch(ModernFormsSwitch): 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 538bd3a741d..6b3caf23a1c 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 be1cf61ae09..f1212c75f25 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 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( ), 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 e5346e00081..a66c37fabb5 100644 --- a/homeassistant/components/wiz/sensor.py +++ b/homeassistant/components/wiz/sensor.py @@ -23,7 +23,6 @@ from .models import WizData 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 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( 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 656219f13bb..b75e199fe33 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 a1eb6ded51d..522eb5c7cba 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 5a7407836f2..b19c592a5cf 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 @@ INTELLIFIRE_BINARY_SENSORS: tuple[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 @@ INTELLIFIRE_BINARY_SENSORS: tuple[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 debc8237fc8..3911efeb5b9 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 383d61b8d41..05994919296 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 efa567d55cb..5da3c3cdbf8 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 @@ class IntellifireFlameControlEntity(IntellifireEntity, NumberEntity): 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 e888ea1bbcf..bc42b977f12 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 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = ( ), 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 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = ( ), 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 a8c8d76a601..6393a4e070d 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 98abaa38849..1af4d8c0e91 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 5ed9bc3efdd..e562add4e0f 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_HOST = "admin.vilfo.com" 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 b6339cea0d6..7bdba371f49 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 248212a8345..a4aeeb3151b 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 7991525c2a0..d5caf36fa18 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 219dd304dbd..f9d4097fe40 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 619f72ae47f..397d2085357 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 6a82d75a9f8..15f6e153b19 100644 --- a/tests/components/uptimerobot/common.py +++ b/tests/components/uptimerobot/common.py @@ -66,7 +66,7 @@ STATE_UP = "up" 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 147eddacf81..1a4feb59376 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -16,7 +16,8 @@ from homeassistant.helpers import device_registry as dr 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 4a105e5a067..e7c0b7f2a5e 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 @@ PLATFORMS = [Platform.SENSOR] 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 44ffbcdb497..f42c8d94ea2 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import ( 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.helpers.update_coordinator import ( ) 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 @@ INVERTER_SENSORS = ( ), ) +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 20cb12179ee..30dae28c408 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -14,6 +14,7 @@ from homeassistant.components.weather import ( 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 @@ class AccuWeatherEntity( """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 @@ class AccuWeatherEntity( 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 dd5dca8c069..b9e66d51874 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -22,6 +22,7 @@ from homeassistant.components.weather import ( 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 b177a4c524f..269695a668d 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 @@ from .const import ( ) _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 e06324f93ec..8c78bbfb58d 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 aba97c8ea8c..b99b621c614 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -9,6 +9,22 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ { "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 0e57373625a..342fe34b97d 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 50e8f89716b..eda8c20b10e 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -55,9 +55,9 @@ KM_PER_MI = DistanceConverter.convert(1, UnitOfLength.MILES, UnitOfLength.KILOME 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 @@ SAFETY_SENSORS = [ 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_2_SENSORS = [ 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 @@ API_GEN_3_SENSORS = [ 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 8474d391141..5e6db32d4ad 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 da26e4f293e..d2b6ab7af15 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -142,7 +142,7 @@ SCENE_ACTIVATION_VALUE_NOTIFICATION_SCHEMA = ( # 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 fd091b2bfe7..8551427cf3e 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 a75cb7ce298..bb894753fb8 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -95,6 +95,8 @@ class ToloSaunaUpdateCoordinator(DataUpdateCoordinator[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 0ee1cb08bb2..124cd45d78b 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 5d041a74104..3b81477ab37 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 849a9f5b3ed..74f2a5a6f55 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 e767be9a3ce..7065290f2a8 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 715a1327e4b..4b76d4270c6 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 aa12198b52c..3e07392c336 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 @@ NUMBERS = ( ), 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 @@ NUMBERS = ( ), 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 a47207d3d98..8e4ecb47f48 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 2c5eccc1c1d..2ff901939ae 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 @@ SENSORS = ( ), 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 @@ SENSORS = ( ), 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 5e6647edae4..f48e26c5276 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 5e12f67a698..87c56869d0c 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -9,19 +9,16 @@ import voluptuous as vol 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 @@ from .util import valid_publish_topic 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 @@ class MqttScene( 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 @@ class MqttScene( 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 dfea7b3f915..141bfc526d3 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 @@ from .test_common import ( 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 6b4c35c4a37..1b8a37e48f4 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 9528ae568fd..86937f3eee6 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 8f16fbeb22c..0d32d476340 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 3ff331e2a00..b37a365ae2d 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 ec437f08d39..54dea780dab 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -215,7 +215,7 @@ MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All( 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 90aa499af4b..8d0ee78eca7 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -25,6 +25,7 @@ from uuid import UUID 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 @@ from . import script_variables as script_variables_helper, template as template_ 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 5ea6df42349..b5c8cc1716e 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 08407e46c1c..4561e8a53e1 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 5397fed5e1d..40ff869c43b 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 60da3df393e..b3b9e1a98ef 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 117de2116a4..77c2a28190b 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations 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 a124362251a..741d2b580e4 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 f919834139b..14e8900f000 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations 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 e42ee4478e0..0ad2942fb04 100644 --- a/homeassistant/components/thomson/device_tracker.py +++ b/homeassistant/components/thomson/device_tracker.py @@ -3,7 +3,7 @@ from __future__ import annotations 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 94fa194fa09..85f0f4eee15 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -11,10 +11,10 @@ MINOR_VERSION: Final = 8 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 c8f4bc835ce..aa0aceb7365 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 4c2d803a549..66568cf5400 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 6575d2f8fb3..1df65353855 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 f3d0defac4d..8258543df1d 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -144,10 +144,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 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 86937f3eee6..f859919fe07 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 0d32d476340..e31b5a32d70 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 b37a365ae2d..00f14b65a13 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 178859ecbe7..33cafe3b1dd 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 c7ab0d08693..ddcbab7dfc0 100644 --- a/homeassistant/backports/functools.py +++ b/homeassistant/backports/functools.py @@ -3,9 +3,7 @@ from __future__ import annotations 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 6f3d48fc1bb..f946f29bdaa 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 3203964fdc1..bf7c5d9c969 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 48bb2fd1726..a98a3fa6c3f 100644 --- a/homeassistant/components/event/__init__.py +++ b/homeassistant/components/event/__init__.py @@ -4,9 +4,7 @@ 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 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 0bbd89aab86..8bd47faef08 100644 --- a/homeassistant/components/hassio/issues.py +++ b/homeassistant/components/hassio/issues.py @@ -4,9 +4,7 @@ from __future__ import annotations 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 33cb4b9e576..a074b3b9b65 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 8a1f0785435..c04b18b0c25 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 8762769194f..81882137fad 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -3,9 +3,8 @@ from __future__ import annotations 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 061b388ace5..197a35246d2 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -3,8 +3,8 @@ from __future__ import annotations 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 2c5a1c87f29..e1354cb26a5 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 efd58e38e72..096e7cbb105 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 af4248e5e3b..e4ae3cde883 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -4,9 +4,8 @@ from __future__ import annotations 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 0c3a711a738..0f49ab605a7 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -8,9 +8,8 @@ from datetime import timedelta 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 f9c68e2b1f0..36512620e51 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -12,14 +12,13 @@ import hashlib 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 32b095230d9..16bfc93f715 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -6,10 +6,9 @@ from datetime import timedelta 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 a5068e1a47b..7edb11797eb 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -8,10 +8,10 @@ from queue import Queue 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 24c44b901a1..aa3566c5a95 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -8,9 +8,8 @@ from datetime import timedelta 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 0743864aaf7..c99aadb8caa 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -5,7 +5,7 @@ from collections.abc import Callable 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.engine.interfaces import Dialect 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 e8303c12c10..4d76c803da6 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -11,9 +11,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 +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 caa3d974d04..c237a820e58 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -8,11 +8,10 @@ import datetime 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 923b6167851..61df78307f0 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -5,9 +5,8 @@ from dataclasses import dataclass 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 3752f9c9cb5..228e2071b4a 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -4,8 +4,8 @@ from __future__ import annotations 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 2658f50edad..998e5a55e63 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -5,10 +5,9 @@ import base64 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 f52b78b5a52..db03a1ccf2e 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -5,10 +5,9 @@ from dataclasses import dataclass 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 c63db816711..89bd601fdae 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -7,9 +7,7 @@ from dataclasses import dataclass 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 dc4283b4a76..7c6bbd2d2ee 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -7,12 +7,12 @@ import contextlib 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 010c0f63e27..b3b6e7f0483 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -4,9 +4,8 @@ from __future__ import annotations 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 51ab65e3318..1455173b27c 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -8,9 +8,8 @@ from enum import Enum 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 97258a77e2b..43f487f61d4 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -5,9 +5,7 @@ import asyncio 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 29d6cafe3c8..807a5e73d00 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -3,9 +3,8 @@ from __future__ import annotations 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 27b71484f3e..e6f2f6ab482 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -4,9 +4,8 @@ from __future__ import annotations 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 d13dd871865..49ba46038f9 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -5,9 +5,8 @@ import enum 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 451d96a122b..f975cc5116d 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -3,9 +3,8 @@ from __future__ import annotations 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 35d835c8f16..8d04987d4fa 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -4,9 +4,8 @@ from __future__ import annotations 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 a1c09b8815f..6fa80406e61 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -11,9 +11,7 @@ import functools 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 6ea03e85c43..8bb30f5d57d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -31,6 +31,7 @@ from typing import ( Any, Generic, ParamSpec, + Self, TypeVar, cast, overload, @@ -38,7 +39,6 @@ from typing import ( 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 6f125ce359a..c0a5860529e 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -7,9 +7,8 @@ import copy 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 ba69a76fbdd..a580c013cd0 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -5,9 +5,8 @@ from collections import OrderedDict 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 93b199b1db5..ed02f8a710e 100644 --- a/homeassistant/helpers/httpx_client.py +++ b/homeassistant/helpers/httpx_client.py @@ -3,10 +3,9 @@ from __future__ import annotations 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 ab3b93cf3c4..4dd71a584ec 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -4,9 +4,7 @@ from __future__ import annotations 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 c7087918cf0..b97f781eaf3 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -4,10 +4,9 @@ from __future__ import annotations 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 9d9f5d986a0..6c1de55748f 100644 --- a/homeassistant/util/timeout.py +++ b/homeassistant/util/timeout.py @@ -8,9 +8,7 @@ from __future__ import annotations 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 40417752719..55bee20df56 100644 --- a/tests/components/recorder/db_schema_30.py +++ b/tests/components/recorder/db_schema_30.py @@ -9,7 +9,7 @@ from collections.abc import Callable 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 import ( 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 03a71697227..660a2a54d4b 100644 --- a/tests/components/recorder/db_schema_32.py +++ b/tests/components/recorder/db_schema_32.py @@ -9,7 +9,7 @@ from collections.abc import Callable 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 import ( 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 21f6bd7a549..acd520cebaa 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 @@ class MyStromBulbMock(MyStromDeviceMock): 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 5c371a0e2ee..bf35484f53e 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 @@ class TwitchMock: 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 @@ class TwitchMock: 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 e20e28b3d0a..5972f9809c0 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 8258543df1d..f215b649bb2 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -11,14 +11,11 @@ import re 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 88a433fe3fa..091c1b88e30 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 a3a5b7246b6..f37e120db68 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 @@ class DeviceFlow: # 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 1b173e3a377..da43ae973cd 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -449,7 +449,7 @@ class DataManager: 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 fc20ecd406c..d279fe981d4 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 dcb1608b710..e3fb26ffe22 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 19c85be0d9d..bfe94bbf304 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 253d44ee9ee..27f67dad322 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 4769da29019..02a11b3fe7a 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 e9f9458611a..940d0ff6c55 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 f21f3a1b26f..a9e286907d5 100644 --- a/tests/components/metoffice/test_init.py +++ b/tests/components/metoffice/test_init.py @@ -15,9 +15,7 @@ from .const import DOMAIN, METOFFICE_CONFIG_WAVERTREE, TEST_COORDINATES_WAVERTRE 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 28bf8eda997..6e40dd66efe 100644 --- a/tests/components/metoffice/test_sensor.py +++ b/tests/components/metoffice/test_sensor.py @@ -24,9 +24,7 @@ from .const import ( 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 0e5a934c7d0..673475c0303 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -23,9 +23,7 @@ from .const import ( 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 85d4b16048a..673ac1a72d4 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 a164cfbeb78..18b33a6ef0c 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 2c76c947350..32d4fabb02b 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 @@ async def test_statistic_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)) async def test_statistic_during_period_hole( recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -797,9 +793,7 @@ async def test_statistic_during_period_hole( } -@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 1288c0ae177..6307125930c 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 1b8a37e48f4..40fd1c2eef0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -111,7 +111,7 @@ asyncio.set_event_loop_policy = lambda policy: None 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 1780cd053f5..78aad44fa66 100644 --- a/homeassistant/helpers/aiohttp_compat.py +++ b/homeassistant/helpers/aiohttp_compat.py @@ -12,7 +12,7 @@ class CancelOnDisconnectRequestHandler(web_protocol.RequestHandler): 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 58136005053..c0e85f3dc58 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 052edf09bec..8b5dd91f64c 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -629,6 +629,11 @@ DHCP: list[dict[str, str | bool]] = [ "hostname": "hs*", "macaddress": "9C5322*", }, + { + "domain": "tplink", + "hostname": "k[lps]*", + "macaddress": "9C5322*", + }, { "domain": "tplink", "hostname": "hs*", @@ -686,94 +691,99 @@ DHCP: list[dict[str, str | bool]] = [ }, { "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 1c6acc516b8..950dc2a591a 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 0fb043c4cc4..16fb746049d 100644 --- a/homeassistant/components/nanoleaf/entity.py +++ b/homeassistant/components/nanoleaf/entity.py @@ -14,10 +14,12 @@ from .const import DOMAIN 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 20992594cb8..f0425594763 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 @@ class NanoleafLight(NanoleafEntity, LightEntity): """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 4f3e82042f6..db191405522 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 fb13e86dd1d..4a36535cc9b 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 2cfbc537dbb..6b0a4cd6b26 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 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): 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 2d147d243f2..b7870e9cca0 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 @@ class RuntimeEntryData: 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 @@ class RuntimeEntryData: 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 @@ class RuntimeEntryData: 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 @@ class RuntimeEntryData: 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 4741eaaa6fb..345be0c4b6d 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 @@ class ESPHomeManager: ) ) - 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 @@ class ESPHomeManager: @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 e809089da11..f373c2fdb17 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 e268d065e21..e55d4583275 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 6c034e674ee..83661a58280 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 4d76c803da6..cbdaa24ec83 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -602,14 +602,11 @@ class SensorEntity(Entity): 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 acbf5089f96..8eda93c2d46 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations 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 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): 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 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): 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 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): 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 bcd402e6a63..1ad29c72c26 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -126,7 +126,8 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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 8cb34be927f..e147055df05 100644 --- a/homeassistant/components/dlna_dms/config_flow.py +++ b/homeassistant/components/dlna_dms/config_flow.py @@ -67,7 +67,8 @@ class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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 842fe4298cf..e0342c5f0d4 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 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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 d0c51e39987..1211d4dfa46 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 c3251cd31a2..43e60638ba9 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations 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 1d6ac0eaf80..c8c2998458f 100644 --- a/tests/components/dlna_dms/test_config_flow.py +++ b/tests/components/dlna_dms/test_config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations 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 00fc77076e8..6133a382855 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 952e6c606c2..de48e8fae20 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -139,11 +139,11 @@ class SpotifyMediaPlayer(MediaPlayerEntity): @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 234795e9014..c171c95e659 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 34fc5713271..ec94d4c227c 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -40,11 +40,12 @@ from homeassistant.const import ( 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 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): 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 e7668bdc3ff..54a9c5c0796 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -34,8 +34,8 @@ from homeassistant.const import ( 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 683a5dab9dd..e2b327c6823 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 b99b621c614..7b0aa78d69e 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -525,6 +525,11 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "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 e31b5a32d70..0429a807259 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 00f14b65a13..e07523e1892 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 879ab4f7bc4..197745b70f1 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -105,6 +105,22 @@ HHCCJCY10_SERVICE_INFO = BluetoothServiceInfoBleak( 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 @@ MISCALE_V2_SERVICE_INFO = BluetoothServiceInfoBleak( 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 40d89a8214d..fff8d9b20f1 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -6,6 +6,7 @@ from homeassistant.core import HomeAssistant 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 ab3c8ddea0b..59580d5725e 100644 --- a/homeassistant/components/ld2410_ble/binary_sensor.py +++ b/homeassistant/components/ld2410_ble/binary_sensor.py @@ -21,14 +21,10 @@ ENTITY_DESCRIPTIONS = ( 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 6d0e8e4feb9..806832e9fca 100644 --- a/homeassistant/components/ld2410_ble/sensor.py +++ b/homeassistant/components/ld2410_ble/sensor.py @@ -21,84 +21,76 @@ from .models import LD2410BLEData 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 @@ MOTION_ENERGY_GATES = [ 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 e2be7e6deff..7e919675426 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 185482d62e3..b86c013f104 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 24d62d76026..c5222112c02 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 @@ OVERLAY_ENTITY_DESCRIPTION = 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 2b8bc4060d6..36a2ab671c9 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 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): 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 c825bafc4b9..5e3065bfb53 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 @@ class TadoHomeEntity(Entity): 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 7742f6b0dca..f7ba1682e18 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 @@ HOME_SENSORS = [ ), 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 @@ HOME_SENSORS = [ ), 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 @@ HOME_SENSORS = [ 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 @@ TEMPERATURE_ENTITY_DESCRIPTION = 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 @@ HUMIDITY_ENTITY_DESCRIPTION = 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 @@ HEATING_ENTITY_DESCRIPTION = 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 70ff38b10be..9858b7aa51b 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 f7a1dcd0966..6d17c85c981 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 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): ) 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 9226543abef..1e2f53efeb5 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 405eb86e6ec..9ec6447b32c 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -25,7 +25,7 @@ from homeassistant.const import ( ) 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 async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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 a5360090bb9..cc0f37ea145 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -60,6 +60,7 @@ ABBREVIATIONS = { "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 ef2c771218a..cd4470ef22d 100644 --- a/homeassistant/components/mqtt/config_integration.py +++ b/homeassistant/components/mqtt/config_integration.py @@ -22,6 +22,7 @@ from . import ( 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 @@ CONFIG_SCHEMA_BASE = vol.Schema( 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 d09a2bb8cb6..fb1989069af 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -112,6 +112,7 @@ PLATFORMS = [ Platform.CAMERA, Platform.CLIMATE, Platform.DEVICE_TRACKER, + Platform.EVENT, Platform.COVER, Platform.FAN, Platform.HUMIDIFIER, @@ -138,6 +139,7 @@ RELOADABLE_PLATFORMS = [ 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 70e5ac9e535..8e563a48cdd 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -52,6 +52,7 @@ SUPPORTED_COMPONENTS = [ "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 00000000000..5a94ec754c0 --- /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 00000000000..bc7b8b43523 --- /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 c0e85f3dc58..c33106d13cc 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 0429a807259..729cd3b478a 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 e07523e1892..0620b82e14b 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 c593c4fa419..53e2c3c9fe5 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 @@ class AxisCamera(AxisEntity, MjpegCamera): 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 f53e69fba9f..8f3c8b9a8b6 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -62,6 +62,8 @@ class AxisNetworkDevice: 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 277f24513de..20dfedd717b 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 df5d071ddbe..a76aa40ebc8 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 849581b6f9f..461139a15ea 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 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = { 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 e954a55b280..2d72cdbf203 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 e5ed8613fc4..4301bb7d5a0 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 2c6883e4a71..13c9293daa7 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 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { 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 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = { 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 7d6c57de296..b12cdb570eb 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -57,6 +57,7 @@ CONF_IS_NITROGEN_DIOXIDE = "is_nitrogen_dioxide" 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 @@ ENTITY_CONDITIONS = { 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 @@ CONDITION_SCHEMA = vol.All( 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 1bb41eb2d30..1c0da89692b 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -56,6 +56,7 @@ CONF_NITROGEN_DIOXIDE = "nitrogen_dioxide" 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 @@ ENTITY_TRIGGERS = { 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 @@ TRIGGER_SCHEMA = vol.All( 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 c4c1f81109d..1db5e4c8cfd 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 74c165e9d20..9ac8bd22027 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 b5d425029d0..c5406a85fc0 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 287229c9fd1..e16ab3d91ae 100644 --- a/homeassistant/components/roborock/const.py +++ b/homeassistant/components/roborock/const.py @@ -6,4 +6,10 @@ CONF_ENTRY_CODE = "code" 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 90ca13c5146..86d578d852a 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -31,7 +31,7 @@ class RoborockEntity(Entity): @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 @@ class RoborockEntity(Entity): 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 @@ class RoborockCoordinatedEntity( 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 00000000000..4eaf1464f89 --- /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 3b3e6221895..3989f08505b 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 00000000000..b660bfc2969 --- /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 8b8548ad1f9..c7cada9ba0a 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 mock_setup_integration( @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 ce28638c3f3..07e96afaced 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 9817951b094..c8a355a0f7c 100644 --- a/homeassistant/components/renson/sensor.py +++ b/homeassistant/components/renson/sensor.py @@ -50,6 +50,16 @@ from . import RensonCoordinator, RensonData 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 @@ SENSORS: tuple[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 @@ SENSORS: tuple[RensonSensorEntityDescription, ...] = ( ), RensonSensorEntityDescription( key="CO2_FIELD", - name="CO2 quality", field=CO2_FIELD, raw_format=True, state_class=SensorStateClass.MEASUREMENT, @@ -94,7 +103,7 @@ SENSORS: tuple[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 @@ SENSORS: tuple[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 @@ SENSORS: tuple[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 @@ SENSORS: tuple[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 @@ SENSORS: tuple[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 @@ SENSORS: tuple[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 @@ SENSORS: tuple[RensonSensorEntityDescription, ...] = ( ), RensonSensorEntityDescription( key="HUMIDITY_FIELD", - name="Relative humidity", field=HUMIDITY_FIELD, raw_format=False, device_class=SensorDeviceClass.HUMIDITY, @@ -162,15 +170,15 @@ SENSORS: tuple[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 @@ SENSORS: tuple[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 @@ SENSORS: tuple[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 @@ SENSORS: tuple[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 @@ SENSORS: tuple[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 @@ class RensonSensor(RensonEntity, SensorEntity): 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 16c5de158a9..06636c9d503 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 6ed071b8b9e..9a805b43c4f 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -14,6 +14,7 @@ from homeassistant.components import ( camera, climate, cover, + event, fan, group, humidifier, @@ -527,6 +528,26 @@ class CoverCapabilities(AlexaEntity): 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 ebab3bcee8c..04bb561560f 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, cast 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_enable_proactive_mode(hass, smart_home_config): 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 a2dcdedd470..477e7884e4f 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 030b0aa322e..0ab8962077b 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 cd39609d9d5..a3d7709a5d5 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 @@ class UpnpEntity(CoordinatorEntity[UpnpDataUpdateCoordinator]): 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 6f0fe340f30..46d748f6939 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 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( ), 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 ea052f0b45a..7ce1798c351 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 f29d7ac9276..7dfbb144b01 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 de48e8fae20..41d27b68672 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -12,7 +12,9 @@ from spotipy import SpotifyException from yarl import URL from homeassistant.components.media_player import ( + ATTR_MEDIA_ENQUEUE, BrowseMedia, + MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -336,6 +338,10 @@ class SpotifyMediaPlayer(MediaPlayerEntity): """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 @@ class SpotifyMediaPlayer(MediaPlayerEntity): ): 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 224d98d92bf..689f984722d 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 729cd3b478a..af9ecf757f5 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 0620b82e14b..e87fbf33bd7 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 c3cd21e6b2d..b644c300979 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -298,7 +298,8 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | 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 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): async def _async_wait_push_loop(self) -> None: """Wait for data push from server.""" - cleanup = False while True: try: number_of_messages = await self._async_fetch_number_of_messages() @@ -412,9 +412,6 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): 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 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): ) await self._cleanup() await asyncio.sleep(BACKOFF_TIME) - finally: - if cleanup: - await self._cleanup() async def shutdown(self, *_: Any) -> None: """Close resources.""" 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 461139a15ea..3d7dba15b0e 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 13c9293daa7..6e4f355f852 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 3e9dfaad923..c4fc4b66f37 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -27,6 +27,7 @@ from homeassistant.const import ( UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, + UnitOfFrequency, UnitOfPower, UnitOfTemperature, ) @@ -496,6 +497,16 @@ RPC_SENSORS: Final = { 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 @@ RPC_SENSORS: Final = { 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 6946b21761c..38ae94d4fa3 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 debf1f4ea0d..791ae9ee7c4 100644 --- a/homeassistant/components/vulcan/calendar.py +++ b/homeassistant/components/vulcan/calendar.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import date, datetime, timedelta import logging +from zoneinfo import ZoneInfo from aiohttp import ClientConnectorError from vulcan import UnauthorizedCertificateException @@ -107,8 +108,12 @@ class VulcanCalendarEntity(CalendarEntity): 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 @@ class VulcanCalendarEntity(CalendarEntity): ), ) 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 06baef1bd0e..43411e9ec0d 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -33,6 +33,7 @@ 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, TrackTemplate, TrackTemplateResult, TrackTemplateResultInfo, @@ -41,7 +42,7 @@ from homeassistant.helpers.event import ( ) 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 @@ class BayesianBinarySensor(BinarySensorEntity): """ @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 703ad671799..4111777375d 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 @@ from .const import ( @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 b7254c5c347..004a71fa810 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -10,7 +10,7 @@ import functools as ft 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 .entity_registry import EVENT_ENTITY_REGISTRY_UPDATED 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 async_track_state_change( 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 @@ track_state_change = threaded_listener_factory(async_track_state_change) 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 5a76fd262a8..9e3f9de34fa 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 @@ UNDEFINED = UndefinedType._singleton # pylint: disable=protected-access # 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 9db1f2ae2e7..d1cae2b0fad 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 ea3aacf43a4..4e6d44a8868 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 69e115470ad..91984cf6247 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -694,7 +694,7 @@ class BluesoundPlayer(MediaPlayerEntity): 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 d56b2b0ddfa..c85f0d2bff1 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 76385167d38..0597145c2da 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 97f0a968cff..7313be1920c 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 7a6e7c18e13..199cbacfa15 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -161,10 +161,10 @@ class HomematicipGenericEntity(Entity): 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 b936497cfc6..2876a4d49a1 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 01c459acaea..7fab99a6f39 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -200,9 +200,7 @@ class NetatmoCamera(NetatmoBase, Camera): 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 10d005d1043..39d41369a4b 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 294931b7538..f1c515d37f7 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -109,10 +109,7 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): @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 33d8c7b5e67..8fe1d0482e9 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 fbbfdef6f02..4b4878fa1c8 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -111,9 +111,7 @@ class ThermoworksSmokeSensor(SensorEntity): 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 abb8aa186c9..c85bc9fd473 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -35,7 +35,7 @@ from .models import async_wemo_data _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 28b51fcb447..e91cbe1ff62 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 d3b8a145df7..f51d469bde7 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 767b0bca742..9326869b272 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 c659ca5c585..4277f01037f 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 0eaadae4931..de284bb8c16 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 4ccf7323148..bba5ee124ba 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -3013,11 +3013,6 @@ DEVICES = [ 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 a181e9d1548..882fa31f121 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 322cd1e4d2b..350ea692338 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 227a343a7a4..9aabc3cea5e 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 9d00282d8da..d32e71c71c0 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 caae5801b21..61b6b05d9d6 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 8112726607e..4b4f0358bb9 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 cf1bafe24fb..2f66bf836ea 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 aa0aceb7365..5b8853bafb4 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 af9ecf757f5..ba451aca05c 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 e87fbf33bd7..77ed4016645 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 453e259bf46..c912c3419d7 100644 --- a/homeassistant/components/flume/binary_sensor.py +++ b/homeassistant/components/flume/binary_sensor.py @@ -25,6 +25,7 @@ from .const import ( KEY_DEVICE_TYPE, NOTIFICATION_HIGH_FLOW, NOTIFICATION_LEAK_DETECTED, + NOTIFICATION_LOW_BATTERY, ) from .coordinator import ( FlumeDeviceConnectionUpdateCoordinator, @@ -67,6 +68,12 @@ FLUME_BINARY_NOTIFICATION_SENSORS: tuple[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 1889cca8fa5..9e932cce4dd 100644 --- a/homeassistant/components/flume/const.py +++ b/homeassistant/components/flume/const.py @@ -47,3 +47,4 @@ NOTIFICATION_BRIDGE_DISCONNECT = "Bridge Disconnection" 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 | 41 ++++++------------- .../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, 84 insertions(+), 151 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 33cafe3b1dd..871244b4567 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -1,32 +1,15 @@ -"""Enum backports from standard lib.""" +"""Enum backports from standard lib. + +This file contained the backport of the StrEnum of Python 3.11. + +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 -from enum import Enum -from typing import Any, Self +from enum import StrEnum - -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): - 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 - - @staticmethod - def _generate_next_value_( - name: str, start: int, count: int, last_values: list[Any] - ) -> Any: - """Make `auto()` explicitly unsupported. - - 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 e6e628f834d..f14a1ce66e0 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 891fc639fee..1be9ddbb14f 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -4,12 +4,12 @@ from __future__ import annotations 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 a737490f22f..cb19811d650 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 1c2d6d779fb..79e20c6f571 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -3,12 +3,12 @@ from __future__ import annotations 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 5925a97422a..34b621802f9 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 0e2790a2e85..901acdcdec1 100644 --- a/homeassistant/components/button/__init__.py +++ b/homeassistant/components/button/__init__.py @@ -3,12 +3,12 @@ from __future__ import annotations 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 ab5832e48ab..f745f60b51a 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 41d4646aeae..23c76c151d7 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 a3965552b16..354b972e2b7 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -4,14 +4,13 @@ from __future__ import annotations 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 ad68472d9b0..3a0b0afd7c9 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 0d07abde2bd..20f97be1eb1 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 8fc55830c63..6352d98da3c 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -4,6 +4,7 @@ from __future__ import annotations 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.exceptions import UpnpActionError, UpnpConnectionError, U 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 a98a3fa6c3f..98dd6036bc9 100644 --- a/homeassistant/components/event/__init__.py +++ b/homeassistant/components/event/__init__.py @@ -3,10 +3,10 @@ from __future__ import annotations 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 1ce21081f9c..16015ec5837 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 @@ from fritzconnection.core.exceptions import ( 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 f7ce25c2ebe..5065aa65b4d 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 @@ from requests.exceptions import ConnectionError as RequestsConnectionError 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 4f224660ae9..75050374e52 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 ed2be40f30f..adf6bd3a35a 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations 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 typing import Any, cast 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 2bc314f169a..0735f2645cc 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 79effa6f0c2..a525c626f14 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -3,12 +3,12 @@ from __future__ import annotations 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 35601cf2b1f..09c0714cbeb 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 733a1344538..7640925451a 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -4,12 +4,12 @@ from __future__ import annotations 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 0f49ab605a7..f7f0150bdd2 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -5,14 +5,13 @@ from collections.abc import Iterable 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 dcd4348a561..49996408a1d 100644 --- a/homeassistant/components/logger/helpers.py +++ b/homeassistant/components/logger/helpers.py @@ -5,10 +5,10 @@ from collections import defaultdict 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 36512620e51..39b67477f97 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -7,6 +7,7 @@ from collections.abc import Callable 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 async_timeout 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 9ad7b983c7f..2c609750153 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 fb11400a312..5a966a4455c 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -7,12 +7,12 @@ from collections import deque 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 3d7dba15b0e..9248d3f9e57 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 581720c2730..c9e8e3703db 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -3,12 +3,12 @@ from __future__ import annotations 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 288c184984d..38e45457462 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 @@ from aioqsw.const import ( 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 d4131fdb022..61ef1be500a 100644 --- a/homeassistant/components/rainmachine/util.py +++ b/homeassistant/components/rainmachine/util.py @@ -4,9 +4,9 @@ from __future__ import annotations 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 ec5c5c984b5..fc7683db901 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 6e4f355f852..139725ee1ab 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 608798976ba..cc82f0ad700 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 e6a1f16eede..162369fd27d 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 typing import Any 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 cdd5ac2a567..1a125da6a6b 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 6eb2a275e18..bf3c3424142 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -3,11 +3,11 @@ from __future__ import annotations 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 17e95486298..0f7d1407fc5 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 f07a672afbd..4182b177bf6 100644 --- a/homeassistant/components/text/__init__.py +++ b/homeassistant/components/text/__init__.py @@ -3,13 +3,13 @@ from __future__ import annotations 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 c2c9c207c02..cd92e62b864 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 20dc724deb9..acf9f8bbd2c 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -3,11 +3,11 @@ from __future__ import annotations 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 13ab6d38e8a..b9d01629536 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations 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 typing import Any, Final, final 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 a6f92541a10..9bab8232dab 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 da43ae973cd..9282e3977c1 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -6,7 +6,7 @@ from collections.abc import Callable 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 @@ from withings_api.common import ( 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 1193b6f612a..02d8977c604 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 947e5157a8a..9569ba97167 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -3,6 +3,7 @@ from __future__ import annotations 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 @@ from zwave_js_server.model.value import ( 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 84d49ff7b9d..1ec4f8d1601 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 6fa80406e61..15fcb9a50de 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -6,7 +6,7 @@ from collections import ChainMap 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 types import MappingProxyType 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 85f0f4eee15..513d72555a5 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 8bb30f5d57d..3673f9acba5 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -43,7 +43,6 @@ import voluptuous as vol 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 @@ BLOCK_LOG_TIMEOUT = 60 ServiceResponse = JsonObjectType | None -class ConfigSource(StrEnum): +class ConfigSource(enum.StrEnum): """Source of core configuration.""" DEFAULT = "default" @@ -1669,7 +1668,7 @@ class StateMachine: ) -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 c0a5860529e..e0408a24b2e 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -5,13 +5,13 @@ import abc 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 8d0ee78eca7..122fd752a84 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -9,7 +9,7 @@ from datetime import ( 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 @@ from uuid import UUID 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 a59313ed886..c65e87a2119 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -3,6 +3,7 @@ from __future__ import annotations 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 @@ 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 diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index cabac2617c2..5fc4609d812 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -12,6 +12,7 @@ from __future__ import annotations 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 @@ from typing import TYPE_CHECKING, Any, TypeVar, cast 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 afe2d98ed0b..9bd6ebffadb 100644 --- a/homeassistant/helpers/issue_registry.py +++ b/homeassistant/helpers/issue_registry.py @@ -3,12 +3,12 @@ from __future__ import annotations 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 b97f781eaf3..8ec8d5eac3e 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 664d6f15650..84585d7a8c7 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 be44c4256ce..8b3aea61ff4 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 3f701810a5d..00000000000 --- 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 06b876eac8d..00000000000 --- 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 52b81bb3836..a9ee5242592 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 3d3077a1c6a..674dea752a0 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 5cb9e594cb2..3d2d95fd26f 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 61e8471b9d8..e975960bbe0 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 5c2663d251c..b4396f617cd 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -350,6 +350,11 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { 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 c65e87a2119..45a4459b5d3 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -6,10 +6,11 @@ from collections.abc import Coroutine, ValuesView 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 @@ DEVICE_INFO_TYPES = { 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 5fc4609d812..248db9d5180 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -15,9 +15,10 @@ from datetime import datetime, timedelta 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 004a71fa810..830b6100111 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 functools as ft 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.loader import bind_hass 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 @@ _LOGGER = logging.getLogger(__name__) 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 5a85b4a4c38..9eca5e772b0 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -25,7 +25,6 @@ from homeassistant.const import ( ) 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 @@ PARALLEL_UPDATES = 1 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 @@ class AccuWeatherSensor( 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 5972f9809c0..bf71ed4d255 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 2f66bf836ea..7f5a67f4220 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 ba451aca05c..8b81bb84103 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 77ed4016645..ba9cd91180f 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 4bdabdf9c96..33df9822ac2 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -28,7 +28,6 @@ from homeassistant.const import ( ) from homeassistant.core import ( CALLBACK_TYPE, - Event, HomeAssistant, ServiceCall, State, @@ -38,13 +37,16 @@ from homeassistant.core import ( 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 @@ class Group(Entity): """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 @@ class Group(Entity): 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 112b111bdca..7415ee8c60d 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -21,11 +21,14 @@ from homeassistant.const import ( 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 @@ class BinarySensorGroup(GroupEntity, BinarySensorEntity): """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 38928302eb1..784ac9a94af 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -38,11 +38,14 @@ from homeassistant.const import ( 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 @@ class CoverGroup(GroupEntity, CoverEntity): 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 0c4c59d2454..1fcb859f926 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -35,11 +35,14 @@ from homeassistant.const import ( 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 @@ class FanGroup(GroupEntity, FanEntity): 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 33d240a9a4d..e0f7974631b 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -44,11 +44,14 @@ from homeassistant.const import ( 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 @@ class LightGroup(GroupEntity, LightEntity): """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 07d08c7851d..233d1155c53 100644 --- a/homeassistant/components/group/lock.py +++ b/homeassistant/components/group/lock.py @@ -28,11 +28,14 @@ from homeassistant.const import ( 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 @@ class LockGroup(GroupEntity, LockEntity): """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 ad375630bea..f0d076ec130 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -44,11 +44,14 @@ from homeassistant.const import ( 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 @@ class MediaPlayerGroup(MediaPlayerEntity): } @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 4c6e8dccc1e..d62447d9947 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -33,11 +33,19 @@ from homeassistant.const import ( 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 @@ class SensorGroup(GroupEntity, SensorEntity): """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 4b6b959ba17..f62c805ba1d 100644 --- a/homeassistant/components/group/switch.py +++ b/homeassistant/components/group/switch.py @@ -19,11 +19,14 @@ from homeassistant.const import ( 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 @@ class SwitchGroup(GroupEntity, SwitchEntity): """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 93a5d272965..24ec07b6a87 100644 --- a/homeassistant/components/history/websocket_api.py +++ b/homeassistant/components/history/websocket_api.py @@ -30,10 +30,12 @@ from homeassistant.core import ( 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 3a1ec971b54..c2ea9823535 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -23,7 +23,11 @@ from homeassistant.core import ( 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 async_subscribe_events( 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 fd2a5afff1c..ffd345cea3b 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -15,12 +15,15 @@ from homeassistant.const import ( 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 @@ class LightSwitch(LightEntity): """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 54a2784467d..9412c612ca2 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -14,10 +14,8 @@ from homeassistant.const import ( ) from homeassistant.core import ( CALLBACK_TYPE, - Event, HassJob, HomeAssistant, - State, callback, ) from homeassistant.helpers import ( @@ -26,9 +24,12 @@ from homeassistant.helpers import ( 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 3740a6b177a..434957dc131 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -21,6 +21,7 @@ from homeassistant.exceptions import TemplateError 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 @@ from homeassistant.helpers.event import ( 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 f779f5d8e94..654d0bce13b 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -110,6 +110,13 @@ UNAUTHORIZED_USER = [ {"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 26689582fae..a1f7504762a 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -25,7 +25,6 @@ from homeassistant.core import HomeAssistant, callback 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 @@ class DemoSensor(SensorEntity): 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 @@ class DemoSumSensor(RestoreSensor): 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 830b6100111..12cf58eaa2b 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -10,12 +10,11 @@ import functools as ft 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.const import ( ) 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 @@ class _TrackStateChangeFiltered: 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 @@ class _TrackStateChangeFiltered: ) @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 @@ class _TrackStateChangeFiltered: @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 @@ class _TrackStateChangeFiltered: 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 async_track_template( 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 @@ class TrackTemplateResultInfo: 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 @@ class TrackTemplateResultInfo: 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 @@ class TrackTemplateResultInfo: @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 @@ class TrackTemplateResultInfo: TrackTemplateResultListener = Callable[ [ - Event | None, + EventType[EventStateChangedData] | None, list[TrackTemplateResult], ], None, @@ -1315,11 +1316,11 @@ def async_track_same_state( 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 async_track_same_state( 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 ddcbab7dfc0..83d66a39f71 100644 --- a/homeassistant/backports/functools.py +++ b/homeassistant/backports/functools.py @@ -6,22 +6,21 @@ from types import GenericAlias 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 @@ class cached_property(Generic[_T, _R]): # pylint: disable=invalid-name ) @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 473e08f5c80..87435d8e2c1 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 5b8853bafb4..087fe4297ec 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 8b81bb84103..731d3c750a8 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 ba9cd91180f..cf55bdda53c 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 7fc780d7976..ce2d5e64743 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -10,7 +10,6 @@ from homeassistant import exceptions 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 @@ from homeassistant.helpers import ( 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 a29cb5ff6da..5b3cd8590a7 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -12,15 +12,16 @@ from homeassistant.const import ( 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 @@ async def async_attach_trigger( ) @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 @@ async def async_attach_trigger( 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 @@ async def async_attach_trigger( 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 00168ef3898..f88047795ca 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -41,13 +41,16 @@ from homeassistant.const import ( 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 @@ class HomeAccessory(Accessory): # type: ignore[misc] 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 @@ class HomeAccessory(Accessory): # type: ignore[misc] 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 @@ class HomeAccessory(Accessory): # type: ignore[misc] 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 3bc2b1ed6ae..62d27245a1c 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -14,11 +14,13 @@ from pyhap.const import CATEGORY_CAMERA 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 @@ class Camera(HomeAccessory, PyhapCamera): 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 @@ class Camera(HomeAccessory, PyhapCamera): ) @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 05feb580572..ea0a5054ffd 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -31,7 +31,11 @@ from homeassistant.const import ( 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 @@ class GarageDoorOpener(HomeAccessory): 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 33c35908cd1..f9f572a096c 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -21,8 +21,12 @@ from homeassistant.const import ( 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 @@ class HumidifierDehumidifier(HomeAccessory): 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 e4ae3cde883..5ce64de9b33 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -26,7 +26,7 @@ from homeassistant.const import ( 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 import ( ) 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 @@ class IntegrationSensor(RestoreSensor): 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 308fc4eacd1..e14ee501d7b 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -17,9 +17,12 @@ from homeassistant.const import ( 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 @@ class KNXExposeSensor: 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 adb251bd71a..69cd1ef3d11 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -33,10 +33,11 @@ from homeassistant.exceptions import HomeAssistantError 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 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): 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 d1ea9695322..cc26a684a8d 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -22,14 +22,18 @@ from homeassistant.const import ( 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 @@ class MinMaxSensor(SensorEntity): # 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 @@ class MinMaxSensor(SensorEntity): @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 d7d495b55bf..9b3fb0f29c8 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -25,16 +25,17 @@ from homeassistant.const import ( 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 @@ class Alert(Entity): 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 4d6ff95b810..6abc5d3d5d0 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -17,10 +17,13 @@ from homeassistant.const import ( 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 @@ class CompensationSensor(SensorEntity): 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 @@ class CompensationSensor(SensorEntity): 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 af04da27406..de9f06a0e88 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -18,7 +18,7 @@ from homeassistant.const import ( 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 import ( ) 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 @@ class DerivativeSensor(RestoreSensor, SensorEntity): _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 654d0bce13b..f0a54ba0ea9 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -63,7 +63,11 @@ from homeassistant.const import ( 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 345be0c4b6d..71dc02acf02 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -34,7 +34,10 @@ 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.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.issue_registry import ( ) 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 @@ class ESPHomeManager: """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 a1470baa4d2..c240d04ec1a 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -34,13 +34,21 @@ from homeassistant.const import ( 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 @@ class SensorFilter(SensorEntity): 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 e3eed8866c8..d3d80747127 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -37,18 +37,25 @@ from homeassistant.const import ( 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 @@ class GenericThermostat(ClimateEntity, RestoreEntity): # 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 @@ class GenericThermostat(ClimateEntity, RestoreEntity): 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 @@ class GenericThermostat(ClimateEntity, RestoreEntity): 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 7d44da9f5f6..6d4d6e55fa9 100644 --- a/homeassistant/components/history_stats/coordinator.py +++ b/homeassistant/components/history_stats/coordinator.py @@ -5,10 +5,14 @@ from datetime import timedelta 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 @@ class HistoryStatsUpdateCoordinator(DataUpdateCoordinator[HistoryStatsState]): 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 ee3ab9817ea..ce3844475c5 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -16,11 +16,14 @@ from homeassistant.const import ( 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 @@ class MoldIndicator(SensorEntity): """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 @@ class MoldIndicator(SensorEntity): 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 @@ class MoldIndicator(SensorEntity): 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 @@ class MoldIndicator(SensorEntity): 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 e385156c6d1..ed88e50b932 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -19,13 +19,16 @@ from homeassistant.const import ( 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 @@ class Plant(Entity): 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 078eb59fe72..e86a4741080 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -32,7 +32,6 @@ from homeassistant.const import ( ) from homeassistant.core import ( CALLBACK_TYPE, - Event, HomeAssistant, State, callback, @@ -41,12 +40,18 @@ from homeassistant.core import ( 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 @@ class StatisticsSensor(SensorEntity): """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 a73271bdc83..36f8a651f06 100644 --- a/homeassistant/components/switch_as_x/entity.py +++ b/homeassistant/components/switch_as_x/entity.py @@ -15,7 +15,11 @@ from homeassistant.const import ( 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 @@ class BaseEntity(Entity): """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 09f928303bf..a6621c096c3 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -21,7 +21,7 @@ from homeassistant.const import ( 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 import ( ) 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 @@ class ThresholdSensor(BinarySensorEntity): 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 e43032a580f..020f7903060 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -29,9 +29,12 @@ from homeassistant.core import HomeAssistant, callback 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 @@ class SensorTrend(BinarySensorEntity): """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 @@ class SensorTrend(BinarySensorEntity): 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 68ce8e9b96c..94034cdffe5 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -85,13 +85,14 @@ from homeassistant.helpers import config_validation as cv 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 @@ class UniversalMediaPlayer(MediaPlayerEntity): """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 db03a1ccf2e..7301158d6c6 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -26,7 +26,7 @@ from homeassistant.const import ( 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.dispatcher import async_dispatcher_connect 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 @@ class UtilityMeterSensor(RestoreSensor): 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 @@ class UtilityMeterSensor(RestoreSensor): 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 @@ class UtilityMeterSensor(RestoreSensor): 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 43f487f61d4..7f34629400f 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -8,7 +8,7 @@ import logging 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 @@ from homeassistant.helpers.dispatcher import ( 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 @@ class ZhaGroupEntity(BaseZhaEntity): 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 336d6287f18..b0a3702b5c9 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -32,7 +32,11 @@ from homeassistant.helpers import ( 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 9f9ec48f347..b3cfd1b65f2 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -51,10 +51,11 @@ from homeassistant.helpers.device_registry import ( ) 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 @@ class DeviceTrackerWatcher(WatcherBase): 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 1de6ec98520..104e05605cb 100644 --- a/homeassistant/components/emulated_hue/config.py +++ b/homeassistant/components/emulated_hue/config.py @@ -15,13 +15,14 @@ from homeassistant.components import ( 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 @@ class Config: 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 8d04987d4fa..77c225d72ec 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -37,7 +37,7 @@ from homeassistant.helpers import ( 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 434957dc131..ee33e20173c 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 dcf8f2a525e..6c05ce2fe4f 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 zigpy.zcl.foundation import ( 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 @@ if TYPE_CHECKING: 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 @@ class ClusterHandler(LogMixin): 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 1897383b6c4..7e0e8eaab85 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.device import ZHADevice 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 @@ async def test_invalid_cluster_handler(hass: HomeAssistant, caplog) -> None: 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 d1003418487..7c4198bd881 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -26,6 +26,7 @@ from homeassistant.const import ( 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 54dea780dab..0a2ee68f7c4 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -54,6 +54,7 @@ from homeassistant.helpers.typing import ( 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 ef64a86c6e8..e2ad91e990e 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -8,9 +8,10 @@ import voluptuous as vol 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 55dc69540fd..a720c1831d7 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -45,7 +45,7 @@ from .event import ( 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 @@ class Entity(ABC): 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 248db9d5180..a46dd3c3a52 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 12cf58eaa2b..b31efac92bc 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 f78b4e1401d..88779e03b6c 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 @@ class YeelightNightlightModeSensor(YeelightEntity, BinarySensorEntity): """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 53211115dd6..9422ec9980d 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 9ac457b79e9..35739b0f596 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -472,11 +472,6 @@ class YeelightGenericLight(YeelightEntity, LightEntity): 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 @@ class YeelightColorLightSupport(YeelightGenericLight): 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 @@ class YeelightNightLightMode(YeelightGenericLight): 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 @@ class YeelightWithAmbientAndNightlight(YeelightWithNightLight): 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 @@ class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch): 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 03a93bd9a5b..ab22f42dae3 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 0a2ee68f7c4..ee7095bb3bc 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -34,7 +34,10 @@ from homeassistant.helpers import ( 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 @@ class MqttDiscoveryDeviceUpdate(ABC): ) 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 45a4459b5d3..f1eed86f10c 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -10,7 +10,6 @@ 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 @@ -97,12 +96,25 @@ DEVICE_INFO_TYPES = { 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 a720c1831d7..acb5568ccb0 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -40,7 +40,7 @@ from homeassistant.loader import bind_hass 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 @@ class Entity(ABC): 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 ee7095bb3bc..70b681ffbb2 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -28,7 +28,7 @@ from homeassistant.const import ( 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 acb5568ccb0..8e07897c84f 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -34,7 +34,7 @@ from homeassistant.const import ( 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 43411e9ec0d..49965a38b77 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -27,7 +27,7 @@ from homeassistant.const import ( 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 @@ class BayesianBinarySensor(BinarySensorEntity): @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 @@ class BayesianBinarySensor(BinarySensorEntity): 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 0cc53d5fb2d..113da3aa3ee 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.const import CONF_FOR, CONF_PLATFORM, CONF_VALUE_TEMPLATE 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 @@ async def async_attach_trigger( # 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 @@ async def async_attach_trigger( } @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 @@ async def async_attach_trigger( 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 94034cdffe5..c221a10284a 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -87,6 +87,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( EventStateChangedData, TrackTemplate, + TrackTemplateResult, async_track_state_change_event, async_track_template_result, ) @@ -192,7 +193,10 @@ class UniversalMediaPlayer(MediaPlayerEntity): 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 ea00de33390..bbcbfa6ecb8 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -26,6 +26,7 @@ from homeassistant.exceptions import ( 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 @@ from homeassistant.helpers.json import ( 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 b31efac92bc..e615a6422f0 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -888,9 +888,7 @@ class TrackTemplateResultInfo: 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 @@ TrackTemplateResultListener = Callable[ 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 fcd98a77831..e60c58456d9 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -15,7 +15,6 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_ICON, @@ -33,7 +32,12 @@ from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads 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 @@ from .template import ( render_complex, result_as_boolean, ) -from .typing import ConfigType +from .typing import ConfigType, EventType _LOGGER = logging.getLogger(__name__) @@ -327,14 +331,14 @@ class TemplateEntity(Entity): @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 ee33e20173c..3c81977c393 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 @@ async def test_track_template_result(hass: HomeAssistant) -> None: ) @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 @@ async def test_track_template_result(hass: HomeAssistant) -> None: 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 @@ async def test_track_template_result_none(hass: HomeAssistant) -> None: ) @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 @@ async def test_track_template_result_none(hass: HomeAssistant) -> None: 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 @@ async def test_track_template_result_super_template(hass: HomeAssistant) -> None ) @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 @@ async def test_track_template_result_super_template(hass: HomeAssistant) -> None 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 @@ async def test_track_template_result_super_template_initially_false( ) @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 @@ async def test_track_template_result_super_template_initially_false( 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 @@ async def test_track_template_result_super_template_2( 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 @@ async def test_track_template_result_super_template_2( ) @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 @@ async def test_track_template_result_super_template_2( 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 @@ async def test_track_template_result_super_template_2_initially_false( 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 @@ async def test_track_template_result_super_template_2_initially_false( ) @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 @@ async def test_track_template_result_super_template_2_initially_false( 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 @@ async def test_track_template_result_iterator(hass: HomeAssistant) -> None: 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 @@ async def test_track_template_result_errors( 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 24632e78454..5527f5ec9f1 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -9,7 +9,6 @@ import voluptuous as vol 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.core import ( ) 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 @@ async def async_attach_trigger( **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 77c225d72ec..bfc9c2fce09 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( ATTR_EDITABLE, - ATTR_ENTITY_ID, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_PERSONS, @@ -378,10 +377,12 @@ class Zone(collection.CollectionEntity): 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 3c81977c393..2b77da09778 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 af27766f514..69e56ba0333 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -5,8 +5,10 @@ from dataclasses import dataclass 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 @@ class HistoryStats: 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 @@ class HistoryStats: ) ): 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 ce2d5e64743..eec66a560a5 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 0e3bcbfee86..8287c2b7845 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -37,9 +37,11 @@ from homeassistant.const import ( 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 ed88e50b932..28cdece0b02 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -179,7 +179,7 @@ class Plant(Entity): 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 c7988c02e6a..3daa6f96fdf 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -15,7 +15,7 @@ import voluptuous as vol 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 @@ 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.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 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): 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 7df3b177217..b7fe0fbf364 100644 --- a/homeassistant/components/switch_as_x/cover.py +++ b/homeassistant/components/switch_as_x/cover.py @@ -17,9 +17,11 @@ from homeassistant.const import ( 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 @@ class CoverSwitch(BaseEntity, CoverEntity): ) @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 36f8a651f06..3718c4ebe99 100644 --- a/homeassistant/components/switch_as_x/entity.py +++ b/homeassistant/components/switch_as_x/entity.py @@ -12,7 +12,7 @@ from homeassistant.const import ( 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 @@ class BaseEntity(Entity): ) @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 @@ class BaseToggleEntity(BaseEntity, ToggleEntity): ) @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 9778caf8e60..9e7606865a1 100644 --- a/homeassistant/components/switch_as_x/lock.py +++ b/homeassistant/components/switch_as_x/lock.py @@ -13,9 +13,11 @@ from homeassistant.const import ( 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 @@ class LockSwitch(BaseEntity, LockEntity): ) @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 e60c58456d9..b7be7c2c9a6 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -26,7 +26,7 @@ from homeassistant.const import ( 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 @@ class _TemplateAttribute: @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 2b77da09778..9436226b335 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 30e239a65a9..a915d886138 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 731d3c750a8..9c0659fac39 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 cf55bdda53c..e6f46447159 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 0457cfaa810..a3093991225 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -7,7 +7,7 @@ from dataclasses import dataclass 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 @@ class MatterEntity(Entity): @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 e49f670d371..dffbdc42dbe 100644 --- a/homeassistant/components/radio_browser/media_source.py +++ b/homeassistant/components/radio_browser/media_source.py @@ -28,7 +28,7 @@ CODEC_TO_MIMETYPE = { 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 5470a94896d..74823dea953 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 597b8e1ae1f..1754c166ef7 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 7348e1bf3e2..56ee3f74140 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -573,6 +573,7 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: 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 test_async_get_all_descriptions(hass: HomeAssistant) -> None: ] == "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 e62cb1143b5..907ff84491b 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 @@ class ClimateEntity(Entity): 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 @@ class ClimateEntity(Entity): 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 @@ class ClimateEntity(Entity): 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 b3d5f66ae8c..b20b0213835 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -3,7 +3,11 @@ from __future__ import annotations 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 0cc88baf32f..8e6820fb5bb 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 00000000000..b5fdeb612c4 --- /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 1a308f9dff9..b310b2bb2ba 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 ba9571a95f5..6ba6f3915ec 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -4,6 +4,7 @@ from __future__ import annotations 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 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): 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 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): ) 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 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): 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 86d578d852a..c40e47ada99 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 @@ class RoborockEntity(Entity): 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 a0b3d5be295..312753ced01 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -9,13 +9,11 @@ from typing import Any 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 3fb8bf00b8a..bfe6fe6c80c 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -11,6 +11,7 @@ from homeassistant.config_entries import ConfigEntry 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 @@ class APCUPSdData: 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 @@ class APCUPSdData: """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 d45ad561d8d..bac8d18d58b 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -9,7 +9,6 @@ from homeassistant.components.binary_sensor import ( ) 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 @@ class OnlineStatus(BinarySensorEntity): # 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 8b7034357df..745be7e2d63 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -21,7 +21,6 @@ from homeassistant.const import ( 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 @@ class APCUPSdSensor(SensorEntity): # 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 f5c3f573030..b8a83f950d0 100644 --- a/tests/components/apcupsd/__init__.py +++ b/tests/components/apcupsd/__init__.py @@ -20,6 +20,7 @@ MOCK_STATUS: Final = OrderedDict( ("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 a9ef4328e86..6ac7992f404 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 6e00a382e79..8c29edabbc1 100644 --- a/tests/components/apcupsd/test_init.py +++ b/tests/components/apcupsd/test_init.py @@ -8,6 +8,7 @@ from homeassistant.components.apcupsd import DOMAIN 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 d1cae2b0fad..5e24796323c 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 00000000000..7a487f3a385 --- /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 087fe4297ec..e1a90290c10 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 1df65353855..7826de94f9d 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 d4445c95369..aee5d454e23 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 5e24796323c..b7b351b755f 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 3f4069752f2..fe6ed408298 100644 --- a/homeassistant/components/discovergy/sensor.py +++ b/homeassistant/components/discovergy/sensor.py @@ -101,6 +101,7 @@ ELECTRICITY_SENSORS: tuple[DiscovergySensorEntityDescription, ...] = ( device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + alternative_keys=["voltage1"], ), DiscovergySensorEntityDescription( key="phase2Voltage", @@ -110,6 +111,7 @@ ELECTRICITY_SENSORS: tuple[DiscovergySensorEntityDescription, ...] = ( device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + alternative_keys=["voltage2"], ), DiscovergySensorEntityDescription( key="phase3Voltage", @@ -119,6 +121,7 @@ ELECTRICITY_SENSORS: tuple[DiscovergySensorEntityDescription, ...] = ( 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 fc26dd85ea3..72dcf08a2d0 100644 --- a/homeassistant/components/lastfm/__init__.py +++ b/homeassistant/components/lastfm/__init__.py @@ -4,12 +4,17 @@ from __future__ import annotations 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 00000000000..533f9ec3b09 --- /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 c51868394de..116a0813387 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 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.helpers.update_coordinator import ( + CoordinatorEntity, +) from .const import ( ATTR_LAST_PLAYED, @@ -24,9 +27,9 @@ from .const import ( CONF_USERS, DEFAULT_NAME, DOMAIN, - LOGGER, STATE_NOT_SCROBBLING, ) +from .coordinator import LastFMDataUpdateCoordinator, LastFMUserData PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -36,11 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -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 dde914d51cc..7e6bb6500b2 100644 --- a/tests/components/lastfm/__init__.py +++ b/tests/components/lastfm/__init__.py @@ -75,7 +75,7 @@ class MockUser: 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 a28e085c104..e64cf6b2629 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 ab9358be1d3..049f2a74250 100644 --- a/tests/components/lastfm/test_sensor.py +++ b/tests/components/lastfm/test_sensor.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant 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 @@ LEGACY_CONFIG = { 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 eab4c1df3a6..67368596439 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -256,7 +256,7 @@ class DenonDevice(MediaPlayerEntity): 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 87435d8e2c1..92daffc6c8b 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 e1a90290c10..0dca45102b1 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 9c0659fac39..dff3628abb4 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 e6f46447159..dfdc7010496 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 cbe4bf9069c..781e784fe6a 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 33c43936544..b9b235ab41e 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 a161c3ecde1..5ee0102ce17 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 51acbe8c7d9..3e34176771c 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 0dca45102b1..2e1b00fc053 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 dff3628abb4..b15c4f97c88 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 dfdc7010496..a0b49ab41f0 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 c4fc4b66f37..8c98eb6473c 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -337,6 +337,14 @@ RPC_SENSORS: Final = { 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 @@ RPC_SENSORS: Final = { 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 @@ RPC_SENSORS: Final = { 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 @@ RPC_SENSORS: Final = { 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 @@ RPC_SENSORS: Final = { 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 c782a8f0f3b..e0143d49259 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -20,7 +20,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send 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 @@ from .const import ( ) CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP] -DEFAULT_NAME = "Asuswrt" SCAN_INTERVAL = timedelta(seconds=30) @@ -179,6 +178,44 @@ class AsusWrtRouter: 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 @@ class AsusWrtRouter: 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 @@ class AsusWrtRouter: 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 accd1eba59b..7f54bc29393 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -22,6 +22,7 @@ from homeassistant.helpers.update_coordinator import ( 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 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity): ) -> 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 c28d71c1a29..2d7bda491a8 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -9,9 +9,13 @@ from homeassistant.components import device_tracker, sensor 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 @@ CONFIG_DATA = { 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_2 = "A2:B2:C2:D2:E2:F2" 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 | 52 ++++++++----- tests/components/androidtv/patchers.py | 4 + .../components/androidtv/test_media_player.py | 75 +++++++++++++++---- 3 files changed, 99 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 8f5f3bdfe56..4f927f242df 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.device_registry import CONNECTION_NETWORK_MAC 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_DEVICE_PATH = "device_path" 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 @@ class ADBDevice(MediaPlayerEntity): 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 @@ class ADBDevice(MediaPlayerEntity): ) ) - @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 + 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) - media_data = await self._adb_screencap() - if media_data: - return media_data, "image/png" + @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 - # If an exception occurred and the device is no longer available, write the state - if not self.available: - self.async_write_ha_state() - - 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 @@ class AndroidTVDevice(ADBDevice): if not self.available: return + prev_app_id = self._attr_app_id # Get the updated state and attributes. ( state, @@ -514,6 +527,8 @@ class AndroidTVDevice(ADBDevice): 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 @@ class FireTVDevice(ADBDevice): 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 @@ class FireTVDevice(ADBDevice): 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 f0fca5aae90..aae99b34438 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 c7083626e15..847bc5c7d2f 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.const import ( 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() - + resp = await client.get(state.attributes["entity_picture"]) + content = await resp.read() assert content == b"image" - with patch( + 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 + ): + async_fire_time_changed(hass, next_update, True) + await hass.async_block_till_done() + patch_screen_cap.assert_not_called() + + 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() + + +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 9299b1ed0b0..4c58f82b8e7 100644 --- a/homeassistant/components/androidtv_remote/__init__.py +++ b/homeassistant/components/androidtv_remote/__init__.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant, callback 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 @@ PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.REMOTE] 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 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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 f7e1078d3fa..b8399fd7ba2 100644 --- a/homeassistant/components/androidtv_remote/config_flow.py +++ b/homeassistant/components/androidtv_remote/config_flow.py @@ -15,11 +15,12 @@ import voluptuous as vol 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 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 82f494b81aa..44d7098adc1 100644 --- a/homeassistant/components/androidtv_remote/const.py +++ b/homeassistant/components/androidtv_remote/const.py @@ -4,3 +4,6 @@ from __future__ import annotations 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 0bc1f1b904f..41b056269f2 100644 --- a/homeassistant/components/androidtv_remote/helpers.py +++ b/homeassistant/components/androidtv_remote/helpers.py @@ -3,11 +3,14 @@ from __future__ import annotations 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 983c604370b..dbbf6a2d383 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 ec368081a95..4e0067152e7 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 38007962e4e..9702959649d 100644 --- a/homeassistant/components/ezviz/light.py +++ b/homeassistant/components/ezviz/light.py @@ -8,7 +8,7 @@ from pyezviz.exceptions import HTTPError, PyEzvizError 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 @@ class EzvizLight(EzvizEntity, LightEntity): ) 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 @@ class EzvizLight(EzvizEntity, LightEntity): 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 318798fdc5f..3bbe3e0b184 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -185,7 +185,15 @@ class JellyfinSource(MediaSource): 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 @@ class JellyfinSource(MediaSource): 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 @@ class JellyfinSource(MediaSource): 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 @@ class JellyfinSource(MediaSource): 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 @@ class JellyfinSource(MediaSource): 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 @@ class JellyfinSource(MediaSource): 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 @@ class JellyfinSource(MediaSource): 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 2e1b00fc053..f2f3f482fe8 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 7826de94f9d..a7fd2e24ce5 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 aee5d454e23..098cf402e73 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 801b27ee971..da897a9767f 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta 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 @@ class MinecraftServer: 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 @@ class MinecraftServer: # 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 @@ class MinecraftServer: 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 @@ class MinecraftServer: 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 @@ class MinecraftServerEntity(Entity): 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 691aea0f75e..b402b7cfff0 100644 --- a/homeassistant/components/minecraft_server/config_flow.py +++ b/homeassistant/components/minecraft_server/config_flow.py @@ -8,6 +8,7 @@ import voluptuous as vol 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 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN): # 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 b831e1eae90..27019cb80a8 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 b15c4f97c88..394bf420796 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 a0b49ab41f0..2b84604df48 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 25c0cec441b..ac5ae7dbc6e 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -4,7 +4,7 @@ import asyncio 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 @@ from tests.common import MockConfigEntry 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 @@ class QueryMock: 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 7f54bc29393..4f9ec0af411 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -50,14 +50,14 @@ UNIT_DEVICES = "Devices" 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 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( ), 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 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( ), 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 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( ), 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 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( ), 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 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( ), 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 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( ), 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 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( ), 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 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( ), 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 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( ), 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 f6ccb5a7c9c..52b9f919434 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 245dbd0a19e..19fd0b518b2 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 2e868542457..6f919ee50da 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 f2f3f482fe8..abee7ee1d00 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 394bf420796..2def3fca3bd 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 2b84604df48..1264ef4ff35 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 abee7ee1d00..d2ec1c7bdda 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 f215b649bb2..9302d547786 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -102,7 +102,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 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 01e124dea1a..4bf076a59bc 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -32,13 +32,14 @@ def async_refresh_after( 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 e4f91f282f6..db7e6ff355e 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 7471ed8982b..ba4949434f7 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 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( ), 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 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( ), 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 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( 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 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( 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 @@ class SmartPlugSensor(CoordinatedTPLinkEntity, SensorEntity): 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 6daa5c9cb1a..750d422cd0d 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 aa0616447cc..d82308a2e32 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 @@ class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): """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 @@ class SmartPlugSwitch(CoordinatedTPLinkEntity, SwitchEntity): 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 ba4419500f4..1e5e03c0f37 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 c30098f254b..f0ee9576cc7 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -80,6 +80,7 @@ from .schema import ( ButtonSchema, ClimateSchema, CoverSchema, + DateSchema, EventSchema, ExposeSchema, FanSchema, @@ -136,6 +137,7 @@ CONFIG_SCHEMA = vol.Schema( **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 bdc480851c3..c96f10736dd 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -127,6 +127,7 @@ SUPPORTED_PLATFORMS: Final = [ 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 00000000000..1f286d59ecb --- /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 86bf790a077..40cc2232d8f 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 00000000000..bfde519f3c0 --- /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 3930c50c70c..1810d52323c 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -12,15 +12,18 @@ from homeassistant.components.bluetooth import ( 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_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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 3d7bdfd0b48..f7c4c87014c 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 @@ from homeassistant.components.binary_sensor import ( 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 @@ class XiaomiBluetoothSensorEntity( @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 dda6c61d8aa..1566478bcea 100644 --- a/homeassistant/components/xiaomi_ble/const.py +++ b/homeassistant/components/xiaomi_ble/const.py @@ -6,6 +6,7 @@ from typing import Final, TypedDict 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 00000000000..2a4b35f6171 --- /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 84ef91bf5a8..f0f0d7fa71e 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -5,9 +5,7 @@ from xiaomi_ble import DeviceClass, SensorUpdate, Units 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.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 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 fff8d9b20f1..7f39228a012 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 @@ from . import ( 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 781e784fe6a..9bd0672179a 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 d2ec1c7bdda..232b2821209 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 2def3fca3bd..ba62e1d6f0a 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 1264ef4ff35..e8e0df2ef32 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 75ad26665c3..d24fd8d1f14 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -17,7 +17,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import format_mac from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DOMAIN -from .exceptions import ReolinkException, UserNotAdmin +from .exceptions import ReolinkException, ReolinkWebhookException, UserNotAdmin from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) @@ -133,6 +133,12 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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 7d8c3a213eb..2389c433b20 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 7d25fd62811..b6e48cab7b2 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant import config_entries, data_entry_flow 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 b1bbd8d0945..1ca833c6a74 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 ba62e1d6f0a..f601fd7b88f 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 e8e0df2ef32..4b9be3ca91b 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 8c98eb6473c..b52e176b521 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -661,6 +661,16 @@ RPC_SENSORS: Final = { 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 29b550463d0..765eec2d288 100644 --- a/homeassistant/components/airzone_cloud/binary_sensor.py +++ b/homeassistant/components/airzone_cloud/binary_sensor.py @@ -4,7 +4,14 @@ from __future__ import annotations 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 homeassistant.helpers.entity_platform import AddEntitiesCallback 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 @@ class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity): } +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 37357bf59da..14f7a078156 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 a8cb539bb1d..0c26755f948 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 aa2d0c32d16..65b12b64e58 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 232b2821209..031e64453f4 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 f601fd7b88f..e36f8fe52fa 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 4b9be3ca91b..f2e3d281f88 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 | 74 +++++++++++--------- homeassistant/components/ecovacs/vacuum.py | 13 ++-- 2 files changed, 46 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/ecovacs/__init__.py b/homeassistant/components/ecovacs/__init__.py index cd87b175cf9..9cb8a8c38d8 100644 --- a/homeassistant/components/ecovacs/__init__.py +++ b/homeassistant/components/ecovacs/__init__.py @@ -46,54 +46,60 @@ ECOVACS_API_DEVICEID = "".join( ) -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] = [] - - 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 = ecovacs_api.devices() - _LOGGER.debug("Ecobot devices: %s", devices) - - for device in devices: - _LOGGER.info( - "Discovered Ecovacs device on account: %s with nickname %s", - device.get("did"), - device.get("nick"), + 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), ) - 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) + ecovacs_devices = ecovacs_api.devices() + _LOGGER.debug("Ecobot devices: %s", ecovacs_devices) - def stop(event: object) -> None: + 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.append(vacbot) + return devices + + hass.data[ECOVACS_DEVICES] = await hass.async_add_executor_job(get_devices) + + 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 ba922a30b84..2ec9a1a3e4a 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -28,18 +28,20 @@ ATTR_ERROR = "error" 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 | 193 +++------- 2 files changed, 393 insertions(+), 145 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 00000000000..a124ef57693 --- /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 c5681e4a278..4b8521b5798 100644 --- a/tests/components/devolo_home_network/test_button.py +++ b/tests/components/devolo_home_network/test_button.py @@ -3,19 +3,18 @@ from unittest.mock import AsyncMock 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,164 +39,68 @@ async def test_button_setup(hass: HomeAssistant) -> None: await hass.config_entries.async_unload(entry.entry_id) -@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 -) -> 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.""" - entry = configure_integration(hass) - device_name = entry.title.replace(" ", "_").lower() - state_key = f"{PLATFORM}.{device_name}_start_wps" - - 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.device.async_start_wps.call_count == 1 - - await hass.config_entries.async_unload(entry.entry_id) - - @pytest.mark.parametrize( - ("name", "trigger_method"), + ("name", "api_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"], + [ + "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", + ], ], ) -async def test_device_failure( +@pytest.mark.freeze_time("2023-01-13 12:00:00+00:00") +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 device failure.""" + """Test a button.""" 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() + assert hass.states.get(state_key) == snapshot + assert entity_registry.async_get(state_key) == snapshot + # 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" + api = getattr(mock_device, api_name) + assert getattr(api, trigger_method).call_count == 1 + + # 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 db191405522..1032ac2db0a 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 59dfb7c269c..c007de78130 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -35,6 +35,7 @@ PLATFORMS_BY_TYPE: dict[str, list] = { 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 00000000000..1c04de956c6 --- /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 01e8425c13b..7f03aef1d97 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -263,6 +263,17 @@ class EzvizCamera(EzvizEntity, Camera): 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 0245edc0e3e..d60c4816d24 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 8953d9facd0..bf2ccb16b03 100644 --- a/homeassistant/components/scrape/__init__.py +++ b/homeassistant/components/scrape/__init__.py @@ -20,7 +20,10 @@ from homeassistant.const import ( 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 @@ from .coordinator import ScrapeCoordinator 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 5ddd6c48e43..a68083856f7 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -6,13 +6,20 @@ from typing import Any, cast 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.exceptions import PlatformNotReady 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 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], TemplateSensor): 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 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], TemplateSensor): 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 44c264520d6..60cde48e5bf 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.components.sensor import ( ) 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.const import ( ) 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 | 58 +++++++++++++++++-- 7 files changed, 113 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/loqed/const.py b/homeassistant/components/loqed/const.py index 6b1c0311a2d..59011f26566 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 507debc02ab..42e0d523aba 100644 --- a/homeassistant/components/loqed/coordinator.py +++ b/homeassistant/components/loqed/coordinator.py @@ -6,13 +6,13 @@ from aiohttp.web import Request 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 @@ class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]): 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 @@ class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]): 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 @@ class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]): 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 1000d8f804d..25d1f15486d 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 3d31194f5a6..59b91fea195 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 be57237afdc..616c0cb0552 100644 --- a/tests/components/loqed/conftest.py +++ b/tests/components/loqed/conftest.py @@ -9,6 +9,7 @@ from loqedAPI import loqed 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 cf53fcf56a9..e42c39b60f5 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 960ad9def6b..057061f5915 100644 --- a/tests/components/loqed/test_init.py +++ b/tests/components/loqed/test_init.py @@ -10,6 +10,7 @@ from homeassistant.components.loqed.const import DOMAIN 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 - ), 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() - lock.registerWebhook.assert_called_with("http://hook_id") + 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.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_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 c10cc8158ea..c3633800685 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(): - device_coordinator = YoLinkCoordinator(hass, device) + 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(): + 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 f22e416511b..e322961d179 100644 --- a/homeassistant/components/yolink/coordinator.py +++ b/homeassistant/components/yolink/coordinator.py @@ -20,7 +20,12 @@ _LOGGER = logging.getLogger(__name__) 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 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]): 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 0d1f1e590b4..6cc1ea3acd6 100644 --- a/homeassistant/components/yolink/cover.py +++ b/homeassistant/components/yolink/cover.py @@ -4,7 +4,7 @@ from __future__ import annotations 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 @@ class YoLinkCoverEntity(YoLinkEntity, CoverEntity): """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 088ddd114f8..ced0d527c7d 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 149bdc0adf8..e4d0aa38fbe 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -8,6 +8,7 @@ from yolink.const import ( 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 @@ SENSOR_DEVICE_TYPE = [ 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 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( 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 415c1e9584d..018fcb84988 100644 --- a/homeassistant/components/yolink/switch.py +++ b/homeassistant/components/yolink/switch.py @@ -5,6 +5,7 @@ from collections.abc import Callable 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 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity): 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 e36f8fe52fa..0e2b4467c8b 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 f2e3d281f88..34faeba0e2a 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 658b5d368d0..3a834261af5 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 0c9c829631a..9a6c528c336 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 2a279840a9e..c45d8c42546 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 9c13bcc8c94..79f885cad18 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 0c5e6f4139b..a0a3ee71a9c 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 11c85ebf872..5fde0fc9fa1 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 92213f39fce..9b71595e58f 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 a7db00b8f17..0b1f2677d6a 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 17a2b0b53be..953d9791f2f 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 a161d48398f..93e55071178 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 f313a117c44..dcd36671fce 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 f4177e8c300..d5329598655 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 984bbdfe7c1..d52b7c18c41 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 1375ae45392..dfc5e279dc5 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 5b2e5da8902..6fae364df3b 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 7cdf12ab6bd..a21c811af47 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 1ca833c6a74..36b9631c801 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 df77d60c141..8834a538be9 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 5ee0102ce17..1a613a82098 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 3e34176771c..5a1eef40001 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 232493234bb..63da470c5cd 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 0185c727f67..d1897b53e04 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 d50561a33a4..d3dbaad98e3 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 91df756dafe..d9da13d2381 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 3d2ba299628..035c4bdda45 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 76e79fcf949..3b8b727015b 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 6cb8e2b7d92..79999eb8ad9 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 823f2c5463f..9accda95912 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 5d0fa29b2e8..e9a24cfe1e1 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 a81e7518132..183919f05f2 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 94659df8547..a8a5ae062f7 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 0ce54496539..71dbb786eb5 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 0e2b4467c8b..731c4f18c45 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 34faeba0e2a..426f249c2f3 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 cb027f32e0a..327db05cb00 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -37,6 +37,7 @@ from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, UnitOfTemper 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 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): 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 3b86cbdd5a4..934307947d8 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 753c107c2ee..23d34c131b8 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -40,6 +40,7 @@ from homeassistant.const import ( 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 d283b668995..12f2d49e416 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -308,7 +308,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): 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 b5cea06c719..e03bd50d483 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -9,6 +9,7 @@ DOMAIN = "unifi" 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 00000000000..730720753d4 --- /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 9bfb01e5a88..c34d1035158 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 731c4f18c45..cde697360aa 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 426f249c2f3..70e07baf8d6 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 00000000000..77b171118a1 --- /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 1032ac2db0a..05a86ddebd1 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 da897a9767f..aef6c94767f 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -10,16 +10,12 @@ from mcstatus.server import JavaServer 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 @@ class MinecraftServer: 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 ecf7d747770..5c9cb5f42e1 100644 --- a/homeassistant/components/minecraft_server/binary_sensor.py +++ b/homeassistant/components/minecraft_server/binary_sensor.py @@ -7,8 +7,9 @@ from homeassistant.config_entries import ConfigEntry 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 00000000000..02875cb69f2 --- /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 5d056d98dd1..3a9e4b8f0a0 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -7,7 +7,7 @@ from homeassistant.const import UnitOfTime 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 @@ from .const import ( 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 a21f492c439..0ab175691f8 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -15,6 +15,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType SUPPORT_FLAGS_HEATER = ( WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.ON_OFF | WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.AWAY_MODE ) @@ -103,3 +104,11 @@ class DemoWaterHeater(WaterHeaterEntity): """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 511518279cb..cf4b788480f 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 @@ class AtwWaterHeater(WaterHeaterEntity): """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 3cd9378cfca..b31d1306c55 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 @@ class WaterHeaterEntity(Entity): 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 b42109ee649..b60cfdd8c48 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 a03e93cde41..5ddb61d28b0 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 9e45b4e39bf..cc91f57d872 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 ece283f4bab..0d2d73d17fd 100644 --- a/tests/components/water_heater/common.py +++ b/tests/components/water_heater/common.py @@ -11,8 +11,11 @@ from homeassistant.components.water_heater import ( 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 00000000000..66276f0bc88 --- /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 | 114 ++++++------------ 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, 270 insertions(+), 587 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 64abf1a6753..f8a9008d9b3 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 @@ class AsyncConfigEntryAuth: 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 fa3bc6c8237..50dee14d61a 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 @@ from .const import ( ) -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 @@ class OAuth2FlowHandler( 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 @@ class OAuth2FlowHandler( 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 @@ class OAuth2FlowHandler( 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 @@ class YouTubeOptionsFlowHandler(OptionsFlowWithConfigEntry): 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 72629544895..cb9d1e8214e 100644 --- a/homeassistant/components/youtube/coordinator.py +++ b/homeassistant/components/youtube/coordinator.py @@ -4,12 +4,13 @@ from __future__ import annotations 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 @@ from .const import ( ) -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 @@ class YouTubeDataUpdateCoordinator(DataUpdateCoordinator): ) 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 2f9238dec26..46deaf40450 100644 --- a/homeassistant/components/youtube/entity.py +++ b/homeassistant/components/youtube/entity.py @@ -9,7 +9,7 @@ from .const import ATTR_TITLE, DOMAIN, MANUFACTURER 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 fbc02bda006..b37d242fe52 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 b5d3fc79b39..a63b8fb0c0b 100644 --- a/homeassistant/components/youtube/sensor.py +++ b/homeassistant/components/youtube/sensor.py @@ -30,9 +30,10 @@ from .entity import YouTubeChannelEntity 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 @@ SENSOR_TYPES = [ 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 @@ SENSOR_TYPES = [ 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 @@ class YouTubeSensor(YouTubeChannelEntity, SensorEntity): @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 cde697360aa..d4836ea1522 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 70e07baf8d6..6d3a9d3819f 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 15a43d7a62f..3c46ff92661 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 youtubeaio.models import YouTubeChannel, YouTubePlaylistItem, YouTubeSubscription +from youtubeaio.types import AuthScope 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.""" - - 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) - - -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 @@ class MockService: 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) + async def set_user_authentication( + self, token: str, scopes: list[AuthScope] + ) -> None: + """Authenticate the user.""" + self._authenticated = True - def playlistItems(self) -> MockPlaylistItems: - """Return a mock object.""" - return MockPlaylistItems(self._playlist_items_fixture) + 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) - def subscriptions(self) -> MockSubscriptions: - """Return a mock object.""" - return MockSubscriptions(self._subscriptions_fixture) + 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 d87a3c07679..a8a333190ee 100644 --- a/tests/components/youtube/conftest.py +++ b/tests/components/youtube/conftest.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant 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 24e71ad91ab..f2757b169bb 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 00000000000..98b9a11737e --- /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 6b5d66d6501..00000000000 --- 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 430ad3715cc..00000000000 --- 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 21cb09bd886..00000000000 --- 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 d4c28730cab..00000000000 --- 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 bdbedfcf4c9..00000000000 --- 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 6a41465ac92..a938cb8daad 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 b643bdeb979..e3bfa4ec4bd 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 5b91ff958f8..97875004d11 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.core import HomeAssistant 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 @@ from .conftest import ( 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 f2c5274c4a7..7dc368a5860 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 05a86ddebd1..30f768e01a4 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 9818e3d3197..dffeb08e014 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 918ad4c2343..ef9634e1527 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 00000000000..3ae6b1c70cf --- /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 00000000000..89109f01948 --- /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 00000000000..4a3ef8aa1c5 --- /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 00000000000..c2c80aaa402 --- /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 00000000000..907b6247172 --- /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 00000000000..3e0ba997cd4 --- /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 00000000000..8ddb4c1af7c --- /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 00000000000..ce3e473159a --- /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 00000000000..4f32f237c00 --- /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 00000000000..19056180f17 --- /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 d1b330b5dbe..78c98bcc03d 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -4,6 +4,7 @@ To update, run python3 -m script.hassfest """ APPLICATION_CREDENTIALS = [ + "electric_kiwi", "geocaching", "google", "google_assistant_sdk", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6d9a132a29a..7283b187ba0 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -117,6 +117,7 @@ FLOWS = { "efergy", "eight_sleep", "electrasmart", + "electric_kiwi", "elgato", "elkm1", "elmax", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 99566340ccd..18e7f1c22e1 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 66568cf5400..7d1ec19c4d5 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 d4836ea1522..f64d8f8e66b 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 6d3a9d3819f..440d4a22c0c 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 00000000000..7f5e08a56b5 --- /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 00000000000..525f5742382 --- /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 00000000000..51d00722341 --- /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 f0ee9576cc7..1bb6d9bbdd2 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -81,6 +81,7 @@ from .schema import ( ClimateSchema, CoverSchema, DateSchema, + DateTimeSchema, EventSchema, ExposeSchema, FanSchema, @@ -138,6 +139,7 @@ CONFIG_SCHEMA = vol.Schema( **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 c96f10736dd..519d5d0742d 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -128,6 +128,7 @@ SUPPORTED_PLATFORMS: Final = [ 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 00000000000..fc63df04233 --- /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 40cc2232d8f..8240fbaf3c1 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 00000000000..f9d9f039367 --- /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 8f56d1a2e9c..b127bf8d1d7 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -172,11 +172,18 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( 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 30f768e01a4..6bc69125c30 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 ef9634e1527..f09785a7781 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 00000000000..a1cf1c907a6 --- /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 00000000000..37087d4ea1a --- /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 00000000000..114867b8d95 --- /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 00000000000..f1c72aa55c4 --- /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 00000000000..a630a3dedbd --- /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 00000000000..379291eb626 --- /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 00000000000..a9921de85d3 --- /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 7283b187ba0..b4b9c409c6e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -106,6 +106,7 @@ FLOWS = { "dsmr", "dsmr_reader", "dunehd", + "duotecno", "dwd_weather_warnings", "dynalite", "eafm", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 18e7f1c22e1..ebe16947a51 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 f64d8f8e66b..617a2664217 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 440d4a22c0c..650a78954de 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 00000000000..9cb20bcaec6 --- /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 00000000000..82c3e0c7f44 --- /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 00000000000..a2dc265ae6e --- /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 b127bf8d1d7..493b627f9b4 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -187,7 +187,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( ), 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 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( ), 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 44b0a497881..4053d2658f5 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 a3093991225..0082370d5ff 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -78,6 +78,9 @@ class MatterEntity(Entity): 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 ed2bfb5ffac..0c41b58c63d 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -3,7 +3,6 @@ from __future__ import annotations from abc import ABC, abstractmethod import logging -from typing import cast from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -70,7 +69,7 @@ class BasePassiveBluetoothCoordinator(ABC): 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 031e64453f4..c3bd83679ae 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 a7fd2e24ce5..1f179518fd9 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 098cf402e73..9f5023c9a1c 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 4f32f237c00..a657b768aa5 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 e16ab3d91ae..2fc59134d14 100644 --- a/homeassistant/components/roborock/const.py +++ b/homeassistant/components/roborock/const.py @@ -11,5 +11,6 @@ PLATFORMS = [ 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 3989f08505b..cd629e208e3 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 00000000000..514d147d469 --- /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 00000000000..6ba996ca23f --- /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 f29a114620a..f45d2852df0 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -64,6 +64,8 @@ from .const import ( 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 @@ CONF_HUMIDITY_MIN = "min_humidity" # 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 fb1989069af..fcdfeb4bd7d 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -40,6 +40,8 @@ CONF_MODE_COMMAND_TOPIC = "mode_command_topic" 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 17e9430dba3..08b9d36d850 100644 --- a/homeassistant/components/mqtt/water_heater.py +++ b/homeassistant/components/mqtt/water_heater.py @@ -51,6 +51,8 @@ from .const import ( 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 @@ VALUE_TEMPLATE_KEYS = ( COMMAND_TEMPLATE_KEYS = { CONF_MODE_COMMAND_TEMPLATE, CONF_TEMP_COMMAND_TEMPLATE, + CONF_POWER_COMMAND_TEMPLATE, } @@ -98,6 +101,7 @@ TOPIC_KEYS = ( 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 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( 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 @@ class MqttWaterHeater(MqttTemperatureControlEntity, WaterHeaterEntity): ): 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 @@ class MqttWaterHeater(MqttTemperatureControlEntity, WaterHeaterEntity): 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 c4f798e05ec..245af5c6918 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 54f6fca83d4..fe1045203d8 100644 --- a/homeassistant/components/discovergy/__init__.py +++ b/homeassistant/components/discovergy/__init__.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant 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 d6b81ed8837..3434b1dd84c 100644 --- a/homeassistant/components/discovergy/config_flow.py +++ b/homeassistant/components/discovergy/config_flow.py @@ -16,7 +16,7 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD 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 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 866e9f11def..f410eb94bcf 100644 --- a/homeassistant/components/discovergy/const.py +++ b/homeassistant/components/discovergy/const.py @@ -3,4 +3,3 @@ from __future__ import annotations DOMAIN = "discovergy" MANUFACTURER = "Discovergy" -APP_NAME = "homeassistant" diff --git a/homeassistant/components/discovergy/coordinator.py b/homeassistant/components/discovergy/coordinator.py index e3b6e91e03f..6ee5a4c3e84 100644 --- a/homeassistant/components/discovergy/coordinator.py +++ b/homeassistant/components/discovergy/coordinator.py @@ -47,9 +47,7 @@ class DiscovergyUpdateCoordinator(DataUpdateCoordinator[Reading]): 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 02d5585c1dc..a7c79bf3b13 100644 --- a/homeassistant/components/discovergy/diagnostics.py +++ b/homeassistant/components/discovergy/diagnostics.py @@ -19,9 +19,9 @@ TO_REDACT_METER = { "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 c929386e8e8..23d7f1ad5bf 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 fe6ed408298..79fc6af1b9a 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 @@ class DiscovergySensor(CoordinatorEntity[DiscovergyUpdateCoordinator], SensorEnt 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 617a2664217..e903c807480 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 650a78954de..87b42c66399 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 313985bd7d2..ea0fe84852f 100644 --- a/tests/components/discovergy/conftest.py +++ b/tests/components/discovergy/conftest.py @@ -14,7 +14,7 @@ from tests.components.discovergy.const import GET_METERS @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 2205a70830e..5c233d50ba8 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 f42a4a983fb..bc4fd2d9e9d 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 1d465dda0e0..b9da2bb7e6f 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 18a132be6a8..70b28e34dd0 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -18,11 +18,14 @@ from aiounifi.models.event import Event, EventKey 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 730720753d4..c26f06cb5f2 100644 --- a/homeassistant/components/unifi/image.py +++ b/homeassistant/components/unifi/image.py @@ -8,24 +8,26 @@ 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.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 846c6d12234..ca11cdfea30 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.clients import Clients 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.dpi_restriction_group import DPIRestrictionGroup 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 @@ from .entity import ( 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 @@ ENTITY_DESCRIPTIONS: tuple[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 f93abc291b8..ad5131614af 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -580,6 +580,43 @@ OUTLET_UP1 = { } +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 dc403611da2..39eab6e7e0a 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -106,6 +106,10 @@ class BSBLANClimate( @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 0e945d13d48..5abb888513d 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 ebe16947a51..6bc96ea15bc 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 9bd0672179a..cbeab2abec0 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 c3bd83679ae..13d67cc95f5 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 e903c807480..56516f1cbbd 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 87b42c66399..a1775c0ef16 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 67e401cd40a..2ae036080f8 100644 --- a/homeassistant/components/bluetooth/wrappers.py +++ b/homeassistant/components/bluetooth/wrappers.py @@ -6,13 +6,18 @@ from collections.abc import Callable 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 @@ class HaBleakScannerWrapper(BaseBleakScanner): 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 @@ class HaBleakScannerWrapper(BaseBleakScanner): """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 24f1039175b..21fade843f5 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -2386,6 +2386,65 @@ async def test_wrapped_instance_with_service_uuids( 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 07c5585833d..47e742bdb76 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 13d67cc95f5..cf576fc1c83 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 56516f1cbbd..4892fddd5ba 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 a1775c0ef16..0820a3026e1 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 f09785a7781..8a72fadfbd9 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 da805999d53..197356b2092 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 00000000000..6e3ffb5e2b1 --- /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 7e511ed7d2c..ccea69f8b7f 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 6c6d3acb30e..f3fb13589bb 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 0616b774951..4ef1070d12d 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -7,6 +7,7 @@ from python_opensky import BoundingBox, OpenSky, StateVector 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 @@ from homeassistant.const import ( 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 @@ from .const import ( ATTR_CALLSIGN, ATTR_ICAO24, ATTR_SENSOR, + CLIENT, CONF_ALTITUDE, DEFAULT_ALTITUDE, DOMAIN, @@ -36,6 +38,7 @@ from .const import ( # 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -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 @@ class OpenSkySensor(SensorEntity): 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 00000000000..768ffde155f --- /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 b4b9c409c6e..2359ac79e04 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -325,6 +325,7 @@ FLOWS = { "openexchangerates", "opengarage", "openhome", + "opensky", "opentherm_gw", "openuv", "openweathermap", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 6bc96ea15bc..938ffa13ab5 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 0820a3026e1..d2a9c711039 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 00000000000..f985f068ab1 --- /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 00000000000..63e514d0d8f --- /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 00000000000..e785a5f3a8f --- /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 00000000000..be1c21627f0 --- /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 00000000000..1768efebc78 --- /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 1f890a22a64..90ad926aeb7 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 53646c1e435..6af64060ab5 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 37b16530b0d..f761badfa2b 100644 --- a/homeassistant/components/whirlpool/sensor.py +++ b/homeassistant/components/whirlpool/sensor.py @@ -70,6 +70,7 @@ ICON_D = "mdi:tumble-dryer" 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 @@ class WasherDryerClass(SensorEntity): 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 @@ class WasherDryerTimeClass(RestoreSensor): """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 be78b0e2df8..4e451f46e9b 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -4,13 +4,14 @@ from unittest.mock import MagicMock 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 b37d242fe52..a1a71f6712e 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 4892fddd5ba..92d3ae94fb1 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 d2a9c711039..d10f29c5374 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 e0143d49259..8f7229bf5ad 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -377,7 +377,7 @@ class AsusWrtRouter: 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 65b12b64e58..a8f24a335f0 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 c709322e0b7..d6a29eb73f3 100644 --- a/homeassistant/components/shopping_list/intent.py +++ b/homeassistant/components/shopping_list/intent.py @@ -29,7 +29,6 @@ class AddItemIntent(intent.IntentHandler): 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 cf576fc1c83..a9239bcfda8 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 92d3ae94fb1..a51c0e57a6d 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 d10f29c5374..b2d39c0d787 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 85d5b5daa91..a08823255e9 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 899fd761d5e..af9af468453 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 e5f1e30efdb..a28b1ee0cfb 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 e7ebb7b546b..08f25d20eff 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 a51c0e57a6d..6b77bbc03b9 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 b2d39c0d787..1320097746a 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 296857e1cfa..fcfe71a2858 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -171,6 +171,7 @@ ENTITY_DESCRIPTIONS: tuple[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 @@ ENTITY_DESCRIPTIONS: tuple[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 70b28e34dd0..54b9cb12157 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 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): 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 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): 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 c26f06cb5f2..25c368880fa 100644 --- a/homeassistant/components/unifi/image.py +++ b/homeassistant/components/unifi/image.py @@ -67,6 +67,7 @@ ENTITY_DESCRIPTIONS: tuple[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 3682fa0bf6c..8cdc0dcbb71 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -14,9 +14,11 @@ import aiounifi 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 @@ from .entity import ( 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 @@ ENTITY_DESCRIPTIONS: tuple[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 @@ ENTITY_DESCRIPTIONS: tuple[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 @@ ENTITY_DESCRIPTIONS: tuple[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 @@ ENTITY_DESCRIPTIONS: tuple[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 ca11cdfea30..64e3ec2455c 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -186,6 +186,7 @@ ENTITY_DESCRIPTIONS: tuple[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 @@ ENTITY_DESCRIPTIONS: tuple[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 @@ ENTITY_DESCRIPTIONS: tuple[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 @@ ENTITY_DESCRIPTIONS: tuple[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 @@ ENTITY_DESCRIPTIONS: tuple[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 ea02b144a2f..661a9016bdc 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -74,6 +74,7 @@ ENTITY_DESCRIPTIONS: tuple[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 bf7ba4d53c0..d619cd4c3c9 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -19,6 +19,7 @@ from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY 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 @@ DEVICE_1 = { "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 95353e84754..5f2f58ce98a 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 6b77bbc03b9..9830e646566 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 1320097746a..1ed9ff34b10 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 5b1242423c7..958f46a5e04 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -163,6 +163,7 @@ class HistoryStatsSensor(HistoryStatsSensorBase): 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 @@ class HistoryStatsSensor(HistoryStatsSensorBase): 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 28e24b587aa..ddd11c0d768 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -334,7 +334,7 @@ async def test_measure_multiple(recorder_mock: Recorder, hass: HomeAssistant) -> 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 @@ async def test_measure(recorder_mock: Recorder, hass: HomeAssistant) -> None: 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 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin 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 @@ async def test_does_not_work_into_the_future( 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 @@ async def test_measure_sliding_window( 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 @@ async def test_measure_sliding_window( 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 @@ async def test_measure_from_end_going_backwards( 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 @@ async def test_measure_from_end_going_backwards( 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 @@ async def test_measure_cet(recorder_mock: Recorder, hass: HomeAssistant) -> None 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 @@ async def test_end_time_with_microseconds_zeroed( 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 @@ async def test_end_time_with_microseconds_zeroed( 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 @@ async def test_end_time_with_microseconds_zeroed( 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 6ef5917a0fb..d101517e002 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -95,6 +95,13 @@ class HKDevice: # 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 @@ class HKDevice: 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 0dfaf6e538c..cde9aa732c3 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -53,6 +53,9 @@ HOMEKIT_ACCESSORY_DISPATCH = { 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 229c8aecc00..bbc56ddd4a4 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 @@ async def async_setup_triggers_for_entry( 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 00000000000..9d70127f74a --- /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 e47ae0fca84..901378c8cb9 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 00000000000..9731f429eaf --- /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 8a72fadfbd9..7f925f69809 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 5125f69d03a..df135b65bbe 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 7f925f69809..10acd5dd65a 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 00000000000..a2767cb749b --- /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 00000000000..a72e450e2e5 --- /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 00000000000..1e6c26a057b --- /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 00000000000..995953c5e36 --- /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 00000000000..118392c6a69 --- /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 00000000000..a51954496cd --- /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 00000000000..c1760d3261b --- /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 00000000000..7d48635781b --- /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 00000000000..71ec95f825c --- /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 2359ac79e04..10221d1d589 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -338,6 +338,7 @@ FLOWS = { "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 938ffa13ab5..85138b82c82 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 9830e646566..d40375adfc0 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 1ed9ff34b10..ddb0f2f221a 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 00000000000..ac3f9bda7dd --- /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 00000000000..ffc2f88d5a8 --- /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 00000000000..93ade373315 --- /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 00000000000..216ca3427c5 --- /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 efa81c7b73c..9f4c30d91ba 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -7,13 +7,24 @@ from typing import Literal 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 @@ from .const import ( 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 @@ class OpenAIAgent(conversation.AbstractConversationAgent): 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 00000000000..81818fb3e71 --- /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 9583e759bd2..542fe06dd56 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 8ec8d5eac3e..08975c5c881 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 fe23bbac56c..1b9f81f60c0 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 118392c6a69..c8a01623c7d 100644 --- a/homeassistant/components/pegel_online/entity.py +++ b/homeassistant/components/pegel_online/entity.py @@ -8,7 +8,7 @@ from .const import DOMAIN 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 02919baa8f1..52a6b4162fe 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -337,10 +337,16 @@ class MatterLight(MatterEntity, LightEntity): # 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 00000000000..2a6ada2bf05 --- /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 00000000000..e6055d66462 --- /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 00000000000..a9bb8803f09 --- /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 00000000000..7cce2b5cf1a --- /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 00000000000..6808e129f87 --- /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 00000000000..355328481c3 --- /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 00000000000..2af337b64a4 --- /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 00000000000..7f0de5c464a --- /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 00000000000..cf1018dccef --- /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 00000000000..a54dfca196d --- /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 00000000000..a0d26cf7b13 --- /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 00000000000..e0c58729ce5 --- /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 00000000000..2ffcd22ade1 --- /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 00000000000..97a837399d0 --- /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 00000000000..e4402a7a3c2 --- /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 00000000000..4c1fa71a4b8 --- /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 00000000000..5af296c9bef --- /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 00000000000..5df86ac39a2 --- /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 85138b82c82..a3a8c334c11 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 6bc69125c30..fb1869b2489 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 a1cf1c907a6..668a38dae5b 100644 --- a/homeassistant/components/duotecno/__init__.py +++ b/homeassistant/components/duotecno/__init__.py @@ -11,7 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady 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 00000000000..13e3df8fc0a --- /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 0b4bacf00ca..c971bf8465e 100644 --- a/homeassistant/components/matter/discovery.py +++ b/homeassistant/components/matter/discovery.py @@ -12,6 +12,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 .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 @@ DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = { 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 00000000000..3a1faa6dcbe --- /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 85434407a10..2237f0ade98 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 61f1ca9180a..bfdba33327b 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 d40375adfc0..1db78e0c099 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 ddb0f2f221a..4701dafe91d 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 00000000000..15c93825307 --- /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 00000000000..30763c88e5b --- /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 00000000000..911dd0fe389 --- /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 4b54f9746a0..4c3ce680323 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 1db78e0c099..edd1edd7c2f 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 4701dafe91d..a07488a1898 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 cb9d1e8214e..07420233baf 100644 --- a/homeassistant/components/youtube/coordinator.py +++ b/homeassistant/components/youtube/coordinator.py @@ -5,13 +5,13 @@ from datetime import timedelta 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 @@ class YouTubeDataUpdateCoordinator(DataUpdateCoordinator[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 a63b8fb0c0b..99cd3ecf095 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 3c46ff92661..665f5f3a762 100644 --- a/tests/components/youtube/__init__.py +++ b/tests/components/youtube/__init__.py @@ -11,7 +11,7 @@ from tests.common import load_fixture class MockYouTube: """Service which returns mock objects.""" - _authenticated = False + _thrown_error: Exception | None = None def __init__( self, @@ -28,7 +28,6 @@ class MockYouTube: 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 @@ class MockYouTube: 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 @@ class MockYouTube: 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 a8a333190ee..8b6ce5d00a2 100644 --- a/tests/components/youtube/conftest.py +++ b/tests/components/youtube/conftest.py @@ -18,7 +18,7 @@ from tests.common import MockConfigEntry 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 7dc368a5860..9f0b63bc062 100644 --- a/tests/components/youtube/test_sensor.py +++ b/tests/components/youtube/test_sensor.py @@ -3,12 +3,11 @@ from datetime import timedelta 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 c39fbed180c..0e1688221b3 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -29,6 +29,7 @@ HUB_BUSY_SLEEP = 0.5 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 798148b92c0..d7d254b64a8 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 @@ DEFAULT_ALLOW_UNREACHABLE = False # 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 00000000000..8e34f7a22bf --- /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 aef5dba1986..a6920293ac1 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 466b593b56a..a3027736661 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -3,11 +3,6 @@ from __future__ import annotations 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.core import CALLBACK_TYPE, callback 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 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( } ) -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 01b9c7f84b8..415fe1324b7 100644 --- a/tests/components/hue/const.py +++ b/tests/components/hue/const.py @@ -18,6 +18,7 @@ FAKE_DEVICE = { {"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 @@ FAKE_SCENE = { "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 a7ad7ec1a00..371975e12a5 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 ef309849faa..28aa8626c42 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 ab400c53ee4..bfc0b612c1f 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 00000000000..e3f50318f61 --- /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 f2c1571fda2..957aa4a7806 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -123,7 +123,7 @@ class HueLight(HueBaseEntity, LightEntity): """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 513d72555a5..c965cf78528 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 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 1f179518fd9..a15355dc9cf 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 6600a8240a0..627736f605d 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -97,9 +97,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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 a9239bcfda8..3ce056e1a46 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 9302d547786..02d528c33e2 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -123,10 +123,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 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 30ecf16bb37..29dd56c11ec 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 b0a3702b5c9..04aafc8a99d 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -11,7 +11,14 @@ from pathlib import Path 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 @@ _ENTITY_REGISTRY_UPDATE_FIELDS = ["aliases", "name", "original_name"] 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 @@ class DefaultAgent(AbstractConversationAgent): } 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 @@ class DefaultAgent(AbstractConversationAgent): 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 @@ class DefaultAgent(AbstractConversationAgent): "'%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 a8f24a335f0..1eb58e96ff9 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 b64b74c5fa6..71ddb5c1237 100644 --- a/homeassistant/components/conversation/trigger.py +++ b/homeassistant/components/conversation/trigger.py @@ -3,7 +3,7 @@ from __future__ import annotations 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 3ce056e1a46..3210d76964e 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 edd1edd7c2f..2be19a3052c 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 a07488a1898..220c21bada3 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 8ef0cef52f9..f9fe284bcb0 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 af9af468453..c3c2e621260 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 522162fa457..3f4dd9e3a7e 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 39dbef48647..67bce66e167 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 aed12596176..072f0988c19 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 efa462ee4e6..5fda5f532a3 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 | 40 ++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/zodiac/__init__.py b/homeassistant/components/zodiac/__init__.py index 81d5b5bdc21..48d1d8aa7aa 100644 --- a/homeassistant/components/zodiac/__init__.py +++ b/homeassistant/components/zodiac/__init__.py @@ -17,27 +17,27 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the zodiac component.""" - - 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 + 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", + }, + ) + + 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 1ef87e84933..b221db96262 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 2be19a3052c..049199b3890 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 220c21bada3..67ab0c73b79 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 995953c5e36..8fab3ce36ae 100644 --- a/homeassistant/components/pegel_online/coordinator.py +++ b/homeassistant/components/pegel_online/coordinator.py @@ -31,10 +31,10 @@ class PegelOnlineDataUpdateCoordinator(DataUpdateCoordinator[PegelOnlineData]): 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 c1760d3261b..c8dac75bcf2 100644 --- a/homeassistant/components/pegel_online/model.py +++ b/homeassistant/components/pegel_online/model.py @@ -8,4 +8,4 @@ from aiopegelonline import CurrentMeasurement 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 7d48635781b..14ec0c2d032 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 71ec95f825c..930e349f9c3 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 89bd601fdae..f0c32f2d8cc 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -272,8 +272,6 @@ class WeatherEntity(Entity): "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 d82308a2e32..6c843246663 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -84,6 +84,8 @@ class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): 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 ba5f25a1c02..8c5c206ae9f 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -45,7 +45,6 @@ _KNOWN_LRU_CLASSES = ( "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 70b681ffbb2..9f0849a4d4c 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -1135,7 +1135,16 @@ class MqttEntity( "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 5a30a3a65de..23367d7829f 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -212,6 +212,26 @@ async def test_availability_with_shared_state_topic( 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 @@ async def test_availability_with_shared_state_topic( "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 5f6aa63ce2f..d26116a7818 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 049199b3890..2f9b876a2a1 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 67ab0c73b79..3742431f646 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 bfdba33327b..c68b38bbb8c 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 cbdef90f623..0c8e90b8895 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 @@ class SQLSensor(ManualTriggerEntity, SensorEntity): 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 @@ class SQLSensor(ManualTriggerEntity, SensorEntity): 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 f1eed86f10c..5764f65957e 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast 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 @@ ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30 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 @@ class DeviceRegistry: 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 @@ class DeviceRegistry: *, 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 8e07897c84f..7d240cc0320 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -15,6 +15,7 @@ from timeit import default_timer as timer 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 3e59b08cfa8..0210d7ba75d 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 1f7e579ea95..3eaad662d8b 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1857,11 +1857,6 @@ async def test_device_name_defaulting_config_entry( "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 668a38dae5b..98003c3e8c4 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 13e3df8fc0a..0fd212df085 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 bc9b8722c58..63fe28cd400 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -92,6 +92,6 @@ class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity): 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 9214b9daeac..fa82c058f5b 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 dbd2c08b28e..0dd694a47d6 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -99,26 +99,26 @@ class HydrawiseSwitch(HydrawiseEntity, SwitchEntity): 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 48c9cdcf042..d9e6d809960 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 2f9b876a2a1..f99b58edd3e 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 b9b235ab41e..d35cf90c60f 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 f99b58edd3e..35563221833 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 3742431f646..9f3e1c60dd1 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 d7d254b64a8..38c2587bc1a 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -43,11 +43,11 @@ 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 + # 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 21d0da074a7..00000000000 --- 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 a6920293ac1..6d65abc8d5f 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 bfc0b612c1f..e89f53af73a 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 e3f50318f61..4dbb104357d 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 3f49efcdeb7..00000000000 --- 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 d101517e002..4ba22317644 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -142,7 +142,7 @@ class HKDevice: 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 @@ class HKDevice: 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 @@ class HKDevice: 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 @@ class HKDevice: 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 f6aadfac7ac..046dc9f17ec 100644 --- a/homeassistant/components/homekit_controller/entity.py +++ b/homeassistant/components/homekit_controller/entity.py @@ -58,7 +58,9 @@ class HomeKitEntity(Entity): 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 3210d76964e..a0046569eb8 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 02d528c33e2..b2954dc777b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -61,7 +61,9 @@ CONSTRAINT_BASE = """ 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 f859919fe07..8cc80ef864e 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 35563221833..29420b03e8b 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 9f3e1c60dd1..b186f63d129 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 c965cf78528..c856cf47329 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 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 a15355dc9cf..1b621d828fb 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 47fbde3ef20..bc3b68922b0 100644 --- a/homeassistant/components/verisure/coordinator.py +++ b/homeassistant/components/verisure/coordinator.py @@ -7,7 +7,6 @@ from time import sleep from verisure import ( Error as VerisureError, LoginError as VerisureLoginError, - ResponseError as VerisureResponseError, Session as Verisure, ) @@ -50,7 +49,7 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): 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 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): 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 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): 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 66dccdc07de..98440f67e4c 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 29420b03e8b..f9ea0c59975 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 b186f63d129..ea3923064eb 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 00f0e0f518b..25994d56250 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 f9ea0c59975..7a23a68bd91 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 ea3923064eb..0a568957800 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 cbeab2abec0..bc07e2b94ae 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 a0046569eb8..be1dff7623d 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 7a23a68bd91..da60be35125 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 0a568957800..70252698d2c 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 4ee97020724..f25c3410edb 100644 --- a/homeassistant/components/jellyfin/__init__.py +++ b/homeassistant/components/jellyfin/__init__.py @@ -4,6 +4,7 @@ from typing import Any 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 3d5b150f39f..f4ab98ca268 100644 --- a/homeassistant/components/jellyfin/coordinator.py +++ b/homeassistant/components/jellyfin/coordinator.py @@ -47,6 +47,7 @@ class JellyfinDataUpdateCoordinator(DataUpdateCoordinator[JellyfinDataT], ABC): 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 @@ class SessionsDataUpdateCoordinator( 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 56e352bd71f..9af73391d18 100644 --- a/tests/components/jellyfin/test_init.py +++ b/tests/components/jellyfin/test_init.py @@ -4,10 +4,29 @@ from unittest.mock import MagicMock 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 f90b887d04c..26182102442 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 da60be35125..2f9a9033d28 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 70252698d2c..31561fd3c20 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 81fbda63fef..36b1661e1ac 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -457,7 +457,7 @@ class ReolinkHost: 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 a68083856f7..cc4cd269606 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -157,7 +157,6 @@ class ScrapeSensor( """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 0c8e90b8895..aecc34d7009 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -344,16 +344,15 @@ class SQLSensor(ManualTriggerEntity, SensorEntity): 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 b7be7c2c9a6..2e5cebf8571 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -626,6 +626,11 @@ class ManualTriggerEntity(TriggerBaseEntity): ) -> 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 a35650f0092..d00f1b33ccc 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -127,7 +127,12 @@ class MediaExtractor: _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 98dd6036bc9..f6ba2d79bfe 100644 --- a/homeassistant/components/event/__init__.py +++ b/homeassistant/components/event/__init__.py @@ -45,7 +45,6 @@ __all__ = [ "EventDeviceClass", "EventEntity", "EventEntityDescription", - "EventEntityFeature", ] # mypy: disallow-any-generics @@ -104,7 +103,7 @@ class EventExtraStoredData(ExtraStoredData): 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 2e54e773a44..5464a241b7a 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 2f9a9033d28..471246ea230 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 31561fd3c20..f4c318f45dd 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 08f25d20eff..c054af8f43c 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 471246ea230..74cc2d7f124 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 f4c318f45dd..8c40163b0c6 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 36b1661e1ac..5882e5e66a4 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -267,7 +267,19 @@ class ReolinkHost: 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 @@ class ReolinkHost: 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 ea02bfedefb..a89ee370920 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 c07a083ac52..96474ceb7eb 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 be1dff7623d..04c0b0fd44f 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 74cc2d7f124..e5d8223eeaf 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 8c40163b0c6..978874dddfa 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 6b0a4cd6b26..b308d8dc08c 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 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): 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 c856cf47329..43809524d4a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 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 1b621d828fb..aaf057da02c 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 3f5487881a3..e64a2f14667 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 @@ class WyomingSttProvider(stt.SpeechToTextEntity): """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 @@ class WyomingSttProvider(stt.SpeechToTextEntity): ) 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 0dd9041a0d5..6b4e705914f 100644 --- a/tests/components/wyoming/conftest.py +++ b/tests/components/wyoming/conftest.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch 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 08fe6a1ef8e..784f89b2ab8 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 021419f3a5e..1938d44d310 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 test_streaming_audio(hass: HomeAssistant, init_wyoming_stt, snapshot) "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 test_streaming_audio(hass: HomeAssistant, init_wyoming_stt, snapshot) 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 test_streaming_audio_connection_lost( "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 test_streaming_audio_oserror(hass: HomeAssistant, init_wyoming_stt) -> "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 2de87659919..88eec9780a1 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -9,6 +9,7 @@ import logging 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_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b 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 5882e5e66a4..5a0289c38b1 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -24,7 +24,7 @@ from homeassistant.helpers.network import NoURLAvailableError, get_url 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 @@ class ReolinkHost: 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 25994d56250..fa61f873cca 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 e5d8223eeaf..3c3bdaea46b 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 978874dddfa..bebdfa4dfbe 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 5a0289c38b1..9bcafb8f00d 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -26,6 +26,7 @@ from .exceptions import ReolinkSetupException, ReolinkWebhookException, UserNotA 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 @@ class ReolinkHost: # 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 @@ class ReolinkHost: 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 @@ class ReolinkHost: "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 @@ class ReolinkHost: 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 1e588d5e3a1..f5f581760c1 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 8ecc8a0e8c4..6f72f31ae8f 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -7,7 +7,12 @@ from datetime import timedelta 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.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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, DOMAIN @@ -187,3 +192,5 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): 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 3feddacd4e5..cb7a969379e 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 3c3bdaea46b..fb98e1633b5 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 bebdfa4dfbe..cd42bdfa9ca 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 e8eabe887f2..07fbc0ca8c5 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -36,6 +36,7 @@ from homeassistant.core import ( ) 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 @@ from .const import ( DEFAULT_WILL, DEFAULT_WS_HEADERS, DEFAULT_WS_PATH, + DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, PROTOCOL_5, @@ -93,6 +95,10 @@ SUBSCRIBE_COOLDOWN = 0.1 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 @@ class MQTT: @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 @@ class MQTT: 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 9f0849a4d4c..70156703155 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 @@ class MqttEntity( 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 @@ class MqttEntity( @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 @@ class MqttEntity( 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 @@ class MqttEntity( # 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 @@ class MqttEntity( "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 5a966a4455c..9afa3de3f48 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 f314ddd47d3..55677798a08 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 23367d7829f..18269eb6970 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -6,14 +6,19 @@ 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.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 @@ async def test_availability_with_shared_state_topic( @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 @@ async def test_availability_with_shared_state_topic( DEFAULT_SENSOR_NAME, None, True, + 0, ), ( # default_entity_name_with_device_name { @@ -111,6 +124,7 @@ async def test_availability_with_shared_state_topic( "Test MQTT Sensor", "Test", False, + 0, ), ( # name_follows_device_class { @@ -127,6 +141,7 @@ async def test_availability_with_shared_state_topic( "Test Humidity", "Test", False, + 0, ), ( # name_follows_device_class_without_device_name { @@ -143,6 +158,7 @@ async def test_availability_with_shared_state_topic( "Humidity", None, True, + 0, ), ( # name_overrides_device_class { @@ -160,6 +176,7 @@ async def test_availability_with_shared_state_topic( "Test MySensor", "Test", False, + 0, ), ( # name_set_no_device_name_set { @@ -177,6 +194,7 @@ async def test_availability_with_shared_state_topic( "MySensor", None, True, + 0, ), ( # none_entity_name_with_device_name { @@ -194,6 +212,7 @@ async def test_availability_with_shared_state_topic( "Test", "Test", False, + 0, ), ( # none_entity_name_without_device_name { @@ -211,8 +230,9 @@ async def test_availability_with_shared_state_topic( "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 @@ async def test_availability_with_shared_state_topic( "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 @@ async def test_availability_with_shared_state_topic( "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 bb19d2e1655..3dbd8aa32bc 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 fb98e1633b5..b708fbe3f5f 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 cd42bdfa9ca..e31b4513070 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 b02938b1652..f69451290bc 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 7f2aee9d7bd..415b07dc7e6 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 5537a18725c..c318d662028 100644 --- a/homeassistant/components/radarr/coordinator.py +++ b/homeassistant/components/radarr/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations 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 7e574b1e3e0..069eeabe8d8 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 00000000000..db9e720d285 --- /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 00000000000..085467fda6a --- /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 00000000000..25a93baa10d --- /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 d3dde74dcbf..f4f863d9bb6 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 . import setup_integration 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 582698e39d5..5fa44f10c2c 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 8a7fe185662..b2702ed1815 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 19fd0b518b2..04ba4cc1a6a 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 b708fbe3f5f..9e51bab7254 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 e31b4513070..28d677a795e 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 c054af8f43c..c0eb319c10c 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 9e51bab7254..ae8111ec0eb 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 28d677a795e..5f2d89c544e 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 bfecce8d3ed..18b83013d70 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 ae8111ec0eb..ba56ae548e6 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 5f2d89c544e..78f01c83e39 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 3dbd8aa32bc..cb189116eeb 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 ba56ae548e6..c61ebf9527b 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 78f01c83e39..3af0fd1fa42 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 efcf92600b8..ab76a8dffdd 100644 --- a/homeassistant/components/starlink/sensor.py +++ b/homeassistant/components/starlink/sensor.py @@ -130,6 +130,6 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( 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 e970f040b5f..98cc4c4b4e8 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 3a3d9afb643..7045a90e6e9 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 47e742bdb76..2210a44039e 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 04c0b0fd44f..3a887804470 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 c61ebf9527b..7ff7d9b82f5 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 3af0fd1fa42..23df6a82bcd 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 43809524d4a..936393cc8cb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 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 aaf057da02c..a47d883d146 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 a630a3dedbd..3089e3b515b 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 7ff7d9b82f5..aed6674d810 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 23df6a82bcd..64118d5a30b 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 21effd3da3a..f68ffbd0eaf 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv 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 @@ class RovaData: 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 b308d8dc08c..c35b4dc9b13 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -161,14 +161,29 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): 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 e55d4583275..ac121a93eff 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 25c368880fa..dc4fb93eded 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 7694a85b8ed..5e33377ec0e 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 aed6674d810..8d52c7a3927 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 64118d5a30b..2a7ec674fc3 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 5764f65957e..4dd9233c6ab 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 @@ class DeviceRegistry: 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 @@ class DeviceRegistry: 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 0210d7ba75d..9ebee025bd5 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 ef31a887691..522b60749d0 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 936393cc8cb..0dfe6664dcc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 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 a47d883d146..692ad56dcc7 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 92daffc6c8b..73ebe15d0c7 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 3a887804470..076e534a6b0 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 8d52c7a3927..a1f09f5ec36 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 2a7ec674fc3..395417cebb4 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 aa89d87b6be..a9e50f5d39f 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 3089e3b515b..ae82574146e 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 a3a8c334c11..350bcde8236 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 958f46a5e04..baa39468bc1 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -174,7 +174,10 @@ class HistoryStatsSensor(HistoryStatsSensorBase): 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 ddd11c0d768..bb4b5b275d2 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -386,6 +386,7 @@ async def test_measure(recorder_mock: Recorder, hass: HomeAssistant) -> None: "start": "{{ as_timestamp(utcnow()) - 3600 }}", "end": "{{ utcnow() }}", "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", }, { "platform": "history_stats", @@ -413,7 +414,7 @@ async def test_measure(recorder_mock: Recorder, hass: HomeAssistant) -> None: 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 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin "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 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin 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 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin 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 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin 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 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin 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 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin 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 @@ async def test_does_not_work_into_the_future( "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 @@ async def test_does_not_work_into_the_future( 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 @@ async def test_does_not_work_into_the_future( 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 @@ async def test_does_not_work_into_the_future( 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 @@ async def test_does_not_work_into_the_future( 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 @@ async def test_does_not_work_into_the_future( 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 @@ async def test_measure_sliding_window( "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 @@ async def test_measure_sliding_window( 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 @@ async def test_measure_sliding_window( 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 @@ async def test_measure_from_end_going_backwards( "duration": {"hours": 1}, "end": "{{ utcnow() }}", "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", }, { "platform": "history_stats", @@ -1269,7 +1306,7 @@ async def test_measure_from_end_going_backwards( 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 @@ async def test_measure_from_end_going_backwards( 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 @@ async def test_measure_cet(recorder_mock: Recorder, hass: HomeAssistant) -> None "start": "{{ as_timestamp(utcnow()) - 3600 }}", "end": "{{ utcnow() }}", "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", }, { "platform": "history_stats", @@ -1362,7 +1400,7 @@ async def test_measure_cet(recorder_mock: Recorder, hass: HomeAssistant) -> None 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 @@ async def test_end_time_with_microseconds_zeroed( "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 @@ async def test_end_time_with_microseconds_zeroed( 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 @@ async def test_end_time_with_microseconds_zeroed( 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 @@ async def test_end_time_with_microseconds_zeroed( 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 @@ async def test_end_time_with_microseconds_zeroed( 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 @@ async def test_end_time_with_microseconds_zeroed( 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 @@ async def test_end_time_with_microseconds_zeroed( 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 0dfe6664dcc..db7ef9e305a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 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 692ad56dcc7..26a9525a176 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 2210a44039e..84d1d4f5e27 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 076e534a6b0..7401c747890 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 a1f09f5ec36..28140477411 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 395417cebb4..03dc3bbf994 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