From 729c6db082eb92033b5cc70deff59baa1a9b81ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 31 Jan 2024 09:47:31 -1000 Subject: [PATCH 0001/1367] Bump govee-ble to 0.31.0 (#109235) --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 1cfa367ebe7..64feedc44c1 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -90,5 +90,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/govee_ble", "iot_class": "local_push", - "requirements": ["govee-ble==0.27.3"] + "requirements": ["govee-ble==0.31.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 290a8ef9050..cf45302ca5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -961,7 +961,7 @@ goslide-api==0.5.1 gotailwind==0.2.2 # homeassistant.components.govee_ble -govee-ble==0.27.3 +govee-ble==0.31.0 # homeassistant.components.govee_light_local govee-local-api==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c611169af69..22fe88663ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -778,7 +778,7 @@ googlemaps==2.5.1 gotailwind==0.2.2 # homeassistant.components.govee_ble -govee-ble==0.27.3 +govee-ble==0.31.0 # homeassistant.components.govee_light_local govee-local-api==1.4.1 From 552d14204d256003ed9dc5994d856db7464131ef Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Wed, 31 Jan 2024 20:32:27 +0000 Subject: [PATCH 0002/1367] Bump ring_doorbell to 0.8.6 (#109199) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 85cab6f1763..a2ccb2bf444 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/ring", "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], - "requirements": ["ring-doorbell[listen]==0.8.5"] + "requirements": ["ring-doorbell[listen]==0.8.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index cf45302ca5b..66a649f3614 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2429,7 +2429,7 @@ rfk101py==0.0.1 rflink==0.0.65 # homeassistant.components.ring -ring-doorbell[listen]==0.8.5 +ring-doorbell[listen]==0.8.6 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 22fe88663ab..9ea9c6f0c56 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1860,7 +1860,7 @@ reolink-aio==0.8.7 rflink==0.0.65 # homeassistant.components.ring -ring-doorbell[listen]==0.8.5 +ring-doorbell[listen]==0.8.6 # homeassistant.components.roku rokuecp==0.18.1 From 1584f02e7199566c4fe3fda4e763d121c4af70bb Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 1 Feb 2024 06:52:58 +1000 Subject: [PATCH 0003/1367] Fix time to arrival to timestamp in Tessie (#109172) * Fix time to arrival * Update snapshot * Freeze time for snapshot * Fix docstring * Add available_fn * Update snapshot * Dont use variance for full charge * Remove unrelated changes * Revert snapshot * Rename hours_to_datetime --- homeassistant/components/tessie/sensor.py | 23 +++-- homeassistant/components/tessie/strings.json | 2 +- .../tessie/snapshots/test_sensor.ambr | 92 +++++++++---------- tests/components/tessie/test_sensor.py | 8 +- 4 files changed, 69 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/tessie/sensor.py b/homeassistant/components/tessie/sensor.py index 07f54ebde5b..ae9e06b2b35 100644 --- a/homeassistant/components/tessie/sensor.py +++ b/homeassistant/components/tessie/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 typing import cast from homeassistant.components.sensor import ( SensorDeviceClass, @@ -29,6 +30,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util +from homeassistant.util.variance import ignore_variance from .const import DOMAIN from .coordinator import TessieStateUpdateCoordinator @@ -36,8 +38,8 @@ from .entity import TessieEntity @callback -def hours_to_datetime(value: StateType) -> datetime | None: - """Convert relative hours into absolute datetime.""" +def minutes_to_datetime(value: StateType) -> datetime | None: + """Convert relative minutes into absolute datetime.""" if isinstance(value, (int, float)) and value > 0: return dt_util.now() + timedelta(minutes=value) return None @@ -48,6 +50,7 @@ class TessieSensorEntityDescription(SensorEntityDescription): """Describes Tessie Sensor entity.""" value_fn: Callable[[StateType], StateType | datetime] = lambda x: x + available_fn: Callable[[StateType], bool] = lambda _: True DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( @@ -95,7 +98,7 @@ DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( key="charge_state_minutes_to_full_charge", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=hours_to_datetime, + value_fn=minutes_to_datetime, ), TessieSensorEntityDescription( key="charge_state_battery_range", @@ -219,9 +222,12 @@ DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( ), TessieSensorEntityDescription( key="drive_state_active_route_minutes_to_arrival", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfTime.MINUTES, - device_class=SensorDeviceClass.DURATION, + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=ignore_variance( + lambda value: dt_util.now() + timedelta(minutes=cast(float, value)), + timedelta(seconds=30), + ), + available_fn=lambda x: x is not None, ), TessieSensorEntityDescription( key="drive_state_active_route_destination", @@ -262,3 +268,8 @@ class TessieSensorEntity(TessieEntity, SensorEntity): def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" return self.entity_description.value_fn(self.get()) + + @property + def available(self) -> bool: + """Return if sensor is available.""" + return super().available and self.entity_description.available_fn(self.get()) diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 57ba1f12bec..8340557843d 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -142,7 +142,7 @@ "drive_state_active_route_miles_to_arrival": { "name": "Distance to arrival" }, - "drive_state_active_route_time_to_arrival": { + "drive_state_active_route_minutes_to_arrival": { "name": "Time to arrival" }, "drive_state_active_route_destination": { diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index 921aba0b330..2f5e1e8ddb2 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -493,54 +493,6 @@ 'state': '22.5', }) # --- -# name: test_sensors[sensor.test_duration-entry] - 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.test_duration', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Duration', - 'platform': 'tessie', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'drive_state_active_route_minutes_to_arrival', - 'unique_id': 'VINVINVIN-drive_state_active_route_minutes_to_arrival', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.test_duration-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'Test Duration', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.test_duration', - 'last_changed': , - 'last_updated': , - 'state': '59.2', - }) -# --- # name: test_sensors[sensor.test_inside_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -953,6 +905,50 @@ 'state': '65', }) # --- +# name: test_sensors[sensor.test_time_to_arrival-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_time_to_arrival', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Time to arrival', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state_active_route_minutes_to_arrival', + 'unique_id': 'VINVINVIN-drive_state_active_route_minutes_to_arrival', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_time_to_arrival-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Test Time to arrival', + }), + 'context': , + 'entity_id': 'sensor.test_time_to_arrival', + 'last_changed': , + 'last_updated': , + 'state': '2024-01-01T00:59:12+00:00', + }) +# --- # name: test_sensors[sensor.test_time_to_full_charge-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tessie/test_sensor.py b/tests/components/tessie/test_sensor.py index fef251f0108..090f9df0ca5 100644 --- a/tests/components/tessie/test_sensor.py +++ b/tests/components/tessie/test_sensor.py @@ -1,4 +1,5 @@ """Test the Tessie sensor platform.""" +from freezegun.api import FrozenDateTimeFactory from syrupy import SnapshotAssertion from homeassistant.const import Platform @@ -9,10 +10,15 @@ from .common import assert_entities, setup_platform async def test_sensors( - hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Tests that the sensor entities are correct.""" + freezer.move_to("2024-01-01 00:00:00+00:00") + entry = await setup_platform(hass, [Platform.SENSOR]) assert_entities(hass, entry.entry_id, entity_registry, snapshot) From d1c61c911d72dd906900ac052ae81c9480e058f6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 31 Jan 2024 22:05:13 +0100 Subject: [PATCH 0004/1367] Bump version to 2024.3.0dev0 (#109238) --- .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 f7854ef88df..522544bba80 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -36,7 +36,7 @@ env: CACHE_VERSION: 5 PIP_CACHE_VERSION: 4 MYPY_CACHE_VERSION: 7 - HA_SHORT_VERSION: "2024.2" + HA_SHORT_VERSION: "2024.3" DEFAULT_PYTHON: "3.11" ALL_PYTHON_VERSIONS: "['3.11', '3.12']" # 10.3 is the oldest supported version diff --git a/homeassistant/const.py b/homeassistant/const.py index 8db9be36902..cd68b98b2e3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -15,7 +15,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 -MINOR_VERSION: Final = 2 +MINOR_VERSION: Final = 3 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 462d3d326d5..d7680e5e871 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.0.dev0" +version = "2024.3.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2aead3eefc9e6945b6cfb663b356eb93ebbda624 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 31 Jan 2024 22:10:32 +0100 Subject: [PATCH 0005/1367] Fix kitchen sink tests (#109243) --- tests/components/kitchen_sink/test_init.py | 47 +++++++++++++++++----- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/tests/components/kitchen_sink/test_init.py b/tests/components/kitchen_sink/test_init.py index 71f3a83c701..c65d53478d2 100644 --- a/tests/components/kitchen_sink/test_init.py +++ b/tests/components/kitchen_sink/test_init.py @@ -102,6 +102,7 @@ async def test_demo_statistics_growth( assert statistics[statistic_id][0]["sum"] <= (2**20 + 24) +@pytest.mark.freeze_time("2023-10-21") async def test_issues_created( mock_history, hass: HomeAssistant, @@ -125,7 +126,7 @@ async def test_issues_created( "issues": [ { "breaks_in_ha_version": "2023.1.1", - "created": ANY, + "created": "2023-10-21T00:00:00+00:00", "dismissed_version": None, "domain": DOMAIN, "ignored": False, @@ -139,7 +140,7 @@ async def test_issues_created( }, { "breaks_in_ha_version": "2023.1.1", - "created": ANY, + "created": "2023-10-21T00:00:00+00:00", "dismissed_version": None, "domain": DOMAIN, "ignored": False, @@ -153,7 +154,7 @@ async def test_issues_created( }, { "breaks_in_ha_version": None, - "created": ANY, + "created": "2023-10-21T00:00:00+00:00", "dismissed_version": None, "domain": DOMAIN, "ignored": False, @@ -167,7 +168,7 @@ async def test_issues_created( }, { "breaks_in_ha_version": None, - "created": ANY, + "created": "2023-10-21T00:00:00+00:00", "dismissed_version": None, "domain": DOMAIN, "ignored": False, @@ -181,7 +182,7 @@ async def test_issues_created( }, { "breaks_in_ha_version": None, - "created": ANY, + "created": "2023-10-21T00:00:00+00:00", "dismissed_version": None, "domain": DOMAIN, "is_fixable": True, @@ -193,6 +194,20 @@ async def test_issues_created( "translation_placeholders": None, "ignored": False, }, + { + "breaks_in_ha_version": None, + "created": "2023-10-21T00:00:00+00:00", + "dismissed_version": None, + "domain": "homeassistant", + "is_fixable": False, + "issue_domain": DOMAIN, + "issue_id": ANY, + "learn_more_url": None, + "severity": "error", + "translation_key": "config_entry_reauth", + "translation_placeholders": None, + "ignored": False, + }, ] } @@ -242,7 +257,7 @@ async def test_issues_created( "issues": [ { "breaks_in_ha_version": "2023.1.1", - "created": ANY, + "created": "2023-10-21T00:00:00+00:00", "dismissed_version": None, "domain": DOMAIN, "ignored": False, @@ -256,7 +271,7 @@ async def test_issues_created( }, { "breaks_in_ha_version": None, - "created": ANY, + "created": "2023-10-21T00:00:00+00:00", "dismissed_version": None, "domain": DOMAIN, "ignored": False, @@ -270,7 +285,7 @@ async def test_issues_created( }, { "breaks_in_ha_version": None, - "created": ANY, + "created": "2023-10-21T00:00:00+00:00", "dismissed_version": None, "domain": DOMAIN, "ignored": False, @@ -284,7 +299,7 @@ async def test_issues_created( }, { "breaks_in_ha_version": None, - "created": ANY, + "created": "2023-10-21T00:00:00+00:00", "dismissed_version": None, "domain": DOMAIN, "is_fixable": True, @@ -296,5 +311,19 @@ async def test_issues_created( "translation_placeholders": None, "ignored": False, }, + { + "breaks_in_ha_version": None, + "created": "2023-10-21T00:00:00+00:00", + "dismissed_version": None, + "domain": "homeassistant", + "is_fixable": False, + "issue_domain": DOMAIN, + "issue_id": ANY, + "learn_more_url": None, + "severity": "error", + "translation_key": "config_entry_reauth", + "translation_placeholders": None, + "ignored": False, + }, ] } From 08f8f84f617e398aecfc2dac255a289c63fc5711 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 31 Jan 2024 19:34:33 -0500 Subject: [PATCH 0006/1367] Bump opower to 0.3.0 (#109248) Co-authored-by: Dan Swartz <3066652+swartzd@users.noreply.github.com> --- 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 e654c044c16..ce1ed47dcc6 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.2.0"] + "requirements": ["opower==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 66a649f3614..a2a9d9072a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1465,7 +1465,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.2.0 +opower==0.3.0 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ea9c6f0c56..1db9b89861a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1153,7 +1153,7 @@ openerz-api==0.3.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.2.0 +opower==0.3.0 # homeassistant.components.oralb oralb-ble==0.17.6 From 2b525ed2e91d0d412fc1c6316eb89bf9b7fbc4eb Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 31 Jan 2024 20:55:48 -0500 Subject: [PATCH 0007/1367] Make zwave_js last seen sensor enabled by default (#109191) * Make zwave_js last seen sensor enabled by default * Add test * Fix test * improve tests --- homeassistant/components/zwave_js/sensor.py | 3 ++- .../zwave_js/fixtures/zp3111-5_state.json | 3 ++- tests/components/zwave_js/test_discovery.py | 24 ++++++++++++------- tests/components/zwave_js/test_init.py | 16 ++++++++----- tests/components/zwave_js/test_sensor.py | 21 +++++++++++++--- 5 files changed, 48 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 9fed7158d4a..0b9defc5f62 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -343,6 +343,7 @@ class ZWaveJSStatisticsSensorEntityDescription(SensorEntityDescription): convert: Callable[ [ControllerStatisticsDataType | NodeStatisticsDataType, str], Any ] = lambda statistics, key: statistics.get(key) + entity_registry_enabled_default: bool = False # Controller statistics descriptions @@ -487,6 +488,7 @@ ENTITY_DESCRIPTION_NODE_STATISTICS_LIST = [ else None ) ), + entity_registry_enabled_default=True, ), ] @@ -930,7 +932,6 @@ class ZWaveStatisticsSensor(SensorEntity): entity_description: ZWaveJSStatisticsSensorEntityDescription _attr_should_poll = False _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_entity_registry_enabled_default = False _attr_has_entity_name = True def __init__( diff --git a/tests/components/zwave_js/fixtures/zp3111-5_state.json b/tests/components/zwave_js/fixtures/zp3111-5_state.json index 68bb0f03af8..55f27b7fa5a 100644 --- a/tests/components/zwave_js/fixtures/zp3111-5_state.json +++ b/tests/components/zwave_js/fixtures/zp3111-5_state.json @@ -694,7 +694,8 @@ "commandsRX": 0, "commandsDroppedRX": 0, "commandsDroppedTX": 0, - "timeoutResponse": 0 + "timeoutResponse": 0, + "lastSeen": "2024-01-01T12:00:00+00" }, "highestSecurityClass": -1, "isControllerNode": false diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 569e36d3b5c..67f4a8d962f 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -22,9 +22,10 @@ from homeassistant.components.zwave_js.discovery import ( from homeassistant.components.zwave_js.discovery_data_template import ( DynamicCurrentTempClimateDataTemplate, ) +from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNKNOWN, EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er async def test_iblinds_v2(hass: HomeAssistant, client, iblinds_v2, integration) -> None: @@ -224,14 +225,21 @@ async def test_indicator_test( This test covers indicators that we don't already have device fixtures for. """ + device = dr.async_get(hass).async_get_device( + identifiers={get_device_id(client.driver, indicator_test)} + ) + assert device ent_reg = er.async_get(hass) - assert len(hass.states.async_entity_ids(NUMBER_DOMAIN)) == 0 - assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 1 # only ping - assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 1 - assert ( - len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 - ) # include node + controller status - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 + entities = er.async_entries_for_device(ent_reg, device.id) + + def len_domain(domain): + return len([entity for entity in entities if entity.domain == domain]) + + assert len_domain(NUMBER_DOMAIN) == 0 + assert len_domain(BUTTON_DOMAIN) == 1 # only ping + assert len_domain(BINARY_SENSOR_DOMAIN) == 1 + assert len_domain(SENSOR_DOMAIN) == 3 # include node status + last seen + assert len_domain(SWITCH_DOMAIN) == 1 entity_id = "binary_sensor.this_is_a_fake_device_binary_sensor" entry = ent_reg.async_get(entity_id) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 7f3a9428dad..4555ee59e1e 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -227,14 +227,16 @@ async def test_on_node_added_not_ready( client.driver.receive_event(event) await hass.async_block_till_done() - # the only entities are the node status sensor and ping button - assert len(hass.states.async_all()) == 3 - device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device # no extended device identifier yet assert len(device.identifiers) == 1 + ent_reg = er.async_get(hass) + entities = er.async_entries_for_device(ent_reg, device.id) + # the only entities are the node status sensor, last_seen sensor, and ping button + assert len(entities) == 3 + async def test_existing_node_ready( hass: HomeAssistant, client, multisensor_6, integration @@ -329,14 +331,16 @@ async def test_existing_node_not_ready( assert not device.model assert not device.sw_version - # the only entities are the node status sensor and ping button - assert len(hass.states.async_all()) == 3 - device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device # no extended device identifier yet assert len(device.identifiers) == 1 + ent_reg = er.async_get(hass) + entities = er.async_entries_for_device(ent_reg, device.id) + # the only entities are the node status sensor, last_seen sensor, and ping button + assert len(entities) == 3 + async def test_existing_node_not_replaced_when_not_ready( hass: HomeAssistant, diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 4e88b2b50cc..a3d36b84382 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -731,14 +731,13 @@ NODE_STATISTICS_SUFFIXES = { NODE_STATISTICS_SUFFIXES_UNKNOWN = { "round_trip_time": 6, "rssi": 7, - "last_seen": "2024-01-01T00:00:00+00:00", } -async def test_statistics_sensors( +async def test_statistics_sensors_no_last_seen( hass: HomeAssistant, zp3111, client, integration, caplog: pytest.LogCaptureFixture ) -> None: - """Test statistics sensors.""" + """Test all statistics sensors but last seen which is enabled by default.""" ent_reg = er.async_get(hass) for prefix, suffixes in ( @@ -880,6 +879,22 @@ async def test_statistics_sensors( ) +async def test_last_seen_statistics_sensors( + hass: HomeAssistant, zp3111, client, integration +) -> None: + """Test last_seen statistics sensors.""" + ent_reg = er.async_get(hass) + + entity_id = f"{NODE_STATISTICS_ENTITY_PREFIX}last_seen" + entry = ent_reg.async_get(entity_id) + assert entry + assert not entry.disabled + + state = hass.states.get(entity_id) + assert state + assert state.state == "2024-01-01T12:00:00+00:00" + + ENERGY_PRODUCTION_ENTITY_MAP = { "energy_production_power": { "state": 1.23, From 31094e72a07e377b323a090b28ab82ef6503675d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 1 Feb 2024 03:38:16 +0100 Subject: [PATCH 0008/1367] Pass verify_ssl to created session in Omada (#109212) * Pass verify_ssl to created session in Omada * Fix tests * Fix tests --- homeassistant/components/tplink_omada/config_flow.py | 4 +++- tests/components/tplink_omada/test_config_flow.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink_omada/config_flow.py b/homeassistant/components/tplink_omada/config_flow.py index f6a75abe6d8..3f27417894d 100644 --- a/homeassistant/components/tplink_omada/config_flow.py +++ b/homeassistant/components/tplink_omada/config_flow.py @@ -61,7 +61,9 @@ async def create_omada_client( is not None ): # TP-Link API uses cookies for login session, so an unsafe cookie jar is required for IP addresses - websession = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) + websession = async_create_clientsession( + hass, cookie_jar=CookieJar(unsafe=True), verify_ssl=verify_ssl + ) else: websession = async_get_clientsession(hass, verify_ssl=verify_ssl) diff --git a/tests/components/tplink_omada/test_config_flow.py b/tests/components/tplink_omada/test_config_flow.py index cf3fddf5943..1a9635d44cb 100644 --- a/tests/components/tplink_omada/test_config_flow.py +++ b/tests/components/tplink_omada/test_config_flow.py @@ -401,7 +401,7 @@ async def test_create_omada_client_with_ip_creates_clientsession( hass, { "host": "10.10.10.10", - "verify_ssl": True, # Verify is meaningless for IP + "verify_ssl": True, "username": "test-username", "password": "test-password", }, @@ -412,5 +412,5 @@ async def test_create_omada_client_with_ip_creates_clientsession( "https://10.10.10.10", "test-username", "test-password", "ws" ) mock_create_clientsession.assert_called_once_with( - hass, cookie_jar=mock_jar.return_value + hass, cookie_jar=mock_jar.return_value, verify_ssl=True ) From 5c3bf13ca4bf434ad2e9766cdebab20de9394470 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 31 Jan 2024 16:42:58 -1000 Subject: [PATCH 0009/1367] Only decode msg topic once when handling mqtt payloads (#109258) --- homeassistant/components/mqtt/client.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 164632cdd10..2f6c6dc648c 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -818,25 +818,29 @@ class MQTT: @callback def _mqtt_handle_message(self, msg: mqtt.MQTTMessage) -> None: + topic = msg.topic + # msg.topic is a property that decodes the topic to a string + # every time it is accessed. Save the result to avoid + # decoding the same topic multiple times. _LOGGER.debug( "Received%s message on %s (qos=%s): %s", " retained" if msg.retain else "", - msg.topic, + topic, msg.qos, msg.payload[0:8192], ) timestamp = dt_util.utcnow() - subscriptions = self._matching_subscriptions(msg.topic) + subscriptions = self._matching_subscriptions(topic) for subscription in subscriptions: if msg.retain: retained_topics = self._retained_topics.setdefault(subscription, set()) # Skip if the subscription already received a retained message - if msg.topic in retained_topics: + if topic in retained_topics: continue # Remember the subscription had an initial retained message - self._retained_topics[subscription].add(msg.topic) + self._retained_topics[subscription].add(topic) payload: SubscribePayloadType = msg.payload if subscription.encoding is not None: @@ -846,7 +850,7 @@ class MQTT: _LOGGER.warning( "Can't decode payload %s on %s with encoding %s (for %s)", msg.payload[0:8192], - msg.topic, + topic, subscription.encoding, subscription.job, ) @@ -854,7 +858,7 @@ class MQTT: self.hass.async_run_hass_job( subscription.job, ReceiveMessage( - msg.topic, + topic, payload, msg.qos, msg.retain, From c355dd77a4a730dadcb3b7de4b14c23460a3f5c7 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 1 Feb 2024 08:26:39 +0100 Subject: [PATCH 0010/1367] Fix ZHA update entity not updating installed version (#109260) --- homeassistant/components/zha/update.py | 1 + tests/components/zha/test_update.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/update.py b/homeassistant/components/zha/update.py index 93912fc68db..e92424acf47 100644 --- a/homeassistant/components/zha/update.py +++ b/homeassistant/components/zha/update.py @@ -122,6 +122,7 @@ class ZHAFirmwareUpdateEntity(ZhaEntity, UpdateEntity): self._latest_version_firmware = image self._attr_latest_version = f"0x{image.header.file_version:08x}" self._image_type = image.header.image_type + self._attr_installed_version = self.determine_installed_version() self.async_write_ha_state() @callback diff --git a/tests/components/zha/test_update.py b/tests/components/zha/test_update.py index c1424ca1730..981b8ba5e1b 100644 --- a/tests/components/zha/test_update.py +++ b/tests/components/zha/test_update.py @@ -108,12 +108,18 @@ async def setup_test_data( return zha_device, cluster, fw_image, installed_fw_version +@pytest.mark.parametrize("initial_version_unknown", (False, True)) async def test_firmware_update_notification_from_zigpy( - hass: HomeAssistant, zha_device_joined_restored, zigpy_device + hass: HomeAssistant, + zha_device_joined_restored, + zigpy_device, + initial_version_unknown, ) -> None: """Test ZHA update platform - firmware update notification.""" zha_device, cluster, fw_image, installed_fw_version = await setup_test_data( - zha_device_joined_restored, zigpy_device + zha_device_joined_restored, + zigpy_device, + skip_attribute_plugs=initial_version_unknown, ) entity_id = find_entity_id(Platform.UPDATE, zha_device, hass) From 0caed803201e3dfce5d714d84592782be3a9d328 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 08:28:23 +0100 Subject: [PATCH 0011/1367] Bump sigstore/cosign-installer from 3.3.0 to 3.4.0 (#109278) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 16a48d3cb48..62bb1263a59 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -341,7 +341,7 @@ jobs: uses: actions/checkout@v4.1.1 - name: Install Cosign - uses: sigstore/cosign-installer@v3.3.0 + uses: sigstore/cosign-installer@v3.4.0 with: cosign-release: "v2.0.2" From 723d9c4c8a5e4abe5aaee41aee5ef4c1e89f927f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 1 Feb 2024 08:52:07 +0100 Subject: [PATCH 0012/1367] Fix dalkin climate warnings (#109279) --- homeassistant/components/daikin/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 0c955b1ce4f..047acd3cccf 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -142,7 +142,11 @@ class DaikinClimate(ClimateEntity): ATTR_SWING_MODE: self._attr_swing_modes, } - self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + self._attr_supported_features = ( + ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TARGET_TEMPERATURE + ) if api.device.support_away_mode or api.device.support_advanced_modes: self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE From 5d3364521f8e9138b721c1d2265d25da65d8f631 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 31 Jan 2024 21:53:18 -1000 Subject: [PATCH 0013/1367] Fix app name sorting in apple_tv (#109274) --- homeassistant/components/apple_tv/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index e8fd9d5acfc..789415a1717 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -155,7 +155,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): else: self._app_list = { app_name: app.identifier - for app in sorted(apps, key=lambda app: app_name.lower()) + for app in sorted(apps, key=lambda app: (app.name or "").lower()) if (app_name := app.name) is not None } self.async_write_ha_state() From 8afcd53af6d842c760199252dbbd0b36dfc18f48 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 31 Jan 2024 21:56:57 -1000 Subject: [PATCH 0014/1367] Restore support for packages being installed from urls with fragments (#109267) --- homeassistant/util/package.py | 23 +++++++++++++++++++++-- tests/util/test_package.py | 15 ++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index d487edee4a4..ce6276ef4d4 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -9,6 +9,7 @@ import os from pathlib import Path from subprocess import PIPE, Popen import sys +from urllib.parse import urlparse from packaging.requirements import InvalidRequirement, Requirement @@ -40,14 +41,32 @@ def is_installed(requirement_str: str) -> bool: expected input is a pip compatible package specifier (requirement string) e.g. "package==1.0.0" or "package>=1.0.0,<2.0.0" + For backward compatibility, it also accepts a URL with a fragment + e.g. "git+https://github.com/pypa/pip#pip>=1" + Returns True when the requirement is met. Returns False when the package is not installed or doesn't meet req. """ try: req = Requirement(requirement_str) except InvalidRequirement: - _LOGGER.error("Invalid requirement '%s'", requirement_str) - return False + if "#" not in requirement_str: + _LOGGER.error("Invalid requirement '%s'", requirement_str) + return False + + # This is likely a URL with a fragment + # example: git+https://github.com/pypa/pip#pip>=1 + + # fragment support was originally used to install zip files, and + # we no longer do this in Home Assistant. However, custom + # components started using it to install packages from git + # urls which would make it would be a breaking change to + # remove it. + try: + req = Requirement(urlparse(requirement_str).fragment) + except InvalidRequirement: + _LOGGER.error("Invalid requirement '%s'", requirement_str) + return False try: if (installed_version := version(req.name)) is None: diff --git a/tests/util/test_package.py b/tests/util/test_package.py index e940fdf6f9c..42ba0131d71 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -217,7 +217,7 @@ async def test_async_get_user_site(mock_env_copy) -> None: assert ret == os.path.join(deps_dir, "lib_dir") -def test_check_package_global() -> None: +def test_check_package_global(caplog: pytest.LogCaptureFixture) -> None: """Test for an installed package.""" pkg = metadata("homeassistant") installed_package = pkg["name"] @@ -229,10 +229,19 @@ def test_check_package_global() -> None: assert package.is_installed(f"{installed_package}<={installed_version}") assert not package.is_installed(f"{installed_package}<{installed_version}") + assert package.is_installed("-1 invalid_package") is False + assert "Invalid requirement '-1 invalid_package'" in caplog.text -def test_check_package_zip() -> None: - """Test for an installed zip package.""" + +def test_check_package_fragment(caplog: pytest.LogCaptureFixture) -> None: + """Test for an installed package with a fragment.""" assert not package.is_installed(TEST_ZIP_REQ) + assert package.is_installed("git+https://github.com/pypa/pip#pip>=1") + assert not package.is_installed("git+https://github.com/pypa/pip#-1 invalid") + assert ( + "Invalid requirement 'git+https://github.com/pypa/pip#-1 invalid'" + in caplog.text + ) def test_get_is_installed() -> None: From a075accbe30702d89b9933825e4b53145c5aad2c Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 1 Feb 2024 08:57:12 +0100 Subject: [PATCH 0015/1367] Fix two icon translations for La Marzocco (#109284) --- homeassistant/components/lamarzocco/icons.json | 2 +- homeassistant/components/lamarzocco/switch.py | 1 + tests/components/lamarzocco/snapshots/test_switch.ambr | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lamarzocco/icons.json b/homeassistant/components/lamarzocco/icons.json index c893ba42848..70adfe95134 100644 --- a/homeassistant/components/lamarzocco/icons.json +++ b/homeassistant/components/lamarzocco/icons.json @@ -82,7 +82,7 @@ "off": "mdi:alarm-off" } }, - "steam_boiler_enable": { + "steam_boiler": { "default": "mdi:water-boiler", "state": { "on": "mdi:water-boiler", diff --git a/homeassistant/components/lamarzocco/switch.py b/homeassistant/components/lamarzocco/switch.py index 4cab49064e7..0d4d8d7dc8e 100644 --- a/homeassistant/components/lamarzocco/switch.py +++ b/homeassistant/components/lamarzocco/switch.py @@ -28,6 +28,7 @@ class LaMarzoccoSwitchEntityDescription( ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = ( LaMarzoccoSwitchEntityDescription( key="main", + translation_key="main", name=None, control_fn=lambda coordinator, state: coordinator.lm.set_power(state), is_on_fn=lambda coordinator: coordinator.lm.current_status["power"], diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index bf7062d65bd..789e979894e 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -65,7 +65,7 @@ 'platform': 'lamarzocco', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'main', 'unique_id': 'GS01234_main', 'unit_of_measurement': None, }) From 421e2761858baae9516285ba204b0fcb2c1e27f5 Mon Sep 17 00:00:00 2001 From: Jan Rieger Date: Thu, 1 Feb 2024 08:58:32 +0100 Subject: [PATCH 0016/1367] Add icon translations to GPSd (#108602) --- homeassistant/components/gpsd/icons.json | 13 +++++++++++++ homeassistant/components/gpsd/sensor.py | 9 --------- 2 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/gpsd/icons.json diff --git a/homeassistant/components/gpsd/icons.json b/homeassistant/components/gpsd/icons.json new file mode 100644 index 00000000000..b29640e0001 --- /dev/null +++ b/homeassistant/components/gpsd/icons.json @@ -0,0 +1,13 @@ +{ + "entity": { + "sensor": { + "mode": { + "default": "mdi:crosshairs", + "state": { + "2d_fix": "mdi:crosshairs-gps", + "3d_fix": "mdi:crosshairs-gps" + } + } + } + } +} diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index 932db081598..d5d25397f2a 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -144,12 +144,3 @@ class GpsdSensor(SensorEntity): ATTR_CLIMB: self.agps_thread.data_stream.climb, ATTR_MODE: self.agps_thread.data_stream.mode, } - - @property - def icon(self) -> str: - """Return the icon of the sensor.""" - mode = self.agps_thread.data_stream.mode - - if isinstance(mode, int) and mode >= 2: - return "mdi:crosshairs-gps" - return "mdi:crosshairs" From 175ec812694bc76fba79da4c669cc7a6cf2c9037 Mon Sep 17 00:00:00 2001 From: Luis Andrade Date: Thu, 1 Feb 2024 03:00:22 -0500 Subject: [PATCH 0017/1367] bugfix: name missing in getLogger (#109282) --- homeassistant/components/wyoming/satellite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wyoming/satellite.py b/homeassistant/components/wyoming/satellite.py index 0cb2796b9f0..ea7a7d5df0c 100644 --- a/homeassistant/components/wyoming/satellite.py +++ b/homeassistant/components/wyoming/satellite.py @@ -25,7 +25,7 @@ from .const import DOMAIN from .data import WyomingService from .devices import SatelliteDevice -_LOGGER = logging.getLogger() +_LOGGER = logging.getLogger(__name__) _SAMPLES_PER_CHUNK: Final = 1024 _RECONNECT_SECONDS: Final = 10 From e11e54fd506a6cae81c4833bf34715f8a7a96d2e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 1 Feb 2024 09:02:38 +0100 Subject: [PATCH 0018/1367] Apply review comments on proximity (#109249) use a named tuple as TrackedEntityDescriptor --- homeassistant/components/proximity/sensor.py | 27 +++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/proximity/sensor.py b/homeassistant/components/proximity/sensor.py index a1bd4d33914..4b1e1d1f29d 100644 --- a/homeassistant/components/proximity/sensor.py +++ b/homeassistant/components/proximity/sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import NamedTuple + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -50,6 +52,13 @@ SENSORS_PER_PROXIMITY: list[SensorEntityDescription] = [ ] +class TrackedEntityDescriptor(NamedTuple): + """Descriptor of a tracked entity.""" + + entity_id: str + identifier: str + + def _device_info(coordinator: ProximityDataUpdateCoordinator) -> DeviceInfo: return DeviceInfo( identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, @@ -70,23 +79,17 @@ async def async_setup_entry( for description in SENSORS_PER_PROXIMITY ] - tracked_entity_descriptors = [] + tracked_entity_descriptors: list[TrackedEntityDescriptor] = [] entity_reg = er.async_get(hass) for tracked_entity_id in coordinator.tracked_entities: if (entity_entry := entity_reg.async_get(tracked_entity_id)) is not None: tracked_entity_descriptors.append( - { - "entity_id": tracked_entity_id, - "identifier": entity_entry.id, - } + TrackedEntityDescriptor(tracked_entity_id, entity_entry.id) ) else: tracked_entity_descriptors.append( - { - "entity_id": tracked_entity_id, - "identifier": tracked_entity_id, - } + TrackedEntityDescriptor(tracked_entity_id, tracked_entity_id) ) entities += [ @@ -139,15 +142,15 @@ class ProximityTrackedEntitySensor( self, description: SensorEntityDescription, coordinator: ProximityDataUpdateCoordinator, - tracked_entity_descriptor: dict[str, str], + tracked_entity_descriptor: TrackedEntityDescriptor, ) -> None: """Initialize the proximity.""" super().__init__(coordinator) self.entity_description = description - self.tracked_entity_id = tracked_entity_descriptor["entity_id"] + self.tracked_entity_id = tracked_entity_descriptor.entity_id - self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{tracked_entity_descriptor['identifier']}_{description.key}" + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{tracked_entity_descriptor.identifier}_{description.key}" self._attr_name = f"{self.tracked_entity_id.split('.')[-1]} {description.name}" self._attr_device_info = _device_info(coordinator) From 697d4987c17c408d20a08abf904f942f11a23f1c Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Thu, 1 Feb 2024 09:04:02 +0100 Subject: [PATCH 0019/1367] Fix Xiaomi-ble automations for multiple button devices (#109251) --- homeassistant/components/xiaomi_ble/const.py | 7 ++ .../components/xiaomi_ble/device_trigger.py | 67 +++++++++++-------- .../components/xiaomi_ble/strings.json | 3 + .../xiaomi_ble/test_device_trigger.py | 64 +++++++++++++++++- 4 files changed, 112 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/const.py b/homeassistant/components/xiaomi_ble/const.py index b6a6369e258..1accfd9dc55 100644 --- a/homeassistant/components/xiaomi_ble/const.py +++ b/homeassistant/components/xiaomi_ble/const.py @@ -21,8 +21,15 @@ XIAOMI_BLE_EVENT: Final = "xiaomi_ble_event" EVENT_CLASS_BUTTON: Final = "button" EVENT_CLASS_MOTION: Final = "motion" +BUTTON: Final = "button" +DOUBLE_BUTTON: Final = "double_button" +TRIPPLE_BUTTON: Final = "tripple_button" +MOTION: Final = "motion" + BUTTON_PRESS: Final = "button_press" BUTTON_PRESS_DOUBLE_LONG: Final = "button_press_double_long" +DOUBLE_BUTTON_PRESS_DOUBLE_LONG: Final = "double_button_press_double_long" +TRIPPLE_BUTTON_PRESS_DOUBLE_LONG: Final = "tripple_button_press_double_long" MOTION_DEVICE: Final = "motion_device" diff --git a/homeassistant/components/xiaomi_ble/device_trigger.py b/homeassistant/components/xiaomi_ble/device_trigger.py index a2373da89b4..6d29af9ac11 100644 --- a/homeassistant/components/xiaomi_ble/device_trigger.py +++ b/homeassistant/components/xiaomi_ble/device_trigger.py @@ -21,15 +21,21 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import ( + BUTTON, BUTTON_PRESS, BUTTON_PRESS_DOUBLE_LONG, CONF_SUBTYPE, DOMAIN, + DOUBLE_BUTTON, + DOUBLE_BUTTON_PRESS_DOUBLE_LONG, EVENT_CLASS, EVENT_CLASS_BUTTON, EVENT_CLASS_MOTION, EVENT_TYPE, + MOTION, MOTION_DEVICE, + TRIPPLE_BUTTON, + TRIPPLE_BUTTON_PRESS_DOUBLE_LONG, XIAOMI_BLE_EVENT, ) @@ -39,47 +45,47 @@ TRIGGERS_BY_TYPE = { MOTION_DEVICE: ["motion_detected"], } +EVENT_TYPES = { + BUTTON: ["button"], + DOUBLE_BUTTON: ["button_left", "button_right"], + TRIPPLE_BUTTON: ["button_left", "button_middle", "button_right"], + MOTION: ["motion"], +} + @dataclass class TriggerModelData: """Data class for trigger model data.""" - schema: vol.Schema event_class: str + event_types: list[str] triggers: list[str] TRIGGER_MODEL_DATA = { BUTTON_PRESS: TriggerModelData( - schema=DEVICE_TRIGGER_BASE_SCHEMA.extend( - { - vol.Required(CONF_TYPE): vol.In([EVENT_CLASS_BUTTON]), - vol.Required(CONF_SUBTYPE): vol.In(TRIGGERS_BY_TYPE[BUTTON_PRESS]), - } - ), event_class=EVENT_CLASS_BUTTON, + event_types=EVENT_TYPES[BUTTON], triggers=TRIGGERS_BY_TYPE[BUTTON_PRESS], ), BUTTON_PRESS_DOUBLE_LONG: TriggerModelData( - schema=DEVICE_TRIGGER_BASE_SCHEMA.extend( - { - vol.Required(CONF_TYPE): vol.In([EVENT_CLASS_BUTTON]), - vol.Required(CONF_SUBTYPE): vol.In( - TRIGGERS_BY_TYPE[BUTTON_PRESS_DOUBLE_LONG] - ), - } - ), event_class=EVENT_CLASS_BUTTON, + event_types=EVENT_TYPES[BUTTON], + triggers=TRIGGERS_BY_TYPE[BUTTON_PRESS_DOUBLE_LONG], + ), + DOUBLE_BUTTON_PRESS_DOUBLE_LONG: TriggerModelData( + event_class=EVENT_CLASS_BUTTON, + event_types=EVENT_TYPES[DOUBLE_BUTTON], + triggers=TRIGGERS_BY_TYPE[BUTTON_PRESS_DOUBLE_LONG], + ), + TRIPPLE_BUTTON_PRESS_DOUBLE_LONG: TriggerModelData( + event_class=EVENT_CLASS_BUTTON, + event_types=EVENT_TYPES[TRIPPLE_BUTTON], triggers=TRIGGERS_BY_TYPE[BUTTON_PRESS_DOUBLE_LONG], ), MOTION_DEVICE: TriggerModelData( - schema=DEVICE_TRIGGER_BASE_SCHEMA.extend( - { - vol.Required(CONF_TYPE): vol.In([EVENT_CLASS_MOTION]), - vol.Required(CONF_SUBTYPE): vol.In(TRIGGERS_BY_TYPE[MOTION_DEVICE]), - } - ), event_class=EVENT_CLASS_MOTION, + event_types=EVENT_TYPES[MOTION], triggers=TRIGGERS_BY_TYPE[MOTION_DEVICE], ), } @@ -90,13 +96,13 @@ MODEL_DATA = { "MS1BB(MI)": TRIGGER_MODEL_DATA[BUTTON_PRESS], "RTCGQ02LM": TRIGGER_MODEL_DATA[BUTTON_PRESS], "SJWS01LM": TRIGGER_MODEL_DATA[BUTTON_PRESS], - "K9B-1BTN": TRIGGER_MODEL_DATA[BUTTON_PRESS_DOUBLE_LONG], - "K9B-2BTN": TRIGGER_MODEL_DATA[BUTTON_PRESS_DOUBLE_LONG], - "K9B-3BTN": TRIGGER_MODEL_DATA[BUTTON_PRESS_DOUBLE_LONG], "K9BB-1BTN": TRIGGER_MODEL_DATA[BUTTON_PRESS_DOUBLE_LONG], "YLAI003": TRIGGER_MODEL_DATA[BUTTON_PRESS_DOUBLE_LONG], "XMWXKG01LM": TRIGGER_MODEL_DATA[BUTTON_PRESS_DOUBLE_LONG], - "XMWXKG01YL": TRIGGER_MODEL_DATA[BUTTON_PRESS_DOUBLE_LONG], + "K9B-1BTN": TRIGGER_MODEL_DATA[BUTTON_PRESS_DOUBLE_LONG], + "XMWXKG01YL": TRIGGER_MODEL_DATA[DOUBLE_BUTTON_PRESS_DOUBLE_LONG], + "K9B-2BTN": TRIGGER_MODEL_DATA[DOUBLE_BUTTON_PRESS_DOUBLE_LONG], + "K9B-3BTN": TRIGGER_MODEL_DATA[TRIPPLE_BUTTON_PRESS_DOUBLE_LONG], "MUE4094RT": TRIGGER_MODEL_DATA[MOTION_DEVICE], } @@ -107,7 +113,13 @@ async def async_validate_trigger_config( """Validate trigger config.""" device_id = config[CONF_DEVICE_ID] if model_data := _async_trigger_model_data(hass, device_id): - return model_data.schema(config) # type: ignore[no-any-return] + schema = DEVICE_TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(model_data.event_types), + vol.Required(CONF_SUBTYPE): vol.In(model_data.triggers), + } + ) + return schema(config) # type: ignore[no-any-return] return config @@ -120,7 +132,7 @@ async def async_get_triggers( if not (model_data := _async_trigger_model_data(hass, device_id)): return [] - event_type = model_data.event_class + event_types = model_data.event_types event_subtypes = model_data.triggers return [ { @@ -132,6 +144,7 @@ async def async_get_triggers( CONF_TYPE: event_type, CONF_SUBTYPE: event_subtype, } + for event_type in event_types for event_subtype in event_subtypes ] diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 2017ee674bb..c7cbe43bd94 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -48,6 +48,9 @@ }, "trigger_type": { "button": "Button \"{subtype}\"", + "button_left": "Button Left \"{subtype}\"", + "button_middle": "Button Middle \"{subtype}\"", + "button_right": "Button Right \"{subtype}\"", "motion": "{subtype}" } }, diff --git a/tests/components/xiaomi_ble/test_device_trigger.py b/tests/components/xiaomi_ble/test_device_trigger.py index 5c86173ca01..31f896680bf 100644 --- a/tests/components/xiaomi_ble/test_device_trigger.py +++ b/tests/components/xiaomi_ble/test_device_trigger.py @@ -164,8 +164,8 @@ async def test_get_triggers_double_button(hass: HomeAssistant) -> None: CONF_PLATFORM: "device", CONF_DOMAIN: DOMAIN, CONF_DEVICE_ID: device.id, - CONF_TYPE: "button", - CONF_SUBTYPE: "press", + CONF_TYPE: "button_right", + CONF_SUBTYPE: "long_press", "metadata": {}, } triggers = await async_get_device_automations( @@ -334,6 +334,66 @@ async def test_if_fires_on_button_press(hass: HomeAssistant, calls) -> None: await hass.async_block_till_done() +async def test_if_fires_on_double_button_long_press(hass: HomeAssistant, calls) -> None: + """Test for button press event trigger firing.""" + mac = "DC:ED:83:87:12:73" + data = {"bindkey": "b93eb3787eabda352edd94b667f5d5a9"} + entry = await _async_setup_xiaomi_device(hass, mac, data) + + # Emit left button press event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + mac, + b"XYI\x19Ks\x12\x87\x83\xed\xdc!\xad\xb4\xcd\x02\x00\x00,\xf3\xd9\x83", + ), + ) + + # wait for the device being created + await hass.async_block_till_done() + + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) + device_id = device.id + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + CONF_TYPE: "button_right", + CONF_SUBTYPE: "press", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_right_button_press"}, + }, + }, + ] + }, + ) + # Emit right button press event + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + mac, + b"XYI\x19Ps\x12\x87\x83\xed\xdc\x13~~\xbe\x02\x00\x00\xf0\\;4", + ), + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == "test_trigger_right_button_press" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + async def test_if_fires_on_motion_detected(hass: HomeAssistant, calls) -> None: """Test for motion event trigger firing.""" mac = "DE:70:E8:B2:39:0C" From f87550c9090514bcf60163856b1def411fbee3f7 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 1 Feb 2024 02:08:32 -0600 Subject: [PATCH 0020/1367] Update rokuecp to 0.19 (#109100) --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 6fe70a3ab65..4e255fcf86c 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -11,7 +11,7 @@ "iot_class": "local_polling", "loggers": ["rokuecp"], "quality_scale": "silver", - "requirements": ["rokuecp==0.18.1"], + "requirements": ["rokuecp==0.19.0"], "ssdp": [ { "st": "roku:ecp", diff --git a/requirements_all.txt b/requirements_all.txt index a2a9d9072a3..62ad5051483 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2441,7 +2441,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.18.1 +rokuecp==0.19.0 # homeassistant.components.romy romy==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1db9b89861a..77d4d1e943b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1863,7 +1863,7 @@ rflink==0.0.65 ring-doorbell[listen]==0.8.6 # homeassistant.components.roku -rokuecp==0.18.1 +rokuecp==0.19.0 # homeassistant.components.romy romy==0.0.7 From 459022d030c477b98bcd141bf8e2dd48e2507972 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 1 Feb 2024 09:30:29 +0100 Subject: [PATCH 0021/1367] Remove quality scale platinum from daikin integration (#109292) --- homeassistant/components/daikin/manifest.json | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 7c7f5ce7f2a..0b97ff6b902 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -6,7 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/daikin", "iot_class": "local_polling", "loggers": ["pydaikin"], - "quality_scale": "platinum", "requirements": ["pydaikin==2.11.1"], "zeroconf": ["_dkapi._tcp.local."] } From 68697230038d24900558e87a0fd486e8192d2bd4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 1 Feb 2024 10:20:52 +0100 Subject: [PATCH 0022/1367] Fix device class repairs issues UOM placeholders in Group (#109294) --- homeassistant/components/group/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index 402a8242af7..3c8f7059901 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -559,7 +559,7 @@ class SensorGroup(GroupEntity, SensorEntity): "entity_id": self.entity_id, "device_class": device_class, "source_entities": ", ".join(self._entity_ids), - "uoms:": ", ".join(unit_of_measurements), + "uoms": ", ".join(unit_of_measurements), }, ) else: @@ -574,7 +574,7 @@ class SensorGroup(GroupEntity, SensorEntity): translation_placeholders={ "entity_id": self.entity_id, "source_entities": ", ".join(self._entity_ids), - "uoms:": ", ".join(unit_of_measurements), + "uoms": ", ".join(unit_of_measurements), }, ) return None From f791c77f3b517a14233c414cc6f6a2955c93bfb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 1 Feb 2024 11:11:28 +0100 Subject: [PATCH 0023/1367] Bump hass-nabucasa from 0.75.1 to 0.76.0 (#109296) --- homeassistant/components/cloud/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/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index f7337e1d771..d314aac2092 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -8,5 +8,5 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["hass_nabucasa"], - "requirements": ["hass-nabucasa==0.75.1"] + "requirements": ["hass-nabucasa==0.76.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cf9fa157f26..c5cea22795a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.1.0 habluetooth==2.4.0 -hass-nabucasa==0.75.1 +hass-nabucasa==0.76.0 hassil==1.6.0 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240131.0 diff --git a/requirements_all.txt b/requirements_all.txt index 62ad5051483..84a55262a16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1019,7 +1019,7 @@ habitipy==0.2.0 habluetooth==2.4.0 # homeassistant.components.cloud -hass-nabucasa==0.75.1 +hass-nabucasa==0.76.0 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77d4d1e943b..84ce0a83a06 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -827,7 +827,7 @@ habitipy==0.2.0 habluetooth==2.4.0 # homeassistant.components.cloud -hass-nabucasa==0.75.1 +hass-nabucasa==0.76.0 # homeassistant.components.conversation hassil==1.6.0 From 286c5faa792b37c9f290bc875f90a1f36d7d946a Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 1 Feb 2024 11:14:33 +0100 Subject: [PATCH 0024/1367] Address late review of Tankerkoenig package move (#109277) --- homeassistant/components/tankerkoenig/config_flow.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tankerkoenig/config_flow.py b/homeassistant/components/tankerkoenig/config_flow.py index e15bfbfeb94..9bdf5ef0fe0 100644 --- a/homeassistant/components/tankerkoenig/config_flow.py +++ b/homeassistant/components/tankerkoenig/config_flow.py @@ -57,8 +57,11 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - _data: dict[str, Any] = {} - _stations: dict[str, str] = {} + def __init__(self) -> None: + """Init the FlowHandler.""" + super().__init__() + self._data: dict[str, Any] = {} + self._stations: dict[str, str] = {} @staticmethod @callback From 1af25bc010ed7d64c560a4e3f07b8105d3f9c548 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 1 Feb 2024 05:21:40 -0500 Subject: [PATCH 0025/1367] Remove deprecation warnings for zwave_js climate TURN_ON/TURN_OFF features (#109242) --- homeassistant/components/zwave_js/climate.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 2506db13f6d..f5ad8ce36cd 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -129,6 +129,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): """Representation of a Z-Wave climate.""" _attr_precision = PRECISION_TENTHS + _enable_turn_on_off_backwards_compatibility = False def __init__( self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo @@ -193,6 +194,16 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): self._set_modes_and_presets() if self._current_mode and len(self._hvac_presets) > 1: self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE + if HVACMode.OFF in self._hvac_modes: + self._attr_supported_features |= ClimateEntityFeature.TURN_OFF + + # We can only support turn on if we are able to turn the device off, + # otherwise the device can be considered always on + if len(self._hvac_modes) == 2 or any( + mode in self._hvac_modes + for mode in (HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL) + ): + self._attr_supported_features |= ClimateEntityFeature.TURN_ON # If any setpoint value exists, we can assume temperature # can be set if any(self._setpoint_values.values()): From 9bb6d756f17a4a834004a0660773993cc68c48e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Thu, 1 Feb 2024 12:59:41 +0100 Subject: [PATCH 0026/1367] Bump airthings-ble to 0.6.1 (#109302) Bump airthings-ble --- homeassistant/components/airthings_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airthings_ble/manifest.json b/homeassistant/components/airthings_ble/manifest.json index 03b42410d66..97e27793da2 100644 --- a/homeassistant/components/airthings_ble/manifest.json +++ b/homeassistant/components/airthings_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/airthings_ble", "iot_class": "local_polling", - "requirements": ["airthings-ble==0.6.0"] + "requirements": ["airthings-ble==0.6.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 84a55262a16..109152e088d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -410,7 +410,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.6.0 +airthings-ble==0.6.1 # homeassistant.components.airthings airthings-cloud==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 84ce0a83a06..906085c2d4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -383,7 +383,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.6.0 +airthings-ble==0.6.1 # homeassistant.components.airthings airthings-cloud==0.2.0 From d2dee9e32792071d468bf8283966d129fd0bdf00 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 1 Feb 2024 13:29:01 +0100 Subject: [PATCH 0027/1367] Update ruff to 0.1.15 (#109303) --- .pre-commit-config.yaml | 2 +- .../components/bluetooth/passive_update_processor.py | 1 + homeassistant/components/easyenergy/diagnostics.py | 1 + homeassistant/components/easyenergy/sensor.py | 1 + homeassistant/components/energyzero/diagnostics.py | 1 + homeassistant/components/energyzero/sensor.py | 1 + homeassistant/components/nuki/__init__.py | 1 + homeassistant/components/sensor/__init__.py | 1 + homeassistant/components/snapcast/server.py | 1 + homeassistant/components/wemo/wemo_device.py | 1 + .../components/zwave_js/discovery_data_template.py | 2 ++ homeassistant/helpers/config_validation.py | 1 + homeassistant/helpers/template.py | 1 + requirements_test_pre_commit.txt | 2 +- tests/components/blackbird/test_media_player.py | 4 ++-- tests/components/cast/test_media_player.py | 7 ++++--- tests/components/directv/test_media_player.py | 8 ++++---- tests/components/monoprice/test_media_player.py | 4 ++-- tests/components/nx584/test_binary_sensor.py | 2 ++ tests/components/roku/test_media_player.py | 8 ++++---- tests/components/ws66i/test_media_player.py | 4 ++-- 21 files changed, 35 insertions(+), 19 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0db0244edc9..cc3be5c2391 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.1.8 + rev: v0.1.15 hooks: - id: ruff args: diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 601f78d4c8d..43991672e81 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -51,6 +51,7 @@ class PassiveBluetoothEntityKey: Example: key: temperature device_id: outdoor_sensor_1 + """ key: str diff --git a/homeassistant/components/easyenergy/diagnostics.py b/homeassistant/components/easyenergy/diagnostics.py index 6bc5ed3803a..0c885174872 100644 --- a/homeassistant/components/easyenergy/diagnostics.py +++ b/homeassistant/components/easyenergy/diagnostics.py @@ -21,6 +21,7 @@ def get_gas_price(data: EasyEnergyData, hours: int) -> float | None: Returns: The gas market price value. + """ if not data.gas_today: return None diff --git a/homeassistant/components/easyenergy/sensor.py b/homeassistant/components/easyenergy/sensor.py index 7298c49660f..0aab8c6ffa6 100644 --- a/homeassistant/components/easyenergy/sensor.py +++ b/homeassistant/components/easyenergy/sensor.py @@ -208,6 +208,7 @@ def get_gas_price(data: EasyEnergyData, hours: int) -> float | None: Returns: The gas market price value. + """ if data.gas_today is None: return None diff --git a/homeassistant/components/energyzero/diagnostics.py b/homeassistant/components/energyzero/diagnostics.py index 3b0c05b7368..b4018a32d3d 100644 --- a/homeassistant/components/energyzero/diagnostics.py +++ b/homeassistant/components/energyzero/diagnostics.py @@ -21,6 +21,7 @@ def get_gas_price(data: EnergyZeroData, hours: int) -> float | None: Returns: The gas market price value. + """ if not data.gas_today: return None diff --git a/homeassistant/components/energyzero/sensor.py b/homeassistant/components/energyzero/sensor.py index 59c44c1aad8..50cc2f21bd3 100644 --- a/homeassistant/components/energyzero/sensor.py +++ b/homeassistant/components/energyzero/sensor.py @@ -140,6 +140,7 @@ def get_gas_price(data: EnergyZeroData, hours: int) -> float | None: Returns: The gas market price value. + """ if data.gas_today is None: return None diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 42d95f85937..41fc4c2e03e 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -332,6 +332,7 @@ class NukiCoordinator(DataUpdateCoordinator[None]): # pylint: disable=hass-enfo Returns: A dict with the events to be fired. The event type is the key and the device ids are the value + """ events: dict[str, set[str]] = defaultdict(set) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 05fec64608f..7d104fa2bbe 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -502,6 +502,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): Note: suggested_unit_of_measurement is stored in the entity registry the first time the entity is seen, and then never updated. + """ if hasattr(self, "_attr_suggested_unit_of_measurement"): return self._attr_suggested_unit_of_measurement diff --git a/homeassistant/components/snapcast/server.py b/homeassistant/components/snapcast/server.py index 6a787dd5e88..bac51150eba 100644 --- a/homeassistant/components/snapcast/server.py +++ b/homeassistant/components/snapcast/server.py @@ -134,6 +134,7 @@ class HomeAssistantSnapcast: ---------- client : Snapclient Snapcast client to be added to HA. + """ if not self.hass_async_add_entities: return diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index 2c216100244..a54610e9a8b 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -55,6 +55,7 @@ class OptionsValidationError(Exception): field_key must also match one of the field names inside the Options class. error_key: Name of the options.error key that corresponds to this error. message: Message for the Exception class. + """ super().__init__(message) self.field_key = field_key diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index b633e2a614f..61a0cfdb802 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -564,6 +564,7 @@ class ConfigurableFanValueMappingDataTemplate( `configuration_value_to_fan_value_mapping` maps the values from `configuration_option` to the value mapping object. + """ def resolve_data( @@ -634,6 +635,7 @@ class FixedFanValueMappingDataTemplate( ) ), ), + """ def get_fan_value_mapping( diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index bdf9897a4ba..7d63fb4a7b4 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -364,6 +364,7 @@ def domain_key(config_key: Any) -> str: 'hue 1' returns 'hue' 'hue ' raises 'hue ' raises + """ if not isinstance(config_key, str): raise vol.Invalid("invalid domain", path=[config_key]) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 8d837bc9bc6..1bb7220f784 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -2295,6 +2295,7 @@ def iif( {{ is_state("device_tracker.frenck", "home") | iif("yes", "no") }} {{ iif(1==2, "yes", "no") }} {{ (1 == 1) | iif("yes", "no") }} + """ if value is None and if_none is not _SENTINEL: return if_none diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index a02eed66ffa..e12fefa0768 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit codespell==2.2.2 -ruff==0.1.8 +ruff==0.1.15 yamllint==1.32.0 diff --git a/tests/components/blackbird/test_media_player.py b/tests/components/blackbird/test_media_player.py index 2185c549d9b..de8be9d4ed5 100644 --- a/tests/components/blackbird/test_media_player.py +++ b/tests/components/blackbird/test_media_player.py @@ -287,10 +287,10 @@ async def test_state(hass: HomeAssistant, media_player_entity, mock_blackbird) - async def test_supported_features(media_player_entity) -> None: """Test supported features property.""" assert ( - MediaPlayerEntityFeature.TURN_ON + media_player_entity.supported_features + == MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.SELECT_SOURCE - == media_player_entity.supported_features ) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 55e4d8d5c65..2af5e67f845 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -2285,6 +2285,7 @@ async def test_cast_platform_play_media_local_media( quick_play_mock.assert_called() app_data = quick_play_mock.call_args[0][2] # No authSig appended - assert app_data[ - "media_id" - ] == f"{network.get_url(hass)}/api/hls/bla/master_playlist.m3u8?token=bla" + assert ( + app_data["media_id"] + == f"{network.get_url(hass)}/api/hls/bla/master_playlist.m3u8?token=bla" + ) diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 48b12132cbe..55dd2e758dc 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -172,7 +172,8 @@ async def test_supported_features( # Features supported for main DVR state = hass.states.get(MAIN_ENTITY_ID) assert ( - MediaPlayerEntityFeature.PAUSE + state.attributes.get("supported_features") + == MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.PLAY_MEDIA @@ -180,19 +181,18 @@ async def test_supported_features( | MediaPlayerEntityFeature.NEXT_TRACK | MediaPlayerEntityFeature.PREVIOUS_TRACK | MediaPlayerEntityFeature.PLAY - == state.attributes.get("supported_features") ) # Feature supported for clients. state = hass.states.get(CLIENT_ENTITY_ID) assert ( - MediaPlayerEntityFeature.PAUSE + state.attributes.get("supported_features") + == MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.STOP | MediaPlayerEntityFeature.NEXT_TRACK | MediaPlayerEntityFeature.PREVIOUS_TRACK | MediaPlayerEntityFeature.PLAY - == state.attributes.get("supported_features") ) diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index c2f9ef01111..36577c259d0 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -360,13 +360,13 @@ async def test_supported_features(hass: HomeAssistant) -> None: state = hass.states.get(ZONE_1_ID) assert ( - MediaPlayerEntityFeature.VOLUME_MUTE + state.attributes["supported_features"] + == MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.SELECT_SOURCE - == state.attributes["supported_features"] ) diff --git a/tests/components/nx584/test_binary_sensor.py b/tests/components/nx584/test_binary_sensor.py index b11865448b6..044fbc6ae9e 100644 --- a/tests/components/nx584/test_binary_sensor.py +++ b/tests/components/nx584/test_binary_sensor.py @@ -27,6 +27,7 @@ def fake_zones(): Returns: list: List of fake zones + """ return [ {"name": "front", "number": 1}, @@ -44,6 +45,7 @@ def client(fake_zones): Yields: MagicMock: Client Mock + """ with mock.patch.object(nx584_client, "Client") as _mock_client: client = nx584_client.Client.return_value diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index c186741aac9..776962e071c 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -189,7 +189,8 @@ async def test_supported_features( # Features supported for Rokus state = hass.states.get(MAIN_ENTITY_ID) assert ( - MediaPlayerEntityFeature.PREVIOUS_TRACK + state.attributes.get("supported_features") + == MediaPlayerEntityFeature.PREVIOUS_TRACK | MediaPlayerEntityFeature.NEXT_TRACK | MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_MUTE @@ -200,7 +201,6 @@ async def test_supported_features( | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.BROWSE_MEDIA - == state.attributes.get("supported_features") ) @@ -214,7 +214,8 @@ async def test_tv_supported_features( state = hass.states.get(TV_ENTITY_ID) assert state assert ( - MediaPlayerEntityFeature.PREVIOUS_TRACK + state.attributes.get("supported_features") + == MediaPlayerEntityFeature.PREVIOUS_TRACK | MediaPlayerEntityFeature.NEXT_TRACK | MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_MUTE @@ -225,7 +226,6 @@ async def test_tv_supported_features( | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.BROWSE_MEDIA - == state.attributes.get("supported_features") ) diff --git a/tests/components/ws66i/test_media_player.py b/tests/components/ws66i/test_media_player.py index c4a10197a34..a79ff96cfd7 100644 --- a/tests/components/ws66i/test_media_player.py +++ b/tests/components/ws66i/test_media_player.py @@ -260,13 +260,13 @@ async def test_supported_features(hass: HomeAssistant) -> None: state = hass.states.get(ZONE_1_ID) assert ( - MediaPlayerEntityFeature.VOLUME_MUTE + state.attributes["supported_features"] + == MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.SELECT_SOURCE - == state.attributes["supported_features"] ) From 61c82718f29329a2d0d706382b970d8fa8a8ad02 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 1 Feb 2024 16:53:53 +0100 Subject: [PATCH 0028/1367] Don't log warning for core integrations on new feature flags in Climate (#109250) * Don't log for core integration on Climate new feature flags * Add test * Fix test --- homeassistant/components/climate/__init__.py | 3 + tests/components/climate/test_init.py | 114 +++++++++++++++---- 2 files changed, 97 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 43d98ad6bbd..bf663fac365 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -339,6 +339,9 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def _report_turn_on_off(feature: str, method: str) -> None: """Log warning not implemented turn on/off feature.""" + module = type(self).__module__ + if module and "custom_components" not in module: + return report_issue = self._suggest_report_issue() if feature.startswith("TURN"): message = ( diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index 9bf89df7fd7..f764ad77aa9 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -3,7 +3,7 @@ from __future__ import annotations from enum import Enum from types import ModuleType -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest import voluptuous as vol @@ -415,23 +415,26 @@ async def test_warning_not_implemented_turn_on_off_feature( MockPlatform(async_setup_entry=async_setup_entry_climate_platform), ) - config_entry = MockConfigEntry(domain="test") - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patch.object( + MockClimateEntityTest, "__module__", "tests.custom_components.climate.test_init" + ): + config_entry = MockConfigEntry(domain="test") + 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("climate.test") assert state is not None assert ( - "Entity climate.test (.MockClimateEntityTest'>)" " does not set ClimateEntityFeature.TURN_OFF but implements the turn_off method." " Please report it to the author of the 'test' custom integration" in caplog.text ) assert ( - "Entity climate.test (.MockClimateEntityTest'>)" " does not set ClimateEntityFeature.TURN_ON but implements the turn_on method." " Please report it to the author of the 'test' custom integration" @@ -520,16 +523,19 @@ async def test_implicit_warning_not_implemented_turn_on_off_feature( MockPlatform(async_setup_entry=async_setup_entry_climate_platform), ) - config_entry = MockConfigEntry(domain="test") - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patch.object( + MockClimateEntityTest, "__module__", "tests.custom_components.climate.test_init" + ): + config_entry = MockConfigEntry(domain="test") + 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("climate.test") assert state is not None assert ( - "Entity climate.test (.MockClimateEntityTest'>)" " implements HVACMode(s): off, heat and therefore implicitly supports the turn_on/turn_off" " methods without setting the proper ClimateEntityFeature. Please report it to the author" @@ -584,10 +590,13 @@ async def test_no_warning_implemented_turn_on_off_feature( MockPlatform(async_setup_entry=async_setup_entry_climate_platform), ) - config_entry = MockConfigEntry(domain="test") - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patch.object( + MockClimateEntityTest, "__module__", "tests.custom_components.climate.test_init" + ): + config_entry = MockConfigEntry(domain="test") + 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("climate.test") assert state is not None @@ -652,10 +661,13 @@ async def test_no_warning_integration_has_migrated( MockPlatform(async_setup_entry=async_setup_entry_climate_platform), ) - config_entry = MockConfigEntry(domain="test") - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patch.object( + MockClimateEntityTest, "__module__", "tests.custom_components.climate.test_init" + ): + config_entry = MockConfigEntry(domain="test") + 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("climate.test") assert state is not None @@ -672,3 +684,65 @@ async def test_no_warning_integration_has_migrated( " implements HVACMode(s): off, heat and therefore implicitly supports the off, heat methods" not in caplog.text ) + + +async def test_no_warning_on_core_integrations_for_on_off_feature_flags( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, config_flow_fixture: None +) -> None: + """Test we don't warn on core integration on new turn_on/off feature flags.""" + + class MockClimateEntityTest(MockClimateEntity): + """Mock Climate device.""" + + def turn_on(self) -> None: + """Turn on.""" + + def turn_off(self) -> None: + """Turn off.""" + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) + return True + + async def async_setup_entry_climate_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test climate platform via config entry.""" + async_add_entities( + [MockClimateEntityTest(name="test", entity_id="climate.test")] + ) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=async_setup_entry_init, + ), + built_in=False, + ) + mock_platform( + hass, + "test.climate", + MockPlatform(async_setup_entry=async_setup_entry_climate_platform), + ) + + with patch.object( + MockClimateEntityTest, "__module__", "homeassistant.components.test.climate" + ): + config_entry = MockConfigEntry(domain="test") + 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("climate.test") + assert state is not None + + assert ( + "does not set ClimateEntityFeature.TURN_OFF but implements the turn_off method." + not in caplog.text + ) From fa17693e3c1607c89d09352f3b976e2b9c31a1cb Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 1 Feb 2024 17:05:02 +0100 Subject: [PATCH 0029/1367] Bump pytedee_async to 0.2.13 (#109307) bump --- homeassistant/components/tedee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tedee/manifest.json b/homeassistant/components/tedee/manifest.json index dbed87bb890..0a13b2266fa 100644 --- a/homeassistant/components/tedee/manifest.json +++ b/homeassistant/components/tedee/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["http", "webhook"], "documentation": "https://www.home-assistant.io/integrations/tedee", "iot_class": "local_push", - "requirements": ["pytedee-async==0.2.12"] + "requirements": ["pytedee-async==0.2.13"] } diff --git a/requirements_all.txt b/requirements_all.txt index 109152e088d..da3092eab7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2163,7 +2163,7 @@ pyswitchbee==1.8.0 pytautulli==23.1.1 # homeassistant.components.tedee -pytedee-async==0.2.12 +pytedee-async==0.2.13 # homeassistant.components.tfiac pytfiac==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 906085c2d4a..f8da20de5f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1675,7 +1675,7 @@ pyswitchbee==1.8.0 pytautulli==23.1.1 # homeassistant.components.tedee -pytedee-async==0.2.12 +pytedee-async==0.2.13 # homeassistant.components.motionmount python-MotionMount==0.3.1 From fe0228139e72bddfc5ad48022d24f268bf08af35 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 1 Feb 2024 17:07:08 +0100 Subject: [PATCH 0030/1367] Do not use a battery device class for Shelly analog input sensor (#109311) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 57e60c8fc48..e46800963a3 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -958,7 +958,6 @@ RPC_SENSORS: Final = { sub_key="percent", name="Analog input", native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, ), } From cb5be4901bf59dbea7766bb4119197838b260af5 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 1 Feb 2024 17:07:55 +0100 Subject: [PATCH 0031/1367] Verify Ecovacs mqtt config (#109306) --- homeassistant/components/ecovacs/controller.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/ecovacs/controller.py b/homeassistant/components/ecovacs/controller.py index 06e3a1acccd..27a1996c3e9 100644 --- a/homeassistant/components/ecovacs/controller.py +++ b/homeassistant/components/ecovacs/controller.py @@ -74,11 +74,16 @@ class EcovacsController: async def initialize(self) -> None: """Init controller.""" + mqtt_config_verfied = False try: devices = await self._api_client.get_devices() credentials = await self._authenticator.authenticate() for device_config in devices: if isinstance(device_config, DeviceInfo): + # MQTT device + if not mqtt_config_verfied: + await self._mqtt.verify_config() + mqtt_config_verfied = True device = Device(device_config, self._authenticator) await device.initialize(self._mqtt) self.devices.append(device) From c41f33da711889b7829caf99d2208968c649e808 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 2 Feb 2024 02:08:41 +1000 Subject: [PATCH 0032/1367] Add climate on/off feature to Tessie (#109239) --- homeassistant/components/tessie/climate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tessie/climate.py b/homeassistant/components/tessie/climate.py index 8d27305cb0b..d143771ee2c 100644 --- a/homeassistant/components/tessie/climate.py +++ b/homeassistant/components/tessie/climate.py @@ -45,7 +45,10 @@ class TessieClimateEntity(TessieEntity, ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF] _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE ) _attr_preset_modes: list = [ TessieClimateKeeper.OFF, From c61a2b46d4ac1d26bc56352fcdaef637f9814d61 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 2 Feb 2024 02:09:24 +1000 Subject: [PATCH 0033/1367] Add climate turn on/off feature to Teslemetry (#109241) --- homeassistant/components/teslemetry/climate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/teslemetry/climate.py b/homeassistant/components/teslemetry/climate.py index cea56f35b15..b626d3ef759 100644 --- a/homeassistant/components/teslemetry/climate.py +++ b/homeassistant/components/teslemetry/climate.py @@ -39,7 +39,10 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF] _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE ) _attr_preset_modes = ["off", "keep", "dog", "camp"] From ed726db97408616eb1105f8cb8e8820dabab8b6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Feb 2024 12:34:23 -0600 Subject: [PATCH 0034/1367] Fix race in loading service descriptions (#109316) --- homeassistant/helpers/service.py | 5 ++ tests/helpers/test_service.py | 79 ++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 5a9786eb0fa..30516e3a099 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -608,6 +608,11 @@ async def async_get_all_descriptions( # Files we loaded for missing descriptions loaded: dict[str, JSON_TYPE] = {} + # We try to avoid making a copy in the event the cache is good, + # but now we must make a copy in case new services get added + # while we are loading the missing ones so we do not + # add the new ones to the cache without their descriptions + services = {domain: service.copy() for domain, service in services.items()} if domains_with_missing_services: ints_or_excs = await async_get_integrations(hass, domains_with_missing_services) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 07e68e081b3..90f9b65aaba 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -1,4 +1,5 @@ """Test service helpers.""" +import asyncio from collections.abc import Iterable from copy import deepcopy from typing import Any @@ -782,6 +783,84 @@ async def test_async_get_all_descriptions_dynamically_created_services( } +async def test_async_get_all_descriptions_new_service_added_while_loading( + hass: HomeAssistant, +) -> None: + """Test async_get_all_descriptions when a new service is added while loading translations.""" + 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_domain = logger.DOMAIN + logger_config = {logger_domain: {}} + + translations_called = asyncio.Event() + translations_wait = asyncio.Event() + + 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.""" + translations_called.set() + await translations_wait.wait() + 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", + f"{translation_key_prefix}.fields.level.example": "Field example", + } + + with patch( + "homeassistant.helpers.service.translation.async_get_translations", + side_effect=async_get_translations, + ): + await async_setup_component(hass, logger_domain, logger_config) + task = asyncio.create_task(service.async_get_all_descriptions(hass)) + await translations_called.wait() + # Now register a new service while translations are being loaded + 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"} + ) + translations_wait.set() + descriptions = await task + + # Two domains should be present + assert len(descriptions) == 2 + + logger_descriptions = descriptions[logger_domain] + + # The new service was loaded after the translations were loaded + # so it should not appear until the next time we fetch + assert "new_service" not in logger_descriptions + + set_default_level = logger_descriptions["set_default_level"] + + assert set_default_level["name"] == "Translated name" + assert set_default_level["description"] == "Translated description" + set_default_level_fields = set_default_level["fields"] + assert set_default_level_fields["level"]["name"] == "Field name" + assert set_default_level_fields["level"]["description"] == "Field description" + assert set_default_level_fields["level"]["example"] == "Field example" + + 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_register_with_mixed_case(hass: HomeAssistant) -> None: """Test registering a service with mixed case. From c2c98bd04c8c3cfc8ae948ec5c2a2be3ce117e26 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 1 Feb 2024 13:40:29 -0600 Subject: [PATCH 0035/1367] Move default response out of sentence trigger registration and into agent (#109317) * Move default response out of trigger and into agent * Add test --- .../components/conversation/default_agent.py | 7 ++- .../components/conversation/trigger.py | 7 ++- tests/components/conversation/test_trigger.py | 58 +++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index c9119935213..123dc7aba1d 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -238,7 +238,10 @@ class DefaultAgent(AbstractConversationAgent): ) ) - # Use last non-empty result as response + # Use last non-empty result as response. + # + # There may be multiple copies of a trigger running when editing in + # the UI, so it's critical that we filter out empty responses here. response_text: str | None = None for trigger_response in trigger_responses: response_text = response_text or trigger_response @@ -246,7 +249,7 @@ class DefaultAgent(AbstractConversationAgent): # Convert to conversation result response = intent.IntentResponse(language=language) response.response_type = intent.IntentResponseType.ACTION_DONE - response.async_set_speech(response_text or "") + response.async_set_speech(response_text or "Done") return ConversationResult(response=response) diff --git a/homeassistant/components/conversation/trigger.py b/homeassistant/components/conversation/trigger.py index d38bb69f3e1..4600135c1e5 100644 --- a/homeassistant/components/conversation/trigger.py +++ b/homeassistant/components/conversation/trigger.py @@ -98,7 +98,12 @@ async def async_attach_trigger( # mypy does not understand the type narrowing, unclear why return automation_result.conversation_response # type: ignore[return-value] - return "Done" + # It's important to return None here instead of a string. + # + # When editing in the UI, a copy of this trigger is registered. + # If we return a string from here, there is a race condition between the + # two trigger copies for who will provide a response. + return None default_agent = await _get_agent_manager(hass).async_get_agent(HOME_ASSISTANT_AGENT) assert isinstance(default_agent, DefaultAgent) diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index e40c7554fdd..26626a04079 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -7,6 +7,7 @@ from homeassistant.helpers import trigger from homeassistant.setup import async_setup_component from tests.common import async_mock_service +from tests.typing import WebSocketGenerator @pytest.fixture @@ -99,6 +100,63 @@ async def test_response(hass: HomeAssistant, setup_comp) -> None: assert service_response["response"]["speech"]["plain"]["speech"] == response +async def test_subscribe_trigger_does_not_interfere_with_responses( + hass: HomeAssistant, setup_comp, hass_ws_client: WebSocketGenerator +) -> None: + """Test that subscribing to a trigger from the websocket API does not interfere with responses.""" + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 5, + "type": "subscribe_trigger", + "trigger": {"platform": "conversation", "command": ["test sentence"]}, + } + ) + + service_response = await hass.services.async_call( + "conversation", + "process", + { + "text": "test sentence", + }, + blocking=True, + return_response=True, + ) + + # Default response, since no automations with responses are registered + assert service_response["response"]["speech"]["plain"]["speech"] == "Done" + + # Now register a trigger with a response + assert await async_setup_component( + hass, + "automation", + { + "automation test1": { + "trigger": { + "platform": "conversation", + "command": ["test sentence"], + }, + "action": { + "set_conversation_response": "test response", + }, + } + }, + ) + + service_response = await hass.services.async_call( + "conversation", + "process", + { + "text": "test sentence", + }, + blocking=True, + return_response=True, + ) + + # Response will now come through + assert service_response["response"]["speech"]["plain"]["speech"] == "test response" + + async def test_same_trigger_multiple_sentences( hass: HomeAssistant, calls, setup_comp ) -> None: From 0cc8b2edf90a86862ebfe7824597d8b15cd211b5 Mon Sep 17 00:00:00 2001 From: Josh Pettersen <12600312+bubonicbob@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:01:05 -0800 Subject: [PATCH 0036/1367] Remove battery charge sensor from powerwall (#109271) --- homeassistant/components/powerwall/sensor.py | 16 ---------------- tests/components/powerwall/test_sensor.py | 2 -- 2 files changed, 18 deletions(-) diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 24aeb9e4f4e..9e17cd32e9c 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -113,12 +113,6 @@ POWERWALL_INSTANT_SENSORS = ( ) -def _get_battery_charge(battery_data: BatteryResponse) -> float: - """Get the current value in %.""" - ratio = float(battery_data.energy_remaining) / float(battery_data.capacity) - return round(100 * ratio, 1) - - BATTERY_INSTANT_SENSORS: list[PowerwallSensorEntityDescription] = [ PowerwallSensorEntityDescription[BatteryResponse, int]( key="battery_capacity", @@ -202,16 +196,6 @@ BATTERY_INSTANT_SENSORS: list[PowerwallSensorEntityDescription] = [ suggested_display_precision=1, value_fn=lambda battery_data: battery_data.energy_remaining, ), - PowerwallSensorEntityDescription[BatteryResponse, float]( - key="charge", - translation_key="charge", - entity_category=EntityCategory.DIAGNOSTIC, - state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.BATTERY, - native_unit_of_measurement=PERCENTAGE, - suggested_display_precision=0, - value_fn=_get_battery_charge, - ), PowerwallSensorEntityDescription[BatteryResponse, str]( key="grid_state", translation_key="grid_state", diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index 11b4f25e4a3..2de79a6a6dc 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -157,7 +157,6 @@ async def test_sensors( float(hass.states.get("sensor.mysite_tg0123456789ab_battery_remaining").state) == 14.715 ) - assert float(hass.states.get("sensor.mysite_tg0123456789ab_charge").state) == 100.0 assert ( str(hass.states.get("sensor.mysite_tg0123456789ab_grid_state").state) == "grid_compliant" @@ -187,7 +186,6 @@ async def test_sensors( float(hass.states.get("sensor.mysite_tg9876543210ba_battery_remaining").state) == 15.137 ) - assert float(hass.states.get("sensor.mysite_tg9876543210ba_charge").state) == 100.0 assert ( str(hass.states.get("sensor.mysite_tg9876543210ba_grid_state").state) == "grid_compliant" From c2525d53dd26ba5081c6287a1479e512b77ba886 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:07:47 -0500 Subject: [PATCH 0037/1367] Add translations for zwave_js entities and services (#109188) --- homeassistant/components/zwave_js/button.py | 2 +- homeassistant/components/zwave_js/icons.json | 53 +- homeassistant/components/zwave_js/sensor.py | 70 +- .../components/zwave_js/strings.json | 791 ++++++++++-------- 4 files changed, 513 insertions(+), 403 deletions(-) diff --git a/homeassistant/components/zwave_js/button.py b/homeassistant/components/zwave_js/button.py index 33d1e6dfa63..876cf60b4cb 100644 --- a/homeassistant/components/zwave_js/button.py +++ b/homeassistant/components/zwave_js/button.py @@ -86,13 +86,13 @@ class ZWaveNodePingButton(ButtonEntity): _attr_should_poll = False _attr_entity_category = EntityCategory.CONFIG _attr_has_entity_name = True + _attr_translation_key = "ping" def __init__(self, driver: Driver, node: ZwaveNode) -> None: """Initialize a ping Z-Wave device button entity.""" self.node = node # Entity class attributes - self._attr_name = "Ping" self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.ping" # device may not be precreated in main handler yet diff --git a/homeassistant/components/zwave_js/icons.json b/homeassistant/components/zwave_js/icons.json index 2280811d3da..2956cf2c6e0 100644 --- a/homeassistant/components/zwave_js/icons.json +++ b/homeassistant/components/zwave_js/icons.json @@ -1,14 +1,34 @@ { "entity": { + "button": { + "ping": { + "default": "mdi:crosshairs-gps" + } + }, "sensor": { + "can": { + "default": "mdi:car-brake-alert" + }, + "commands_dropped": { + "default": "mdi:trash-can" + }, "controller_status": { "default": "mdi:help-rhombus", "state": { + "jammed": "mdi:lock", "ready": "mdi:check", - "unresponsive": "mdi:bell-off", - "jammed": "mdi:lock" + "unresponsive": "mdi:bell-off" } }, + "last_seen": { + "default": "mdi:timer-sync" + }, + "messages_dropped": { + "default": "mdi:trash-can" + }, + "nak": { + "default": "mdi:hand-back-left-off" + }, "node_status": { "default": "mdi:help-rhombus", "state": { @@ -18,7 +38,36 @@ "dead": "mdi:robot-dead", "unknown": "mdi:help-rhombus" } + }, + "successful_commands": { + "default": "mdi:check" + }, + "successful_messages": { + "default": "mdi:check" + }, + "timeout_ack": { + "default": "mdi:ear-hearing-off" + }, + "timeout_callback": { + "default": "mdi:timer-sand-empty" + }, + "timeout_response": { + "default": "mdi:timer-sand-empty" } } + }, + "services": { + "bulk_set_partial_config_parameters": "mdi:cogs", + "clear_lock_usercode": "mdi:eraser", + "invoke_cc_api": "mdi:api", + "multicast_set_value": "mdi:list-box", + "ping": "mdi:crosshairs-gps", + "refresh_notifications": "mdi:bell", + "refresh_value": "mdi:refresh", + "reset_meter": "mdi:meter-electric", + "set_config_parameter": "mdi:cog", + "set_lock_configuration": "mdi:shield-lock", + "set_lock_usercode": "mdi:lock-smart", + "set_value": "mdi:form-textbox" } } diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 0b9defc5f62..0240725ca2d 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -350,55 +350,61 @@ class ZWaveJSStatisticsSensorEntityDescription(SensorEntityDescription): ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [ ZWaveJSStatisticsSensorEntityDescription( key="messagesTX", - name="Successful messages (TX)", + translation_key="successful_messages", + translation_placeholders={"direction": "TX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="messagesRX", - name="Successful messages (RX)", + translation_key="successful_messages", + translation_placeholders={"direction": "RX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="messagesDroppedTX", - name="Messages dropped (TX)", + translation_key="messages_dropped", + translation_placeholders={"direction": "TX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="messagesDroppedRX", - name="Messages dropped (RX)", + translation_key="messages_dropped", + translation_placeholders={"direction": "RX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( - key="NAK", - name="Messages not accepted", + key="NAK", translation_key="nak", state_class=SensorStateClass.TOTAL + ), + ZWaveJSStatisticsSensorEntityDescription( + key="CAN", translation_key="can", state_class=SensorStateClass.TOTAL + ), + ZWaveJSStatisticsSensorEntityDescription( + key="timeoutACK", + translation_key="timeout_ack", state_class=SensorStateClass.TOTAL, ), - ZWaveJSStatisticsSensorEntityDescription( - key="CAN", name="Collisions", state_class=SensorStateClass.TOTAL - ), - ZWaveJSStatisticsSensorEntityDescription( - key="timeoutACK", name="Missing ACKs", state_class=SensorStateClass.TOTAL - ), ZWaveJSStatisticsSensorEntityDescription( key="timeoutResponse", - name="Timed out responses", + translation_key="timeout_response", state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="timeoutCallback", - name="Timed out callbacks", + translation_key="timeout_callback", state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="backgroundRSSI.channel0.average", - name="Average background RSSI (channel 0)", + translation_key="average_background_rssi", + translation_placeholders={"channel": "0"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, convert=convert_dict_of_dicts, ), ZWaveJSStatisticsSensorEntityDescription( key="backgroundRSSI.channel0.current", - name="Current background RSSI (channel 0)", + translation_key="current_background_rssi", + translation_placeholders={"channel": "0"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, @@ -406,14 +412,16 @@ ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [ ), ZWaveJSStatisticsSensorEntityDescription( key="backgroundRSSI.channel1.average", - name="Average background RSSI (channel 1)", + translation_key="average_background_rssi", + translation_placeholders={"channel": "1"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, convert=convert_dict_of_dicts, ), ZWaveJSStatisticsSensorEntityDescription( key="backgroundRSSI.channel1.current", - name="Current background RSSI (channel 1)", + translation_key="current_background_rssi", + translation_placeholders={"channel": "1"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, @@ -421,14 +429,16 @@ ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [ ), ZWaveJSStatisticsSensorEntityDescription( key="backgroundRSSI.channel2.average", - name="Average background RSSI (channel 2)", + translation_key="average_background_rssi", + translation_placeholders={"channel": "2"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, convert=convert_dict_of_dicts, ), ZWaveJSStatisticsSensorEntityDescription( key="backgroundRSSI.channel2.current", - name="Current background RSSI (channel 2)", + translation_key="current_background_rssi", + translation_placeholders={"channel": "2"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, @@ -440,46 +450,50 @@ ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [ ENTITY_DESCRIPTION_NODE_STATISTICS_LIST = [ ZWaveJSStatisticsSensorEntityDescription( key="commandsRX", - name="Successful commands (RX)", + translation_key="successful_commands", + translation_placeholders={"direction": "RX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="commandsTX", - name="Successful commands (TX)", + translation_key="successful_commands", + translation_placeholders={"direction": "TX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="commandsDroppedRX", - name="Commands dropped (RX)", + translation_key="commands_dropped", + translation_placeholders={"direction": "RX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="commandsDroppedTX", - name="Commands dropped (TX)", + translation_key="commands_dropped", + translation_placeholders={"direction": "TX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="timeoutResponse", - name="Timed out responses", + translation_key="timeout_response", state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="rtt", - name="Round Trip Time", + translation_key="rtt", native_unit_of_measurement=UnitOfTime.MILLISECONDS, device_class=SensorDeviceClass.DURATION, state_class=SensorStateClass.MEASUREMENT, ), ZWaveJSStatisticsSensorEntityDescription( key="rssi", - name="RSSI", + translation_key="rssi", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, ), ZWaveJSStatisticsSensorEntityDescription( key="lastSeen", - name="Last Seen", + translation_key="last_seen", device_class=SensorDeviceClass.TIMESTAMP, convert=( lambda statistics, key: ( diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index db19c0fceeb..9e2317ba728 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -1,6 +1,133 @@ { + "config": { + "abort": { + "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", + "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.", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "discovery_requires_supervisor": "Discovery requires the supervisor.", + "not_zwave_device": "Discovered device is not a Z-Wave device.", + "not_zwave_js_addon": "Discovered add-on is not the official Z-Wave JS add-on." + }, + "error": { + "addon_start_failed": "Failed to start the Z-Wave JS add-on. Check the configuration.", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_ws_url": "Invalid websocket URL", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "flow_title": "{name}", + "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." + }, + "step": { + "configure_addon": { + "data": { + "s0_legacy_key": "S0 Key (Legacy)", + "s2_access_control_key": "S2 Access Control Key", + "s2_authenticated_key": "S2 Authenticated Key", + "s2_unauthenticated_key": "S2 Unauthenticated Key", + "usb_path": "[%key:common::config_flow::data::usb_path%]" + }, + "description": "The add-on will generate security keys if those fields are left empty.", + "title": "Enter the Z-Wave JS add-on configuration" + }, + "hassio_confirm": { + "title": "Set up Z-Wave JS integration with the Z-Wave JS add-on" + }, + "install_addon": { + "title": "The Z-Wave JS add-on installation has started" + }, + "manual": { + "data": { + "url": "[%key:common::config_flow::data::url%]" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Use the Z-Wave JS Supervisor add-on" + }, + "description": "Do you want to use the Z-Wave JS Supervisor add-on?", + "title": "Select connection method" + }, + "start_addon": { + "title": "The Z-Wave JS add-on is starting." + }, + "usb_confirm": { + "description": "Do you want to set up {name} with the Z-Wave JS add-on?" + }, + "zeroconf_confirm": { + "description": "Do you want to add the Z-Wave JS Server with home ID {home_id} found at {url} to Home Assistant?", + "title": "Discovered Z-Wave JS Server" + } + } + }, + "device_automation": { + "action_type": { + "clear_lock_usercode": "Clear usercode on {entity_name}", + "ping": "Ping device", + "refresh_value": "Refresh the value(s) for {entity_name}", + "reset_meter": "Reset meters on {subtype}", + "set_config_parameter": "Set value of config parameter {subtype}", + "set_lock_usercode": "Set a usercode on {entity_name}", + "set_value": "Set value of a Z-Wave Value" + }, + "condition_type": { + "config_parameter": "Config parameter {subtype} value", + "node_status": "Node status", + "value": "Current value of a Z-Wave Value" + }, + "trigger_type": { + "event.notification.entry_control": "Sent an Entry Control notification", + "event.notification.notification": "Sent a notification", + "event.value_notification.basic": "Basic CC event on {subtype}", + "event.value_notification.central_scene": "Central Scene action on {subtype}", + "event.value_notification.scene_activation": "Scene Activation on {subtype}", + "state.node_status": "Node status changed", + "zwave_js.value_updated.config_parameter": "Value change on config parameter {subtype}", + "zwave_js.value_updated.value": "Value change on a Z-Wave JS Value" + } + }, "entity": { + "button": { + "ping": { + "name": "Ping" + } + }, "sensor": { + "average_background_rssi": { + "name": "Average background RSSI (channel {channel})" + }, + "can": { + "name": "Collisions" + }, + "commands_dropped": { + "name": "Commands dropped ({direction})" + }, + "controller_status": { + "name": "Status", + "state": { + "jammed": "Jammed", + "ready": "Ready", + "unresponsive": "Unresponsive" + } + }, + "current_background_rssi": { + "name": "Current background RSSI (channel {channel})" + }, + "last_seen": { + "name": "Last seen" + }, + "messages_dropped": { + "name": "Messages dropped ({direction})" + }, + "nak": { + "name": "Messages not accepted" + }, "node_status": { "name": "Node status", "state": { @@ -11,434 +138,354 @@ "unknown": "Unknown" } }, - "controller_status": { - "name": "Status", - "state": { - "ready": "Ready", - "unresponsive": "Unresponsive", - "jammed": "Jammed" - } + "rssi": { + "name": "RSSI" + }, + "rtt": { + "name": "Round trip time" + }, + "successful_commands": { + "name": "Successful commands ({direction})" + }, + "successful_messages": { + "name": "Successful messages ({direction})" + }, + "timeout_ack": { + "name": "Missing ACKs" + }, + "timeout_callback": { + "name": "Timed out callbacks" + }, + "timeout_response": { + "name": "Timed out responses" } } }, - "config": { - "flow_title": "{name}", - "step": { - "manual": { - "data": { - "url": "[%key:common::config_flow::data::url%]" - } - }, - "usb_confirm": { - "description": "Do you want to set up {name} with the Z-Wave JS add-on?" - }, - "on_supervisor": { - "title": "Select connection method", - "description": "Do you want to use the Z-Wave JS Supervisor add-on?", - "data": { - "use_addon": "Use the Z-Wave JS Supervisor add-on" - } - }, - "install_addon": { - "title": "The Z-Wave JS add-on installation has started" - }, - "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.", - "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" - } - }, - "start_addon": { - "title": "The Z-Wave JS add-on is starting." - }, - "hassio_confirm": { - "title": "Set up Z-Wave JS integration with the Z-Wave JS add-on" - }, - "zeroconf_confirm": { - "description": "Do you want to add the Z-Wave JS Server with home ID {home_id} found at {url} to Home Assistant?", - "title": "Discovered Z-Wave JS Server" - } - }, - "error": { - "addon_start_failed": "Failed to start the Z-Wave JS add-on. Check the configuration.", - "invalid_ws_url": "Invalid websocket 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%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "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.", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "discovery_requires_supervisor": "Discovery requires the supervisor.", - "not_zwave_device": "Discovered device is not a Z-Wave device.", - "not_zwave_js_addon": "Discovered add-on is not the official Z-Wave JS add-on." - }, - "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." - } - }, - "options": { - "step": { - "manual": { - "data": { - "url": "[%key:common::config_flow::data::url%]" - } - }, - "on_supervisor": { - "title": "[%key:component::zwave_js::config::step::on_supervisor::title%]", - "description": "[%key:component::zwave_js::config::step::on_supervisor::description%]", - "data": { - "use_addon": "[%key:component::zwave_js::config::step::on_supervisor::data::use_addon%]" - } - }, - "install_addon": { - "title": "[%key:component::zwave_js::config::step::install_addon::title%]" - }, - "configure_addon": { - "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": "[%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": "[%key:component::zwave_js::config::step::start_addon::title%]" - } - }, - "error": { - "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": "[%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": "[%key:component::zwave_js::config::progress::install_addon%]", - "start_addon": "[%key:component::zwave_js::config::progress::start_addon%]" - } - }, - "device_automation": { - "trigger_type": { - "event.notification.entry_control": "Sent an Entry Control notification", - "event.notification.notification": "Sent a notification", - "event.value_notification.basic": "Basic CC event on {subtype}", - "event.value_notification.central_scene": "Central Scene action on {subtype}", - "event.value_notification.scene_activation": "Scene Activation on {subtype}", - "zwave_js.value_updated.config_parameter": "Value change on config parameter {subtype}", - "zwave_js.value_updated.value": "Value change on a Z-Wave JS Value", - "state.node_status": "Node status changed" - }, - "condition_type": { - "node_status": "Node status", - "config_parameter": "Config parameter {subtype} value", - "value": "Current value of a Z-Wave Value" - }, - "action_type": { - "clear_lock_usercode": "Clear usercode on {entity_name}", - "set_lock_usercode": "Set a usercode on {entity_name}", - "set_config_parameter": "Set value of config parameter {subtype}", - "set_value": "Set value of a Z-Wave Value", - "refresh_value": "Refresh the value(s) for {entity_name}", - "ping": "Ping device", - "reset_meter": "Reset meters on {subtype}" - } - }, "issues": { - "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." - }, "device_config_file_changed": { - "title": "Device configuration file changed: {device_name}", "fix_flow": { + "abort": { + "cannot_connect": "Cannot connect to {device_name}. Please try again later after confirming that your Z-Wave network is up and connected to Home Assistant.", + "issue_ignored": "Device config file update for {device_name} ignored." + }, "step": { "init": { + "description": "The device configuration file for {device_name} has changed.\n\nZ-Wave JS discovers a lot of device metadata by interviewing the device. However, some of the information has to be loaded from a configuration file. Some of this information is only evaluated once, during the device interview.\n\nWhen a device config file is updated, this information may be stale and and the device must be re-interviewed to pick up the changes.\n\n This is not a required operation and device functionality will be impacted during the re-interview process, but you may see improvements for your device once it is complete.\n\nIf you decide to proceed with the re-interview, it will take place in the background.", "menu_options": { "confirm": "Re-interview device", "ignore": "Ignore device config update" }, - "title": "Device configuration file changed: {device_name}", - "description": "The device configuration file for {device_name} has changed.\n\nZ-Wave JS discovers a lot of device metadata by interviewing the device. However, some of the information has to be loaded from a configuration file. Some of this information is only evaluated once, during the device interview.\n\nWhen a device config file is updated, this information may be stale and and the device must be re-interviewed to pick up the changes.\n\n This is not a required operation and device functionality will be impacted during the re-interview process, but you may see improvements for your device once it is complete.\n\nIf you decide to proceed with the re-interview, it will take place in the background." + "title": "Device configuration file changed: {device_name}" } - }, - "abort": { - "cannot_connect": "Cannot connect to {device_name}. Please try again later after confirming that your Z-Wave network is up and connected to Home Assistant.", - "issue_ignored": "Device config file update for {device_name} ignored." } + }, + "title": "Device configuration file changed: {device_name}" + }, + "invalid_server_version": { + "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.", + "title": "Newer version of Z-Wave JS Server needed" + } + }, + "options": { + "abort": { + "addon_get_discovery_info_failed": "[%key:component::zwave_js::config::abort::addon_get_discovery_info_failed%]", + "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%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "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." + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_ws_url": "[%key:component::zwave_js::config::error::invalid_ws_url%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "progress": { + "install_addon": "[%key:component::zwave_js::config::progress::install_addon%]", + "start_addon": "[%key:component::zwave_js::config::progress::start_addon%]" + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emulate Hardware", + "log_level": "Log level", + "s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon::data::s0_legacy_key%]", + "s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_access_control_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%]", + "usb_path": "[%key:common::config_flow::data::usb_path%]" + }, + "description": "[%key:component::zwave_js::config::step::configure_addon::description%]", + "title": "[%key:component::zwave_js::config::step::configure_addon::title%]" + }, + "install_addon": { + "title": "[%key:component::zwave_js::config::step::install_addon::title%]" + }, + "manual": { + "data": { + "url": "[%key:common::config_flow::data::url%]" + } + }, + "on_supervisor": { + "data": { + "use_addon": "[%key:component::zwave_js::config::step::on_supervisor::data::use_addon%]" + }, + "description": "[%key:component::zwave_js::config::step::on_supervisor::description%]", + "title": "[%key:component::zwave_js::config::step::on_supervisor::title%]" + }, + "start_addon": { + "title": "[%key:component::zwave_js::config::step::start_addon::title%]" } } }, "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). Cannot be combined with value_size or value_format." - }, - "value": { - "name": "Value", - "description": "The new value to set for this configuration parameter." - }, - "value_size": { - "name": "Value size", - "description": "Size of the value, either 1, 2, or 4. Used in combination with value_format when a config parameter is not defined in your device's configuration file. Cannot be combined with bitmask." - }, - "value_format": { - "name": "Value format", - "description": "Format of the value, 0 for signed integer, 1 for unsigned integer, 2 for enumerated, 3 for bitfield. Used in combination with value_size when a config parameter is not defined in your device's configuration file. Cannot be combined with bitmask." - } - } - }, "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%]" + "description": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::description%]", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]" }, "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%]" + "description": "[%key:component::zwave_js::services::set_config_parameter::fields::parameter::description%]", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::parameter::name%]" }, "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." + "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.", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]" } - } + }, + "name": "Bulk set partial configuration parameters (advanced)." }, - "refresh_value": { - "name": "Refresh values", - "description": "Force updates the values of a Z-Wave entity.", + "clear_lock_usercode": { + "description": "Clears a user code from a lock.", "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)." + "code_slot": { + "description": "Code slot to clear code from.", + "name": "Code slot" } - } - }, - "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." - } - } + }, + "name": "Clear lock user code" }, "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." + "description": "The ID of the command class that you want to issue a command to.", + "name": "[%key:component::zwave_js::services::set_value::fields::command_class::name%]" }, "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." + "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.", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]" }, "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." + "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.", + "name": "Method name" }, "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." + "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.", + "name": "Parameters" } - } + }, + "name": "Invoke a Command Class API on a node (advanced)" + }, + "multicast_set_value": { + "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": { + "description": "Whether command should be broadcast to all devices on the network.", + "name": "Broadcast?" + }, + "command_class": { + "description": "[%key:component::zwave_js::services::set_value::fields::command_class::description%]", + "name": "[%key:component::zwave_js::services::set_value::fields::command_class::name%]" + }, + "endpoint": { + "description": "[%key:component::zwave_js::services::set_value::fields::endpoint::description%]", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]" + }, + "options": { + "description": "[%key:component::zwave_js::services::set_value::fields::options::description%]", + "name": "[%key:component::zwave_js::services::set_value::fields::options::name%]" + }, + "property": { + "description": "[%key:component::zwave_js::services::set_value::fields::property::description%]", + "name": "[%key:component::zwave_js::services::set_value::fields::property::name%]" + }, + "property_key": { + "description": "[%key:component::zwave_js::services::set_value::fields::property_key::description%]", + "name": "[%key:component::zwave_js::services::set_value::fields::property_key::name%]" + }, + "value": { + "description": "[%key:component::zwave_js::services::set_value::fields::value::description%]", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]" + } + }, + "name": "Set a value on multiple devices via multicast (advanced)" + }, + "ping": { + "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.", + "name": "Ping a node" }, "refresh_notifications": { - "name": "Refresh notifications on a node (advanced)", "description": "Refreshes notifications on a node based on notification type and optionally notification event.", "fields": { - "notification_type": { - "name": "Notification Type", - "description": "The Notification Type number as defined in the Z-Wave specs." - }, "notification_event": { - "name": "Notification Event", - "description": "The Notification Event number as defined in the Z-Wave specs." + "description": "The Notification Event number as defined in the Z-Wave specs.", + "name": "Notification Event" + }, + "notification_type": { + "description": "The Notification Type number as defined in the Z-Wave specs.", + "name": "Notification Type" } - } + }, + "name": "Refresh notifications on a node (advanced)" + }, + "refresh_value": { + "description": "Force updates the values of a Z-Wave entity.", + "fields": { + "entity_id": { + "description": "Entities to refresh.", + "name": "Entities" + }, + "refresh_all_values": { + "description": "Whether to refresh all values (true) or just the primary value (false).", + "name": "Refresh all values?" + } + }, + "name": "Refresh values" + }, + "reset_meter": { + "description": "Resets the meters on a node.", + "fields": { + "meter_type": { + "description": "The type of meter to reset. Not all meters support the ability to pick a meter type to reset.", + "name": "Meter type" + }, + "value": { + "description": "The value that meters should be reset to. Not all meters support the ability to be reset to a specific value.", + "name": "Target value" + } + }, + "name": "Reset meters on a node" + }, + "set_config_parameter": { + "description": "Changes the configuration parameters of your Z-Wave devices.", + "fields": { + "bitmask": { + "description": "Target a specific bitmask (see the documentation for more information). Cannot be combined with value_size or value_format.", + "name": "Bitmask" + }, + "endpoint": { + "description": "The configuration parameter's endpoint.", + "name": "Endpoint" + }, + "parameter": { + "description": "The name (or ID) of the configuration parameter you want to configure.", + "name": "Parameter" + }, + "value": { + "description": "The new value to set for this configuration parameter.", + "name": "Value" + }, + "value_format": { + "description": "Format of the value, 0 for signed integer, 1 for unsigned integer, 2 for enumerated, 3 for bitfield. Used in combination with value_size when a config parameter is not defined in your device's configuration file. Cannot be combined with bitmask.", + "name": "Value format" + }, + "value_size": { + "description": "Size of the value, either 1, 2, or 4. Used in combination with value_format when a config parameter is not defined in your device's configuration file. Cannot be combined with bitmask.", + "name": "Value size" + } + }, + "name": "Set device configuration parameter" }, "set_lock_configuration": { - "name": "Set lock configuration", "description": "Sets the configuration for a lock.", "fields": { - "operation_type": { - "name": "Operation Type", - "description": "The operation type of the lock." - }, - "lock_timeout": { - "name": "Lock timeout", - "description": "Seconds until lock mode times out. Should only be used if operation type is `timed`." - }, - "outside_handles_can_open_door_configuration": { - "name": "Outside handles can open door configuration", - "description": "A list of four booleans which indicate which outside handles can open the door." - }, - "inside_handles_can_open_door_configuration": { - "name": "Inside handles can open door configuration", - "description": "A list of four booleans which indicate which inside handles can open the door." - }, "auto_relock_time": { - "name": "Auto relock time", - "description": "Duration in seconds until lock returns to secure state. Only enforced when operation type is `constant`." - }, - "hold_and_release_time": { - "name": "Hold and release time", - "description": "Duration in seconds the latch stays retracted." - }, - "twist_assist": { - "name": "Twist assist", - "description": "Enable Twist Assist." + "description": "Duration in seconds until lock returns to secure state. Only enforced when operation type is `constant`.", + "name": "Auto relock time" }, "block_to_block": { - "name": "Block to block", - "description": "Enable block-to-block functionality." + "description": "Enable block-to-block functionality.", + "name": "Block to block" + }, + "hold_and_release_time": { + "description": "Duration in seconds the latch stays retracted.", + "name": "Hold and release time" + }, + "inside_handles_can_open_door_configuration": { + "description": "A list of four booleans which indicate which inside handles can open the door.", + "name": "Inside handles can open door configuration" + }, + "lock_timeout": { + "description": "Seconds until lock mode times out. Should only be used if operation type is `timed`.", + "name": "Lock timeout" + }, + "operation_type": { + "description": "The operation type of the lock.", + "name": "Operation Type" + }, + "outside_handles_can_open_door_configuration": { + "description": "A list of four booleans which indicate which outside handles can open the door.", + "name": "Outside handles can open door configuration" + }, + "twist_assist": { + "description": "Enable Twist Assist.", + "name": "Twist assist" } - } + }, + "name": "Set lock configuration" + }, + "set_lock_usercode": { + "description": "Sets a user code on a lock.", + "fields": { + "code_slot": { + "description": "Code slot to set the code.", + "name": "[%key:component::zwave_js::services::clear_lock_usercode::fields::code_slot::name%]" + }, + "usercode": { + "description": "Lock code to set.", + "name": "Code" + } + }, + "name": "Set lock user code" + }, + "set_value": { + "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": { + "description": "The ID of the command class for the value.", + "name": "Command class" + }, + "endpoint": { + "description": "The endpoint for the value.", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]" + }, + "options": { + "description": "Set value options map. Refer to the Z-Wave JS documentation for more information on what options can be set.", + "name": "Options" + }, + "property": { + "description": "The ID of the property for the value.", + "name": "Property" + }, + "property_key": { + "description": "The ID of the property key for the value.", + "name": "Property key" + }, + "value": { + "description": "The new value to set.", + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::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.", + "name": "Wait for result?" + } + }, + "name": "Set a value (advanced)" } } } From a1eaa5cbf2633754d157324a8a40f6100fcb4d05 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 1 Feb 2024 14:10:24 -0600 Subject: [PATCH 0038/1367] Migrate to new intent error response keys (#109269) --- .../components/conversation/default_agent.py | 120 +++++++++--------- .../components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../conversation/snapshots/test_init.ambr | 18 +-- .../conversation/test_default_agent.py | 110 ++++++++++++++-- 7 files changed, 174 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 123dc7aba1d..a2cb3b68041 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -12,22 +12,15 @@ import re from typing import IO, Any from hassil.expression import Expression, ListReference, Sequence -from hassil.intents import ( - Intents, - ResponseType, - SlotList, - TextSlotList, - WildcardSlotList, -) +from hassil.intents import Intents, SlotList, TextSlotList, WildcardSlotList from hassil.recognize import ( MISSING_ENTITY, RecognizeResult, - UnmatchedEntity, UnmatchedTextEntity, recognize_all, ) from hassil.util import merge_dict -from home_assistant_intents import get_intents, get_languages +from home_assistant_intents import ErrorKey, get_intents, get_languages import yaml from homeassistant import core, setup @@ -262,7 +255,7 @@ class DefaultAgent(AbstractConversationAgent): return _make_error_result( language, intent.IntentResponseErrorCode.NO_INTENT_MATCH, - self._get_error_text(ResponseType.NO_INTENT, lang_intents), + self._get_error_text(ErrorKey.NO_INTENT, lang_intents), conversation_id, ) @@ -276,9 +269,7 @@ class DefaultAgent(AbstractConversationAgent): else "", result.unmatched_entities_list, ) - error_response_type, error_response_args = _get_unmatched_response( - result.unmatched_entities - ) + error_response_type, error_response_args = _get_unmatched_response(result) return _make_error_result( language, intent.IntentResponseErrorCode.NO_VALID_TARGETS, @@ -328,7 +319,7 @@ class DefaultAgent(AbstractConversationAgent): return _make_error_result( language, intent.IntentResponseErrorCode.FAILED_TO_HANDLE, - self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents), + self._get_error_text(ErrorKey.HANDLE_ERROR, lang_intents), conversation_id, ) except intent.IntentUnexpectedError: @@ -336,7 +327,7 @@ class DefaultAgent(AbstractConversationAgent): return _make_error_result( language, intent.IntentResponseErrorCode.UNKNOWN, - self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents), + self._get_error_text(ErrorKey.HANDLE_ERROR, lang_intents), conversation_id, ) @@ -798,7 +789,7 @@ class DefaultAgent(AbstractConversationAgent): def _get_error_text( self, - response_type: ResponseType, + error_key: ErrorKey, lang_intents: LanguageIntents | None, **response_args, ) -> str: @@ -806,7 +797,7 @@ class DefaultAgent(AbstractConversationAgent): if lang_intents is None: return _DEFAULT_ERROR_TEXT - response_key = response_type.value + response_key = error_key.value response_str = ( lang_intents.error_responses.get(response_key) or _DEFAULT_ERROR_TEXT ) @@ -919,59 +910,72 @@ def _make_error_result( return ConversationResult(response, conversation_id) -def _get_unmatched_response( - unmatched_entities: dict[str, UnmatchedEntity], -) -> tuple[ResponseType, dict[str, Any]]: - error_response_type = ResponseType.NO_INTENT - error_response_args: dict[str, Any] = {} +def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str, Any]]: + """Get key and template arguments for error when there are unmatched intent entities/slots.""" - if unmatched_name := unmatched_entities.get("name"): - # Unmatched device or entity - assert isinstance(unmatched_name, UnmatchedTextEntity) - error_response_type = ResponseType.NO_ENTITY - error_response_args["entity"] = unmatched_name.text + # Filter out non-text and missing context entities + unmatched_text: dict[str, str] = { + key: entity.text.strip() + for key, entity in result.unmatched_entities.items() + if isinstance(entity, UnmatchedTextEntity) and entity.text != MISSING_ENTITY + } - elif unmatched_area := unmatched_entities.get("area"): - # Unmatched area - assert isinstance(unmatched_area, UnmatchedTextEntity) - error_response_type = ResponseType.NO_AREA - error_response_args["area"] = unmatched_area.text + if unmatched_area := unmatched_text.get("area"): + # area only + return ErrorKey.NO_AREA, {"area": unmatched_area} - return error_response_type, error_response_args + # Area may still have matched + matched_area: str | None = None + if matched_area_entity := result.entities.get("area"): + matched_area = matched_area_entity.text.strip() + + if unmatched_name := unmatched_text.get("name"): + if matched_area: + # device in area + return ErrorKey.NO_ENTITY_IN_AREA, { + "entity": unmatched_name, + "area": matched_area, + } + + # device only + return ErrorKey.NO_ENTITY, {"entity": unmatched_name} + + # Default error + return ErrorKey.NO_INTENT, {} def _get_no_states_matched_response( no_states_error: intent.NoStatesMatchedError, -) -> tuple[ResponseType, dict[str, Any]]: - """Return error response type and template arguments for error.""" - if not ( - no_states_error.area - and (no_states_error.device_classes or no_states_error.domains) - ): - # Device class and domain must be paired with an area for the error - # message. - return ResponseType.NO_INTENT, {} +) -> tuple[ErrorKey, dict[str, Any]]: + """Return key and template arguments for error when intent returns no matching states.""" - error_response_args: dict[str, Any] = {"area": no_states_error.area} - - # Check device classes first, since it's more specific than domain + # Device classes should be checked before domains if no_states_error.device_classes: - # No exposed entities of a particular class in an area. - # Example: "close the bedroom windows" - # - # Only use the first device class for the error message - error_response_args["device_class"] = next(iter(no_states_error.device_classes)) + device_class = next(iter(no_states_error.device_classes)) # first device class + if no_states_error.area: + # device_class in area + return ErrorKey.NO_DEVICE_CLASS_IN_AREA, { + "device_class": device_class, + "area": no_states_error.area, + } - return ResponseType.NO_DEVICE_CLASS, error_response_args + # device_class only + return ErrorKey.NO_DEVICE_CLASS, {"device_class": device_class} - # No exposed entities of a domain in an area. - # Example: "turn on lights in kitchen" - assert no_states_error.domains - # - # Only use the first domain for the error message - error_response_args["domain"] = next(iter(no_states_error.domains)) + if no_states_error.domains: + domain = next(iter(no_states_error.domains)) # first domain + if no_states_error.area: + # domain in area + return ErrorKey.NO_DOMAIN_IN_AREA, { + "domain": domain, + "area": no_states_error.area, + } - return ResponseType.NO_DOMAIN, error_response_args + # domain only + return ErrorKey.NO_DOMAIN, {"domain": domain} + + # Default error + return ErrorKey.NO_INTENT, {} def _collect_list_references(expression: Expression, list_names: set[str]) -> None: diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 89dd880f69e..ea0a11ae657 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.6.0", "home-assistant-intents==2024.1.29"] + "requirements": ["hassil==1.6.0", "home-assistant-intents==2024.2.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c5cea22795a..1b47b2693b0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ hass-nabucasa==0.76.0 hassil==1.6.0 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240131.0 -home-assistant-intents==2024.1.29 +home-assistant-intents==2024.2.1 httpx==0.26.0 ifaddr==0.2.0 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index da3092eab7a..2b804e3720a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1062,7 +1062,7 @@ holidays==0.41 home-assistant-frontend==20240131.0 # homeassistant.components.conversation -home-assistant-intents==2024.1.29 +home-assistant-intents==2024.2.1 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8da20de5f7..10c89637f25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -858,7 +858,7 @@ holidays==0.41 home-assistant-frontend==20240131.0 # homeassistant.components.conversation -home-assistant-intents==2024.1.29 +home-assistant-intents==2024.2.1 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 23dab0902a9..468f3215cb7 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -339,7 +339,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'An unexpected error occurred while handling the intent', + 'speech': 'An unexpected error occurred', }), }), }), @@ -379,7 +379,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'An unexpected error occurred while handling the intent', + 'speech': 'An unexpected error occurred', }), }), }), @@ -519,7 +519,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device or entity called late added alias', + 'speech': 'Sorry, I am not aware of any device called late added alias', }), }), }), @@ -539,7 +539,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device or entity called kitchen light', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -679,7 +679,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device or entity called late added light', + 'speech': 'Sorry, I am not aware of any device called late added light', }), }), }), @@ -759,7 +759,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device or entity called kitchen light', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -779,7 +779,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device or entity called my cool light', + 'speech': 'Sorry, I am not aware of any device called my cool light', }), }), }), @@ -919,7 +919,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device or entity called kitchen light', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -969,7 +969,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device or entity called renamed light', + 'speech': 'Sorry, I am not aware of any device called renamed light', }), }), }), diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index b992b0086d7..d7182aa3c2f 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -2,6 +2,7 @@ from collections import defaultdict from unittest.mock import AsyncMock, patch +from hassil.recognize import Intent, IntentData, MatchEntity, RecognizeResult import pytest from homeassistant.components import conversation @@ -430,8 +431,8 @@ async def test_device_area_context( ) -async def test_error_missing_entity(hass: HomeAssistant, init_components) -> None: - """Test error message when entity is missing.""" +async def test_error_no_device(hass: HomeAssistant, init_components) -> None: + """Test error message when device/entity is missing.""" result = await conversation.async_converse( hass, "turn on missing entity", None, Context(), None ) @@ -440,11 +441,11 @@ async def test_error_missing_entity(hass: HomeAssistant, init_components) -> Non assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS assert ( result.response.speech["plain"]["speech"] - == "Sorry, I am not aware of any device or entity called missing entity" + == "Sorry, I am not aware of any device called missing entity" ) -async def test_error_missing_area(hass: HomeAssistant, init_components) -> None: +async def test_error_no_area(hass: HomeAssistant, init_components) -> None: """Test error message when area is missing.""" result = await conversation.async_converse( hass, "turn on the lights in missing area", None, Context(), None @@ -458,10 +459,60 @@ async def test_error_missing_area(hass: HomeAssistant, init_components) -> None: ) -async def test_error_no_exposed_for_domain( +async def test_error_no_device_in_area( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: - """Test error message when no entities for a domain are exposed in an area.""" + """Test error message when area is missing a device/entity.""" + area_registry.async_get_or_create("kitchen") + result = await conversation.async_converse( + hass, "turn on missing entity in the kitchen", None, Context(), None + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I am not aware of any device called missing entity in the kitchen area" + ) + + +async def test_error_no_domain( + hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry +) -> None: + """Test error message when no devices/entities exist for a domain.""" + + # We don't have a sentence for turning on all fans + fan_domain = MatchEntity(name="domain", value="fan", text="") + recognize_result = RecognizeResult( + intent=Intent("HassTurnOn"), + intent_data=IntentData([]), + entities={"domain": fan_domain}, + entities_list=[fan_domain], + ) + + with patch( + "homeassistant.components.conversation.default_agent.recognize_all", + return_value=[recognize_result], + ): + result = await conversation.async_converse( + hass, "turn on the fans", None, Context(), None + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert ( + result.response.error_code + == intent.IntentResponseErrorCode.NO_VALID_TARGETS + ) + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I am not aware of any fan" + ) + + +async def test_error_no_domain_in_area( + hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry +) -> None: + """Test error message when no devices/entities for a domain exist in an area.""" area_registry.async_get_or_create("kitchen") result = await conversation.async_converse( hass, "turn on the lights in the kitchen", None, Context(), None @@ -475,10 +526,43 @@ async def test_error_no_exposed_for_domain( ) -async def test_error_no_exposed_for_device_class( +async def test_error_no_device_class( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: - """Test error message when no entities of a device class are exposed in an area.""" + """Test error message when no entities of a device class exist.""" + + # We don't have a sentence for opening all windows + window_class = MatchEntity(name="device_class", value="window", text="") + recognize_result = RecognizeResult( + intent=Intent("HassTurnOn"), + intent_data=IntentData([]), + entities={"device_class": window_class}, + entities_list=[window_class], + ) + + with patch( + "homeassistant.components.conversation.default_agent.recognize_all", + return_value=[recognize_result], + ): + result = await conversation.async_converse( + hass, "open the windows", None, Context(), None + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert ( + result.response.error_code + == intent.IntentResponseErrorCode.NO_VALID_TARGETS + ) + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I am not aware of any window" + ) + + +async def test_error_no_device_class_in_area( + hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry +) -> None: + """Test error message when no entities of a device class exist in an area.""" area_registry.async_get_or_create("bedroom") result = await conversation.async_converse( hass, "open bedroom windows", None, Context(), None @@ -492,8 +576,8 @@ async def test_error_no_exposed_for_device_class( ) -async def test_error_match_failure(hass: HomeAssistant, init_components) -> None: - """Test response with complete match failure.""" +async def test_error_no_intent(hass: HomeAssistant, init_components) -> None: + """Test response with an intent match failure.""" with patch( "homeassistant.components.conversation.default_agent.recognize_all", return_value=[], @@ -506,6 +590,10 @@ async def test_error_match_failure(hass: HomeAssistant, init_components) -> None assert ( result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH ) + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I couldn't understand that" + ) async def test_no_states_matched_default_error( @@ -601,5 +689,5 @@ async def test_all_domains_loaded( assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS assert ( result.response.speech["plain"]["speech"] - == "Sorry, I am not aware of any device or entity called test light" + == "Sorry, I am not aware of any device called test light" ) From c1f883519daa235e6ead9db768ecf550356cc763 Mon Sep 17 00:00:00 2001 From: Ryan Fleming Date: Thu, 1 Feb 2024 15:15:41 -0500 Subject: [PATCH 0039/1367] Add connect octoprint printer service (#99899) * Add connect octoprint printer service * Review changes * String updates * Swap exception type --- .../components/octoprint/__init__.py | 56 +++++++++++++++- homeassistant/components/octoprint/const.py | 3 + .../components/octoprint/services.yaml | 27 ++++++++ .../components/octoprint/strings.json | 29 ++++++++ tests/components/octoprint/test_servics.py | 66 +++++++++++++++++++ 5 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/octoprint/services.yaml create mode 100644 tests/components/octoprint/test_servics.py diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 50ba6c964f3..1a96078c003 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import cast import aiohttp from pyoctoprintapi import OctoprintClient @@ -11,24 +12,28 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_BINARY_SENSORS, + CONF_DEVICE_ID, CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PATH, CONF_PORT, + CONF_PROFILE_NAME, CONF_SENSORS, CONF_SSL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import ServiceValidationError import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.device_registry as dr from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify as util_slugify from homeassistant.util.ssl import get_default_context, get_default_no_verify_context -from .const import DOMAIN +from .const import CONF_BAUDRATE, DOMAIN, SERVICE_CONNECT from .coordinator import OctoprintDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -122,6 +127,15 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +SERVICE_CONNECT_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Optional(CONF_PROFILE_NAME): cv.string, + vol.Optional(CONF_PORT): cv.string, + vol.Optional(CONF_BAUDRATE): cv.positive_int, + } +) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the OctoPrint component.""" @@ -194,6 +208,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + async def async_printer_connect(call: ServiceCall) -> None: + """Connect to a printer.""" + client = async_get_client_for_service_call(hass, call) + await client.connect( + printer_profile=call.data.get(CONF_PROFILE_NAME), + port=call.data.get(CONF_PORT), + baud_rate=call.data.get(CONF_BAUDRATE), + ) + + if not hass.services.has_service(DOMAIN, SERVICE_CONNECT): + hass.services.async_register( + DOMAIN, + SERVICE_CONNECT, + async_printer_connect, + schema=SERVICE_CONNECT_SCHEMA, + ) + return True @@ -205,3 +236,24 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +def async_get_client_for_service_call( + hass: HomeAssistant, call: ServiceCall +) -> OctoprintClient: + """Get the client related to a service call (by device ID).""" + device_id = call.data[CONF_DEVICE_ID] + device_registry = dr.async_get(hass) + + if device_entry := device_registry.async_get(device_id): + for entry_id in device_entry.config_entries: + if data := hass.data[DOMAIN].get(entry_id): + return cast(OctoprintClient, data["client"]) + + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="missing_client", + translation_placeholders={ + "device_id": device_id, + }, + ) diff --git a/homeassistant/components/octoprint/const.py b/homeassistant/components/octoprint/const.py index df22cb8d8f8..2d2a9e4a907 100644 --- a/homeassistant/components/octoprint/const.py +++ b/homeassistant/components/octoprint/const.py @@ -3,3 +3,6 @@ DOMAIN = "octoprint" DEFAULT_NAME = "OctoPrint" + +SERVICE_CONNECT = "printer_connect" +CONF_BAUDRATE = "baudrate" diff --git a/homeassistant/components/octoprint/services.yaml b/homeassistant/components/octoprint/services.yaml new file mode 100644 index 00000000000..2cb4a6f3c2d --- /dev/null +++ b/homeassistant/components/octoprint/services.yaml @@ -0,0 +1,27 @@ +printer_connect: + fields: + device_id: + required: true + selector: + device: + integration: octoprint + profile_name: + required: false + selector: + text: + port: + required: false + selector: + text: + baudrate: + required: false + selector: + select: + options: + - "9600" + - "19200" + - "38400" + - "57600" + - "115200" + - "230400" + - "250000" diff --git a/homeassistant/components/octoprint/strings.json b/homeassistant/components/octoprint/strings.json index 63d9753ee1d..e9df0ed755c 100644 --- a/homeassistant/components/octoprint/strings.json +++ b/homeassistant/components/octoprint/strings.json @@ -35,5 +35,34 @@ "progress": { "get_api_key": "Open the OctoPrint UI and click 'Allow' on the Access Request for 'Home Assistant'." } + }, + "exceptions": { + "missing_client": { + "message": "No client for device ID: {device_id}" + } + }, + "services": { + "printer_connect": { + "name": "Connect to a printer", + "description": "Instructs the octoprint server to connect to a printer.", + "fields": { + "device_id": { + "name": "Server", + "description": "The server that should connect." + }, + "profile_name": { + "name": "Profile name", + "description": "Printer profile to connect with." + }, + "port": { + "name": "Serial port", + "description": "Port name to connect on." + }, + "baudrate": { + "name": "Baudrate", + "description": "Baud rate." + } + } + } } } diff --git a/tests/components/octoprint/test_servics.py b/tests/components/octoprint/test_servics.py new file mode 100644 index 00000000000..70e983c4bb4 --- /dev/null +++ b/tests/components/octoprint/test_servics.py @@ -0,0 +1,66 @@ +"""Test the OctoPrint services.""" +from unittest.mock import patch + +from homeassistant.components.octoprint.const import ( + CONF_BAUDRATE, + DOMAIN, + SERVICE_CONNECT, +) +from homeassistant.const import ATTR_DEVICE_ID, CONF_PORT, CONF_PROFILE_NAME +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry, + async_get as async_get_dev_reg, +) + +from . import init_integration + + +async def test_connect_default(hass) -> None: + """Test the connect to printer service.""" + await init_integration(hass, "sensor") + + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, "uuid")[0] + + # Test pausing the printer when it is printing + with patch("pyoctoprintapi.OctoprintClient.connect") as connect_command: + await hass.services.async_call( + DOMAIN, + SERVICE_CONNECT, + { + ATTR_DEVICE_ID: device.id, + }, + blocking=True, + ) + + assert len(connect_command.mock_calls) == 1 + connect_command.assert_called_with( + port=None, printer_profile=None, baud_rate=None + ) + + +async def test_connect_all_arguments(hass) -> None: + """Test the connect to printer service.""" + await init_integration(hass, "sensor") + + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, "uuid")[0] + + # Test pausing the printer when it is printing + with patch("pyoctoprintapi.OctoprintClient.connect") as connect_command: + await hass.services.async_call( + DOMAIN, + SERVICE_CONNECT, + { + ATTR_DEVICE_ID: device.id, + CONF_PROFILE_NAME: "Test Profile", + CONF_PORT: "VIRTUAL", + CONF_BAUDRATE: 9600, + }, + blocking=True, + ) + + assert len(connect_command.mock_calls) == 1 + connect_command.assert_called_with( + port="VIRTUAL", printer_profile="Test Profile", baud_rate=9600 + ) From 3511f3541835187988056a1ac9bee6eb3c1d6eab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 1 Feb 2024 22:06:34 +0100 Subject: [PATCH 0040/1367] Fix custom attribute lookup in Traccar Server (#109331) --- .../components/traccar_server/coordinator.py | 14 ++++++++------ .../components/traccar_server/device_tracker.py | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/traccar_server/coordinator.py b/homeassistant/components/traccar_server/coordinator.py index 337d0dcafbb..90c910e6062 100644 --- a/homeassistant/components/traccar_server/coordinator.py +++ b/homeassistant/components/traccar_server/coordinator.py @@ -93,10 +93,9 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat skip_accuracy_filter = False for custom_attr in self.custom_attributes: - attr[custom_attr] = getattr( - device["attributes"], + attr[custom_attr] = device["attributes"].get( custom_attr, - getattr(position["attributes"], custom_attr, None), + position["attributes"].get(custom_attr, None), ) if custom_attr in self.skip_accuracy_filter_for: skip_accuracy_filter = True @@ -151,13 +150,16 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat device = get_device(event["deviceId"], devices) self.hass.bus.async_fire( # This goes against two of the HA core guidelines: - # 1. Event names should be prefixed with the domain name of the integration + # 1. Event names should be prefixed with the domain name of + # the integration # 2. This should be event entities - # However, to not break it for those who currently use the "old" integration, this is kept as is. + # + # However, to not break it for those who currently use + # the "old" integration, this is kept as is. f"traccar_{EVENTS[event['type']]}", { "device_traccar_id": event["deviceId"], - "device_name": getattr(device, "name", None), + "device_name": device["name"] if device else None, "type": event["type"], "serverTime": event["eventTime"], "attributes": event["attributes"], diff --git a/homeassistant/components/traccar_server/device_tracker.py b/homeassistant/components/traccar_server/device_tracker.py index 2abcc6398fb..226d942e465 100644 --- a/homeassistant/components/traccar_server/device_tracker.py +++ b/homeassistant/components/traccar_server/device_tracker.py @@ -51,12 +51,13 @@ class TraccarServerDeviceTracker(TraccarServerEntity, TrackerEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return device specific attributes.""" + geofence_name = self.traccar_geofence["name"] if self.traccar_geofence else None return { **self.traccar_attributes, ATTR_ADDRESS: self.traccar_position["address"], ATTR_ALTITUDE: self.traccar_position["altitude"], ATTR_CATEGORY: self.traccar_device["category"], - ATTR_GEOFENCE: getattr(self.traccar_geofence, "name", None), + ATTR_GEOFENCE: geofence_name, ATTR_MOTION: self.traccar_position["attributes"].get("motion", False), ATTR_SPEED: self.traccar_position["speed"], ATTR_STATUS: self.traccar_device["status"], From 26be6a677c73a07d68074fa2abcaad85d3bbacd2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 1 Feb 2024 22:13:41 +0100 Subject: [PATCH 0041/1367] Update Home Assistant base image to 2024.02.0 (#109329) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index 824d580913d..d0baa4ac18e 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:2024.01.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.01.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.01.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.01.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.01.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.02.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.02.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.02.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.02.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.02.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From 8038d833e8f82fa7d381fd153cb153924412d29a Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 1 Feb 2024 22:28:02 +0100 Subject: [PATCH 0042/1367] Add device class to tesla wall connector session energy (#109333) --- homeassistant/components/tesla_wall_connector/sensor.py | 2 ++ tests/components/tesla_wall_connector/test_sensor.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tesla_wall_connector/sensor.py b/homeassistant/components/tesla_wall_connector/sensor.py index 09933d628fe..67d3d4ba22e 100644 --- a/homeassistant/components/tesla_wall_connector/sensor.py +++ b/homeassistant/components/tesla_wall_connector/sensor.py @@ -153,7 +153,9 @@ WALL_CONNECTOR_SENSORS = [ key="session_energy_wh", translation_key="session_energy_wh", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].session_energy_wh, + device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.MEASUREMENT, ), WallConnectorSensorDescription( diff --git a/tests/components/tesla_wall_connector/test_sensor.py b/tests/components/tesla_wall_connector/test_sensor.py index 684d7de0e82..28b50ba72ea 100644 --- a/tests/components/tesla_wall_connector/test_sensor.py +++ b/tests/components/tesla_wall_connector/test_sensor.py @@ -47,7 +47,7 @@ async def test_sensors(hass: HomeAssistant) -> None: "sensor.tesla_wall_connector_phase_c_voltage", "232.1", "230" ), EntityAndExpectedValues( - "sensor.tesla_wall_connector_session_energy", "1234.56", "112.2" + "sensor.tesla_wall_connector_session_energy", "1.23456", "0.1122" ), ] From 5a35c2e1e99ebee62655561eaef405c988ed437a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Feb 2024 15:39:44 -0600 Subject: [PATCH 0043/1367] Fix stale camera error message in img_util (#109325) --- homeassistant/components/camera/img_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/camera/img_util.py b/homeassistant/components/camera/img_util.py index dcb321d5ebb..e41e43c3a3c 100644 --- a/homeassistant/components/camera/img_util.py +++ b/homeassistant/components/camera/img_util.py @@ -98,6 +98,6 @@ class TurboJPEGSingleton: TurboJPEGSingleton.__instance = TurboJPEG() except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Error loading libturbojpeg; Cameras may impact HomeKit performance" + "Error loading libturbojpeg; Camera snapshot performance will be sub-optimal" ) TurboJPEGSingleton.__instance = False From b32371b5c94a40d70044de9298c58f7d50c328a2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 07:39:54 +0100 Subject: [PATCH 0044/1367] Add TURN_ON/OFF ClimateEntityFeature for Matter (#108974) * Add TURN_ON/OFF ClimateEntityFeature for Matter * Adjust matter --- homeassistant/components/matter/climate.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index a22f9174d2a..8769fc430d8 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -73,11 +73,8 @@ 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 + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -99,6 +96,13 @@ class MatterClimate(MatterEntity, ClimateEntity): self._attr_hvac_modes.append(HVACMode.COOL) if feature_map & ThermostatFeature.kAutoMode: self._attr_hvac_modes.append(HVACMode.HEAT_COOL) + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.TURN_OFF + ) + if any(mode for mode in self.hvac_modes if mode != HVACMode.OFF): + self._attr_supported_features |= ClimateEntityFeature.TURN_ON async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" From b471b9926d4a4b812c96e872857050214ab2a644 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 2 Feb 2024 02:58:03 -0500 Subject: [PATCH 0045/1367] Missing template helper translation keys (#109347) --- homeassistant/components/template/strings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 19ad9e5ddeb..79cd0289724 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -75,6 +75,7 @@ "safety": "[%key:component::binary_sensor::entity_component::safety::name%]", "smoke": "[%key:component::binary_sensor::entity_component::smoke::name%]", "sound": "[%key:component::binary_sensor::entity_component::sound::name%]", + "tamper": "[%key:component::binary_sensor::entity_component::tamper::name%]", "update": "[%key:component::binary_sensor::entity_component::update::name%]", "vibration": "[%key:component::binary_sensor::entity_component::vibration::name%]", "window": "[%key:component::binary_sensor::entity_component::window::name%]" @@ -127,6 +128,7 @@ "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", + "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::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%]", From 1c84997c5cd64a7527e6b7a12dcfa40414668cc4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Feb 2024 01:58:55 -0600 Subject: [PATCH 0046/1367] Reduce lock contention when all icons are already cached (#109352) --- homeassistant/helpers/icon.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/homeassistant/helpers/icon.py b/homeassistant/helpers/icon.py index 3486925b095..dd216a78648 100644 --- a/homeassistant/helpers/icon.py +++ b/homeassistant/helpers/icon.py @@ -13,7 +13,6 @@ from homeassistant.util.json import load_json_object from .translation import build_resources -ICON_LOAD_LOCK = "icon_load_lock" ICON_CACHE = "icon_cache" _LOGGER = logging.getLogger(__name__) @@ -73,13 +72,14 @@ async def _async_get_component_icons( class _IconsCache: """Cache for icons.""" - __slots__ = ("_hass", "_loaded", "_cache") + __slots__ = ("_hass", "_loaded", "_cache", "_lock") def __init__(self, hass: HomeAssistant) -> None: """Initialize the cache.""" self._hass = hass self._loaded: set[str] = set() self._cache: dict[str, dict[str, Any]] = {} + self._lock = asyncio.Lock() async def async_fetch( self, @@ -88,7 +88,13 @@ class _IconsCache: ) -> dict[str, dict[str, Any]]: """Load resources into the cache.""" if components_to_load := components - self._loaded: - await self._async_load(components_to_load) + # Icons are never unloaded so if there are no components to load + # we can skip the lock which reduces contention + async with self._lock: + # Check components to load again, as another task might have loaded + # them while we were waiting for the lock. + if components_to_load := components - self._loaded: + await self._async_load(components_to_load) return { component: result @@ -143,21 +149,19 @@ async def async_get_icons( """Return all icons of integrations. If integration specified, load it for that one; otherwise default to loaded - intgrations. + integrations. """ - lock = hass.data.setdefault(ICON_LOAD_LOCK, asyncio.Lock()) - if integrations: components = set(integrations) else: components = { component for component in hass.config.components if "." not in component } - async with lock: - if ICON_CACHE in hass.data: - cache: _IconsCache = hass.data[ICON_CACHE] - else: - cache = hass.data[ICON_CACHE] = _IconsCache(hass) + + if ICON_CACHE in hass.data: + cache: _IconsCache = hass.data[ICON_CACHE] + else: + cache = hass.data[ICON_CACHE] = _IconsCache(hass) return await cache.async_fetch(category, components) From 155499fafe6eaffd943bd6320db7185b08ae3ac4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Feb 2024 02:00:46 -0600 Subject: [PATCH 0047/1367] Load json file as binary instead of decoding to string (#109351) --- homeassistant/util/json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 630c39b3ad4..65f93020cc6 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -74,7 +74,7 @@ def load_json( Defaults to returning empty dict if file is not found. """ try: - with open(filename, encoding="utf-8") as fdesc: + with open(filename, mode="rb") as fdesc: return orjson.loads(fdesc.read()) # type: ignore[no-any-return] except FileNotFoundError: # This is not a fatal error From 9204e85b61af3d8b05bf4c0741a2680e0ea50f15 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:01:38 +0100 Subject: [PATCH 0048/1367] Add sensibo migrated ClimateEntityFeatures (#109340) Adds sensibo migrated ClimateEntityFeatures --- homeassistant/components/sensibo/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index a718cac88fb..bcc851e02ae 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -191,6 +191,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): _attr_name = None _attr_precision = PRECISION_TENTHS _attr_translation_key = "climate_device" + _enable_turn_on_off_backwards_compatibility = False def __init__( self, coordinator: SensiboDataUpdateCoordinator, device_id: str From 582d6968b281dd1cbf29fa31a3dbf2600ea5515e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Feb 2024 02:02:26 -0600 Subject: [PATCH 0049/1367] Avoid de/recode of bytes to string to bytes when writing json files (#109348) --- homeassistant/helpers/json.py | 21 +++++++++++++-------- homeassistant/util/file.py | 14 +++++--------- tests/util/test_file.py | 5 +++++ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index b9862907960..ba2486a196e 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -148,12 +148,17 @@ JSON_DUMP: Final = json_dumps def _orjson_default_encoder(data: Any) -> str: - """JSON encoder that uses orjson with hass defaults.""" + """JSON encoder that uses orjson with hass defaults and returns a str.""" + return _orjson_bytes_default_encoder(data).decode("utf-8") + + +def _orjson_bytes_default_encoder(data: Any) -> bytes: + """JSON encoder that uses orjson with hass defaults and returns bytes.""" return orjson.dumps( data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS, default=json_encoder_default, - ).decode("utf-8") + ) def save_json( @@ -173,11 +178,13 @@ def save_json( if encoder and encoder is not JSONEncoder: # If they pass a custom encoder that is not the # default JSONEncoder, we use the slow path of json.dumps + mode = "w" dump = json.dumps - json_data = json.dumps(data, indent=2, cls=encoder) + json_data: str | bytes = json.dumps(data, indent=2, cls=encoder) else: + mode = "wb" dump = _orjson_default_encoder - json_data = _orjson_default_encoder(data) + json_data = _orjson_bytes_default_encoder(data) except TypeError as error: formatted_data = format_unserializable_data( find_paths_unserializable_data(data, dump=dump) @@ -186,10 +193,8 @@ def save_json( _LOGGER.error(msg) raise SerializationError(msg) from error - if atomic_writes: - write_utf8_file_atomic(filename, json_data, private) - else: - write_utf8_file(filename, json_data, private) + method = write_utf8_file_atomic if atomic_writes else write_utf8_file + method(filename, json_data, private, mode=mode) def find_paths_unserializable_data( diff --git a/homeassistant/util/file.py b/homeassistant/util/file.py index 06471eaca6a..1af65fa51d7 100644 --- a/homeassistant/util/file.py +++ b/homeassistant/util/file.py @@ -17,9 +17,7 @@ class WriteError(HomeAssistantError): def write_utf8_file_atomic( - filename: str, - utf8_data: str, - private: bool = False, + filename: str, utf8_data: bytes | str, private: bool = False, mode: str = "w" ) -> None: """Write a file and rename it into place using atomicwrites. @@ -34,7 +32,7 @@ def write_utf8_file_atomic( negatively impact performance. """ try: - with AtomicWriter(filename, overwrite=True).open() as fdesc: + with AtomicWriter(filename, mode=mode, overwrite=True).open() as fdesc: if not private: os.fchmod(fdesc.fileno(), 0o644) fdesc.write(utf8_data) @@ -44,20 +42,18 @@ def write_utf8_file_atomic( def write_utf8_file( - filename: str, - utf8_data: str, - private: bool = False, + filename: str, utf8_data: bytes | str, private: bool = False, mode: str = "w" ) -> None: """Write a file and rename it into place. Writes all or nothing. """ - tmp_filename = "" + encoding = "utf-8" if "b" not in mode else None try: # Modern versions of Python tempfile create this file with mode 0o600 with tempfile.NamedTemporaryFile( - mode="w", encoding="utf-8", dir=os.path.dirname(filename), delete=False + mode=mode, encoding=encoding, dir=os.path.dirname(filename), delete=False ) as fdesc: fdesc.write(utf8_data) tmp_filename = fdesc.name diff --git a/tests/util/test_file.py b/tests/util/test_file.py index 0b87985fe13..dc09ff83e9e 100644 --- a/tests/util/test_file.py +++ b/tests/util/test_file.py @@ -25,6 +25,11 @@ def test_write_utf8_file_atomic_private(tmpdir: py.path.local, func) -> None: assert fh.read() == '{"some":"data"}' assert os.stat(test_file).st_mode & 0o777 == 0o600 + func(test_file, b'{"some":"data"}', True, mode="wb") + with open(test_file) as fh: + assert fh.read() == '{"some":"data"}' + assert os.stat(test_file).st_mode & 0o777 == 0o600 + def test_write_utf8_file_fails_at_creation(tmpdir: py.path.local) -> None: """Test that failed creation of the temp file does not create an empty file.""" From 449790c1782f1ca52de28855c57d8f3018932f3e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:02:41 +0100 Subject: [PATCH 0050/1367] Add Adax migrated ClimateEntityFeatures (#109341) --- homeassistant/components/adax/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 2ce8adc30d6..6b0adcb52cf 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -74,6 +74,7 @@ class AdaxDevice(ClimateEntity): ) _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None: """Initialize the heater.""" From 72f1d8ec15714122530683d0ae8adf511bb44fd5 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 2 Feb 2024 03:10:51 -0500 Subject: [PATCH 0051/1367] Add Duquesne Light virtual integration supported by opower (#109272) Co-authored-by: Dan Swartz <3066652+swartzd@users.noreply.github.com> --- homeassistant/components/duquesne_light/__init__.py | 1 + homeassistant/components/duquesne_light/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 homeassistant/components/duquesne_light/__init__.py create mode 100644 homeassistant/components/duquesne_light/manifest.json diff --git a/homeassistant/components/duquesne_light/__init__.py b/homeassistant/components/duquesne_light/__init__.py new file mode 100644 index 00000000000..33c35ecb4cd --- /dev/null +++ b/homeassistant/components/duquesne_light/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Duquesne Light.""" diff --git a/homeassistant/components/duquesne_light/manifest.json b/homeassistant/components/duquesne_light/manifest.json new file mode 100644 index 00000000000..3cb01757950 --- /dev/null +++ b/homeassistant/components/duquesne_light/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "duquesne_light", + "name": "Duquesne Light", + "integration_type": "virtual", + "supported_by": "opower" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 21186272bb6..9ae87cbd706 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1342,6 +1342,11 @@ "config_flow": true, "iot_class": "local_push" }, + "duquesne_light": { + "name": "Duquesne Light", + "integration_type": "virtual", + "supported_by": "opower" + }, "dwd_weather_warnings": { "name": "Deutscher Wetterdienst (DWD) Weather Warnings", "integration_type": "service", From d5d74005f42178885610af117d6874373d906732 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:31:09 +0100 Subject: [PATCH 0052/1367] Add migrated ClimateEntityFeature for Nibe Heat Pump (#109140) --- homeassistant/components/nibe_heatpump/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nibe_heatpump/climate.py b/homeassistant/components/nibe_heatpump/climate.py index 38a3a5f825c..3a89f4f6022 100644 --- a/homeassistant/components/nibe_heatpump/climate.py +++ b/homeassistant/components/nibe_heatpump/climate.py @@ -72,6 +72,7 @@ class NibeClimateEntity(CoordinatorEntity[Coordinator], ClimateEntity): _attr_target_temperature_step = 0.5 _attr_max_temp = 35.0 _attr_min_temp = 5.0 + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From 2788576dc956fed01ec0bdb5939f045a5148be69 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:31:35 +0100 Subject: [PATCH 0053/1367] Add TURN_ON/OFF ClimateEntityFeature for Modbus (#109133) --- homeassistant/components/modbus/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 71c01d20205..637478fffd4 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -97,7 +97,12 @@ async def async_setup_platform( class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): """Representation of a Modbus Thermostat.""" - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From 67e6febde47b6ef3c830a2034a427d3ed55a44fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Feb 2024 02:32:17 -0600 Subject: [PATCH 0054/1367] Ensure the purge entities service cleans up the states_meta table (#109344) --- homeassistant/components/recorder/purge.py | 2 ++ tests/components/recorder/test_purge.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 8dd539f84f3..0b63bb8daa2 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -794,4 +794,6 @@ def purge_entity_data( _LOGGER.debug("Purging entity data hasn't fully completed yet") return False + _purge_old_entity_ids(instance, session) + return True diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 1696c9018b4..2a9260a28a4 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -1424,6 +1424,18 @@ async def test_purge_entities( ) assert states_sensor_kept.count() == 10 + # sensor.keep should remain in the StatesMeta table + states_meta_remain = session.query(StatesMeta).filter( + StatesMeta.entity_id == "sensor.keep" + ) + assert states_meta_remain.count() == 1 + + # sensor.purge_entity should be removed from the StatesMeta table + states_meta_remain = session.query(StatesMeta).filter( + StatesMeta.entity_id == "sensor.purge_entity" + ) + assert states_meta_remain.count() == 0 + _add_purge_records(hass) # Confirm calling service without arguments matches all records (default filter behavior) @@ -1437,6 +1449,10 @@ async def test_purge_entities( states = session.query(States) assert states.count() == 0 + # The states_meta table should be empty + states_meta_remain = session.query(StatesMeta) + assert states_meta_remain.count() == 0 + async def _add_test_states(hass: HomeAssistant, wait_recording_done: bool = True): """Add multiple states to the db for testing.""" From 8a22a8d4ba275cb93a047fd9feef71b085a4ad76 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:32:50 +0100 Subject: [PATCH 0055/1367] Add migrated ClimateEntityFeature for Atag (#108961) --- homeassistant/components/atag/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index 9b2729f141e..a5f119e3a2b 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -46,6 +46,7 @@ class AtagThermostat(AtagEntity, ClimateEntity): _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator, atag_id): """Initialize an Atag climate device.""" From b379e9db60ec8dde7689773feb497267e29a1d3e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:33:13 +0100 Subject: [PATCH 0056/1367] Add migrated ClimateEntityFeature for SwitchBot Cloud (#109136) --- homeassistant/components/switchbot_cloud/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/switchbot_cloud/climate.py b/homeassistant/components/switchbot_cloud/climate.py index 803669c806d..d184063939a 100644 --- a/homeassistant/components/switchbot_cloud/climate.py +++ b/homeassistant/components/switchbot_cloud/climate.py @@ -80,6 +80,7 @@ class SwitchBotCloudAirConditionner(SwitchBotCloudEntity, ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_target_temperature = 21 _attr_name = None + _enable_turn_on_off_backwards_compatibility = False async def _do_send_command( self, From 596f61ff07748e1ee896795a8032949fda7d13f3 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:33:54 +0100 Subject: [PATCH 0057/1367] Add TURN_ON/OFF ClimateEntityFeature for Fibaro (#108963) --- homeassistant/components/fibaro/climate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 18fef8dbe7a..42b8a5c0446 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -126,6 +126,8 @@ async def async_setup_entry( class FibaroThermostat(FibaroDevice, ClimateEntity): """Representation of a Fibaro Thermostat.""" + _enable_turn_on_off_backwards_compatibility = False + def __init__(self, fibaro_device: DeviceModel) -> None: """Initialize the Fibaro device.""" super().__init__(fibaro_device) @@ -209,6 +211,11 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): if mode in OPMODES_PRESET: self._attr_preset_modes.append(OPMODES_PRESET[mode]) + if HVACMode.OFF in self._attr_hvac_modes and len(self._attr_hvac_modes) > 1: + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" _LOGGER.debug( From 1f466e737e53e105595aa288d780d073b5359d54 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Feb 2024 09:34:43 +0100 Subject: [PATCH 0058/1367] Use send_json_auto_id in recorder tests (#109355) --- tests/components/recorder/test_statistics.py | 3 +- .../components/recorder/test_websocket_api.py | 291 ++++++------------ 2 files changed, 100 insertions(+), 194 deletions(-) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 69b7f9316f7..00ffdc21b81 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -785,9 +785,8 @@ async def test_import_statistics( } # Adjust the statistics in a different unit - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/adjust_sum_statistics", "statistic_id": statistic_id, "start_time": period2.isoformat(), diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 323b81211d7..e902dd49020 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -156,9 +156,8 @@ async def test_statistics_during_period( await async_wait_recording_done(hass) client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "end_time": now.isoformat(), @@ -170,9 +169,8 @@ async def test_statistics_during_period( assert response["success"] assert response["result"] == {} - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -194,9 +192,8 @@ async def test_statistics_during_period( ] } - await client.send_json( + await client.send_json_auto_id( { - "id": 3, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -226,13 +223,6 @@ async def test_statistic_during_period( offset, ) -> None: """Test statistic_during_period.""" - id = 1 - - def next_id(): - nonlocal id - id += 1 - return id - now = dt_util.utcnow() await async_recorder_block_till_done(hass) @@ -313,9 +303,8 @@ async def test_statistic_during_period( assert run_cache.get_latest_ids({metadata_id}) is not None # No data for this period yet - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "fixed_period": { "start_time": now.isoformat(), @@ -334,9 +323,8 @@ async def test_statistic_during_period( } # This should include imported_statistics_5min[:] - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", } @@ -359,9 +347,8 @@ async def test_statistic_during_period( dt_util.parse_datetime("2022-10-21T07:15:00+00:00") + timedelta(minutes=5 * offset) ).isoformat() - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "fixed_period": { @@ -388,9 +375,8 @@ async def test_statistic_during_period( dt_util.parse_datetime("2022-10-21T08:20:00+00:00") + timedelta(minutes=5 * offset) ).isoformat() - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "fixed_period": { @@ -414,9 +400,8 @@ async def test_statistic_during_period( + timedelta(minutes=5 * offset) ).isoformat() assert imported_stats_5min[26]["start"].isoformat() == start_time - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "fixed_period": { "start_time": start_time, @@ -438,9 +423,8 @@ async def test_statistic_during_period( dt_util.parse_datetime("2022-10-21T06:09:00+00:00") + timedelta(minutes=5 * offset) ).isoformat() - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "fixed_period": { "start_time": start_time, @@ -463,9 +447,8 @@ async def test_statistic_during_period( + timedelta(minutes=5 * offset) ).isoformat() assert imported_stats_5min[26]["start"].isoformat() == end_time - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "fixed_period": { "end_time": end_time, @@ -493,9 +476,8 @@ async def test_statistic_during_period( + timedelta(minutes=5 * offset) ).isoformat() assert imported_stats_5min[32]["start"].isoformat() == end_time - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "fixed_period": { "start_time": start_time, @@ -517,9 +499,8 @@ async def test_statistic_during_period( start_time = "2022-10-21T06:00:00+00:00" assert imported_stats_5min[24 - offset]["start"].isoformat() == start_time assert imported_stats[2]["start"].isoformat() == start_time - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "fixed_period": { "start_time": start_time, @@ -538,9 +519,8 @@ async def test_statistic_during_period( } # This should also include imported_statistics[2:] + imported_statistics_5min[36:] - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "rolling_window": { "duration": {"hours": 1, "minutes": 25}, @@ -559,9 +539,8 @@ async def test_statistic_during_period( } # This should include imported_statistics[2:3] - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "rolling_window": { "duration": {"hours": 1}, @@ -585,9 +564,8 @@ async def test_statistic_during_period( } # Test we can get only selected types - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "types": ["max", "change"], @@ -601,9 +579,8 @@ async def test_statistic_during_period( } # Test we can convert units - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "units": {"energy": "MWh"}, @@ -621,9 +598,8 @@ async def test_statistic_during_period( # Test we can automatically convert units hass.states.async_set("sensor.test", None, attributes=ENERGY_SENSOR_WH_ATTRIBUTES) - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", } @@ -707,9 +683,8 @@ async def test_statistic_during_period_hole( await async_wait_recording_done(hass) # This should include imported_stats[:] - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", } @@ -728,9 +703,8 @@ async def test_statistic_during_period_hole( end_time = "2022-10-21T05:00:00+00:00" assert imported_stats[0]["start"].isoformat() == start_time assert imported_stats[-1]["start"].isoformat() < end_time - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "fixed_period": { @@ -751,9 +725,8 @@ async def test_statistic_during_period_hole( # This should also include imported_stats[:] start_time = "2022-10-20T13:00:00+00:00" end_time = "2022-10-21T08:20:00+00:00" - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "fixed_period": { @@ -776,9 +749,8 @@ async def test_statistic_during_period_hole( end_time = "2022-10-20T23:00:00+00:00" assert imported_stats[1]["start"].isoformat() == start_time assert imported_stats[3]["start"].isoformat() < end_time - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "fixed_period": { @@ -801,9 +773,8 @@ async def test_statistic_during_period_hole( end_time = "2022-10-21T00:00:00+00:00" assert imported_stats[1]["start"].isoformat() > start_time assert imported_stats[3]["start"].isoformat() < end_time - await client.send_json( + await client.send_json_auto_id( { - "id": next_id(), "type": "recorder/statistic_during_period", "statistic_id": "sensor.test", "fixed_period": { @@ -894,9 +865,8 @@ async def test_statistic_during_period_calendar( "homeassistant.components.recorder.websocket_api.statistic_during_period", return_value={}, ) as statistic_during_period: - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistic_during_period", "calendar": calendar_period, "statistic_id": "sensor.test", @@ -956,9 +926,8 @@ async def test_statistics_during_period_unit_conversion( client = await hass_ws_client() # Query in state unit - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -981,9 +950,8 @@ async def test_statistics_during_period_unit_conversion( } # Query in custom unit - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1044,9 +1012,8 @@ async def test_sum_statistics_during_period_unit_conversion( client = await hass_ws_client() # Query in state unit - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1069,9 +1036,8 @@ async def test_sum_statistics_during_period_unit_conversion( } # Query in custom unit - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1121,9 +1087,8 @@ async def test_statistics_during_period_invalid_unit_conversion( client = await hass_ws_client() # Query in state unit - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1135,9 +1100,8 @@ async def test_statistics_during_period_invalid_unit_conversion( assert response["result"] == {} # Query in custom unit - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1176,9 +1140,8 @@ async def test_statistics_during_period_in_the_past( await async_wait_recording_done(hass) client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "end_time": now.isoformat(), @@ -1190,9 +1153,8 @@ async def test_statistics_during_period_in_the_past( assert response["success"] assert response["result"] == {} - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1204,9 +1166,8 @@ async def test_statistics_during_period_in_the_past( assert response["result"] == {} past = now - timedelta(days=3, hours=1) - await client.send_json( + await client.send_json_auto_id( { - "id": 3, "type": "recorder/statistics_during_period", "start_time": past.isoformat(), "statistic_ids": ["sensor.test"], @@ -1229,9 +1190,8 @@ async def test_statistics_during_period_in_the_past( } start_of_day = stats_top_of_hour.replace(hour=0, minute=0) - await client.send_json( + await client.send_json_auto_id( { - "id": 4, "type": "recorder/statistics_during_period", "start_time": stats_top_of_hour.isoformat(), "statistic_ids": ["sensor.test"], @@ -1253,9 +1213,8 @@ async def test_statistics_during_period_in_the_past( ] } - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1272,9 +1231,8 @@ async def test_statistics_during_period_bad_start_time( ) -> None: """Test statistics_during_period.""" client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": "cats", "statistic_ids": ["sensor.test"], @@ -1293,9 +1251,8 @@ async def test_statistics_during_period_bad_end_time( now = dt_util.utcnow() client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "end_time": "dogs", @@ -1315,9 +1272,8 @@ async def test_statistics_during_period_no_statistic_ids( now = dt_util.utcnow() client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "end_time": (now + timedelta(seconds=1)).isoformat(), @@ -1336,9 +1292,8 @@ async def test_statistics_during_period_empty_statistic_ids( now = dt_util.utcnow() client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": [], @@ -1428,7 +1383,7 @@ async def test_list_statistic_ids( await async_recorder_block_till_done(hass) client = await hass_ws_client() - await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [] @@ -1436,7 +1391,7 @@ async def test_list_statistic_ids( hass.states.async_set("sensor.test", 10, attributes=attributes) await async_wait_recording_done(hass) - await client.send_json({"id": 2, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1458,7 +1413,7 @@ async def test_list_statistic_ids( hass.states.async_remove("sensor.test") await hass.async_block_till_done() - await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1474,14 +1429,14 @@ async def test_list_statistic_ids( } ] - await client.send_json( - {"id": 4, "type": "recorder/list_statistic_ids", "statistic_type": "dogs"} + await client.send_json_auto_id( + {"type": "recorder/list_statistic_ids", "statistic_type": "dogs"} ) response = await client.receive_json() assert not response["success"] - await client.send_json( - {"id": 5, "type": "recorder/list_statistic_ids", "statistic_type": "mean"} + await client.send_json_auto_id( + {"type": "recorder/list_statistic_ids", "statistic_type": "mean"} ) response = await client.receive_json() assert response["success"] @@ -1501,8 +1456,8 @@ async def test_list_statistic_ids( else: assert response["result"] == [] - await client.send_json( - {"id": 6, "type": "recorder/list_statistic_ids", "statistic_type": "sum"} + await client.send_json_auto_id( + {"type": "recorder/list_statistic_ids", "statistic_type": "sum"} ) response = await client.receive_json() assert response["success"] @@ -1591,7 +1546,7 @@ async def test_list_statistic_ids_unit_change( await async_recorder_block_till_done(hass) client = await hass_ws_client() - await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [] @@ -1602,7 +1557,7 @@ async def test_list_statistic_ids_unit_change( do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) - await client.send_json({"id": 2, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1621,7 +1576,7 @@ async def test_list_statistic_ids_unit_change( # Change the state unit hass.states.async_set("sensor.test", 10, attributes=attributes2) - await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1642,17 +1597,9 @@ async def test_validate_statistics( recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test validate_statistics can be called.""" - id = 1 - - def next_id(): - nonlocal id - id += 1 - return id async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) + await client.send_json_auto_id({"type": "recorder/validate_statistics"}) response = await client.receive_json() assert response["success"] assert response["result"] == expected_result @@ -1685,9 +1632,8 @@ async def test_clear_statistics( await async_recorder_block_till_done(hass) client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test1", "sensor.test2", "sensor.test3"], @@ -1730,9 +1676,8 @@ async def test_clear_statistics( } assert response["result"] == expected_response - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/clear_statistics", "statistic_ids": ["sensor.test"], } @@ -1742,9 +1687,8 @@ async def test_clear_statistics( await async_recorder_block_till_done(hass) client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 3, "type": "recorder/statistics_during_period", "statistic_ids": ["sensor.test1", "sensor.test2", "sensor.test3"], "start_time": now.isoformat(), @@ -1755,9 +1699,8 @@ async def test_clear_statistics( assert response["success"] assert response["result"] == expected_response - await client.send_json( + await client.send_json_auto_id( { - "id": 4, "type": "recorder/clear_statistics", "statistic_ids": ["sensor.test1", "sensor.test3"], } @@ -1767,9 +1710,8 @@ async def test_clear_statistics( await async_recorder_block_till_done(hass) client = await hass_ws_client() - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "recorder/statistics_during_period", "statistic_ids": ["sensor.test1", "sensor.test2", "sensor.test3"], "start_time": now.isoformat(), @@ -1811,7 +1753,7 @@ async def test_update_statistics_metadata( client = await hass_ws_client() - await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1827,9 +1769,8 @@ async def test_update_statistics_metadata( } ] - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/update_statistics_metadata", "statistic_id": "sensor.test", "unit_of_measurement": new_unit, @@ -1839,7 +1780,7 @@ async def test_update_statistics_metadata( assert response["success"] await async_recorder_block_till_done(hass) - await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1855,9 +1796,8 @@ async def test_update_statistics_metadata( } ] - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1902,7 +1842,7 @@ async def test_change_statistics_unit( client = await hass_ws_client() - await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1918,9 +1858,8 @@ async def test_change_statistics_unit( } ] - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1942,9 +1881,8 @@ async def test_change_statistics_unit( ], } - await client.send_json( + await client.send_json_auto_id( { - "id": 3, "type": "recorder/change_statistics_unit", "statistic_id": "sensor.test", "new_unit_of_measurement": "W", @@ -1955,7 +1893,7 @@ async def test_change_statistics_unit( assert response["success"] await async_recorder_block_till_done(hass) - await client.send_json({"id": 4, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -1971,9 +1909,8 @@ async def test_change_statistics_unit( } ] - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -1997,9 +1934,8 @@ async def test_change_statistics_unit( } # Changing to the same unit is allowed but does nothing - await client.send_json( + await client.send_json_auto_id( { - "id": 6, "type": "recorder/change_statistics_unit", "statistic_id": "sensor.test", "new_unit_of_measurement": "W", @@ -2010,7 +1946,7 @@ async def test_change_statistics_unit( assert response["success"] await async_recorder_block_till_done(hass) - await client.send_json({"id": 7, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -2035,7 +1971,6 @@ async def test_change_statistics_unit_errors( ) -> None: """Test change unit of recorded statistics.""" now = dt_util.utcnow() - ws_id = 0 units = METRIC_SYSTEM attributes = POWER_SENSOR_KW_ATTRIBUTES | {"device_class": None} @@ -2068,19 +2003,14 @@ async def test_change_statistics_unit_errors( } async def assert_statistic_ids(expected): - nonlocal ws_id - ws_id += 1 - await client.send_json({"id": ws_id, "type": "recorder/list_statistic_ids"}) + await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() assert response["success"] assert response["result"] == expected async def assert_statistics(expected): - nonlocal ws_id - ws_id += 1 - await client.send_json( + await client.send_json_auto_id( { - "id": ws_id, "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "statistic_ids": ["sensor.test"], @@ -2106,10 +2036,8 @@ async def test_change_statistics_unit_errors( await assert_statistics(expected_statistics) # Try changing to an invalid unit - ws_id += 1 - await client.send_json( + await client.send_json_auto_id( { - "id": ws_id, "type": "recorder/change_statistics_unit", "statistic_id": "sensor.test", "old_unit_of_measurement": "kW", @@ -2126,10 +2054,8 @@ async def test_change_statistics_unit_errors( await assert_statistics(expected_statistics) # Try changing from the wrong unit - ws_id += 1 - await client.send_json( + await client.send_json_auto_id( { - "id": ws_id, "type": "recorder/change_statistics_unit", "statistic_id": "sensor.test", "old_unit_of_measurement": "W", @@ -2155,7 +2081,7 @@ async def test_recorder_info( # Ensure there are no queued events await async_wait_recording_done(hass) - await client.send_json({"id": 1, "type": "recorder/info"}) + await client.send_json_auto_id({"type": "recorder/info"}) response = await client.receive_json() assert response["success"] assert response["result"] == { @@ -2174,7 +2100,7 @@ async def test_recorder_info_no_recorder( """Test getting recorder status when recorder is not present.""" client = await hass_ws_client() - await client.send_json({"id": 1, "type": "recorder/info"}) + await client.send_json_auto_id({"type": "recorder/info"}) response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "unknown_command" @@ -2199,7 +2125,7 @@ async def test_recorder_info_bad_recorder_config( # Wait for recorder to shut down await hass.async_add_executor_job(recorder.get_instance(hass).join) - await client.send_json({"id": 1, "type": "recorder/info"}) + await client.send_json_auto_id({"type": "recorder/info"}) response = await client.receive_json() assert response["success"] assert response["result"]["recording"] is False @@ -2250,7 +2176,7 @@ async def test_recorder_info_migration_queue_exhausted( client = await hass_ws_client() # Check the status - await client.send_json({"id": 1, "type": "recorder/info"}) + await client.send_json_auto_id({"type": "recorder/info"}) response = await client.receive_json() assert response["success"] assert response["result"]["migration_in_progress"] is True @@ -2262,7 +2188,7 @@ async def test_recorder_info_migration_queue_exhausted( await async_wait_recording_done(hass) # Check the status after migration finished - await client.send_json({"id": 2, "type": "recorder/info"}) + await client.send_json_auto_id({"type": "recorder/info"}) response = await client.receive_json() assert response["success"] assert response["result"]["migration_in_progress"] is False @@ -2278,7 +2204,7 @@ async def test_backup_start_no_recorder( """Test getting backup start when recorder is not present.""" client = await hass_ws_client(hass, hass_supervisor_access_token) - await client.send_json({"id": 1, "type": "backup/start"}) + await client.send_json_auto_id({"type": "backup/start"}) response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "unknown_command" @@ -2303,12 +2229,12 @@ async def test_backup_start_timeout( with patch.object(recorder.core, "DB_LOCK_TIMEOUT", 0): try: - await client.send_json({"id": 1, "type": "backup/start"}) + await client.send_json_auto_id({"type": "backup/start"}) response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "timeout_error" finally: - await client.send_json({"id": 2, "type": "backup/end"}) + await client.send_json_auto_id({"type": "backup/end"}) async def test_backup_end( @@ -2323,11 +2249,11 @@ async def test_backup_end( # Ensure there are no queued events await async_wait_recording_done(hass) - await client.send_json({"id": 1, "type": "backup/start"}) + await client.send_json_auto_id({"type": "backup/start"}) response = await client.receive_json() assert response["success"] - await client.send_json({"id": 2, "type": "backup/end"}) + await client.send_json_auto_id({"type": "backup/end"}) response = await client.receive_json() assert response["success"] @@ -2349,7 +2275,7 @@ async def test_backup_end_without_start( # Ensure there are no queued events await async_wait_recording_done(hass) - await client.send_json({"id": 1, "type": "backup/end"}) + await client.send_json_auto_id({"type": "backup/end"}) response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "database_unlock_failed" @@ -2393,7 +2319,7 @@ async def test_get_statistics_metadata( await async_recorder_block_till_done(hass) client = await hass_ws_client() - await client.send_json({"id": 1, "type": "recorder/get_statistics_metadata"}) + await client.send_json_auto_id({"type": "recorder/get_statistics_metadata"}) response = await client.receive_json() assert response["success"] assert response["result"] == [] @@ -2442,9 +2368,8 @@ async def test_get_statistics_metadata( ) await async_wait_recording_done(hass) - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/get_statistics_metadata", "statistic_ids": ["test:total_gas"], } @@ -2470,9 +2395,8 @@ async def test_get_statistics_metadata( hass.states.async_set("sensor.test2", 10, attributes=attributes) await async_wait_recording_done(hass) - await client.send_json( + await client.send_json_auto_id( { - "id": 3, "type": "recorder/get_statistics_metadata", "statistic_ids": ["sensor.test"], } @@ -2498,9 +2422,8 @@ async def test_get_statistics_metadata( hass.states.async_remove("sensor.test") await hass.async_block_till_done() - await client.send_json( + await client.send_json_auto_id( { - "id": 4, "type": "recorder/get_statistics_metadata", "statistic_ids": ["sensor.test"], } @@ -2568,9 +2491,8 @@ async def test_import_statistics( "unit_of_measurement": "kWh", } - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/import_statistics", "metadata": imported_metadata, "stats": [imported_statistics1, imported_statistics2], @@ -2656,9 +2578,8 @@ async def test_import_statistics( "sum": 6, } - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "recorder/import_statistics", "metadata": imported_metadata, "stats": [external_statistics], @@ -2702,9 +2623,8 @@ async def test_import_statistics( "sum": 5, } - await client.send_json( + await client.send_json_auto_id( { - "id": 3, "type": "recorder/import_statistics", "metadata": imported_metadata, "stats": [external_statistics], @@ -2785,9 +2705,8 @@ async def test_adjust_sum_statistics_energy( "unit_of_measurement": "kWh", } - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/import_statistics", "metadata": imported_metadata, "stats": [imported_statistics1, imported_statistics2], @@ -2852,9 +2771,8 @@ async def test_adjust_sum_statistics_energy( } # Adjust previously inserted statistics in kWh - await client.send_json( + await client.send_json_auto_id( { - "id": 4, "type": "recorder/adjust_sum_statistics", "statistic_id": statistic_id, "start_time": period2.isoformat(), @@ -2893,9 +2811,8 @@ async def test_adjust_sum_statistics_energy( } # Adjust previously inserted statistics in MWh - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "recorder/adjust_sum_statistics", "statistic_id": statistic_id, "start_time": period2.isoformat(), @@ -2981,9 +2898,8 @@ async def test_adjust_sum_statistics_gas( "unit_of_measurement": "m³", } - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/import_statistics", "metadata": imported_metadata, "stats": [imported_statistics1, imported_statistics2], @@ -3048,9 +2964,8 @@ async def test_adjust_sum_statistics_gas( } # Adjust previously inserted statistics in m³ - await client.send_json( + await client.send_json_auto_id( { - "id": 4, "type": "recorder/adjust_sum_statistics", "statistic_id": statistic_id, "start_time": period2.isoformat(), @@ -3089,9 +3004,8 @@ async def test_adjust_sum_statistics_gas( } # Adjust previously inserted statistics in ft³ - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "recorder/adjust_sum_statistics", "statistic_id": statistic_id, "start_time": period2.isoformat(), @@ -3194,9 +3108,8 @@ async def test_adjust_sum_statistics_errors( "unit_of_measurement": statistic_unit, } - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "recorder/import_statistics", "metadata": imported_metadata, "stats": [imported_statistics1, imported_statistics2], @@ -3262,10 +3175,8 @@ async def test_adjust_sum_statistics_errors( } # Try to adjust statistics - msg_id = 2 - await client.send_json( + await client.send_json_auto_id( { - "id": msg_id, "type": "recorder/adjust_sum_statistics", "statistic_id": "sensor.does_not_exist", "start_time": period2.isoformat(), @@ -3282,10 +3193,8 @@ async def test_adjust_sum_statistics_errors( assert stats == previous_stats for unit in invalid_units: - msg_id += 1 - await client.send_json( + await client.send_json_auto_id( { - "id": msg_id, "type": "recorder/adjust_sum_statistics", "statistic_id": statistic_id, "start_time": period2.isoformat(), @@ -3302,10 +3211,8 @@ async def test_adjust_sum_statistics_errors( assert stats == previous_stats for unit in valid_units: - msg_id += 1 - await client.send_json( + await client.send_json_auto_id( { - "id": msg_id, "type": "recorder/adjust_sum_statistics", "statistic_id": statistic_id, "start_time": period2.isoformat(), From 025fe51322824d34fc3f69b3c6bab63d5a183b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 2 Feb 2024 09:36:26 +0100 Subject: [PATCH 0059/1367] Use a mocked API client in Traccar Server tests (#109358) --- tests/components/traccar_server/conftest.py | 77 ++++++++++- .../traccar_server/fixtures/devices.json | 17 +++ .../traccar_server/fixtures/geofences.json | 10 ++ .../traccar_server/fixtures/positions.json | 24 ++++ .../fixtures/reports_events.json | 12 ++ .../traccar_server/fixtures/server.json | 21 +++ .../traccar_server/test_config_flow.py | 124 ++++++++---------- 7 files changed, 210 insertions(+), 75 deletions(-) create mode 100644 tests/components/traccar_server/fixtures/devices.json create mode 100644 tests/components/traccar_server/fixtures/geofences.json create mode 100644 tests/components/traccar_server/fixtures/positions.json create mode 100644 tests/components/traccar_server/fixtures/reports_events.json create mode 100644 tests/components/traccar_server/fixtures/server.json diff --git a/tests/components/traccar_server/conftest.py b/tests/components/traccar_server/conftest.py index 4141b28849c..10cc6192d38 100644 --- a/tests/components/traccar_server/conftest.py +++ b/tests/components/traccar_server/conftest.py @@ -3,12 +3,79 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from pytraccar import ApiClient + +from homeassistant.components.traccar_server.const import ( + CONF_CUSTOM_ATTRIBUTES, + CONF_EVENTS, + CONF_MAX_ACCURACY, + CONF_SKIP_ACCURACY_FILTER_FOR, + DOMAIN, +) +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +from tests.common import ( + MockConfigEntry, + load_json_array_fixture, + load_json_object_fixture, +) @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: - """Override async_setup_entry.""" +def mock_traccar_api_client() -> Generator[AsyncMock, None, None]: + """Mock a Traccar ApiClient client.""" with patch( - "homeassistant.components.traccar_server.async_setup_entry", return_value=True - ) as mock_setup_entry: - yield mock_setup_entry + "homeassistant.components.traccar_server.ApiClient", + autospec=True, + ) as mock_client, patch( + "homeassistant.components.traccar_server.config_flow.ApiClient", + new=mock_client, + ): + client: ApiClient = mock_client.return_value + client.get_devices.return_value = load_json_array_fixture( + "traccar_server/devices.json" + ) + client.get_geofences.return_value = load_json_array_fixture( + "traccar_server/geofences.json" + ) + client.get_positions.return_value = load_json_array_fixture( + "traccar_server/positions.json" + ) + client.get_server.return_value = load_json_object_fixture( + "traccar_server/server.json" + ) + client.get_reports_events.return_value = load_json_array_fixture( + "traccar_server/reports_events.json" + ) + + yield client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a Traccar Server config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="1.1.1.1:8082", + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: "8082", + CONF_USERNAME: "test@example.org", + CONF_PASSWORD: "ThisIsNotThePasswordYouAreL00kingFor", + CONF_SSL: False, + CONF_VERIFY_SSL: True, + }, + options={ + CONF_CUSTOM_ATTRIBUTES: ["custom_attr_1"], + CONF_EVENTS: ["device_moving"], + CONF_MAX_ACCURACY: 5.0, + CONF_SKIP_ACCURACY_FILTER_FOR: [], + }, + ) diff --git a/tests/components/traccar_server/fixtures/devices.json b/tests/components/traccar_server/fixtures/devices.json new file mode 100644 index 00000000000..b04d53d9fdf --- /dev/null +++ b/tests/components/traccar_server/fixtures/devices.json @@ -0,0 +1,17 @@ +[ + { + "id": 0, + "name": "X-Wing", + "uniqueId": "abc123", + "status": "unknown", + "disabled": false, + "lastUpdate": "1970-01-01T00:00:00Z", + "positionId": 0, + "groupId": 0, + "phone": null, + "model": "1337", + "contact": null, + "category": "starfighter", + "attributes": {} + } +] diff --git a/tests/components/traccar_server/fixtures/geofences.json b/tests/components/traccar_server/fixtures/geofences.json new file mode 100644 index 00000000000..5452c0485de --- /dev/null +++ b/tests/components/traccar_server/fixtures/geofences.json @@ -0,0 +1,10 @@ +[ + { + "id": 0, + "name": "Tatooine", + "description": "A harsh desert world orbiting twin suns in the galaxy's Outer Rim", + "area": "string", + "calendarId": 0, + "attributes": {} + } +] diff --git a/tests/components/traccar_server/fixtures/positions.json b/tests/components/traccar_server/fixtures/positions.json new file mode 100644 index 00000000000..6b65116e804 --- /dev/null +++ b/tests/components/traccar_server/fixtures/positions.json @@ -0,0 +1,24 @@ +[ + { + "id": 0, + "deviceId": 0, + "protocol": "C-3PO", + "deviceTime": "1970-01-01T00:00:00Z", + "fixTime": "1970-01-01T00:00:00Z", + "serverTime": "1970-01-01T00:00:00Z", + "outdated": true, + "valid": true, + "latitude": 52.0, + "longitude": 25.0, + "altitude": 546841384638, + "speed": 4568795, + "course": 360, + "address": "Mos Espa", + "accuracy": 3.5, + "network": {}, + "geofenceIds": [0], + "attributes": { + "custom_attr_1": "custom_attr_1_value" + } + } +] diff --git a/tests/components/traccar_server/fixtures/reports_events.json b/tests/components/traccar_server/fixtures/reports_events.json new file mode 100644 index 00000000000..e8280471d96 --- /dev/null +++ b/tests/components/traccar_server/fixtures/reports_events.json @@ -0,0 +1,12 @@ +[ + { + "id": 0, + "type": "deviceMoving", + "eventTime": "2019-08-24T14:15:22Z", + "deviceId": 0, + "positionId": 0, + "geofenceId": 0, + "maintenanceId": 0, + "attributes": {} + } +] diff --git a/tests/components/traccar_server/fixtures/server.json b/tests/components/traccar_server/fixtures/server.json new file mode 100644 index 00000000000..039b6bfa1f4 --- /dev/null +++ b/tests/components/traccar_server/fixtures/server.json @@ -0,0 +1,21 @@ +{ + "id": 0, + "registration": true, + "readonly": true, + "deviceReadonly": true, + "limitCommands": true, + "map": null, + "bingKey": null, + "mapUrl": null, + "poiLayer": null, + "latitude": 0, + "longitude": 0, + "zoom": 0, + "twelveHourFormat": true, + "version": "99.99", + "forceSettings": true, + "coordinateFormat": null, + "openIdEnabled": true, + "openIdForce": true, + "attributes": {} +} diff --git a/tests/components/traccar_server/test_config_flow.py b/tests/components/traccar_server/test_config_flow.py index 028bc99cec5..00a987a4711 100644 --- a/tests/components/traccar_server/test_config_flow.py +++ b/tests/components/traccar_server/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Traccar Server config flow.""" +from collections.abc import Generator from typing import Any -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock import pytest from pytraccar import TraccarException @@ -29,7 +30,10 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: +async def test_form( + hass: HomeAssistant, + mock_traccar_api_client: Generator[AsyncMock, None, None], +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -37,19 +41,15 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.traccar_server.config_flow.ApiClient.get_server", - return_value={"id": "1234"}, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOST: "1.1.1.1", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "1.1.1.1:8082" @@ -61,7 +61,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: CONF_SSL: False, CONF_VERIFY_SSL: True, } - assert len(mock_setup_entry.mock_calls) == 1 + assert result["result"].state == config_entries.ConfigEntryState.LOADED @pytest.mark.parametrize( @@ -73,44 +73,40 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) async def test_form_cannot_connect( hass: HomeAssistant, - mock_setup_entry: AsyncMock, side_effect: Exception, error: str, + mock_traccar_api_client: Generator[AsyncMock, None, None], ) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.traccar_server.config_flow.ApiClient.get_server", - side_effect=side_effect, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOST: "1.1.1.1", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) + mock_traccar_api_client.get_server.side_effect = side_effect + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": error} - with patch( - "homeassistant.components.traccar_server.config_flow.ApiClient.get_server", - return_value={"id": "1234"}, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOST: "1.1.1.1", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) - await hass.async_block_till_done() + mock_traccar_api_client.get_server.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "1.1.1.1:8082" @@ -122,27 +118,23 @@ async def test_form_cannot_connect( CONF_SSL: False, CONF_VERIFY_SSL: True, } - assert len(mock_setup_entry.mock_calls) == 1 + + assert result["result"].state == config_entries.ConfigEntryState.LOADED async def test_options( hass: HomeAssistant, - mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_traccar_api_client: Generator[AsyncMock, None, None], ) -> None: """Test options flow.""" + mock_config_entry.add_to_hass(hass) - config_entry = MockConfigEntry( - domain=DOMAIN, - data={}, - ) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - config_entry.add_to_hass(hass) + assert mock_config_entry.options.get(CONF_MAX_ACCURACY) == 5.0 - assert await hass.config_entries.async_setup(config_entry.entry_id) - - assert CONF_MAX_ACCURACY not in config_entry.options - - result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -151,7 +143,7 @@ async def test_options( await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert config_entry.options == { + assert mock_config_entry.options == { CONF_MAX_ACCURACY: 2.0, CONF_EVENTS: [], CONF_CUSTOM_ATTRIBUTES: [], @@ -234,10 +226,10 @@ async def test_options( ) async def test_import_from_yaml( hass: HomeAssistant, - mock_setup_entry: AsyncMock, imported: dict[str, Any], data: dict[str, Any], options: dict[str, Any], + mock_traccar_api_client: Generator[AsyncMock, None, None], ) -> None: """Test importing configuration from YAML.""" result = await hass.config_entries.flow.async_init( @@ -249,12 +241,10 @@ async def test_import_from_yaml( assert result["title"] == f"{data[CONF_HOST]}:{data[CONF_PORT]}" assert result["data"] == data assert result["options"] == options + assert result["result"].state == config_entries.ConfigEntryState.LOADED -async def test_abort_import_already_configured( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, -) -> None: +async def test_abort_import_already_configured(hass: HomeAssistant) -> None: """Test abort for existing server while importing.""" config_entry = MockConfigEntry( @@ -284,18 +274,12 @@ async def test_abort_import_already_configured( async def test_abort_already_configured( hass: HomeAssistant, - mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_traccar_api_client: Generator[AsyncMock, None, None], ) -> None: """Test abort for existing server.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_HOST: "1.1.1.1", CONF_PORT: "8082"}, - ) - - config_entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(config_entry.entry_id) + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} From 543870d5f122757832f7463f2731a76a78774ffd Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 2 Feb 2024 09:46:53 +0100 Subject: [PATCH 0060/1367] Correct modbus commit validation, too strict on integers (#109338) --- .../components/modbus/base_platform.py | 18 ++++++++---------- tests/components/modbus/test_sensor.py | 10 +++++----- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 877d33afbcc..cdc1e7a6986 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -182,7 +182,6 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): self._data_type = config[CONF_DATA_TYPE] self._structure: str = config[CONF_STRUCTURE] self._scale = config[CONF_SCALE] - self._precision = config.get(CONF_PRECISION, 2) self._offset = config[CONF_OFFSET] self._slave_count = config.get(CONF_SLAVE_COUNT, None) or config.get( CONF_VIRTUAL_COUNT, 0 @@ -196,11 +195,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): DataType.UINT32, DataType.UINT64, ) - if self._value_is_int: - if self._min_value: - self._min_value = round(self._min_value) - if self._max_value: - self._max_value = round(self._max_value) + if not self._value_is_int: + self._precision = config.get(CONF_PRECISION, 2) + else: + self._precision = config.get(CONF_PRECISION, 0) def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]: """Do swap as needed.""" @@ -235,13 +233,13 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): return None val: float | int = self._scale * entry + self._offset if self._min_value is not None and val < self._min_value: - return str(self._min_value) + val = self._min_value if self._max_value is not None and val > self._max_value: - return str(self._max_value) + val = self._max_value if self._zero_suppress is not None and abs(val) <= self._zero_suppress: return "0" - if self._precision == 0 or self._value_is_int: - return str(int(round(val, 0))) + if self._precision == 0: + return str(round(val)) return f"{float(val):.{self._precision}f}" def unpack_structure_result(self, registers: list[int]) -> str | None: diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 7c58290b143..97571041482 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -357,7 +357,7 @@ async def test_config_wrong_struct_sensor( }, [7], False, - "34", + "34.0000", ), ( { @@ -379,7 +379,7 @@ async def test_config_wrong_struct_sensor( }, [9], False, - "18", + "18.5", ), ( { @@ -390,7 +390,7 @@ async def test_config_wrong_struct_sensor( }, [1], False, - "2", + "2.40", ), ( { @@ -401,7 +401,7 @@ async def test_config_wrong_struct_sensor( }, [2], False, - "-8", + "-8.3", ), ( { @@ -676,7 +676,7 @@ async def test_config_wrong_struct_sensor( }, [0x00AB, 0xCDEF], False, - "112594", + "112593.75", ), ( { From e3f1997b6fdf5425ad476ec9cf0e372e9987f408 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:48:01 +0100 Subject: [PATCH 0061/1367] Add TURN_ON/OFF ClimateEntityFeature for KNX (#109138) --- homeassistant/components/knx/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 72039e1300f..1038cdde80f 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -134,12 +134,17 @@ class KNXClimate(KnxEntity, ClimateEntity): _device: XknxClimate _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX climate device.""" super().__init__(_create_climate(xknx, config)) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) - self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_ON + ) + if self._device.supports_on_off: + self._attr_supported_features |= ClimateEntityFeature.TURN_OFF if self.preset_modes: self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE self._attr_target_temperature_step = self._device.temperature_step From 4229c35fcd11270dc23ad911fcac746f92f7d520 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Feb 2024 09:49:32 +0100 Subject: [PATCH 0062/1367] Improve color mode handling in MockLight (#109298) --- tests/components/group/test_light.py | 15 +++++++ tests/components/light/test_init.py | 45 +++++++++++++++++++ .../custom_components/test/light.py | 33 ++++++++++++-- 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 3051ec502a0..59f0a5b7d55 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -279,6 +279,9 @@ async def test_brightness( entity1 = platform.ENTITIES[1] entity1.supported_features = SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None assert await async_setup_component( hass, @@ -350,6 +353,9 @@ async def test_color_hs(hass: HomeAssistant, enable_custom_integrations: None) - entity1 = platform.ENTITIES[1] entity1.supported_features = SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None assert await async_setup_component( hass, @@ -698,6 +704,9 @@ async def test_color_temp( entity1 = platform.ENTITIES[1] entity1.supported_features = SUPPORT_COLOR_TEMP + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None assert await async_setup_component( hass, @@ -838,6 +847,9 @@ async def test_min_max_mireds( entity1 = platform.ENTITIES[1] entity1.supported_features = SUPPORT_COLOR_TEMP + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None entity1._attr_min_color_temp_kelvin = 1 entity1._attr_max_color_temp_kelvin = 1234567890 @@ -1015,6 +1027,9 @@ async def test_supported_color_modes( entity2 = platform.ENTITIES[2] entity2.supported_features = SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity2.supported_color_modes = None + entity2.color_mode = None assert await async_setup_component( hass, diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 69f6a841737..0e3bc1332cf 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -127,6 +127,9 @@ async def test_services( | light.LightEntityFeature.EFFECT | light.LightEntityFeature.TRANSITION ) + # Set color modes to none to trigger backwards compatibility in LightEntity + ent2.supported_color_modes = None + ent2.color_mode = None ent3.supported_features = ( light.LightEntityFeature.FLASH | light.LightEntityFeature.TRANSITION ) @@ -905,9 +908,15 @@ async def test_light_brightness_step( platform.ENTITIES.append(platform.MockLight("Test_1", STATE_ON)) entity0 = platform.ENTITIES[0] entity0.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity0.supported_color_modes = None + entity0.color_mode = None entity0.brightness = 100 entity1 = platform.ENTITIES[1] entity1.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None entity1.brightness = 50 assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -967,6 +976,9 @@ async def test_light_brightness_pct_conversion( platform.init() entity = platform.ENTITIES[0] entity.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity.supported_color_modes = None + entity.color_mode = None entity.brightness = 100 assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1133,17 +1145,29 @@ async def test_light_backwards_compatibility_supported_color_modes( entity1 = platform.ENTITIES[1] entity1.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None entity2 = platform.ENTITIES[2] entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP + # Set color modes to none to trigger backwards compatibility in LightEntity + entity2.supported_color_modes = None + entity2.color_mode = None entity3 = platform.ENTITIES[3] entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity3.supported_color_modes = None + entity3.color_mode = None entity4 = platform.ENTITIES[4] entity4.supported_features = ( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP ) + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1204,20 +1228,32 @@ async def test_light_backwards_compatibility_color_mode( entity1 = platform.ENTITIES[1] entity1.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None entity1.brightness = 100 entity2 = platform.ENTITIES[2] entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP + # Set color modes to none to trigger backwards compatibility in LightEntity + entity2.supported_color_modes = None + entity2.color_mode = None entity2.color_temp_kelvin = 10000 entity3 = platform.ENTITIES[3] entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity3.supported_color_modes = None + entity3.color_mode = None entity3.hs_color = (240, 100) entity4 = platform.ENTITIES[4] entity4.supported_features = ( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP ) + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None entity4.hs_color = (240, 100) entity4.color_temp_kelvin = 10000 @@ -1464,6 +1500,9 @@ async def test_light_service_call_color_conversion( entity4 = platform.ENTITIES[4] entity4.supported_features = light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None entity5 = platform.ENTITIES[5] entity5.supported_color_modes = {light.ColorMode.RGBW} @@ -1905,6 +1944,9 @@ async def test_light_service_call_color_conversion_named_tuple( entity4 = platform.ENTITIES[4] entity4.supported_features = light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None entity5 = platform.ENTITIES[5] entity5.supported_color_modes = {light.ColorMode.RGBW} @@ -2330,6 +2372,9 @@ async def test_light_state_color_conversion( entity3 = platform.ENTITIES[3] entity3.hs_color = (240, 100) entity3.supported_features = light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity3.supported_color_modes = None + entity3.color_mode = None assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index e84e8cbe390..e22aca289a8 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -2,7 +2,7 @@ Call init before using it in your tests to ensure clean test data. """ -from homeassistant.components.light import LightEntity +from homeassistant.components.light import ColorMode, LightEntity from homeassistant.const import STATE_OFF, STATE_ON from tests.common import MockToggleEntity @@ -32,13 +32,21 @@ async def async_setup_platform( async_add_entities_callback(ENTITIES) +TURN_ON_ARG_TO_COLOR_MODE = { + "hs_color": ColorMode.HS, + "xy_color": ColorMode.XY, + "rgb_color": ColorMode.RGB, + "rgbw_color": ColorMode.RGBW, + "rgbww_color": ColorMode.RGBWW, + "color_temp_kelvin": ColorMode.COLOR_TEMP, +} + + class MockLight(MockToggleEntity, LightEntity): """Mock light class.""" - color_mode = None _attr_max_color_temp_kelvin = 6500 _attr_min_color_temp_kelvin = 2000 - supported_color_modes = None supported_features = 0 brightness = None @@ -49,6 +57,23 @@ class MockLight(MockToggleEntity, LightEntity): rgbww_color = None xy_color = None + def __init__( + self, + name, + state, + unique_id=None, + supported_color_modes: set[ColorMode] | None = None, + ): + """Initialize the mock light.""" + super().__init__(name, state, unique_id) + if supported_color_modes is None: + supported_color_modes = {ColorMode.ONOFF} + self._attr_supported_color_modes = supported_color_modes + color_mode = ColorMode.UNKNOWN + if len(supported_color_modes) == 1: + color_mode = next(iter(supported_color_modes)) + self._attr_color_mode = color_mode + def turn_on(self, **kwargs): """Turn the entity on.""" super().turn_on(**kwargs) @@ -65,3 +90,5 @@ class MockLight(MockToggleEntity, LightEntity): setattr(self, key, value) if key == "white": setattr(self, "brightness", value) + if key in TURN_ON_ARG_TO_COLOR_MODE: + self._attr_color_mode = TURN_ON_ARG_TO_COLOR_MODE[key] From effd5b8dddafed1504e525afb034dcd88e8cdace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Fri, 2 Feb 2024 10:24:53 +0100 Subject: [PATCH 0063/1367] Hide unsupported devices in Airthings BLE config flow (#107648) --- .../components/airthings_ble/config_flow.py | 10 +++++ tests/components/airthings_ble/__init__.py | 44 +++++++++++++++++++ .../airthings_ble/test_config_flow.py | 14 ++++++ 3 files changed, 68 insertions(+) diff --git a/homeassistant/components/airthings_ble/config_flow.py b/homeassistant/components/airthings_ble/config_flow.py index b562e837ff4..4228fea50d7 100644 --- a/homeassistant/components/airthings_ble/config_flow.py +++ b/homeassistant/components/airthings_ble/config_flow.py @@ -23,6 +23,13 @@ from .const import DOMAIN, MFCT_ID _LOGGER = logging.getLogger(__name__) +SERVICE_UUIDS = [ + "b42e1f6e-ade7-11e4-89d3-123b93f75cba", + "b42e4a8e-ade7-11e4-89d3-123b93f75cba", + "b42e1c08-ade7-11e4-89d3-123b93f75cba", + "b42e3882-ade7-11e4-89d3-123b93f75cba", +] + @dataclasses.dataclass class Discovery: @@ -147,6 +154,9 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): if MFCT_ID not in discovery_info.manufacturer_data: continue + if not any(uuid in SERVICE_UUIDS for uuid in discovery_info.service_uuids): + continue + try: device = await self._get_device_data(discovery_info) except AirthingsDeviceUpdateError: diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py index da0c312bf28..231ec12cb5f 100644 --- a/tests/components/airthings_ble/__init__.py +++ b/tests/components/airthings_ble/__init__.py @@ -93,6 +93,50 @@ WAVE_SERVICE_INFO = BluetoothServiceInfoBleak( time=0, ) +VIEW_PLUS_SERVICE_INFO = BluetoothServiceInfoBleak( + name="cc-cc-cc-cc-cc-cc", + address="cc:cc:cc:cc:cc:cc", + device=generate_ble_device( + address="cc:cc:cc:cc:cc:cc", + name="Airthings View Plus", + ), + rssi=-61, + manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"}, + service_data={ + "b42eb4a6-ade7-11e4-89d3-123b93f75cba": bytearray( + b"\x01\x02\x03\x04\x00\x05\x00\x06\x00\x07\x00\x08\x00\x09\x00\x0A" + ), + # Manufacturer + "00002a29-0000-1000-8000-00805f9b34fb": bytearray(b"Airthings AS"), + # Model + "00002a24-0000-1000-8000-00805f9b34fb": bytearray(b"2960"), + # Identifier + "00002a25-0000-1000-8000-00805f9b34fb": bytearray(b"123456"), + # SW Version + "00002a26-0000-1000-8000-00805f9b34fb": bytearray(b"A-BLE-1.12.1-master+0"), + # HW Version + "00002a27-0000-1000-8000-00805f9b34fb": bytearray(b"REV 1,0"), + }, + service_uuids=[ + "b42eb4a6-ade7-11e4-89d3-123b93f75cba", + "b42e90a2-ade7-11e4-89d3-123b93f75cba", + "b42e2a68-ade7-11e4-89d3-123b93f75cba", + "00002a29-0000-1000-8000-00805f9b34fb", + "00002a24-0000-1000-8000-00805f9b34fb", + "00002a25-0000-1000-8000-00805f9b34fb", + "00002a26-0000-1000-8000-00805f9b34fb", + "00002a27-0000-1000-8000-00805f9b34fb", + "b42e2d06-ade7-11e4-89d3-123b93f75cba", + ], + source="local", + advertisement=generate_advertisement_data( + manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"}, + service_uuids=["b42e90a2-ade7-11e4-89d3-123b93f75cba"], + ), + connectable=True, + time=0, +) + UNKNOWN_SERVICE_INFO = BluetoothServiceInfoBleak( name="unknown", address="00:cc:cc:cc:cc:cc", diff --git a/tests/components/airthings_ble/test_config_flow.py b/tests/components/airthings_ble/test_config_flow.py index bc009f03027..65ec91e69c2 100644 --- a/tests/components/airthings_ble/test_config_flow.py +++ b/tests/components/airthings_ble/test_config_flow.py @@ -12,6 +12,7 @@ from homeassistant.data_entry_flow import FlowResultType from . import ( UNKNOWN_SERVICE_INFO, + VIEW_PLUS_SERVICE_INFO, WAVE_DEVICE_INFO, WAVE_SERVICE_INFO, patch_airthings_ble, @@ -204,3 +205,16 @@ async def test_user_setup_unable_to_connect(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" + + +async def test_unsupported_device(hass: HomeAssistant) -> None: + """Test the user initiated form with an unsupported device.""" + with patch( + "homeassistant.components.airthings_ble.config_flow.async_discovered_service_info", + return_value=[UNKNOWN_SERVICE_INFO, VIEW_PLUS_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" From c868b79b5abbfa1021296ef671e553ee7657bc15 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Feb 2024 10:37:04 +0100 Subject: [PATCH 0064/1367] Update cryptography to 42.0.2 (#109359) --- 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 1b47b2693b0..fbb072db9c0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ bluetooth-data-tools==1.19.0 cached_ipaddress==0.3.0 certifi>=2021.5.30 ciso8601==2.3.0 -cryptography==42.0.1 +cryptography==42.0.2 dbus-fast==2.21.1 fnv-hash-fast==0.5.0 ha-av==10.1.1 diff --git a/pyproject.toml b/pyproject.toml index d7680e5e871..24a50508722 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ "lru-dict==1.3.0", "PyJWT==2.8.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==42.0.1", + "cryptography==42.0.2", # pyOpenSSL 23.2.0 is required to work with cryptography 41+ "pyOpenSSL==24.0.0", "orjson==3.9.12", diff --git a/requirements.txt b/requirements.txt index 67aad2e9f0d..066855e718b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ ifaddr==0.2.0 Jinja2==3.1.3 lru-dict==1.3.0 PyJWT==2.8.0 -cryptography==42.0.1 +cryptography==42.0.2 pyOpenSSL==24.0.0 orjson==3.9.12 packaging>=23.1 From f22b71d803840a073c82b25129cda21caeb4b1b3 Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Fri, 2 Feb 2024 10:37:49 +0100 Subject: [PATCH 0065/1367] Follow up swiss_public_transport migration fix of unique ids (#107873) improve migration fix of unique ids - follow up to #107087 --- .../swiss_public_transport/__init__.py | 11 +++++++---- .../swiss_public_transport/test_init.py | 19 ++++++++++--------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/swiss_public_transport/__init__.py b/homeassistant/components/swiss_public_transport/__init__.py index a510b5b7414..d87b711e376 100644 --- a/homeassistant/components/swiss_public_transport/__init__.py +++ b/homeassistant/components/swiss_public_transport/__init__.py @@ -89,7 +89,9 @@ async def async_migrate_entry( device_registry, config_entry_id=config_entry.entry_id ) for dev in device_entries: - device_registry.async_remove_device(dev.id) + device_registry.async_update_device( + dev.id, remove_config_entry_id=config_entry.entry_id + ) entity_id = entity_registry.async_get_entity_id( Platform.SENSOR, DOMAIN, "None_departure" @@ -105,12 +107,13 @@ async def async_migrate_entry( ) # Set a valid unique id for config entries - config_entry.unique_id = new_unique_id config_entry.minor_version = 2 - hass.config_entries.async_update_entry(config_entry) + hass.config_entries.async_update_entry(config_entry, unique_id=new_unique_id) _LOGGER.debug( - "Migration to minor version %s successful", config_entry.minor_version + "Migration to version %s.%s successful", + config_entry.version, + config_entry.minor_version, ) return True diff --git a/tests/components/swiss_public_transport/test_init.py b/tests/components/swiss_public_transport/test_init.py index f2b4e41ed71..2c8e12e04bf 100644 --- a/tests/components/swiss_public_transport/test_init.py +++ b/tests/components/swiss_public_transport/test_init.py @@ -45,25 +45,26 @@ CONNECTIONS = [ ] -async def test_migration_1_to_2( +async def test_migration_1_1_to_1_2( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test successful setup.""" + config_entry_faulty = MockConfigEntry( + domain=DOMAIN, + data=MOCK_DATA_STEP, + title="MIGRATION_TEST", + version=1, + minor_version=1, + ) + config_entry_faulty.add_to_hass(hass) + with patch( "homeassistant.components.swiss_public_transport.OpendataTransport", return_value=AsyncMock(), ) as mock: mock().connections = CONNECTIONS - config_entry_faulty = MockConfigEntry( - domain=DOMAIN, - data=MOCK_DATA_STEP, - title="MIGRATION_TEST", - minor_version=1, - ) - config_entry_faulty.add_to_hass(hass) - # Setup the config entry await hass.config_entries.async_setup(config_entry_faulty.entry_id) await hass.async_block_till_done() From 61e6882b91d4d1dc40fcfdc017b6842464cfe3bd Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 2 Feb 2024 11:02:00 +0100 Subject: [PATCH 0066/1367] Bump deebot_client to 5.1.0 (#109360) --- homeassistant/components/ecovacs/config_flow.py | 6 +++++- homeassistant/components/ecovacs/controller.py | 2 +- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ecovacs/test_init.py | 1 + 6 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ecovacs/config_flow.py b/homeassistant/components/ecovacs/config_flow.py index 39c61b3ce23..db3c60fa9e7 100644 --- a/homeassistant/components/ecovacs/config_flow.py +++ b/homeassistant/components/ecovacs/config_flow.py @@ -75,7 +75,7 @@ async def _validate_input( rest_config = create_rest_config( aiohttp_client.async_get_clientsession(hass), device_id=device_id, - country=country, + alpha_2_country=country, override_rest_url=rest_url, ) @@ -266,6 +266,10 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN): # If not we will inform the user about the mismatch. error = None placeholders = None + + # Convert the country to upper case as ISO 3166-1 alpha-2 country codes are upper case + user_input[CONF_COUNTRY] = user_input[CONF_COUNTRY].upper() + if len(user_input[CONF_COUNTRY]) != 2: error = "invalid_country_length" placeholders = {"countries_url": "https://www.iso.org/obp/ui/#search/code/"} diff --git a/homeassistant/components/ecovacs/controller.py b/homeassistant/components/ecovacs/controller.py index 27a1996c3e9..27b64db20b6 100644 --- a/homeassistant/components/ecovacs/controller.py +++ b/homeassistant/components/ecovacs/controller.py @@ -49,7 +49,7 @@ class EcovacsController: create_rest_config( aiohttp_client.async_get_clientsession(self._hass), device_id=self._device_id, - country=country, + alpha_2_country=country, override_rest_url=config.get(CONF_OVERRIDE_REST_URL), ), config[CONF_USERNAME], diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 3472e4746f8..34760ea6aca 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.8", "deebot-client==5.0.0"] + "requirements": ["py-sucks==0.9.8", "deebot-client==5.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2b804e3720a..4aa0c0e08d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -684,7 +684,7 @@ debugpy==1.8.0 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==5.0.0 +deebot-client==5.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10c89637f25..8c0889cdf01 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -559,7 +559,7 @@ dbus-fast==2.21.1 debugpy==1.8.0 # homeassistant.components.ecovacs -deebot-client==5.0.0 +deebot-client==5.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/tests/components/ecovacs/test_init.py b/tests/components/ecovacs/test_init.py index 8557ccb983c..e76001fbaeb 100644 --- a/tests/components/ecovacs/test_init.py +++ b/tests/components/ecovacs/test_init.py @@ -80,6 +80,7 @@ async def test_invalid_auth( ({}, 0), ({DOMAIN: IMPORT_DATA.copy()}, 1), ], + ids=["no_config", "import_config"], ) async def test_async_setup_import( hass: HomeAssistant, From 03daeda9dbf07df1607c09fed24e94565d278497 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Fri, 2 Feb 2024 11:28:51 +0100 Subject: [PATCH 0067/1367] Disable less interesting sensors by default in ViCare integration (#109014) --- homeassistant/components/vicare/sensor.py | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 39b4bd032dc..6c794b548ad 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -145,6 +145,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getDomesticHotWaterMaxTemperature(), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="hotwater_min_temperature", @@ -153,6 +154,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getDomesticHotWaterMinTemperature(), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="hotwater_gas_consumption_today", @@ -167,6 +169,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisWeek(), unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="hotwater_gas_consumption_heating_this_month", @@ -174,6 +177,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisMonth(), unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="hotwater_gas_consumption_heating_this_year", @@ -181,6 +185,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisYear(), unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="gas_consumption_heating_today", @@ -195,6 +200,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasConsumptionHeatingThisWeek(), unit_getter=lambda api: api.getGasConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="gas_consumption_heating_this_month", @@ -202,6 +208,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasConsumptionHeatingThisMonth(), unit_getter=lambda api: api.getGasConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="gas_consumption_heating_this_year", @@ -209,6 +216,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasConsumptionHeatingThisYear(), unit_getter=lambda api: api.getGasConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="gas_consumption_fuelcell_today", @@ -287,6 +295,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasSummaryConsumptionHeatingCurrentMonth(), unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="gas_summary_consumption_heating_currentyear", @@ -295,6 +304,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasSummaryConsumptionHeatingCurrentYear(), unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="gas_summary_consumption_heating_lastsevendays", @@ -303,6 +313,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasSummaryConsumptionHeatingLastSevenDays(), unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="hotwater_gas_summary_consumption_heating_currentday", @@ -319,6 +330,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterCurrentMonth(), unit_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="hotwater_gas_summary_consumption_heating_currentyear", @@ -327,6 +339,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterCurrentYear(), unit_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="hotwater_gas_summary_consumption_heating_lastsevendays", @@ -335,6 +348,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterLastSevenDays(), unit_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="energy_summary_consumption_heating_currentday", @@ -351,6 +365,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerSummaryConsumptionHeatingCurrentMonth(), unit_getter=lambda api: api.getPowerSummaryConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="energy_summary_consumption_heating_currentyear", @@ -359,6 +374,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerSummaryConsumptionHeatingCurrentYear(), unit_getter=lambda api: api.getPowerSummaryConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="energy_summary_consumption_heating_lastsevendays", @@ -367,6 +383,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerSummaryConsumptionHeatingLastSevenDays(), unit_getter=lambda api: api.getPowerSummaryConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="energy_dhw_summary_consumption_heating_currentday", @@ -383,6 +400,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterCurrentMonth(), unit_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="energy_dhw_summary_consumption_heating_currentyear", @@ -391,6 +409,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterCurrentYear(), unit_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="energy_summary_dhw_consumption_heating_lastsevendays", @@ -399,6 +418,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterLastSevenDays(), unit_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="power_production_current", @@ -423,6 +443,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerProductionThisWeek(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="power_production_this_month", @@ -431,6 +452,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerProductionThisMonth(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="power_production_this_year", @@ -439,6 +461,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( value_getter=lambda api: api.getPowerProductionThisYear(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="solar storage temperature", @@ -473,6 +496,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( unit_getter=lambda api: api.getSolarPowerProductionUnit(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="solar power production this month", @@ -482,6 +506,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( unit_getter=lambda api: api.getSolarPowerProductionUnit(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="solar power production this year", @@ -491,6 +516,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( unit_getter=lambda api: api.getSolarPowerProductionUnit(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="power consumption today", @@ -509,6 +535,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( unit_getter=lambda api: api.getPowerConsumptionUnit(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="power consumption this month", @@ -518,6 +545,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( unit_getter=lambda api: api.getPowerConsumptionUnit(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="power consumption this year", @@ -527,6 +555,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( unit_getter=lambda api: api.getPowerConsumptionUnit(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="buffer top temperature", @@ -615,6 +644,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHoursLoadClass1(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="compressor_hours_loadclass2", @@ -623,6 +653,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHoursLoadClass2(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="compressor_hours_loadclass3", @@ -631,6 +662,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHoursLoadClass3(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="compressor_hours_loadclass4", @@ -639,6 +671,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHoursLoadClass4(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="compressor_hours_loadclass5", @@ -647,6 +680,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHoursLoadClass5(), state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, ), ViCareSensorEntityDescription( key="compressor_phase", From a452ad6454fb5119a3dc68b05cddb7060ec73882 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Feb 2024 11:33:03 +0100 Subject: [PATCH 0068/1367] Update sentry-sdk to 1.40.0 (#109363) --- 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 3c3eaeb78e3..e0a2d5f75c4 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.39.2"] + "requirements": ["sentry-sdk==1.40.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4aa0c0e08d6..2b1f9f96404 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2514,7 +2514,7 @@ sensorpro-ble==0.5.3 sensorpush-ble==1.6.2 # homeassistant.components.sentry -sentry-sdk==1.39.2 +sentry-sdk==1.40.0 # homeassistant.components.sfr_box sfrbox-api==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c0889cdf01..8c09602bf8c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1918,7 +1918,7 @@ sensorpro-ble==0.5.3 sensorpush-ble==1.6.2 # homeassistant.components.sentry -sentry-sdk==1.39.2 +sentry-sdk==1.40.0 # homeassistant.components.sfr_box sfrbox-api==0.0.8 From cd1ef93123f84f3adb54140c2c2d1c8e54b7e2b1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Feb 2024 11:43:05 +0100 Subject: [PATCH 0069/1367] Remove suggested area from Verisure (#109364) --- homeassistant/components/verisure/binary_sensor.py | 1 - homeassistant/components/verisure/camera.py | 1 - homeassistant/components/verisure/lock.py | 1 - homeassistant/components/verisure/sensor.py | 2 -- homeassistant/components/verisure/switch.py | 1 - 5 files changed, 6 deletions(-) diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index cadb9b6788d..19a60602540 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -58,7 +58,6 @@ class VerisureDoorWindowSensor( area = self.coordinator.data["door_window"][self.serial_number]["area"] return DeviceInfo( name=area, - suggested_area=area, manufacturer="Verisure", model="Shock Sensor Detector", identifiers={(DOMAIN, self.serial_number)}, diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index a240d45cf7e..e0505328245 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -71,7 +71,6 @@ class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera) area = self.coordinator.data["cameras"][self.serial_number]["device"]["area"] return DeviceInfo( name=area, - suggested_area=area, manufacturer="Verisure", model="SmartCam", identifiers={(DOMAIN, self.serial_number)}, diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 1a81b437116..8e57c9695c0 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -77,7 +77,6 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt area = self.coordinator.data["locks"][self.serial_number]["device"]["area"] return DeviceInfo( name=area, - suggested_area=area, manufacturer="Verisure", model="Lockguard Smartlock", identifiers={(DOMAIN, self.serial_number)}, diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 0fb16aa87c4..51947484dca 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -68,7 +68,6 @@ class VerisureThermometer( area = self.coordinator.data["climate"][self.serial_number]["device"]["area"] return DeviceInfo( name=area, - suggested_area=area, manufacturer="Verisure", model=DEVICE_TYPE_NAME.get(device_type, device_type), identifiers={(DOMAIN, self.serial_number)}, @@ -119,7 +118,6 @@ class VerisureHygrometer( area = self.coordinator.data["climate"][self.serial_number]["device"]["area"] return DeviceInfo( name=area, - suggested_area=area, manufacturer="Verisure", model=DEVICE_TYPE_NAME.get(device_type, device_type), identifiers={(DOMAIN, self.serial_number)}, diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 427ca5e6ea8..96992cadb75 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -53,7 +53,6 @@ class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], Switch ] return DeviceInfo( name=area, - suggested_area=area, manufacturer="Verisure", model="SmartPlug", identifiers={(DOMAIN, self.serial_number)}, From a584429ce0e3782844e35d66992ac8f89c163ea0 Mon Sep 17 00:00:00 2001 From: Jan Rieger Date: Fri, 2 Feb 2024 12:37:23 +0100 Subject: [PATCH 0070/1367] Use translation placeholders in 1-Wire (#109120) --- .../components/onewire/binary_sensor.py | 12 +- homeassistant/components/onewire/sensor.py | 9 +- homeassistant/components/onewire/strings.json | 178 ++---------------- homeassistant/components/onewire/switch.py | 24 ++- .../onewire/snapshots/test_binary_sensor.ambr | 32 ++-- .../onewire/snapshots/test_sensor.ambr | 16 +- .../onewire/snapshots/test_switch.ambr | 68 +++---- 7 files changed, 108 insertions(+), 231 deletions(-) diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 2840cde704b..e7e30588f8a 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -38,7 +38,8 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ... key=f"sensed.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"sensed_{id.lower()}", + translation_key="sensed_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_A_B ), @@ -47,7 +48,8 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ... key=f"sensed.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"sensed_{id}", + translation_key="sensed_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_7 ), @@ -56,7 +58,8 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ... key=f"sensed.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"sensed_{id.lower()}", + translation_key="sensed_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_A_B ), @@ -72,7 +75,8 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = { read_mode=READ_MODE_BOOL, entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.PROBLEM, - translation_key=f"hub_short_{id}", + translation_key="hub_short_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_3 ), diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index cc8b14b5d6e..a7d199c21a9 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -236,7 +236,8 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement="count", read_mode=READ_MODE_INT, state_class=SensorStateClass.TOTAL_INCREASING, - translation_key=f"counter_{id.lower()}", + translation_key="counter_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_A_B ), @@ -276,7 +277,8 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=UnitOfPressure.CBAR, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key=f"moisture_{id}", + translation_key="moisture_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_3 ), @@ -396,7 +398,8 @@ def get_entities( description, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, - translation_key=f"wetness_{s_id}", + translation_key="wetness_id", + translation_placeholders={"id": s_id}, ) override_key = None if description.override_key: diff --git a/homeassistant/components/onewire/strings.json b/homeassistant/components/onewire/strings.json index 753f244cfe9..8dbcbdf8978 100644 --- a/homeassistant/components/onewire/strings.json +++ b/homeassistant/components/onewire/strings.json @@ -21,55 +21,16 @@ }, "entity": { "binary_sensor": { - "sensed_a": { - "name": "Sensed A" + "sensed_id": { + "name": "Sensed {id}" }, - "sensed_b": { - "name": "Sensed B" - }, - "sensed_0": { - "name": "Sensed 0" - }, - "sensed_1": { - "name": "Sensed 1" - }, - "sensed_2": { - "name": "Sensed 2" - }, - "sensed_3": { - "name": "Sensed 3" - }, - "sensed_4": { - "name": "Sensed 4" - }, - "sensed_5": { - "name": "Sensed 5" - }, - "sensed_6": { - "name": "Sensed 6" - }, - "sensed_7": { - "name": "Sensed 7" - }, - "hub_short_0": { - "name": "Hub short on branch 0" - }, - "hub_short_1": { - "name": "Hub short on branch 1" - }, - "hub_short_2": { - "name": "Hub short on branch 2" - }, - "hub_short_3": { - "name": "Hub short on branch 3" + "hub_short_id": { + "name": "Hub short on branch {id}" } }, "sensor": { - "counter_a": { - "name": "Counter A" - }, - "counter_b": { - "name": "Counter B" + "counter_id": { + "name": "Counter {id}" }, "humidity_hih3600": { "name": "HIH3600 humidity" @@ -86,17 +47,8 @@ "humidity_raw": { "name": "Raw humidity" }, - "moisture_1": { - "name": "Moisture 1" - }, - "moisture_2": { - "name": "Moisture 2" - }, - "moisture_3": { - "name": "Moisture 3" - }, - "moisture_4": { - "name": "Moisture 4" + "moisture_id": { + "name": "Moisture {id}" }, "thermocouple_temperature_k": { "name": "Thermocouple K temperature" @@ -113,121 +65,31 @@ "voltage_vis_gradient": { "name": "VIS voltage gradient" }, - "wetness_0": { - "name": "Wetness 0" - }, - "wetness_1": { - "name": "Wetness 1" - }, - "wetness_2": { - "name": "Wetness 2" - }, - "wetness_3": { - "name": "Wetness 3" + "wetness_id": { + "name": "Wetness {id}" } }, "switch": { - "hub_branch_0": { - "name": "Hub branch 0" - }, - "hub_branch_1": { - "name": "Hub branch 1" - }, - "hub_branch_2": { - "name": "Hub branch 2" - }, - "hub_branch_3": { - "name": "Hub branch 3" + "hub_branch_id": { + "name": "Hub branch {id}" }, "iad": { "name": "Current A/D control" }, - "latch_0": { - "name": "Latch 0" + "latch_id": { + "name": "Latch {id}" }, - "latch_1": { - "name": "Latch 1" + "leaf_sensor_id": { + "name": "Leaf sensor {id}" }, - "latch_2": { - "name": "Latch 2" - }, - "latch_3": { - "name": "Latch 3" - }, - "latch_4": { - "name": "Latch 4" - }, - "latch_5": { - "name": "Latch 5" - }, - "latch_6": { - "name": "Latch 6" - }, - "latch_7": { - "name": "Latch 7" - }, - "latch_a": { - "name": "Latch A" - }, - "latch_b": { - "name": "Latch B" - }, - "leaf_sensor_0": { - "name": "Leaf sensor 0" - }, - "leaf_sensor_1": { - "name": "Leaf sensor 1" - }, - "leaf_sensor_2": { - "name": "Leaf sensor 2" - }, - "leaf_sensor_3": { - "name": "Leaf sensor 3" - }, - "moisture_sensor_0": { - "name": "Moisture sensor 0" - }, - "moisture_sensor_1": { - "name": "Moisture sensor 1" - }, - "moisture_sensor_2": { - "name": "Moisture sensor 2" - }, - "moisture_sensor_3": { - "name": "Moisture sensor 3" + "moisture_sensor_id": { + "name": "Moisture sensor {id}" }, "pio": { "name": "Programmed input-output" }, - "pio_0": { - "name": "Programmed input-output 0" - }, - "pio_1": { - "name": "Programmed input-output 1" - }, - "pio_2": { - "name": "Programmed input-output 2" - }, - "pio_3": { - "name": "Programmed input-output 3" - }, - "pio_4": { - "name": "Programmed input-output 4" - }, - "pio_5": { - "name": "Programmed input-output 5" - }, - "pio_6": { - "name": "Programmed input-output 6" - }, - "pio_7": { - "name": "Programmed input-output 7" - }, - "pio_a": { - "name": "Programmed input-output A" - }, - "pio_b": { - "name": "Programmed input-output B" + "pio_id": { + "name": "Programmed input-output {id}" } } }, diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index db9e8f5b0f8..00a3f8f65f4 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -42,7 +42,8 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { key=f"PIO.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"pio_{id.lower()}", + translation_key="pio_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_A_B ] @@ -51,7 +52,8 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { key=f"latch.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"latch_{id.lower()}", + translation_key="latch_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_A_B ] @@ -71,7 +73,8 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { key=f"PIO.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"pio_{id}", + translation_key="pio_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_7 ] @@ -80,7 +83,8 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { key=f"latch.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"latch_{id}", + translation_key="latch_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_7 ] @@ -90,7 +94,8 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { key=f"PIO.{id}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, - translation_key=f"pio_{id.lower()}", + translation_key="pio_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_A_B ), @@ -106,7 +111,8 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = { entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, entity_category=EntityCategory.CONFIG, - translation_key=f"hub_branch_{id}", + translation_key="hub_branch_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_3 ), @@ -117,7 +123,8 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = { entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, entity_category=EntityCategory.CONFIG, - translation_key=f"leaf_sensor_{id}", + translation_key="leaf_sensor_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_3 ] @@ -127,7 +134,8 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = { entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, entity_category=EntityCategory.CONFIG, - translation_key=f"moisture_sensor_{id}", + translation_key="moisture_sensor_id", + translation_placeholders={"id": str(id)}, ) for id in DEVICE_KEYS_0_3 ] diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index 25d47b342c5..8ca1e476820 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -144,7 +144,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_a', + 'translation_key': 'sensed_id', 'unique_id': '/12.111111111111/sensed.A', 'unit_of_measurement': None, }), @@ -173,7 +173,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_b', + 'translation_key': 'sensed_id', 'unique_id': '/12.111111111111/sensed.B', 'unit_of_measurement': None, }), @@ -556,7 +556,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_0', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.0', 'unit_of_measurement': None, }), @@ -585,7 +585,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_1', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.1', 'unit_of_measurement': None, }), @@ -614,7 +614,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_2', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.2', 'unit_of_measurement': None, }), @@ -643,7 +643,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_3', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.3', 'unit_of_measurement': None, }), @@ -672,7 +672,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_4', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.4', 'unit_of_measurement': None, }), @@ -701,7 +701,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_5', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.5', 'unit_of_measurement': None, }), @@ -730,7 +730,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_6', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.6', 'unit_of_measurement': None, }), @@ -759,7 +759,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_7', + 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.7', 'unit_of_measurement': None, }), @@ -960,7 +960,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_a', + 'translation_key': 'sensed_id', 'unique_id': '/3A.111111111111/sensed.A', 'unit_of_measurement': None, }), @@ -989,7 +989,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'sensed_b', + 'translation_key': 'sensed_id', 'unique_id': '/3A.111111111111/sensed.B', 'unit_of_measurement': None, }), @@ -1308,7 +1308,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_short_0', + 'translation_key': 'hub_short_id', 'unique_id': '/EF.111111111113/hub/short.0', 'unit_of_measurement': None, }), @@ -1337,7 +1337,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_short_1', + 'translation_key': 'hub_short_id', 'unique_id': '/EF.111111111113/hub/short.1', 'unit_of_measurement': None, }), @@ -1366,7 +1366,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_short_2', + 'translation_key': 'hub_short_id', 'unique_id': '/EF.111111111113/hub/short.2', 'unit_of_measurement': None, }), @@ -1395,7 +1395,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_short_3', + 'translation_key': 'hub_short_id', 'unique_id': '/EF.111111111113/hub/short.3', 'unit_of_measurement': None, }), diff --git a/tests/components/onewire/snapshots/test_sensor.ambr b/tests/components/onewire/snapshots/test_sensor.ambr index cbcf0d6234e..936018a48c4 100644 --- a/tests/components/onewire/snapshots/test_sensor.ambr +++ b/tests/components/onewire/snapshots/test_sensor.ambr @@ -322,7 +322,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'counter_a', + 'translation_key': 'counter_id', 'unique_id': '/1D.111111111111/counter.A', 'unit_of_measurement': 'count', }), @@ -353,7 +353,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'counter_b', + 'translation_key': 'counter_id', 'unique_id': '/1D.111111111111/counter.B', 'unit_of_measurement': 'count', }), @@ -476,7 +476,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'counter_a', + 'translation_key': 'counter_id', 'unique_id': '/1D.111111111111/counter.A', 'unit_of_measurement': 'count', }), @@ -507,7 +507,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'counter_b', + 'translation_key': 'counter_id', 'unique_id': '/1D.111111111111/counter.B', 'unit_of_measurement': 'count', }), @@ -2478,7 +2478,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wetness_0', + 'translation_key': 'wetness_id', 'unique_id': '/EF.111111111112/moisture/sensor.0', 'unit_of_measurement': '%', }), @@ -2509,7 +2509,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wetness_1', + 'translation_key': 'wetness_id', 'unique_id': '/EF.111111111112/moisture/sensor.1', 'unit_of_measurement': '%', }), @@ -2540,7 +2540,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'moisture_2', + 'translation_key': 'moisture_id', 'unique_id': '/EF.111111111112/moisture/sensor.2', 'unit_of_measurement': , }), @@ -2571,7 +2571,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'moisture_3', + 'translation_key': 'moisture_id', 'unique_id': '/EF.111111111112/moisture/sensor.3', 'unit_of_measurement': , }), diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr index e4d081a409b..24c985a311e 100644 --- a/tests/components/onewire/snapshots/test_switch.ambr +++ b/tests/components/onewire/snapshots/test_switch.ambr @@ -185,7 +185,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_a', + 'translation_key': 'pio_id', 'unique_id': '/12.111111111111/PIO.A', 'unit_of_measurement': None, }), @@ -214,7 +214,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_b', + 'translation_key': 'pio_id', 'unique_id': '/12.111111111111/PIO.B', 'unit_of_measurement': None, }), @@ -243,7 +243,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_a', + 'translation_key': 'latch_id', 'unique_id': '/12.111111111111/latch.A', 'unit_of_measurement': None, }), @@ -272,7 +272,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_b', + 'translation_key': 'latch_id', 'unique_id': '/12.111111111111/latch.B', 'unit_of_measurement': None, }), @@ -720,7 +720,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_0', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.0', 'unit_of_measurement': None, }), @@ -749,7 +749,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_1', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.1', 'unit_of_measurement': None, }), @@ -778,7 +778,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_2', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.2', 'unit_of_measurement': None, }), @@ -807,7 +807,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_3', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.3', 'unit_of_measurement': None, }), @@ -836,7 +836,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_4', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.4', 'unit_of_measurement': None, }), @@ -865,7 +865,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_5', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.5', 'unit_of_measurement': None, }), @@ -894,7 +894,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_6', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.6', 'unit_of_measurement': None, }), @@ -923,7 +923,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_7', + 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.7', 'unit_of_measurement': None, }), @@ -952,7 +952,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_0', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.0', 'unit_of_measurement': None, }), @@ -981,7 +981,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_1', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.1', 'unit_of_measurement': None, }), @@ -1010,7 +1010,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_2', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.2', 'unit_of_measurement': None, }), @@ -1039,7 +1039,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_3', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.3', 'unit_of_measurement': None, }), @@ -1068,7 +1068,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_4', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.4', 'unit_of_measurement': None, }), @@ -1097,7 +1097,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_5', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.5', 'unit_of_measurement': None, }), @@ -1126,7 +1126,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_6', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.6', 'unit_of_measurement': None, }), @@ -1155,7 +1155,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'latch_7', + 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.7', 'unit_of_measurement': None, }), @@ -1452,7 +1452,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_a', + 'translation_key': 'pio_id', 'unique_id': '/3A.111111111111/PIO.A', 'unit_of_measurement': None, }), @@ -1481,7 +1481,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'pio_b', + 'translation_key': 'pio_id', 'unique_id': '/3A.111111111111/PIO.B', 'unit_of_measurement': None, }), @@ -1762,7 +1762,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'leaf_sensor_0', + 'translation_key': 'leaf_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_leaf.0', 'unit_of_measurement': None, }), @@ -1791,7 +1791,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'leaf_sensor_1', + 'translation_key': 'leaf_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_leaf.1', 'unit_of_measurement': None, }), @@ -1820,7 +1820,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'leaf_sensor_2', + 'translation_key': 'leaf_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_leaf.2', 'unit_of_measurement': None, }), @@ -1849,7 +1849,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'leaf_sensor_3', + 'translation_key': 'leaf_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_leaf.3', 'unit_of_measurement': None, }), @@ -1878,7 +1878,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'moisture_sensor_0', + 'translation_key': 'moisture_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_moisture.0', 'unit_of_measurement': None, }), @@ -1907,7 +1907,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'moisture_sensor_1', + 'translation_key': 'moisture_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_moisture.1', 'unit_of_measurement': None, }), @@ -1936,7 +1936,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'moisture_sensor_2', + 'translation_key': 'moisture_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_moisture.2', 'unit_of_measurement': None, }), @@ -1965,7 +1965,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'moisture_sensor_3', + 'translation_key': 'moisture_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_moisture.3', 'unit_of_measurement': None, }), @@ -2128,7 +2128,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_branch_0', + 'translation_key': 'hub_branch_id', 'unique_id': '/EF.111111111113/hub/branch.0', 'unit_of_measurement': None, }), @@ -2157,7 +2157,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_branch_1', + 'translation_key': 'hub_branch_id', 'unique_id': '/EF.111111111113/hub/branch.1', 'unit_of_measurement': None, }), @@ -2186,7 +2186,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_branch_2', + 'translation_key': 'hub_branch_id', 'unique_id': '/EF.111111111113/hub/branch.2', 'unit_of_measurement': None, }), @@ -2215,7 +2215,7 @@ 'platform': 'onewire', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hub_branch_3', + 'translation_key': 'hub_branch_id', 'unique_id': '/EF.111111111113/hub/branch.3', 'unit_of_measurement': None, }), From d3dbd6fa70d1b2021a308cbaf159fdc96f742487 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 2 Feb 2024 21:38:57 +1000 Subject: [PATCH 0071/1367] Change device class of Auto Seat Heater sensors in Tessie (#109240) --- .../components/tessie/binary_sensor.py | 3 - homeassistant/components/tessie/icons.json | 23 +++++ homeassistant/components/tessie/sensor.py | 2 - homeassistant/components/tessie/strings.json | 2 +- .../tessie/snapshots/test_binary_sensors.ambr | 93 +++++++++---------- .../tessie/snapshots/test_sensor.ambr | 6 +- 6 files changed, 71 insertions(+), 58 deletions(-) create mode 100644 homeassistant/components/tessie/icons.json diff --git a/homeassistant/components/tessie/binary_sensor.py b/homeassistant/components/tessie/binary_sensor.py index 594098cddfe..65bfd483f18 100644 --- a/homeassistant/components/tessie/binary_sensor.py +++ b/homeassistant/components/tessie/binary_sensor.py @@ -62,17 +62,14 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = ( ), TessieBinarySensorEntityDescription( key="climate_state_auto_seat_climate_left", - device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, ), TessieBinarySensorEntityDescription( key="climate_state_auto_seat_climate_right", - device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, ), TessieBinarySensorEntityDescription( key="climate_state_auto_steering_wheel_heat", - device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, ), TessieBinarySensorEntityDescription( diff --git a/homeassistant/components/tessie/icons.json b/homeassistant/components/tessie/icons.json new file mode 100644 index 00000000000..caf0524f2e8 --- /dev/null +++ b/homeassistant/components/tessie/icons.json @@ -0,0 +1,23 @@ +{ + "entity": { + "sensor": { + "drive_state_shift_state": { + "default": "mdi:car-shift-pattern" + }, + "drive_state_active_route_destination": { + "default": "mdi:map-marker" + } + }, + "binary_sensor": { + "climate_state_auto_seat_climate_left": { + "default": "mdi:car-seat-heater" + }, + "climate_state_auto_seat_climate_right": { + "default": "mdi:car-seat-heater" + }, + "climate_state_auto_steering_wheel_heat": { + "default": "mdi:steering" + } + } + } +} diff --git a/homeassistant/components/tessie/sensor.py b/homeassistant/components/tessie/sensor.py index ae9e06b2b35..36896863120 100644 --- a/homeassistant/components/tessie/sensor.py +++ b/homeassistant/components/tessie/sensor.py @@ -122,7 +122,6 @@ DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( ), TessieSensorEntityDescription( key="drive_state_shift_state", - icon="mdi:car-shift-pattern", options=["p", "d", "r", "n"], device_class=SensorDeviceClass.ENUM, value_fn=lambda x: x.lower() if isinstance(x, str) else x, @@ -231,7 +230,6 @@ DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( ), TessieSensorEntityDescription( key="drive_state_active_route_destination", - icon="mdi:map-marker", entity_category=EntityCategory.DIAGNOSTIC, ), ) diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 8340557843d..381a5e3d4c0 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -258,7 +258,7 @@ "climate_state_auto_seat_climate_right": { "name": "Auto seat climate right" }, - "climate_state_auto_steering_wheel_heater": { + "climate_state_auto_steering_wheel_heat": { "name": "Auto steering wheel heater" }, "climate_state_cabin_overheat_protection": { diff --git a/tests/components/tessie/snapshots/test_binary_sensors.ambr b/tests/components/tessie/snapshots/test_binary_sensors.ambr index 2fbd6764081..aacaad1d7e4 100644 --- a/tests/components/tessie/snapshots/test_binary_sensors.ambr +++ b/tests/components/tessie/snapshots/test_binary_sensors.ambr @@ -19,7 +19,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Auto seat climate left', 'platform': 'tessie', @@ -33,7 +33,6 @@ # name: test_binary_sensors[binary_sensor.test_auto_seat_climate_left-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'heat', 'friendly_name': 'Test Auto seat climate left', }), 'context': , @@ -63,7 +62,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Auto seat climate right', 'platform': 'tessie', @@ -77,7 +76,6 @@ # name: test_binary_sensors[binary_sensor.test_auto_seat_climate_right-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'heat', 'friendly_name': 'Test Auto seat climate right', }), 'context': , @@ -87,6 +85,49 @@ 'state': 'on', }) # --- +# name: test_binary_sensors[binary_sensor.test_auto_steering_wheel_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_auto_steering_wheel_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto steering wheel heater', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_state_auto_steering_wheel_heat', + 'unique_id': 'VINVINVIN-climate_state_auto_steering_wheel_heat', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[binary_sensor.test_auto_steering_wheel_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Auto steering wheel heater', + }), + 'context': , + 'entity_id': 'binary_sensor.test_auto_steering_wheel_heater', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_binary_sensors[binary_sensor.test_battery_heater-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -527,50 +568,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[binary_sensor.test_heat-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.test_heat', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Heat', - 'platform': 'tessie', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'climate_state_auto_steering_wheel_heat', - 'unique_id': 'VINVINVIN-climate_state_auto_steering_wheel_heat', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[binary_sensor.test_heat-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'heat', - 'friendly_name': 'Test Heat', - }), - 'context': , - 'entity_id': 'binary_sensor.test_heat', - 'last_changed': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_binary_sensors[binary_sensor.test_preconditioning_enabled-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index 2f5e1e8ddb2..0c01fc50244 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -368,7 +368,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:map-marker', + 'original_icon': None, 'original_name': 'Destination', 'platform': 'tessie', 'previous_unique_id': None, @@ -382,7 +382,6 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test Destination', - 'icon': 'mdi:map-marker', }), 'context': , 'entity_id': 'sensor.test_destination', @@ -776,7 +775,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:car-shift-pattern', + 'original_icon': None, 'original_name': 'Shift state', 'platform': 'tessie', 'previous_unique_id': None, @@ -791,7 +790,6 @@ 'attributes': ReadOnlyDict({ 'device_class': 'enum', 'friendly_name': 'Test Shift state', - 'icon': 'mdi:car-shift-pattern', 'options': list([ 'p', 'd', From 90ec361fc95acbd4e0e46d9627c0ae6ab678ba07 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 2 Feb 2024 12:42:12 +0100 Subject: [PATCH 0072/1367] Centralize validation for modbus config (#108906) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/modbus/validators.py | 261 ++++++++---------- tests/components/modbus/test_init.py | 117 ++++---- 2 files changed, 187 insertions(+), 191 deletions(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 76d8e270ffe..37eae23ba82 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -203,141 +203,6 @@ def duplicate_fan_mode_validator(config: dict[str, Any]) -> dict: return config -def scan_interval_validator(config: dict) -> dict: - """Control scan_interval.""" - for hub in config: - minimum_scan_interval = DEFAULT_SCAN_INTERVAL - for component, conf_key in PLATFORMS: - if conf_key not in hub: - continue - - for entry in hub[conf_key]: - scan_interval = entry.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - if scan_interval == 0: - continue - if scan_interval < 5: - _LOGGER.warning( - ( - "%s %s scan_interval(%d) is lower than 5 seconds, " - "which may cause Home Assistant stability issues" - ), - component, - entry.get(CONF_NAME), - scan_interval, - ) - entry[CONF_SCAN_INTERVAL] = scan_interval - minimum_scan_interval = min(scan_interval, minimum_scan_interval) - if ( - CONF_TIMEOUT in hub - and hub[CONF_TIMEOUT] > minimum_scan_interval - 1 - and minimum_scan_interval > 1 - ): - _LOGGER.warning( - "Modbus %s timeout(%d) is adjusted(%d) due to scan_interval", - hub.get(CONF_NAME, ""), - hub[CONF_TIMEOUT], - minimum_scan_interval - 1, - ) - hub[CONF_TIMEOUT] = minimum_scan_interval - 1 - return config - - -def duplicate_entity_validator(config: dict) -> dict: - """Control scan_interval.""" - for hub_index, hub in enumerate(config): - for component, conf_key in PLATFORMS: - if conf_key not in hub: - continue - names: set[str] = set() - errors: list[int] = [] - addresses: set[str] = set() - for index, entry in enumerate(hub[conf_key]): - name = entry[CONF_NAME] - addr = str(entry[CONF_ADDRESS]) - if CONF_INPUT_TYPE in entry: - addr += "_" + str(entry[CONF_INPUT_TYPE]) - elif CONF_WRITE_TYPE in entry: - addr += "_" + str(entry[CONF_WRITE_TYPE]) - if CONF_COMMAND_ON in entry: - addr += "_" + str(entry[CONF_COMMAND_ON]) - if CONF_COMMAND_OFF in entry: - addr += "_" + str(entry[CONF_COMMAND_OFF]) - inx = entry.get(CONF_SLAVE, None) or entry.get(CONF_DEVICE_ADDRESS, 0) - addr += "_" + str(inx) - entry_addrs: set[str] = set() - entry_addrs.add(addr) - - if CONF_TARGET_TEMP in entry: - a = str(entry[CONF_TARGET_TEMP]) - a += "_" + str(inx) - entry_addrs.add(a) - if CONF_HVAC_MODE_REGISTER in entry: - a = str(entry[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS]) - a += "_" + str(inx) - entry_addrs.add(a) - if CONF_FAN_MODE_REGISTER in entry: - a = str( - entry[CONF_FAN_MODE_REGISTER][CONF_ADDRESS] - if isinstance(entry[CONF_FAN_MODE_REGISTER][CONF_ADDRESS], int) - else entry[CONF_FAN_MODE_REGISTER][CONF_ADDRESS][0] - ) - a += "_" + str(inx) - entry_addrs.add(a) - - dup_addrs = entry_addrs.intersection(addresses) - - if len(dup_addrs) > 0: - for addr in dup_addrs: - err = ( - f"Modbus {component}/{name} address {addr} is duplicate, second" - " entry not loaded!" - ) - _LOGGER.warning(err) - errors.append(index) - elif name in names: - err = ( - f"Modbus {component}/{name}  is duplicate, second entry not" - " loaded!" - ) - _LOGGER.warning(err) - errors.append(index) - else: - names.add(name) - addresses.update(entry_addrs) - - for i in reversed(errors): - del config[hub_index][conf_key][i] - return config - - -def duplicate_modbus_validator(config: dict) -> dict: - """Control modbus connection for duplicates.""" - hosts: set[str] = set() - names: set[str] = set() - errors = [] - for index, hub in enumerate(config): - name = hub.get(CONF_NAME, DEFAULT_HUB) - if hub[CONF_TYPE] == SERIAL: - host = hub[CONF_PORT] - else: - host = f"{hub[CONF_HOST]}_{hub[CONF_PORT]}" - if host in hosts: - err = f"Modbus {name} contains duplicate host/port {host}, not loaded!" - _LOGGER.warning(err) - errors.append(index) - elif name in names: - err = f"Modbus {name} is duplicate, second entry not loaded!" - _LOGGER.warning(err) - errors.append(index) - else: - hosts.add(host) - names.add(name) - - for i in reversed(errors): - del config[i] - return config - - def register_int_list_validator(value: Any) -> Any: """Check if a register (CONF_ADRESS) is an int or a list having only 1 register.""" if isinstance(value, int) and value >= 0: @@ -354,7 +219,125 @@ def register_int_list_validator(value: Any) -> Any: def check_config(config: dict) -> dict: """Do final config check.""" - config2 = duplicate_modbus_validator(config) - config3 = scan_interval_validator(config2) - config4 = duplicate_entity_validator(config3) - return config4 + hosts: set[str] = set() + hub_names: set[str] = set() + hub_name_inx = 0 + minimum_scan_interval = 0 + ent_names: set[str] = set() + ent_addr: set[str] = set() + + def validate_modbus(hub: dict, hub_name_inx: int) -> bool: + """Validate modbus entries.""" + host: str = ( + hub[CONF_PORT] + if hub[CONF_TYPE] == SERIAL + else f"{hub[CONF_HOST]}_{hub[CONF_PORT]}" + ) + if CONF_NAME not in hub: + hub[CONF_NAME] = ( + DEFAULT_HUB if not hub_name_inx else f"{DEFAULT_HUB}_{hub_name_inx}" + ) + hub_name_inx += 1 + err = f"Modbus host/port {host} is missing name, added {hub[CONF_NAME]}!" + _LOGGER.warning(err) + name = hub[CONF_NAME] + if host in hosts or name in hub_names: + err = f"Modbus {name} host/port {host} is duplicate, not loaded!" + _LOGGER.warning(err) + return False + hosts.add(host) + hub_names.add(name) + return True + + def validate_entity( + hub_name: str, + entity: dict, + minimum_scan_interval: int, + ent_names: set, + ent_addr: set, + ) -> bool: + """Validate entity.""" + name = entity[CONF_NAME] + addr = str(entity[CONF_ADDRESS]) + scan_interval = entity.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + if scan_interval < 5: + _LOGGER.warning( + ( + "%s %s scan_interval(%d) is lower than 5 seconds, " + "which may cause Home Assistant stability issues" + ), + hub_name, + name, + scan_interval, + ) + entity[CONF_SCAN_INTERVAL] = scan_interval + minimum_scan_interval = min(scan_interval, minimum_scan_interval) + for conf_type in ( + CONF_INPUT_TYPE, + CONF_WRITE_TYPE, + CONF_COMMAND_ON, + CONF_COMMAND_OFF, + ): + if conf_type in entity: + addr += f"_{entity[conf_type]}" + inx = entity.get(CONF_SLAVE, None) or entity.get(CONF_DEVICE_ADDRESS, 0) + addr += f"_{inx}" + loc_addr: set[str] = {addr} + + if CONF_TARGET_TEMP in entity: + loc_addr.add(f"{entity[CONF_TARGET_TEMP]}_{inx}") + if CONF_HVAC_MODE_REGISTER in entity: + loc_addr.add(f"{entity[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS]}_{inx}") + if CONF_FAN_MODE_REGISTER in entity: + loc_addr.add(f"{entity[CONF_FAN_MODE_REGISTER][CONF_ADDRESS]}_{inx}") + + dup_addrs = ent_addr.intersection(loc_addr) + if len(dup_addrs) > 0: + for addr in dup_addrs: + err = ( + f"Modbus {hub_name}/{name} address {addr} is duplicate, second" + " entry not loaded!" + ) + _LOGGER.warning(err) + return False + if name in ent_names: + err = f"Modbus {hub_name}/{name} is duplicate, second entry not loaded!" + _LOGGER.warning(err) + return False + ent_names.add(name) + ent_addr.update(loc_addr) + return True + + hub_inx = 0 + while hub_inx < len(config): + hub = config[hub_inx] + if not validate_modbus(hub, hub_name_inx): + del config[hub_inx] + continue + for _component, conf_key in PLATFORMS: + if conf_key not in hub: + continue + entity_inx = 0 + entities = hub[conf_key] + minimum_scan_interval = 9999 + while entity_inx < len(entities): + if not validate_entity( + hub[CONF_NAME], + entities[entity_inx], + minimum_scan_interval, + ent_names, + ent_addr, + ): + del entities[entity_inx] + else: + entity_inx += 1 + + if hub[CONF_TIMEOUT] >= minimum_scan_interval: + hub[CONF_TIMEOUT] = minimum_scan_interval - 1 + _LOGGER.warning( + "Modbus %s timeout is adjusted(%d) due to scan_interval", + hub[CONF_NAME], + hub[CONF_TIMEOUT], + ) + hub_inx += 1 + return config diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 3c932a24afb..c5b12a112fd 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -79,9 +79,8 @@ from homeassistant.components.modbus.const import ( DataType, ) from homeassistant.components.modbus.validators import ( - duplicate_entity_validator, + check_config, duplicate_fan_mode_validator, - duplicate_modbus_validator, nan_validator, register_int_list_validator, struct_validator, @@ -340,55 +339,46 @@ async def test_exception_struct_validator(do_config) -> None: CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, }, { CONF_NAME: TEST_MODBUS_NAME, CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST + " 2", CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, + }, + { + CONF_NAME: TEST_MODBUS_NAME + "2", + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, }, ], [ { - CONF_NAME: TEST_MODBUS_NAME, CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, }, { CONF_NAME: TEST_MODBUS_NAME + " 2", CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, }, ], ], ) -async def test_duplicate_modbus_validator(do_config) -> None: +async def test_check_config(do_config) -> None: """Test duplicate modbus validator.""" - duplicate_modbus_validator(do_config) + check_config(do_config) assert len(do_config) == 1 -@pytest.mark.parametrize( - "do_config", - [ - { - CONF_ADDRESS: 11, - CONF_FAN_MODE_VALUES: { - CONF_FAN_MODE_ON: 7, - CONF_FAN_MODE_OFF: 9, - CONF_FAN_MODE_HIGH: 9, - }, - } - ], -) -async def test_duplicate_fan_mode_validator(do_config) -> None: - """Test duplicate modbus validator.""" - duplicate_fan_mode_validator(do_config) - assert len(do_config[CONF_FAN_MODE_VALUES]) == 2 - - @pytest.mark.parametrize( "do_config", [ @@ -398,6 +388,7 @@ async def test_duplicate_fan_mode_validator(do_config) -> None: CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, CONF_SENSORS: [ { CONF_NAME: TEST_ENTITY_NAME, @@ -418,6 +409,7 @@ async def test_duplicate_fan_mode_validator(do_config) -> None: CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, CONF_SENSORS: [ { CONF_NAME: TEST_ENTITY_NAME, @@ -432,35 +424,12 @@ async def test_duplicate_fan_mode_validator(do_config) -> None: ], } ], - [ - { - CONF_NAME: TEST_MODBUS_NAME, - CONF_TYPE: TCP, - CONF_HOST: TEST_MODBUS_HOST, - CONF_PORT: TEST_PORT_TCP, - CONF_CLIMATES: [ - { - CONF_NAME: TEST_ENTITY_NAME, - CONF_ADDRESS: 117, - CONF_SLAVE: 0, - }, - { - CONF_NAME: TEST_ENTITY_NAME + " 2", - CONF_ADDRESS: 117, - CONF_SLAVE: 0, - }, - ], - } - ], ], ) -async def test_duplicate_entity_validator(do_config) -> None: +async def test_check_config_sensor(do_config) -> None: """Test duplicate entity validator.""" - duplicate_entity_validator(do_config) - if CONF_SENSORS in do_config[0]: - assert len(do_config[0][CONF_SENSORS]) == 1 - elif CONF_CLIMATES in do_config[0]: - assert len(do_config[0][CONF_CLIMATES]) == 1 + check_config(do_config) + assert len(do_config[0][CONF_SENSORS]) == 1 @pytest.mark.parametrize( @@ -472,6 +441,28 @@ async def test_duplicate_entity_validator(do_config) -> None: CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 117, + CONF_SLAVE: 0, + }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 119, + CONF_SLAVE: 0, + }, + ], + } + ], + [ + { + CONF_NAME: TEST_MODBUS_NAME, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, CONF_CLIMATES: [ { CONF_NAME: TEST_ENTITY_NAME, @@ -492,6 +483,7 @@ async def test_duplicate_entity_validator(do_config) -> None: CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, CONF_CLIMATES: [ { CONF_NAME: TEST_ENTITY_NAME, @@ -526,6 +518,7 @@ async def test_duplicate_entity_validator(do_config) -> None: CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, CONF_CLIMATES: [ { CONF_NAME: TEST_ENTITY_NAME, @@ -561,6 +554,7 @@ async def test_duplicate_entity_validator(do_config) -> None: CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, CONF_CLIMATES: [ { CONF_NAME: TEST_ENTITY_NAME, @@ -592,12 +586,31 @@ async def test_duplicate_entity_validator(do_config) -> None: ], ], ) -async def test_duplicate_entity_validator_with_climate(do_config) -> None: +async def test_check_config_climate(do_config) -> None: """Test duplicate entity validator.""" - duplicate_entity_validator(do_config) + check_config(do_config) assert len(do_config[0][CONF_CLIMATES]) == 1 +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_ADDRESS: 11, + CONF_FAN_MODE_VALUES: { + CONF_FAN_MODE_ON: 7, + CONF_FAN_MODE_OFF: 9, + CONF_FAN_MODE_HIGH: 9, + }, + } + ], +) +async def test_duplicate_fan_mode_validator(do_config) -> None: + """Test duplicate modbus validator.""" + duplicate_fan_mode_validator(do_config) + assert len(do_config[CONF_FAN_MODE_VALUES]) == 2 + + @pytest.mark.parametrize( "do_config", [ From e328d3ec5e01048b55e7602b33c28c573bdca5f0 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 2 Feb 2024 22:21:13 +1000 Subject: [PATCH 0073/1367] Add Charging sensor to Tessie (#108205) --- .../components/tessie/binary_sensor.py | 1 + homeassistant/components/tessie/const.py | 10 +++ homeassistant/components/tessie/sensor.py | 9 ++- homeassistant/components/tessie/strings.json | 11 ++++ .../tessie/snapshots/test_sensor.ambr | 62 +++++++++++++++++++ .../components/tessie/test_binary_sensors.py | 2 + 6 files changed, 94 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tessie/binary_sensor.py b/homeassistant/components/tessie/binary_sensor.py index 65bfd483f18..ff0cf661475 100644 --- a/homeassistant/components/tessie/binary_sensor.py +++ b/homeassistant/components/tessie/binary_sensor.py @@ -41,6 +41,7 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = ( key="charge_state_charging_state", device_class=BinarySensorDeviceClass.BATTERY_CHARGING, is_on=lambda x: x == "Charging", + entity_registry_enabled_default=False, ), TessieBinarySensorEntityDescription( key="charge_state_preconditioning_enabled", diff --git a/homeassistant/components/tessie/const.py b/homeassistant/components/tessie/const.py index 591d4652274..8ec063bf47c 100644 --- a/homeassistant/components/tessie/const.py +++ b/homeassistant/components/tessie/const.py @@ -68,3 +68,13 @@ class TessieChargeCableLockStates(StrEnum): ENGAGED = "Engaged" DISENGAGED = "Disengaged" + + +TessieChargeStates = { + "Starting": "starting", + "Charging": "charging", + "Stopped": "stopped", + "Complete": "complete", + "Disconnected": "disconnected", + "NoPower": "no_power", +} diff --git a/homeassistant/components/tessie/sensor.py b/homeassistant/components/tessie/sensor.py index 36896863120..7c126754fb5 100644 --- a/homeassistant/components/tessie/sensor.py +++ b/homeassistant/components/tessie/sensor.py @@ -32,7 +32,7 @@ from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util from homeassistant.util.variance import ignore_variance -from .const import DOMAIN +from .const import DOMAIN, TessieChargeStates from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity @@ -54,6 +54,13 @@ class TessieSensorEntityDescription(SensorEntityDescription): DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( + TessieSensorEntityDescription( + key="charge_state_charging_state", + icon="mdi:ev-station", + options=list(TessieChargeStates.values()), + device_class=SensorDeviceClass.ENUM, + value_fn=lambda value: TessieChargeStates[cast(str, value)], + ), TessieSensorEntityDescription( key="charge_state_usable_battery_level", state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 381a5e3d4c0..01e6a654163 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -67,6 +67,17 @@ } }, "sensor": { + "charge_state_charging_state": { + "name": "Charging", + "state": { + "starting": "Starting", + "charging": "Charging", + "disconnected": "Disconnected", + "stopped": "Stopped", + "complete": "Complete", + "no_power": "No power" + } + }, "charge_state_usable_battery_level": { "name": "Battery level" }, diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index 0c01fc50244..b9a423bfa9a 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -347,6 +347,68 @@ 'state': '224', }) # --- +# name: test_sensors[sensor.test_charging-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'starting', + 'charging', + 'stopped', + 'complete', + 'disconnected', + 'no_power', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_charging', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ev-station', + 'original_name': 'Charging', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charging_state', + 'unique_id': 'VINVINVIN-charge_state_charging_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_charging-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Test Charging', + 'icon': 'mdi:ev-station', + 'options': list([ + 'starting', + 'charging', + 'stopped', + 'complete', + 'disconnected', + 'no_power', + ]), + }), + 'context': , + 'entity_id': 'sensor.test_charging', + 'last_changed': , + 'last_updated': , + 'state': 'charging', + }) +# --- # name: test_sensors[sensor.test_destination-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tessie/test_binary_sensors.py b/tests/components/tessie/test_binary_sensors.py index ca53a60d493..b6dccd9d3b1 100644 --- a/tests/components/tessie/test_binary_sensors.py +++ b/tests/components/tessie/test_binary_sensors.py @@ -1,4 +1,5 @@ """Test the Tessie binary sensor platform.""" +import pytest from syrupy import SnapshotAssertion from homeassistant.const import Platform @@ -8,6 +9,7 @@ from homeassistant.helpers import entity_registry as er from .common import assert_entities, setup_platform +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_binary_sensors( hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry ) -> None: From 3bab1d7cd5a6b38ee673ceb9e9dce140b69d005a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 2 Feb 2024 13:42:07 +0100 Subject: [PATCH 0074/1367] Specify end_time when importing Elvia data to deal with drift (#109361) --- homeassistant/components/elvia/importer.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/elvia/importer.py b/homeassistant/components/elvia/importer.py index 3fc79240254..69e3d64d09d 100644 --- a/homeassistant/components/elvia/importer.py +++ b/homeassistant/components/elvia/importer.py @@ -38,11 +38,18 @@ class ElviaImporter: self.client = Elvia(meter_value_token=api_token).meter_value() self.metering_point_id = metering_point_id - async def _fetch_hourly_data(self, since: datetime) -> list[MeterValueTimeSeries]: + async def _fetch_hourly_data( + self, + since: datetime, + until: datetime, + ) -> list[MeterValueTimeSeries]: """Fetch hourly data.""" - LOGGER.debug("Fetching hourly data since %s", since) + start_time = since.isoformat() + end_time = until.isoformat() + LOGGER.debug("Fetching hourly data %s - %s", start_time, end_time) all_data = await self.client.get_meter_values( - start_time=since.isoformat(), + start_time=start_time, + end_time=end_time, metering_point_ids=[self.metering_point_id], ) return all_data["meteringpoints"][0]["metervalue"]["timeSeries"] @@ -62,8 +69,10 @@ class ElviaImporter: if not last_stats: # First time we insert 1 years of data (if available) + until = dt_util.utcnow() hourly_data = await self._fetch_hourly_data( - since=dt_util.now() - timedelta(days=365) + since=until - timedelta(days=365), + until=until, ) if hourly_data is None or len(hourly_data) == 0: return @@ -71,7 +80,8 @@ class ElviaImporter: _sum = 0.0 else: hourly_data = await self._fetch_hourly_data( - since=dt_util.utc_from_timestamp(last_stats[statistic_id][0]["end"]) + since=dt_util.utc_from_timestamp(last_stats[statistic_id][0]["end"]), + until=dt_util.utcnow(), ) if ( From a6c697c80f8276692a081865b5bf834aa2d82a45 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 2 Feb 2024 14:03:21 +0100 Subject: [PATCH 0075/1367] Add entity name translations to Tibber (#108797) --- homeassistant/components/tibber/sensor.py | 61 +++++++------- homeassistant/components/tibber/strings.json | 85 ++++++++++++++++++++ 2 files changed, 116 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 467cd2bfd77..52e18c9c6a2 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -64,129 +64,129 @@ PARALLEL_UPDATES = 0 RT_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="averagePower", - name="average power", + translation_key="average_power", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, ), SensorEntityDescription( key="power", - name="power", + translation_key="power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, ), SensorEntityDescription( key="powerProduction", - name="power production", + translation_key="power_production", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, ), SensorEntityDescription( key="minPower", - name="min power", + translation_key="min_power", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, ), SensorEntityDescription( key="maxPower", - name="max power", + translation_key="max_power", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, ), SensorEntityDescription( key="accumulatedConsumption", - name="accumulated consumption", + translation_key="accumulated_consumption", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="accumulatedConsumptionLastHour", - name="accumulated consumption current hour", + translation_key="accumulated_consumption_last_hour", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="estimatedHourConsumption", - name="Estimated consumption current hour", + translation_key="estimated_hour_consumption", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, ), SensorEntityDescription( key="accumulatedProduction", - name="accumulated production", + translation_key="accumulated_production", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="accumulatedProductionLastHour", - name="accumulated production current hour", + translation_key="accumulated_production_last_hour", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="lastMeterConsumption", - name="last meter consumption", + translation_key="last_meter_consumption", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="lastMeterProduction", - name="last meter production", + translation_key="last_meter_production", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="voltagePhase1", - name="voltage phase1", + translation_key="voltage_phase1", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltagePhase2", - name="voltage phase2", + translation_key="voltage_phase2", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltagePhase3", - name="voltage phase3", + translation_key="voltage_phase3", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="currentL1", - name="current L1", + translation_key="current_l1", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="currentL2", - name="current L2", + translation_key="current_l2", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="currentL3", - name="current L3", + translation_key="current_l3", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="signalStrength", - name="signal strength", + translation_key="signal_strength", device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=SensorStateClass.MEASUREMENT, @@ -194,19 +194,19 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="accumulatedReward", - name="accumulated reward", + translation_key="accumulated_reward", device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="accumulatedCost", - name="accumulated cost", + translation_key="accumulated_cost", device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="powerFactor", - name="power factor", + translation_key="power_factor", device_class=SensorDeviceClass.POWER_FACTOR, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -216,23 +216,23 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="month_cost", - name="Monthly cost", + translation_key="month_cost", device_class=SensorDeviceClass.MONETARY, ), SensorEntityDescription( key="peak_hour", - name="Monthly peak hour consumption", + translation_key="peak_hour", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, ), SensorEntityDescription( key="peak_hour_time", - name="Time of max hour consumption", + translation_key="peak_hour_time", device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key="month_cons", - name="Monthly net consumption", + translation_key="month_cons", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, @@ -305,6 +305,8 @@ async def async_setup_entry( class TibberSensor(SensorEntity): """Representation of a generic Tibber sensor.""" + _attr_has_entity_name = True + def __init__( self, *args: Any, tibber_home: tibber.TibberHome, **kwargs: Any ) -> None: @@ -335,6 +337,9 @@ class TibberSensor(SensorEntity): class TibberSensorElPrice(TibberSensor): """Representation of a Tibber sensor for el price.""" + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_translation_key = "electricity_price" + def __init__(self, tibber_home: tibber.TibberHome) -> None: """Initialize the sensor.""" super().__init__(tibber_home=tibber_home) @@ -355,8 +360,6 @@ class TibberSensorElPrice(TibberSensor): "off_peak_2": None, } self._attr_icon = ICON - self._attr_name = f"Electricity price {self._home_name}" - self._attr_state_class = SensorStateClass.MEASUREMENT self._attr_unique_id = self._tibber_home.home_id self._model = "Price Sensor" @@ -424,7 +427,6 @@ class TibberDataSensor(TibberSensor, CoordinatorEntity["TibberDataCoordinator"]) self._attr_unique_id = ( f"{self._tibber_home.home_id}_{self.entity_description.key}" ) - self._attr_name = f"{entity_description.name} {self._home_name}" if entity_description.key == "month_cost": self._attr_native_unit_of_measurement = self._tibber_home.currency @@ -452,7 +454,6 @@ class TibberSensorRT(TibberSensor, CoordinatorEntity["TibberRtDataCoordinator"]) self._model = "Tibber Pulse" self._device_name = f"{self._model} {self._home_name}" - self._attr_name = f"{description.name} {self._home_name}" self._attr_native_value = initial_state self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{description.name}" diff --git a/homeassistant/components/tibber/strings.json b/homeassistant/components/tibber/strings.json index c7cef9f4657..af14c96674d 100644 --- a/homeassistant/components/tibber/strings.json +++ b/homeassistant/components/tibber/strings.json @@ -1,4 +1,89 @@ { + "entity": { + "sensor": { + "electricity_price": { + "name": "Electricity price" + }, + "month_cost": { + "name": "Monthly cost" + }, + "peak_hour": { + "name": "Monthly peak hour consumption" + }, + "peak_hour_time": { + "name": "Time of max hour consumption" + }, + "month_cons": { + "name": "Monthly net consumption" + }, + "average_power": { + "name": "Average power" + }, + "power": { + "name": "Power" + }, + "power_production": { + "name": "Power production" + }, + "min_power": { + "name": "Min power" + }, + "max_power": { + "name": "Max power" + }, + "accumulated_consumption": { + "name": "Accumulated consumption" + }, + "accumulated_consumption_last_hour": { + "name": "Accumulated consumption current hour" + }, + "estimated_hour_consumption": { + "name": "Estimated consumption current hour" + }, + "accumulated_production": { + "name": "Accumulated production" + }, + "accumulated_production_last_hour": { + "name": "Accumulated production current hour" + }, + "last_meter_consumption": { + "name": "Last meter consumption" + }, + "last_meter_production": { + "name": "Last meter production" + }, + "voltage_phase1": { + "name": "Voltage phase1" + }, + "voltage_phase2": { + "name": "Voltage phase2" + }, + "voltage_phase3": { + "name": "Voltage phase3" + }, + "current_l1": { + "name": "Current L1" + }, + "current_l2": { + "name": "Current L2" + }, + "current_l3": { + "name": "Current L3" + }, + "signal_strength": { + "name": "Signal strength" + }, + "accumulated_reward": { + "name": "Accumulated reward" + }, + "accumulated_cost": { + "name": "Accumulated cost" + }, + "power_factor": { + "name": "Power factor" + } + } + }, "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" From 343086a6c835fa19340c4ae7cb774323e4a520d3 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 2 Feb 2024 14:12:26 +0100 Subject: [PATCH 0076/1367] Improve Ecovacs naming (#109372) --- homeassistant/components/ecovacs/strings.json | 16 +- .../ecovacs/snapshots/test_button.ambr | 98 +++++----- .../ecovacs/snapshots/test_select.ambr | 12 +- .../ecovacs/snapshots/test_sensor.ambr | 180 +++++++++--------- .../ecovacs/snapshots/test_switch.ambr | 12 +- tests/components/ecovacs/test_button.py | 13 +- tests/components/ecovacs/test_select.py | 6 +- tests/components/ecovacs/test_sensor.py | 18 +- tests/components/ecovacs/test_switch.py | 8 +- 9 files changed, 182 insertions(+), 181 deletions(-) diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index f56b65a4e46..7a456483877 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -47,13 +47,13 @@ "name": "Relocate" }, "reset_lifespan_brush": { - "name": "Reset brush lifespan" + "name": "Reset main brush lifespan" }, "reset_lifespan_filter": { "name": "Reset filter lifespan" }, "reset_lifespan_side_brush": { - "name": "Reset side brush lifespan" + "name": "Reset side brushes lifespan" } }, "image": { @@ -79,13 +79,13 @@ } }, "lifespan_brush": { - "name": "Brush lifespan" + "name": "Main brush lifespan" }, "lifespan_filter": { "name": "Filter lifespan" }, "lifespan_side_brush": { - "name": "Side brush lifespan" + "name": "Side brushes lifespan" }, "network_ip": { "name": "IP address" @@ -100,7 +100,7 @@ "name": "Area cleaned" }, "stats_time": { - "name": "Time cleaned" + "name": "Cleaning duration" }, "total_stats_area": { "name": "Total area cleaned" @@ -109,12 +109,12 @@ "name": "Total cleanings" }, "total_stats_time": { - "name": "Total time cleaned" + "name": "Total cleaning duration" } }, "select": { "water_amount": { - "name": "Water amount", + "name": "Water flow level", "state": { "high": "High", "low": "Low", @@ -137,7 +137,7 @@ "name": "Advanced mode" }, "carpet_auto_fan_boost": { - "name": "Carpet auto fan speed boost" + "name": "Carpet auto-boost suction" }, "clean_preference": { "name": "Clean preference" diff --git a/tests/components/ecovacs/snapshots/test_button.ambr b/tests/components/ecovacs/snapshots/test_button.ambr index ca61d16602a..45b7ef1cc51 100644 --- a/tests/components/ecovacs/snapshots/test_button.ambr +++ b/tests/components/ecovacs/snapshots/test_button.ambr @@ -42,49 +42,6 @@ 'state': '2024-01-01T00:00:00+00:00', }) # --- -# name: test_buttons[yna5x1][button.ozmo_950_reset_brush_lifespan:entity-registry] - 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.ozmo_950_reset_brush_lifespan', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Reset brush lifespan', - 'platform': 'ecovacs', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'reset_lifespan_brush', - 'unique_id': 'E1234567890000000001_reset_lifespan_brush', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[yna5x1][button.ozmo_950_reset_brush_lifespan:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ozmo 950 Reset brush lifespan', - }), - 'context': , - 'entity_id': 'button.ozmo_950_reset_brush_lifespan', - 'last_changed': , - 'last_updated': , - 'state': '2024-01-01T00:00:00+00:00', - }) -# --- # name: test_buttons[yna5x1][button.ozmo_950_reset_filter_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -128,7 +85,7 @@ 'state': '2024-01-01T00:00:00+00:00', }) # --- -# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brush_lifespan:entity-registry] +# name: test_buttons[yna5x1][button.ozmo_950_reset_main_brush_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -140,7 +97,7 @@ 'disabled_by': None, 'domain': 'button', 'entity_category': , - 'entity_id': 'button.ozmo_950_reset_side_brush_lifespan', + 'entity_id': 'button.ozmo_950_reset_main_brush_lifespan', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -150,7 +107,50 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Reset side brush lifespan', + 'original_name': 'Reset main brush lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reset_lifespan_brush', + 'unique_id': 'E1234567890000000001_reset_lifespan_brush', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[yna5x1][button.ozmo_950_reset_main_brush_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ozmo 950 Reset main brush lifespan', + }), + 'context': , + 'entity_id': 'button.ozmo_950_reset_main_brush_lifespan', + 'last_changed': , + 'last_updated': , + 'state': '2024-01-01T00:00:00+00:00', + }) +# --- +# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brushes_lifespan:entity-registry] + 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.ozmo_950_reset_side_brushes_lifespan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset side brushes lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': 0, @@ -159,13 +159,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brush_lifespan:state] +# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brushes_lifespan:state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ozmo 950 Reset side brush lifespan', + 'friendly_name': 'Ozmo 950 Reset side brushes lifespan', }), 'context': , - 'entity_id': 'button.ozmo_950_reset_side_brush_lifespan', + 'entity_id': 'button.ozmo_950_reset_side_brushes_lifespan', 'last_changed': , 'last_updated': , 'state': '2024-01-01T00:00:00+00:00', diff --git a/tests/components/ecovacs/snapshots/test_select.ambr b/tests/components/ecovacs/snapshots/test_select.ambr index abf37a17256..4b01d448fd8 100644 --- a/tests/components/ecovacs/snapshots/test_select.ambr +++ b/tests/components/ecovacs/snapshots/test_select.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_selects[yna5x1-entity_ids0][select.ozmo_950_water_amount:entity-registry] +# name: test_selects[yna5x1-entity_ids0][select.ozmo_950_water_flow_level:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -18,7 +18,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.ozmo_950_water_amount', + 'entity_id': 'select.ozmo_950_water_flow_level', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -28,7 +28,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Water amount', + 'original_name': 'Water flow level', 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': 0, @@ -37,10 +37,10 @@ 'unit_of_measurement': None, }) # --- -# name: test_selects[yna5x1-entity_ids0][select.ozmo_950_water_amount:state] +# name: test_selects[yna5x1-entity_ids0][select.ozmo_950_water_flow_level:state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ozmo 950 Water amount', + 'friendly_name': 'Ozmo 950 Water flow level', 'options': list([ 'low', 'medium', @@ -49,7 +49,7 @@ ]), }), 'context': , - 'entity_id': 'select.ozmo_950_water_amount', + 'entity_id': 'select.ozmo_950_water_flow_level', 'last_changed': , 'last_updated': , 'state': 'ultrahigh', diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index 3a59b3ba418..f07722afb53 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -88,7 +88,7 @@ 'state': '100', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_brush_lifespan:entity-registry] +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_cleaning_duration:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -99,37 +99,41 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.ozmo_950_brush_lifespan', + 'entity_category': None, + 'entity_id': 'sensor.ozmo_950_cleaning_duration', 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Brush lifespan', + 'original_name': 'Cleaning duration', 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'lifespan_brush', - 'unique_id': 'E1234567890000000001_lifespan_brush', - 'unit_of_measurement': '%', + 'translation_key': 'stats_time', + 'unique_id': 'E1234567890000000001_stats_time', + 'unit_of_measurement': , }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_brush_lifespan:state] +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_cleaning_duration:state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ozmo 950 Brush lifespan', - 'unit_of_measurement': '%', + 'device_class': 'duration', + 'friendly_name': 'Ozmo 950 Cleaning duration', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.ozmo_950_brush_lifespan', + 'entity_id': 'sensor.ozmo_950_cleaning_duration', 'last_changed': , 'last_updated': , - 'state': '80', + 'state': '5.0', }) # --- # name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_error:entity-registry] @@ -263,7 +267,7 @@ 'state': '192.168.0.10', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brush_lifespan:entity-registry] +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_main_brush_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -275,7 +279,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.ozmo_950_side_brush_lifespan', + 'entity_id': 'sensor.ozmo_950_main_brush_lifespan', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -285,29 +289,29 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Side brush lifespan', + 'original_name': 'Main brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'lifespan_side_brush', - 'unique_id': 'E1234567890000000001_lifespan_side_brush', + 'translation_key': 'lifespan_brush', + 'unique_id': 'E1234567890000000001_lifespan_brush', 'unit_of_measurement': '%', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brush_lifespan:state] +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_main_brush_lifespan:state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ozmo 950 Side brush lifespan', + 'friendly_name': 'Ozmo 950 Main brush lifespan', 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.ozmo_950_side_brush_lifespan', + 'entity_id': 'sensor.ozmo_950_main_brush_lifespan', 'last_changed': , 'last_updated': , - 'state': '40', + 'state': '80', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_time_cleaned:entity-registry] +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brushes_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -318,41 +322,37 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.ozmo_950_time_cleaned', + 'entity_category': , + 'entity_id': 'sensor.ozmo_950_side_brushes_lifespan', 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , 'name': None, 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': 'Time cleaned', + 'original_name': 'Side brushes lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'stats_time', - 'unique_id': 'E1234567890000000001_stats_time', - 'unit_of_measurement': , + 'translation_key': 'lifespan_side_brush', + 'unique_id': 'E1234567890000000001_lifespan_side_brush', + 'unit_of_measurement': '%', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_time_cleaned:state] +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brushes_lifespan:state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'Ozmo 950 Time cleaned', - 'unit_of_measurement': , + 'friendly_name': 'Ozmo 950 Side brushes lifespan', + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.ozmo_950_time_cleaned', + 'entity_id': 'sensor.ozmo_950_side_brushes_lifespan', 'last_changed': , 'last_updated': , - 'state': '5.0', + 'state': '40', }) # --- # name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_area_cleaned:entity-registry] @@ -402,6 +402,57 @@ 'state': '60', }) # --- +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleaning_duration:entity-registry] + 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.ozmo_950_total_cleaning_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total cleaning duration', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_stats_time', + 'unique_id': 'E1234567890000000001_total_stats_time', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleaning_duration:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Ozmo 950 Total cleaning duration', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ozmo_950_total_cleaning_duration', + 'last_changed': , + 'last_updated': , + 'state': '40.000', + }) +# --- # name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleanings:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -448,57 +499,6 @@ 'state': '123', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_time_cleaned:entity-registry] - 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.ozmo_950_total_time_cleaned', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total time cleaned', - 'platform': 'ecovacs', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_stats_time', - 'unique_id': 'E1234567890000000001_total_stats_time', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_time_cleaned:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'Ozmo 950 Total time cleaned', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.ozmo_950_total_time_cleaned', - 'last_changed': , - 'last_updated': , - 'state': '40.000', - }) -# --- # name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_wi_fi_rssi:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/ecovacs/snapshots/test_switch.ambr b/tests/components/ecovacs/snapshots/test_switch.ambr index 75441c4f918..c645502a831 100644 --- a/tests/components/ecovacs/snapshots/test_switch.ambr +++ b/tests/components/ecovacs/snapshots/test_switch.ambr @@ -42,7 +42,7 @@ 'state': 'on', }) # --- -# name: test_switch_entities[yna5x1][switch.ozmo_950_carpet_auto_fan_speed_boost:entity-registry] +# name: test_switch_entities[yna5x1][switch.ozmo_950_carpet_auto_boost_suction:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -54,7 +54,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.ozmo_950_carpet_auto_fan_speed_boost', + 'entity_id': 'switch.ozmo_950_carpet_auto_boost_suction', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -64,7 +64,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Carpet auto fan speed boost', + 'original_name': 'Carpet auto-boost suction', 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': 0, @@ -73,13 +73,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch_entities[yna5x1][switch.ozmo_950_carpet_auto_fan_speed_boost:state] +# name: test_switch_entities[yna5x1][switch.ozmo_950_carpet_auto_boost_suction:state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ozmo 950 Carpet auto fan speed boost', + 'friendly_name': 'Ozmo 950 Carpet auto-boost suction', }), 'context': , - 'entity_id': 'switch.ozmo_950_carpet_auto_fan_speed_boost', + 'entity_id': 'switch.ozmo_950_carpet_auto_boost_suction', 'last_changed': , 'last_updated': , 'state': 'on', diff --git a/tests/components/ecovacs/test_button.py b/tests/components/ecovacs/test_button.py index f804e813256..24c926b1f77 100644 --- a/tests/components/ecovacs/test_button.py +++ b/tests/components/ecovacs/test_button.py @@ -33,13 +33,16 @@ def platforms() -> Platform | list[Platform]: "yna5x1", [ ("button.ozmo_950_relocate", SetRelocationState()), - ("button.ozmo_950_reset_brush_lifespan", ResetLifeSpan(LifeSpan.BRUSH)), + ( + "button.ozmo_950_reset_main_brush_lifespan", + ResetLifeSpan(LifeSpan.BRUSH), + ), ( "button.ozmo_950_reset_filter_lifespan", ResetLifeSpan(LifeSpan.FILTER), ), ( - "button.ozmo_950_reset_side_brush_lifespan", + "button.ozmo_950_reset_side_brushes_lifespan", ResetLifeSpan(LifeSpan.SIDE_BRUSH), ), ], @@ -56,7 +59,7 @@ async def test_buttons( entities: list[tuple[str, Command]], ) -> None: """Test that sensor entity snapshots match.""" - assert sorted(hass.states.async_entity_ids()) == [e[0] for e in entities] + assert hass.states.async_entity_ids() == [e[0] for e in entities] device = controller.devices[0] for entity_id, command in entities: assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" @@ -89,9 +92,9 @@ async def test_buttons( ( "yna5x1", [ - "button.ozmo_950_reset_brush_lifespan", + "button.ozmo_950_reset_main_brush_lifespan", "button.ozmo_950_reset_filter_lifespan", - "button.ozmo_950_reset_side_brush_lifespan", + "button.ozmo_950_reset_side_brushes_lifespan", ], ), ], diff --git a/tests/components/ecovacs/test_select.py b/tests/components/ecovacs/test_select.py index cfe34c5a7a6..0d1a5d19116 100644 --- a/tests/components/ecovacs/test_select.py +++ b/tests/components/ecovacs/test_select.py @@ -44,7 +44,7 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus): ( "yna5x1", [ - "select.ozmo_950_water_amount", + "select.ozmo_950_water_flow_level", ], ), ], @@ -58,7 +58,7 @@ async def test_selects( entity_ids: list[str], ) -> None: """Test that select entity snapshots match.""" - assert entity_ids == sorted(hass.states.async_entity_ids()) + assert entity_ids == hass.states.async_entity_ids() for entity_id in entity_ids: assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" assert state.state == STATE_UNKNOWN @@ -83,7 +83,7 @@ async def test_selects( [ ( "yna5x1", - "select.ozmo_950_water_amount", + "select.ozmo_950_water_flow_level", "ultrahigh", "low", SetWaterInfo(WaterAmount.LOW), diff --git a/tests/components/ecovacs/test_sensor.py b/tests/components/ecovacs/test_sensor.py index 18d65349fa2..78755668f0f 100644 --- a/tests/components/ecovacs/test_sensor.py +++ b/tests/components/ecovacs/test_sensor.py @@ -54,18 +54,18 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus): "yna5x1", [ "sensor.ozmo_950_area_cleaned", - "sensor.ozmo_950_battery", - "sensor.ozmo_950_brush_lifespan", - "sensor.ozmo_950_error", - "sensor.ozmo_950_filter_lifespan", - "sensor.ozmo_950_ip_address", - "sensor.ozmo_950_side_brush_lifespan", - "sensor.ozmo_950_time_cleaned", + "sensor.ozmo_950_cleaning_duration", "sensor.ozmo_950_total_area_cleaned", + "sensor.ozmo_950_total_cleaning_duration", "sensor.ozmo_950_total_cleanings", - "sensor.ozmo_950_total_time_cleaned", + "sensor.ozmo_950_battery", + "sensor.ozmo_950_ip_address", "sensor.ozmo_950_wi_fi_rssi", "sensor.ozmo_950_wi_fi_ssid", + "sensor.ozmo_950_main_brush_lifespan", + "sensor.ozmo_950_filter_lifespan", + "sensor.ozmo_950_side_brushes_lifespan", + "sensor.ozmo_950_error", ], ), ], @@ -79,7 +79,7 @@ async def test_sensors( entity_ids: list[str], ) -> None: """Test that sensor entity snapshots match.""" - assert entity_ids == sorted(hass.states.async_entity_ids()) + assert entity_ids == hass.states.async_entity_ids() for entity_id in entity_ids: assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" assert state.state == STATE_UNKNOWN diff --git a/tests/components/ecovacs/test_switch.py b/tests/components/ecovacs/test_switch.py index 43c5d25e18f..35d2f487b95 100644 --- a/tests/components/ecovacs/test_switch.py +++ b/tests/components/ecovacs/test_switch.py @@ -69,7 +69,7 @@ class SwitchTestCase: SetContinuousCleaning, ), SwitchTestCase( - "switch.ozmo_950_carpet_auto_fan_speed_boost", + "switch.ozmo_950_carpet_auto_boost_suction", CarpetAutoFanBoostEvent(True), SetCarpetAutoFanBoost, ), @@ -90,9 +90,7 @@ async def test_switch_entities( device = controller.devices[0] event_bus = device.events - assert sorted(hass.states.async_entity_ids()) == sorted( - test.entity_id for test in tests - ) + assert hass.states.async_entity_ids() == [test.entity_id for test in tests] for test_case in tests: entity_id = test_case.entity_id assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" @@ -139,7 +137,7 @@ async def test_switch_entities( [ "switch.ozmo_950_advanced_mode", "switch.ozmo_950_continuous_cleaning", - "switch.ozmo_950_carpet_auto_fan_speed_boost", + "switch.ozmo_950_carpet_auto_boost_suction", ], ), ], From 0c3541c194b09d6deea02691756d673916c25139 Mon Sep 17 00:00:00 2001 From: Jan Rieger Date: Fri, 2 Feb 2024 14:44:50 +0100 Subject: [PATCH 0077/1367] Add entity description to GPSD (#109320) --- homeassistant/components/gpsd/sensor.py | 46 +++++++++++++++++++------ 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index d5d25397f2a..135d9c6c28f 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -1,6 +1,8 @@ -"""Support for GPSD.""" +"""Sensor platform for GPSD integration.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass import logging from typing import Any @@ -15,6 +17,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, + SensorEntityDescription, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -24,6 +27,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, + EntityCategory, ) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv @@ -43,6 +47,28 @@ ATTR_SPEED = "speed" DEFAULT_NAME = "GPS" +_MODE_VALUES = {2: "2d_fix", 3: "3d_fix"} + + +@dataclass(frozen=True, kw_only=True) +class GpsdSensorDescription(SensorEntityDescription): + """Class describing GPSD sensor entities.""" + + value_fn: Callable[[AGPS3mechanism], str | None] + + +SENSOR_TYPES: tuple[GpsdSensorDescription, ...] = ( + GpsdSensorDescription( + key="mode", + translation_key="mode", + name=None, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + options=list(_MODE_VALUES.values()), + value_fn=lambda agps_thread: _MODE_VALUES.get(agps_thread.data_stream.mode), + ), +) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -64,7 +90,9 @@ async def async_setup_entry( config_entry.data[CONF_HOST], config_entry.data[CONF_PORT], config_entry.entry_id, + description, ) + for description in SENSOR_TYPES ] ) @@ -101,23 +129,23 @@ class GpsdSensor(SensorEntity): """Representation of a GPS receiver available via GPSD.""" _attr_has_entity_name = True - _attr_name = None - _attr_translation_key = "mode" - _attr_device_class = SensorDeviceClass.ENUM - _attr_options = ["2d_fix", "3d_fix"] + + entity_description: GpsdSensorDescription def __init__( self, host: str, port: int, unique_id: str, + description: GpsdSensorDescription, ) -> None: """Initialize the GPSD sensor.""" + self.entity_description = description self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id)}, entry_type=DeviceEntryType.SERVICE, ) - self._attr_unique_id = f"{unique_id}-mode" + self._attr_unique_id = f"{unique_id}-{self.entity_description.key}" self.agps_thread = AGPS3mechanism() self.agps_thread.stream_data(host=host, port=port) @@ -126,11 +154,7 @@ class GpsdSensor(SensorEntity): @property def native_value(self) -> str | None: """Return the state of GPSD.""" - if self.agps_thread.data_stream.mode == 3: - return "3d_fix" - if self.agps_thread.data_stream.mode == 2: - return "2d_fix" - return None + return self.entity_description.value_fn(self.agps_thread) @property def extra_state_attributes(self) -> dict[str, Any]: From 9d22f07fc69e30673483f86750063d8b69202a89 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Feb 2024 15:46:59 +0100 Subject: [PATCH 0078/1367] Use send_json_auto_id in conversation tests (#109354) --- tests/components/conversation/test_init.py | 10 +++------- tests/components/conversation/test_trigger.py | 3 +-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 58e94d27aac..61712761250 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -733,7 +733,7 @@ async def test_ws_api( assert await async_setup_component(hass, "conversation", {}) client = await hass_ws_client(hass) - await client.send_json({"id": 5, "type": "conversation/process", **payload}) + await client.send_json_auto_id({"type": "conversation/process", **payload}) msg = await client.receive_json() @@ -757,18 +757,14 @@ async def test_ws_prepare( client = await hass_ws_client(hass) - msg = { - "id": 5, - "type": "conversation/prepare", - } + msg = {"type": "conversation/prepare"} if agent_id is not None: msg["agent_id"] = agent_id - await client.send_json(msg) + await client.send_json_auto_id(msg) msg = await client.receive_json() assert msg["success"] - assert msg["id"] == 5 # Intents should now be load assert agent._lang_intents.get(hass.config.language) diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index 26626a04079..74df1b7f8a6 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -105,9 +105,8 @@ async def test_subscribe_trigger_does_not_interfere_with_responses( ) -> None: """Test that subscribing to a trigger from the websocket API does not interfere with responses.""" websocket_client = await hass_ws_client() - await websocket_client.send_json( + await websocket_client.send_json_auto_id( { - "id": 5, "type": "subscribe_trigger", "trigger": {"platform": "conversation", "command": ["test sentence"]}, } From 6b7a9843141cb9ee780a0b7113f7cae5fd2b69b8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 Feb 2024 17:30:07 +0100 Subject: [PATCH 0079/1367] Update frontend to 20240202.0 (#109388) --- 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 2b005c7e1ad..039328b9cac 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==20240131.0"] + "requirements": ["home-assistant-frontend==20240202.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fbb072db9c0..950cae8b322 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ habluetooth==2.4.0 hass-nabucasa==0.76.0 hassil==1.6.0 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240131.0 +home-assistant-frontend==20240202.0 home-assistant-intents==2024.2.1 httpx==0.26.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2b1f9f96404..9b386af7103 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1059,7 +1059,7 @@ hole==0.8.0 holidays==0.41 # homeassistant.components.frontend -home-assistant-frontend==20240131.0 +home-assistant-frontend==20240202.0 # homeassistant.components.conversation home-assistant-intents==2024.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c09602bf8c..80077ef43d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -855,7 +855,7 @@ hole==0.8.0 holidays==0.41 # homeassistant.components.frontend -home-assistant-frontend==20240131.0 +home-assistant-frontend==20240202.0 # homeassistant.components.conversation home-assistant-intents==2024.2.1 From 7608f0c9ee0f9aa6aa383d0400368e940ba3503e Mon Sep 17 00:00:00 2001 From: mkmer Date: Fri, 2 Feb 2024 11:31:16 -0500 Subject: [PATCH 0080/1367] Add independent session in honeywell (#108435) --- .../components/honeywell/__init__.py | 15 ++++++++++----- tests/components/honeywell/conftest.py | 19 +++++++++++++++++++ tests/components/honeywell/test_init.py | 16 ++++++++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index c79d99276b1..baabf4ca4d8 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -8,7 +8,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.aiohttp_client import ( + async_create_clientsession, + async_get_clientsession, +) from .const import ( _LOGGER, @@ -48,9 +51,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b username = config_entry.data[CONF_USERNAME] password = config_entry.data[CONF_PASSWORD] - client = aiosomecomfort.AIOSomeComfort( - username, password, session=async_get_clientsession(hass) - ) + if len(hass.config_entries.async_entries(DOMAIN)) > 1: + session = async_create_clientsession(hass) + else: + session = async_get_clientsession(hass) + + client = aiosomecomfort.AIOSomeComfort(username, password, session=session) try: await client.login() await client.discover() @@ -76,7 +82,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if len(devices) == 0: _LOGGER.debug("No devices found") return False - data = HoneywellData(config_entry.entry_id, client, devices) hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = data await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) diff --git a/tests/components/honeywell/conftest.py b/tests/components/honeywell/conftest.py index 876050586d2..5c5b6c0a44a 100644 --- a/tests/components/honeywell/conftest.py +++ b/tests/components/honeywell/conftest.py @@ -39,6 +39,15 @@ def config_data(): } +@pytest.fixture +def another_config_data(): + """Provide configuration data for tests.""" + return { + CONF_USERNAME: "user2", + CONF_PASSWORD: "fake2", + } + + @pytest.fixture def config_options(): """Provide configuratio options for test.""" @@ -55,6 +64,16 @@ def config_entry(config_data, config_options): ) +@pytest.fixture +def config_entry2(another_config_data, config_options): + """Create a mock config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data=another_config_data, + options=config_options, + ) + + @pytest.fixture def device(): """Mock a somecomfort.Device.""" diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index ccfc2c5d264..98578217af6 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -33,6 +33,22 @@ async def test_setup_entry(hass: HomeAssistant, config_entry: MockConfigEntry) - ) # 1 climate entity; 2 sensor entities +@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) +async def test_setup_multiple_entry( + hass: HomeAssistant, config_entry: MockConfigEntry, config_entry2: MockConfigEntry +) -> None: + """Initialize the config entry.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + config_entry2.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry2.entry_id) + await hass.async_block_till_done() + assert config_entry2.state is ConfigEntryState.LOADED + + async def test_setup_multiple_thermostats( hass: HomeAssistant, config_entry: MockConfigEntry, From 2c3a952ef8831e1e6ecbc6fd055b5781e29ebb92 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Feb 2024 20:34:00 +0100 Subject: [PATCH 0081/1367] Update elgato to 5.1.2 (#109391) --- homeassistant/components/elgato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json index 0671a7adb1d..c68902560b9 100644 --- a/homeassistant/components/elgato/manifest.json +++ b/homeassistant/components/elgato/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["elgato==5.1.1"], + "requirements": ["elgato==5.1.2"], "zeroconf": ["_elg._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 9b386af7103..db70d4836bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -758,7 +758,7 @@ ecoaliface==0.4.0 electrickiwi-api==0.8.5 # homeassistant.components.elgato -elgato==5.1.1 +elgato==5.1.2 # homeassistant.components.eliqonline eliqonline==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80077ef43d5..4f1f5b76775 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -618,7 +618,7 @@ easyenergy==2.1.0 electrickiwi-api==0.8.5 # homeassistant.components.elgato -elgato==5.1.1 +elgato==5.1.2 # homeassistant.components.elkm1 elkm1-lib==2.2.6 From e61864c0b577e33a0b3d48f40b4a8832b6ebd4ac Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 2 Feb 2024 21:24:24 +0100 Subject: [PATCH 0082/1367] Bump python-kasa to 0.6.2.1 (#109397) --- 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 15748e83737..a479314d649 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -205,5 +205,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.6.2"] + "requirements": ["python-kasa[speedups]==0.6.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index db70d4836bc..dfa5279a62a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2232,7 +2232,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.6.2 +python-kasa[speedups]==0.6.2.1 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f1f5b76775..aabc7fe1eb0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1708,7 +1708,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.6.2 +python-kasa[speedups]==0.6.2.1 # homeassistant.components.matter python-matter-server==5.4.0 From 430b9cef439cafe37a83341adcc66282a9c5e7dc Mon Sep 17 00:00:00 2001 From: wilburCforce <109390391+wilburCforce@users.noreply.github.com> Date: Fri, 2 Feb 2024 14:42:53 -0600 Subject: [PATCH 0083/1367] Fix device type in Lutron (#109401) remove testing code --- homeassistant/components/lutron/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index d728cfac890..0bd00177cc1 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -42,7 +42,7 @@ async def async_setup_entry( lights = [] for area_name, device in entry_data.lights: - if device.type == "CEILING_FAN_TYPE2": + if device.type == "CEILING_FAN_TYPE": # If this is a fan, check to see if this entity already exists. # If not, do not create a new one. entity_id = ent_reg.async_get_entity_id( From ae5d4e183a546aa3d3d6bf02fbd7b15e6ccf87a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Feb 2024 14:52:09 -0600 Subject: [PATCH 0084/1367] Remove remaning ESPHome files from coveragerc (#109400) --- .coveragerc | 1 - 1 file changed, 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index bcd4e349668..fa0bf2fbd4c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -359,7 +359,6 @@ omit = homeassistant/components/escea/__init__.py homeassistant/components/escea/climate.py homeassistant/components/escea/discovery.py - homeassistant/components/esphome/manager.py homeassistant/components/etherscan/sensor.py homeassistant/components/eufy/* homeassistant/components/eufylife_ble/__init__.py From 09ba46ddb9e577c35eab6634d90ca8d377577cb4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Feb 2024 22:10:30 +0100 Subject: [PATCH 0085/1367] Mask sensitive data in google_assistant logs (#109366) * Mask sensitive data in google_assistant logs * Move common code to homeassistant/util/redact.py * Move to helpers * Add tests * Tweak * Redact additional logs * Fix stale docstring * Don't reveal the length of masked data * Update test --- .../google_assistant/data_redaction.py | 36 +++++++ .../components/google_assistant/helpers.py | 25 +++-- .../components/google_assistant/smart_home.py | 13 ++- homeassistant/helpers/redact.py | 75 +++++++++++++++ .../components/google_assistant/test_http.py | 2 +- tests/helpers/test_redact.py | 94 +++++++++++++++++++ 6 files changed, 232 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/google_assistant/data_redaction.py create mode 100644 homeassistant/helpers/redact.py create mode 100644 tests/helpers/test_redact.py diff --git a/homeassistant/components/google_assistant/data_redaction.py b/homeassistant/components/google_assistant/data_redaction.py new file mode 100644 index 00000000000..ae6fe5f7098 --- /dev/null +++ b/homeassistant/components/google_assistant/data_redaction.py @@ -0,0 +1,36 @@ +"""Helpers to redact Google Assistant data when logging.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +from homeassistant.core import callback +from homeassistant.helpers.redact import async_redact_data, partial_redact + +REQUEST_MSG_TO_REDACT: dict[str, Callable[[str], str]] = { + "agentUserId": partial_redact, + "uuid": partial_redact, + "webhookId": partial_redact, +} + +RESPONSE_MSG_TO_REDACT = REQUEST_MSG_TO_REDACT | {id: partial_redact} + +SYNC_MSG_TO_REDACT = REQUEST_MSG_TO_REDACT + + +@callback +def async_redact_request_msg(msg: dict[str, Any]) -> dict[str, Any]: + """Mask sensitive data in message.""" + return async_redact_data(msg, REQUEST_MSG_TO_REDACT) + + +@callback +def async_redact_response_msg(msg: dict[str, Any]) -> dict[str, Any]: + """Mask sensitive data in message.""" + return async_redact_data(msg, RESPONSE_MSG_TO_REDACT) + + +@callback +def async_redact_sync_msg(msg: dict[str, Any]) -> dict[str, Any]: + """Mask sensitive data in message.""" + return async_redact_data(msg, SYNC_MSG_TO_REDACT) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index f3d0d24f7c8..d75ebb49509 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -32,6 +32,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.event import async_call_later from homeassistant.helpers.network import get_url +from homeassistant.helpers.redact import partial_redact from homeassistant.helpers.storage import Store from homeassistant.util.dt import utcnow @@ -48,6 +49,7 @@ from .const import ( STORE_AGENT_USER_IDS, STORE_GOOGLE_LOCAL_WEBHOOK_ID, ) +from .data_redaction import async_redact_request_msg, async_redact_response_msg from .error import SmartHomeError SYNC_DELAY = 15 @@ -332,8 +334,8 @@ class AbstractConfig(ABC): _LOGGER.debug( "Register webhook handler %s for agent user id %s", - webhook_id, - user_agent_id, + partial_redact(webhook_id), + partial_redact(user_agent_id), ) try: webhook.async_register( @@ -348,8 +350,8 @@ class AbstractConfig(ABC): except ValueError: _LOGGER.warning( "Webhook handler %s for agent user id %s is already defined!", - webhook_id, - user_agent_id, + partial_redact(webhook_id), + partial_redact(user_agent_id), ) setup_successful = False break @@ -374,8 +376,8 @@ class AbstractConfig(ABC): webhook_id = self.get_local_webhook_id(agent_user_id) _LOGGER.debug( "Unregister webhook handler %s for agent user id %s", - webhook_id, - agent_user_id, + partial_redact(webhook_id), + partial_redact(agent_user_id), ) webhook.async_unregister(self.hass, webhook_id) @@ -410,7 +412,7 @@ class AbstractConfig(ABC): "Received local message from %s (JS %s):\n%s\n", request.remote, request.headers.get("HA-Cloud-Version", "unknown"), - pprint.pformat(payload), + pprint.pformat(async_redact_request_msg(payload)), ) if (agent_user_id := self.get_local_agent_user_id(webhook_id)) is None: @@ -421,8 +423,8 @@ class AbstractConfig(ABC): "Cannot process request for webhook %s as no linked agent user is" " found:\n%s\n" ), - webhook_id, - pprint.pformat(payload), + partial_redact(webhook_id), + pprint.pformat(async_redact_request_msg(payload)), ) webhook.async_unregister(self.hass, webhook_id) return None @@ -441,7 +443,10 @@ class AbstractConfig(ABC): ) if _LOGGER.isEnabledFor(logging.DEBUG): - _LOGGER.debug("Responding to local message:\n%s\n", pprint.pformat(result)) + _LOGGER.debug( + "Responding to local message:\n%s\n", + pprint.pformat(async_redact_response_msg(result)), + ) return json_response(result) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index b8c57812540..7d8cc752342 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -18,6 +18,11 @@ from .const import ( EVENT_QUERY_RECEIVED, EVENT_SYNC_RECEIVED, ) +from .data_redaction import ( + async_redact_request_msg, + async_redact_response_msg, + async_redact_sync_msg, +) from .error import SmartHomeError from .helpers import GoogleEntity, RequestData, async_get_entities @@ -42,7 +47,11 @@ async def async_handle_message(hass, config, user_id, message, source): response = await _process(hass, data, message) if response and "errorCode" in response["payload"]: - _LOGGER.error("Error handling message %s: %s", message, response["payload"]) + _LOGGER.error( + "Error handling message %s: %s", + async_redact_request_msg(message), + async_redact_response_msg(response["payload"]), + ) return response @@ -118,7 +127,7 @@ async def async_devices_sync( devices = await async_devices_sync_response(hass, data.config, agent_user_id) response = create_sync_response(agent_user_id, devices) - _LOGGER.debug("Syncing entities response: %s", response) + _LOGGER.debug("Syncing entities response: %s", async_redact_sync_msg(response)) return response diff --git a/homeassistant/helpers/redact.py b/homeassistant/helpers/redact.py new file mode 100644 index 00000000000..f8df73b9180 --- /dev/null +++ b/homeassistant/helpers/redact.py @@ -0,0 +1,75 @@ +"""Helpers to redact sensitive data.""" +from __future__ import annotations + +from collections.abc import Callable, Iterable, Mapping +from typing import Any, TypeVar, cast, overload + +from homeassistant.core import callback + +REDACTED = "**REDACTED**" + +_T = TypeVar("_T") +_ValueT = TypeVar("_ValueT") + + +def partial_redact( + x: str | Any, unmasked_prefix: int = 4, unmasked_suffix: int = 4 +) -> str: + """Mask part of a string with *.""" + if not isinstance(x, str): + return REDACTED + + unmasked = unmasked_prefix + unmasked_suffix + if len(x) < unmasked * 2: + return REDACTED + + if not unmasked_prefix and not unmasked_suffix: + return REDACTED + + suffix = x[-unmasked_suffix:] if unmasked_suffix else "" + return f"{x[:unmasked_prefix]}***{suffix}" + + +@overload +def async_redact_data( # type: ignore[overload-overlap] + data: Mapping, to_redact: Iterable[Any] | Mapping[Any, Callable[[_ValueT], _ValueT]] +) -> dict: + ... + + +@overload +def async_redact_data( + data: _T, to_redact: Iterable[Any] | Mapping[Any, Callable[[_ValueT], _ValueT]] +) -> _T: + ... + + +@callback +def async_redact_data( + data: _T, to_redact: Iterable[Any] | Mapping[Any, Callable[[_ValueT], _ValueT]] +) -> _T: + """Redact sensitive data in a dict.""" + if not isinstance(data, (Mapping, list)): + return data + + if isinstance(data, list): + return cast(_T, [async_redact_data(val, to_redact) for val in data]) + + redacted = {**data} + + for key, value in redacted.items(): + if value is None: + continue + if isinstance(value, str) and not value: + continue + if key in to_redact: + if isinstance(to_redact, Mapping): + redacted[key] = to_redact[key](value) + else: + redacted[key] = REDACTED + elif isinstance(value, Mapping): + redacted[key] = async_redact_data(value, to_redact) + elif isinstance(value, list): + redacted[key] = [async_redact_data(item, to_redact) for item in value] + + return cast(_T, redacted) diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index aa7f8472cab..c6589555c3e 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -466,6 +466,6 @@ async def test_async_enable_local_sdk( ) assert resp.status == HTTPStatus.OK assert ( - "Cannot process request for webhook mock_webhook_id as no linked agent user is found:" + "Cannot process request for webhook **REDACTED** as no linked agent user is found:" in caplog.text ) diff --git a/tests/helpers/test_redact.py b/tests/helpers/test_redact.py new file mode 100644 index 00000000000..73461012907 --- /dev/null +++ b/tests/helpers/test_redact.py @@ -0,0 +1,94 @@ +"""Test the data redation helper.""" +from homeassistant.helpers.redact import REDACTED, async_redact_data, partial_redact + + +def test_redact() -> None: + """Test the async_redact_data helper.""" + data = { + "key1": "value1", + "key2": ["value2_a", "value2_b"], + "key3": [["value_3a", "value_3b"], ["value_3c", "value_3d"]], + "key4": { + "key4_1": "value4_1", + "key4_2": ["value4_2a", "value4_2b"], + "key4_3": [["value4_3a", "value4_3b"], ["value4_3c", "value4_3d"]], + }, + "key5": None, + "key6": "", + "key7": False, + } + + to_redact = { + "key1", + "key3", + "key4_1", + "key5", + "key6", + "key7", + } + + assert async_redact_data(data, to_redact) == { + "key1": REDACTED, + "key2": ["value2_a", "value2_b"], + "key3": REDACTED, + "key4": { + "key4_1": REDACTED, + "key4_2": ["value4_2a", "value4_2b"], + "key4_3": [["value4_3a", "value4_3b"], ["value4_3c", "value4_3d"]], + }, + "key5": None, + "key6": "", + "key7": REDACTED, + } + + +def test_redact_custom_redact_function() -> None: + """Test the async_redact_data helper.""" + data = { + "key1": "val1val1val1val1", + "key2": ["value2_a", "value2_b"], + "key3": [ + ["val_3avalue_3avalue_3a", "value_3bvalue_3bvalue_3b"], + ["value_3cvalue_3cvalue_3c", "value_3dvalue_3dvalue_3d"], + ], + "key4": { + "key4_1": "val4_1val4_1val4_1val4_1", + "key4_2": ["value4_2a", "value4_2b"], + "key4_3": [["value4_3a", "value4_3b"], ["value4_3c", "value4_3d"]], + }, + "key5": None, + "key6": "", + "key7": False, + } + + to_redact = { + "key1": partial_redact, + "key3": partial_redact, # Value is a list, will default to REDACTED + "key4_1": partial_redact, + "key5": partial_redact, + "key6": partial_redact, + "key7": partial_redact, # Value is False, will default to REDACTED + } + + assert async_redact_data(data, to_redact) == { + "key1": "val1***val1", + "key2": ["value2_a", "value2_b"], + "key3": REDACTED, + "key4": { + "key4_1": "val4***l4_1", + "key4_2": ["value4_2a", "value4_2b"], + "key4_3": [["value4_3a", "value4_3b"], ["value4_3c", "value4_3d"]], + }, + "key5": None, + "key6": "", + "key7": REDACTED, + } + + +def test_partial_redact() -> None: + """Test the partial_redact helper.""" + assert partial_redact(None, 0, 0) == REDACTED + assert partial_redact("short_string") == REDACTED + assert partial_redact("long_enough_string") == "long***ring" + assert partial_redact("long_enough_string", 2, 2) == "lo***ng" + assert partial_redact("long_enough_string", 0, 0) == REDACTED From 480932712425415d28878a0e5dfc4ddc1b8d37c4 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 2 Feb 2024 22:42:10 +0100 Subject: [PATCH 0086/1367] Bump aiotankerkoenig to 0.3.0 (#109404) --- homeassistant/components/tankerkoenig/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tankerkoenig/manifest.json b/homeassistant/components/tankerkoenig/manifest.json index bf8896196ef..adea5b96490 100644 --- a/homeassistant/components/tankerkoenig/manifest.json +++ b/homeassistant/components/tankerkoenig/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tankerkoenig", "iot_class": "cloud_polling", "loggers": ["aiotankerkoenig"], - "requirements": ["aiotankerkoenig==0.2.0"] + "requirements": ["aiotankerkoenig==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index dfa5279a62a..13688ee9059 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -377,7 +377,7 @@ aioswitcher==3.4.1 aiosyncthing==0.5.1 # homeassistant.components.tankerkoenig -aiotankerkoenig==0.2.0 +aiotankerkoenig==0.3.0 # homeassistant.components.tractive aiotractive==0.5.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aabc7fe1eb0..8f608ee1c37 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -350,7 +350,7 @@ aioswitcher==3.4.1 aiosyncthing==0.5.1 # homeassistant.components.tankerkoenig -aiotankerkoenig==0.2.0 +aiotankerkoenig==0.3.0 # homeassistant.components.tractive aiotractive==0.5.6 From e567236cac161f126cdb5f9bd95bc2d31711463e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 2 Feb 2024 23:03:55 +0100 Subject: [PATCH 0087/1367] Add diagnostics to proximity (#109393) --- .../components/proximity/diagnostics.py | 49 +++++++++++ .../proximity/snapshots/test_diagnostics.ambr | 86 +++++++++++++++++++ .../components/proximity/test_diagnostics.py | 62 +++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 homeassistant/components/proximity/diagnostics.py create mode 100644 tests/components/proximity/snapshots/test_diagnostics.ambr create mode 100644 tests/components/proximity/test_diagnostics.py diff --git a/homeassistant/components/proximity/diagnostics.py b/homeassistant/components/proximity/diagnostics.py new file mode 100644 index 00000000000..ba5e1f53722 --- /dev/null +++ b/homeassistant/components/proximity/diagnostics.py @@ -0,0 +1,49 @@ +"""Diagnostics support for Proximity.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.device_tracker import ATTR_GPS, ATTR_IP, ATTR_MAC +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.components.person import ATTR_USER_ID +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import ProximityDataUpdateCoordinator + +TO_REDACT = { + ATTR_GPS, + ATTR_IP, + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_MAC, + ATTR_USER_ID, + "context", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: ProximityDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + diag_data = { + "entry": entry.as_dict(), + } + + tracked_states: dict[str, dict] = {} + for tracked_entity_id in coordinator.tracked_entities: + if (state := hass.states.get(tracked_entity_id)) is None: + continue + tracked_states[tracked_entity_id] = state.as_dict() + + diag_data["data"] = { + "proximity": coordinator.data.proximity, + "entities": coordinator.data.entities, + "entity_mapping": coordinator.entity_mapping, + "tracked_states": async_redact_data(tracked_states, TO_REDACT), + } + return diag_data diff --git a/tests/components/proximity/snapshots/test_diagnostics.ambr b/tests/components/proximity/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..f8f7d9b014e --- /dev/null +++ b/tests/components/proximity/snapshots/test_diagnostics.ambr @@ -0,0 +1,86 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'data': dict({ + 'entities': dict({ + 'device_tracker.test1': dict({ + 'dir_of_travel': None, + 'dist_to_zone': 2218752, + 'is_in_ignored_zone': False, + 'name': 'test1', + }), + 'device_tracker.test2': dict({ + 'dir_of_travel': None, + 'dist_to_zone': 4077309, + 'is_in_ignored_zone': False, + 'name': 'test2', + }), + }), + 'entity_mapping': dict({ + 'device_tracker.test1': list([ + 'sensor.home_test1_distance', + 'sensor.home_test1_direction_of_travel', + ]), + 'device_tracker.test2': list([ + 'sensor.home_test2_distance', + 'sensor.home_test2_direction_of_travel', + ]), + 'device_tracker.test3': list([ + 'sensor.home_test3_distance', + 'sensor.home_test3_direction_of_travel', + ]), + }), + 'proximity': dict({ + 'dir_of_travel': 'unknown', + 'dist_to_zone': 2219, + 'nearest': 'test1', + }), + 'tracked_states': dict({ + 'device_tracker.test1': dict({ + 'attributes': dict({ + 'friendly_name': 'test1', + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'context': '**REDACTED**', + 'entity_id': 'device_tracker.test1', + 'state': 'not_home', + }), + 'device_tracker.test2': dict({ + 'attributes': dict({ + 'friendly_name': 'test2', + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'context': '**REDACTED**', + 'entity_id': 'device_tracker.test2', + 'state': 'not_home', + }), + }), + }), + 'entry': dict({ + 'data': dict({ + 'ignored_zones': list([ + ]), + 'tolerance': 1, + 'tracked_entities': list([ + 'device_tracker.test1', + 'device_tracker.test2', + 'device_tracker.test3', + ]), + 'zone': 'zone.home', + }), + 'disabled_by': None, + 'domain': 'proximity', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'home', + 'unique_id': 'proximity_home', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/proximity/test_diagnostics.py b/tests/components/proximity/test_diagnostics.py new file mode 100644 index 00000000000..35ecd152a06 --- /dev/null +++ b/tests/components/proximity/test_diagnostics.py @@ -0,0 +1,62 @@ +"""Tests for proximity diagnostics platform.""" +from __future__ import annotations + +from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.components.proximity.const import ( + CONF_IGNORED_ZONES, + CONF_TOLERANCE, + CONF_TRACKED_ENTITIES, + DOMAIN, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_ZONE +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + hass.states.async_set( + "device_tracker.test1", + "not_home", + {"friendly_name": "test1", "latitude": 20.1, "longitude": 10.1}, + ) + hass.states.async_set( + "device_tracker.test2", + "not_home", + {"friendly_name": "test2", "latitude": 150.1, "longitude": 20.1}, + ) + + mock_entry = MockConfigEntry( + domain=DOMAIN, + title="home", + data={ + CONF_ZONE: "zone.home", + CONF_TRACKED_ENTITIES: [ + "device_tracker.test1", + "device_tracker.test2", + "device_tracker.test3", + ], + CONF_IGNORED_ZONES: [], + CONF_TOLERANCE: 1, + }, + unique_id=f"{DOMAIN}_home", + ) + + mock_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + assert mock_entry.state == ConfigEntryState.LOADED + + assert await get_diagnostics_for_config_entry( + hass, hass_client, mock_entry + ) == snapshot(exclude=props("entry_id", "last_changed", "last_updated")) From 99fcff47f9f52162f7a691b41b212fa420ddec78 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 2 Feb 2024 23:04:41 +0100 Subject: [PATCH 0088/1367] Bump aioelectricitymaps to 0.3.0 (#109399) * Bump aioelectricitymaps to 0.3.0 * Fix tests --- .../components/co2signal/config_flow.py | 9 ++++++--- .../components/co2signal/coordinator.py | 11 +++++++---- .../components/co2signal/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/co2signal/test_config_flow.py | 10 +++++----- tests/components/co2signal/test_sensor.py | 18 ++++++++++++------ 7 files changed, 33 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index 2b2aca0b229..a678868ee18 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -4,8 +4,11 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any -from aioelectricitymaps import ElectricityMaps -from aioelectricitymaps.exceptions import ElectricityMapsError, InvalidToken +from aioelectricitymaps import ( + ElectricityMaps, + ElectricityMapsError, + ElectricityMapsInvalidTokenError, +) import voluptuous as vol from homeassistant import config_entries @@ -146,7 +149,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await fetch_latest_carbon_intensity(self.hass, em, data) - except InvalidToken: + except ElectricityMapsInvalidTokenError: errors["base"] = "invalid_auth" except ElectricityMapsError: errors["base"] = "unknown" diff --git a/homeassistant/components/co2signal/coordinator.py b/homeassistant/components/co2signal/coordinator.py index 115c976b465..b06bee38bc4 100644 --- a/homeassistant/components/co2signal/coordinator.py +++ b/homeassistant/components/co2signal/coordinator.py @@ -4,9 +4,12 @@ from __future__ import annotations from datetime import timedelta import logging -from aioelectricitymaps import ElectricityMaps -from aioelectricitymaps.exceptions import ElectricityMapsError, InvalidToken -from aioelectricitymaps.models import CarbonIntensityResponse +from aioelectricitymaps import ( + CarbonIntensityResponse, + ElectricityMaps, + ElectricityMapsError, + ElectricityMapsInvalidTokenError, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -43,7 +46,7 @@ class CO2SignalCoordinator(DataUpdateCoordinator[CarbonIntensityResponse]): return await fetch_latest_carbon_intensity( self.hass, self.client, self.config_entry.data ) - except InvalidToken as err: + except ElectricityMapsInvalidTokenError as err: raise ConfigEntryAuthFailed from err except ElectricityMapsError as err: raise UpdateFailed(str(err)) from err diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json index 87f2b5c2db0..a4cbed00684 100644 --- a/homeassistant/components/co2signal/manifest.json +++ b/homeassistant/components/co2signal/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aioelectricitymaps"], - "requirements": ["aioelectricitymaps==0.2.0"] + "requirements": ["aioelectricitymaps==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 13688ee9059..b72327edafb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -233,7 +233,7 @@ aioeagle==1.1.0 aioecowitt==2023.5.0 # homeassistant.components.co2signal -aioelectricitymaps==0.2.0 +aioelectricitymaps==0.3.0 # homeassistant.components.emonitor aioemonitor==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f608ee1c37..7acb46665bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioeagle==1.1.0 aioecowitt==2023.5.0 # homeassistant.components.co2signal -aioelectricitymaps==0.2.0 +aioelectricitymaps==0.3.0 # homeassistant.components.emonitor aioemonitor==1.0.5 diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index 5b1ade1ee49..29ce783f33a 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -1,10 +1,10 @@ """Test the CO2 Signal config flow.""" from unittest.mock import AsyncMock, patch -from aioelectricitymaps.exceptions import ( - ElectricityMapsDecodeError, +from aioelectricitymaps import ( + ElectricityMapsConnectionError, ElectricityMapsError, - InvalidToken, + ElectricityMapsInvalidTokenError, ) import pytest @@ -134,11 +134,11 @@ async def test_form_country(hass: HomeAssistant) -> None: ("side_effect", "err_code"), [ ( - InvalidToken, + ElectricityMapsInvalidTokenError, "invalid_auth", ), (ElectricityMapsError("Something else"), "unknown"), - (ElectricityMapsDecodeError("Boom"), "unknown"), + (ElectricityMapsConnectionError("Boom"), "unknown"), ], ids=[ "invalid auth", diff --git a/tests/components/co2signal/test_sensor.py b/tests/components/co2signal/test_sensor.py index b79c8e04c23..4d663e1026b 100644 --- a/tests/components/co2signal/test_sensor.py +++ b/tests/components/co2signal/test_sensor.py @@ -2,10 +2,11 @@ from datetime import timedelta from unittest.mock import AsyncMock -from aioelectricitymaps.exceptions import ( - ElectricityMapsDecodeError, +from aioelectricitymaps import ( + ElectricityMapsConnectionError, + ElectricityMapsConnectionTimeoutError, ElectricityMapsError, - InvalidToken, + ElectricityMapsInvalidTokenError, ) from freezegun.api import FrozenDateTimeFactory import pytest @@ -42,7 +43,8 @@ async def test_sensor( @pytest.mark.parametrize( "error", [ - ElectricityMapsDecodeError, + ElectricityMapsConnectionTimeoutError, + ElectricityMapsConnectionError, ElectricityMapsError, Exception, ], @@ -93,8 +95,12 @@ async def test_sensor_reauth_triggered( assert (state := hass.states.get("sensor.electricity_maps_co2_intensity")) assert state.state == "45.9862319009581" - electricity_maps.latest_carbon_intensity_by_coordinates.side_effect = InvalidToken - electricity_maps.latest_carbon_intensity_by_country_code.side_effect = InvalidToken + electricity_maps.latest_carbon_intensity_by_coordinates.side_effect = ( + ElectricityMapsInvalidTokenError + ) + electricity_maps.latest_carbon_intensity_by_country_code.side_effect = ( + ElectricityMapsInvalidTokenError + ) freezer.tick(timedelta(minutes=20)) async_fire_time_changed(hass) From e0fc328e279d229653755ba37096dd84b8d7613a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:43:32 +0100 Subject: [PATCH 0089/1367] Add new climate feature flags to flexit_bacnet (#109431) --- homeassistant/components/flexit_bacnet/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/flexit_bacnet/climate.py b/homeassistant/components/flexit_bacnet/climate.py index 7740bed73e1..0d8a381a014 100644 --- a/homeassistant/components/flexit_bacnet/climate.py +++ b/homeassistant/components/flexit_bacnet/climate.py @@ -62,13 +62,17 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity): ] _attr_supported_features = ( - ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE + ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_max_temp = MAX_TEMP _attr_min_temp = MIN_TEMP + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator: FlexitCoordinator) -> None: """Initialize the Flexit unit.""" From 97e6391b9a97fd43b65949c11185ab2b98652c91 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:46:14 +0100 Subject: [PATCH 0090/1367] Add migrated climate feature flags to shelly (#109425) --- homeassistant/components/shelly/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 9c43c0b57b8..59343ca6d2f 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -167,6 +167,7 @@ class BlockSleepingClimate( ) _attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"] _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -448,6 +449,7 @@ class RpcClimate(ShellyRpcEntity, ClimateEntity): ) _attr_target_temperature_step = RPC_THERMOSTAT_SETTINGS["step"] _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None: """Initialize.""" From 3c15a2216d910d22570b1578c312cabe50d3254b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:46:27 +0100 Subject: [PATCH 0091/1367] Add migrated climate feature flags to homekit_controller (#109433) --- homeassistant/components/homekit_controller/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 8cc4ec569dd..0ca85da3fa2 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -139,6 +139,7 @@ class HomeKitBaseClimateEntity(HomeKitEntity, ClimateEntity): """The base HomeKit Controller climate entity.""" _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False @callback def _async_reconfigure(self) -> None: From 595dd651bb59ec78d96842c746e6340518ef41fd Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 2 Feb 2024 19:13:17 -0600 Subject: [PATCH 0092/1367] Bump intents to 2024.2.2 (#109412) Bump intents to 2024.2.2 --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index ea0a11ae657..1e46170024c 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.6.0", "home-assistant-intents==2024.2.1"] + "requirements": ["hassil==1.6.0", "home-assistant-intents==2024.2.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 950cae8b322..96efd08bc1f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ hass-nabucasa==0.76.0 hassil==1.6.0 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240202.0 -home-assistant-intents==2024.2.1 +home-assistant-intents==2024.2.2 httpx==0.26.0 ifaddr==0.2.0 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index b72327edafb..154cecbff64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1062,7 +1062,7 @@ holidays==0.41 home-assistant-frontend==20240202.0 # homeassistant.components.conversation -home-assistant-intents==2024.2.1 +home-assistant-intents==2024.2.2 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7acb46665bb..49c46f237ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -858,7 +858,7 @@ holidays==0.41 home-assistant-frontend==20240202.0 # homeassistant.components.conversation -home-assistant-intents==2024.2.1 +home-assistant-intents==2024.2.2 # homeassistant.components.home_connect homeconnect==0.7.2 From 16b20403e6bc09fbebc6aa2c93b40eff23c0fb0e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:06:55 +0100 Subject: [PATCH 0093/1367] Add migrated climate feature flags to zha (#109443) --- homeassistant/components/zha/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 40da264d695..cbc759e7008 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -141,6 +141,7 @@ class Thermostat(ZhaEntity, ClimateEntity): _attr_precision = PRECISION_TENTHS _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key: str = "thermostat" + _enable_turn_on_off_backwards_compatibility = False def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs): """Initialize ZHA Thermostat instance.""" From 61cf7862a0bedc8339828d6d116e9d16a24a1f46 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:26:04 +0100 Subject: [PATCH 0094/1367] Adds new climate feature flags in baf (#109476) --- homeassistant/components/baf/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/baf/climate.py b/homeassistant/components/baf/climate.py index 531659e901f..907e8ff2356 100644 --- a/homeassistant/components/baf/climate.py +++ b/homeassistant/components/baf/climate.py @@ -33,10 +33,15 @@ async def async_setup_entry( class BAFAutoComfort(BAFEntity, ClimateEntity): """BAF climate auto comfort.""" - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_hvac_modes = [HVACMode.OFF, HVACMode.FAN_ONLY] _attr_translation_key = "auto_comfort" + _enable_turn_on_off_backwards_compatibility = False @callback def _async_update_attrs(self) -> None: From ae210886c107d36e4e75560febae35d316f1d3a5 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:26:17 +0100 Subject: [PATCH 0095/1367] Add migrated climate feature flags to nexia (#109472) --- homeassistant/components/nexia/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 32ac8b5320a..63caeb445b7 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -153,6 +153,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): """Provides Nexia Climate support.""" _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__( self, coordinator: NexiaDataUpdateCoordinator, zone: NexiaThermostatZone From 3347a3f8a678fda1ee0c7b9ff332c82873a41682 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 2 Feb 2024 20:26:44 -0600 Subject: [PATCH 0096/1367] More thorough checks in ESPHome voice assistant UDP server (#109394) * More thorough checks in UDP server * Simplify and change to stop_requested * Check transport --- homeassistant/components/esphome/manager.py | 1 - .../components/esphome/voice_assistant.py | 31 ++-- .../esphome/test_voice_assistant.py | 152 ++++++++++++------ 3 files changed, 122 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index f197574c30a..59f37d3a078 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -352,7 +352,6 @@ class ESPHomeManager: if self.voice_assistant_udp_server is not None: _LOGGER.warning("Voice assistant UDP server was not stopped") self.voice_assistant_udp_server.stop() - self.voice_assistant_udp_server.close() self.voice_assistant_udp_server = None hass = self.hass diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index de6b521d980..7c5c74d58ee 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -1,4 +1,5 @@ """ESPHome voice assistant support.""" + from __future__ import annotations import asyncio @@ -67,7 +68,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): """Receive UDP packets and forward them to the voice assistant.""" started = False - stopped = False + stop_requested = False transport: asyncio.DatagramTransport | None = None remote_addr: tuple[str, int] | None = None @@ -92,6 +93,11 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): self._tts_done = asyncio.Event() self._tts_task: asyncio.Task | None = None + @property + def is_running(self) -> bool: + """True if the the UDP server is started and hasn't been asked to stop.""" + return self.started and (not self.stop_requested) + async def start_server(self) -> int: """Start accepting connections.""" @@ -99,7 +105,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): """Accept connection.""" if self.started: raise RuntimeError("Can only start once") - if self.stopped: + if self.stop_requested: raise RuntimeError("No longer accepting connections") self.started = True @@ -124,7 +130,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): @callback def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None: """Handle incoming UDP packet.""" - if not self.started or self.stopped: + if not self.is_running: return if self.remote_addr is None: self.remote_addr = addr @@ -142,19 +148,19 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): def stop(self) -> None: """Stop the receiver.""" self.queue.put_nowait(b"") - self.started = False - self.stopped = True + self.close() def close(self) -> None: """Close the receiver.""" self.started = False - self.stopped = True + self.stop_requested = True + if self.transport is not None: self.transport.close() async def _iterate_packets(self) -> AsyncIterable[bytes]: """Iterate over incoming packets.""" - if not self.started or self.stopped: + if not self.is_running: raise RuntimeError("Not running") while data := await self.queue.get(): @@ -303,8 +309,11 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): async def _send_tts(self, media_id: str) -> None: """Send TTS audio to device via UDP.""" + # Always send stream start/end events + self.handle_event(VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_START, {}) + try: - if self.transport is None: + if (not self.is_running) or (self.transport is None): return extension, data = await tts.async_get_media_source_audio( @@ -337,15 +346,11 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): _LOGGER.debug("Sending %d bytes of audio", audio_bytes_size) - self.handle_event( - VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_START, {} - ) - bytes_per_sample = stt.AudioBitRates.BITRATE_16 // 8 sample_offset = 0 samples_left = audio_bytes_size // bytes_per_sample - while samples_left > 0: + while (samples_left > 0) and self.is_running: bytes_offset = sample_offset * bytes_per_sample chunk: bytes = audio_bytes[bytes_offset : bytes_offset + 1024] samples_in_chunk = len(chunk) // bytes_per_sample diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index 38a33bfdec2..f6665c4ad91 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -70,6 +70,19 @@ def voice_assistant_udp_server_v2( return voice_assistant_udp_server(entry=mock_voice_assistant_v2_entry) +@pytest.fixture +def test_wav() -> bytes: + """Return one second of empty WAV audio.""" + with io.BytesIO() as wav_io: + with wave.open(wav_io, "wb") as wav_file: + wav_file.setframerate(16000) + wav_file.setsampwidth(2) + wav_file.setnchannels(1) + wav_file.writeframes(bytes(_ONE_SECOND)) + + return wav_io.getvalue() + + async def test_pipeline_events( hass: HomeAssistant, voice_assistant_udp_server_v1: VoiceAssistantUDPServer, @@ -241,11 +254,13 @@ async def test_udp_server_multiple( ): await voice_assistant_udp_server_v1.start_server() - with patch( - "homeassistant.components.esphome.voice_assistant.UDP_PORT", - new=unused_udp_port_factory(), - ), pytest.raises(RuntimeError): - pass + with ( + patch( + "homeassistant.components.esphome.voice_assistant.UDP_PORT", + new=unused_udp_port_factory(), + ), + pytest.raises(RuntimeError), + ): await voice_assistant_udp_server_v1.start_server() @@ -257,10 +272,13 @@ async def test_udp_server_after_stopped( ) -> None: """Test that the UDP server raises an error if started after stopped.""" voice_assistant_udp_server_v1.close() - with patch( - "homeassistant.components.esphome.voice_assistant.UDP_PORT", - new=unused_udp_port_factory(), - ), pytest.raises(RuntimeError): + with ( + patch( + "homeassistant.components.esphome.voice_assistant.UDP_PORT", + new=unused_udp_port_factory(), + ), + pytest.raises(RuntimeError), + ): await voice_assistant_udp_server_v1.start_server() @@ -362,35 +380,33 @@ async def test_send_tts_not_called_when_empty( async def test_send_tts( hass: HomeAssistant, voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + test_wav, ) -> None: """Test the UDP server calls sendto to transmit audio data to device.""" - with io.BytesIO() as wav_io: - with wave.open(wav_io, "wb") as wav_file: - wav_file.setframerate(16000) - wav_file.setsampwidth(2) - wav_file.setnchannels(1) - wav_file.writeframes(bytes(_ONE_SECOND)) - - wav_bytes = wav_io.getvalue() - with patch( "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", - return_value=("wav", wav_bytes), + return_value=("wav", test_wav), ): + voice_assistant_udp_server_v2.started = True voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) - - voice_assistant_udp_server_v2._event_callback( - PipelineEvent( - type=PipelineEventType.TTS_END, - data={ - "tts_output": {"media_id": _TEST_MEDIA_ID, "url": _TEST_OUTPUT_URL} - }, + with patch.object( + voice_assistant_udp_server_v2.transport, "is_closing", return_value=False + ): + voice_assistant_udp_server_v2._event_callback( + PipelineEvent( + type=PipelineEventType.TTS_END, + data={ + "tts_output": { + "media_id": _TEST_MEDIA_ID, + "url": _TEST_OUTPUT_URL, + } + }, + ) ) - ) - await voice_assistant_udp_server_v2._tts_done.wait() + await voice_assistant_udp_server_v2._tts_done.wait() - voice_assistant_udp_server_v2.transport.sendto.assert_called() + voice_assistant_udp_server_v2.transport.sendto.assert_called() async def test_send_tts_wrong_sample_rate( @@ -400,17 +416,20 @@ async def test_send_tts_wrong_sample_rate( """Test the UDP server calls sendto to transmit audio data to device.""" with io.BytesIO() as wav_io: with wave.open(wav_io, "wb") as wav_file: - wav_file.setframerate(22050) # should be 16000 + wav_file.setframerate(22050) wav_file.setsampwidth(2) wav_file.setnchannels(1) wav_file.writeframes(bytes(_ONE_SECOND)) wav_bytes = wav_io.getvalue() - - with patch( - "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", - return_value=("wav", wav_bytes), - ), pytest.raises(ValueError): + with ( + patch( + "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", + return_value=("wav", wav_bytes), + ), + pytest.raises(ValueError), + ): + voice_assistant_udp_server_v2.started = True voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) voice_assistant_udp_server_v2._event_callback( @@ -431,10 +450,14 @@ async def test_send_tts_wrong_format( voice_assistant_udp_server_v2: VoiceAssistantUDPServer, ) -> None: """Test that only WAV audio will be streamed.""" - with patch( - "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", - return_value=("raw", bytes(1024)), - ), pytest.raises(ValueError): + with ( + patch( + "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", + return_value=("raw", bytes(1024)), + ), + pytest.raises(ValueError), + ): + voice_assistant_udp_server_v2.started = True voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) voice_assistant_udp_server_v2._event_callback( @@ -450,6 +473,33 @@ async def test_send_tts_wrong_format( await voice_assistant_udp_server_v2._tts_task # raises ValueError +async def test_send_tts_not_started( + hass: HomeAssistant, + voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + test_wav, +) -> None: + """Test the UDP server does not call sendto when not started.""" + with patch( + "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", + return_value=("wav", test_wav), + ): + voice_assistant_udp_server_v2.started = False + voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) + + voice_assistant_udp_server_v2._event_callback( + PipelineEvent( + type=PipelineEventType.TTS_END, + data={ + "tts_output": {"media_id": _TEST_MEDIA_ID, "url": _TEST_OUTPUT_URL} + }, + ) + ) + + await voice_assistant_udp_server_v2._tts_done.wait() + + voice_assistant_udp_server_v2.transport.sendto.assert_not_called() + + async def test_wake_word( hass: HomeAssistant, voice_assistant_udp_server_v2: VoiceAssistantUDPServer, @@ -459,11 +509,12 @@ async def test_wake_word( async def async_pipeline_from_audio_stream(*args, start_stage, **kwargs): assert start_stage == PipelineStage.WAKE_WORD - with patch( - "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", - new=async_pipeline_from_audio_stream, - ), patch( - "asyncio.Event.wait" # TTS wait event + with ( + patch( + "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), + patch("asyncio.Event.wait"), # TTS wait event ): voice_assistant_udp_server_v2.transport = Mock() @@ -515,10 +566,15 @@ async def test_wake_word_abort_exception( async def async_pipeline_from_audio_stream(*args, **kwargs): raise WakeWordDetectionAborted - with patch( - "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", - new=async_pipeline_from_audio_stream, - ), patch.object(voice_assistant_udp_server_v2, "handle_event") as mock_handle_event: + with ( + patch( + "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), + patch.object( + voice_assistant_udp_server_v2, "handle_event" + ) as mock_handle_event, + ): voice_assistant_udp_server_v2.transport = Mock() await voice_assistant_udp_server_v2.run_pipeline( From 0ef95c2d4bf0bf299c9fbe3593f1a1ca4a017bc5 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:30:53 +0100 Subject: [PATCH 0097/1367] Add migrated ClimateEntityFeatures to advantage_air (#109420) * Add migrated ClimateEntityFeatures to advantage_air * AdvantageAirZone --- homeassistant/components/advantage_air/climate.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index d4f3c05902c..870a001a10f 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -83,6 +83,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): _attr_max_temp = 32 _attr_min_temp = 16 _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, instance: AdvantageAirData, ac_key: str) -> None: """Initialize an AdvantageAir AC unit.""" @@ -202,11 +203,16 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): """AdvantageAir MyTemp Zone control.""" _attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT_COOL] - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_target_temperature_step = PRECISION_WHOLE _attr_max_temp = 32 _attr_min_temp = 16 + _enable_turn_on_off_backwards_compatibility = False def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None: """Initialize an AdvantageAir Zone control.""" From 268c84a37957913049093d497ce50f4567cffe02 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:41:25 +0100 Subject: [PATCH 0098/1367] Add Mill migrated ClimateEntityFeatures (#109415) --- homeassistant/components/mill/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index d0b15f5d8ff..2e7b22da833 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -99,6 +99,7 @@ class MillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntity): ) _attr_target_temperature_step = PRECISION_TENTHS _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__( self, coordinator: MillDataUpdateCoordinator, heater: mill.Heater From fbb7c9003ff5c8f2f285e534c8232f7db7b3e147 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:47:09 +0100 Subject: [PATCH 0099/1367] Add new ClimateEntityFeature for Tado (#109416) --- homeassistant/components/tado/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 1193638c10e..dd0d6a22a08 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -131,7 +131,10 @@ def create_climate_entity(tado, name: str, zone_id: int, device_info: dict): zone_type = capabilities["type"] support_flags = ( - ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE + ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) supported_hvac_modes = [ TADO_TO_HA_HVAC_MODE_MAP[CONST_MODE_OFF], @@ -221,6 +224,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): _attr_name = None _attr_translation_key = DOMAIN _available = False + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From 1fba47fc8e1367e8d60c93dc2be6fc496acae6ff Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:50:24 +0100 Subject: [PATCH 0100/1367] Add new climate feature flags to radiotherm (#109466) --- homeassistant/components/radiotherm/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index f5ea14e8f4e..4ab57fd6821 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -106,6 +106,7 @@ class RadioThermostat(RadioThermostatEntity, ClimateEntity): _attr_temperature_unit = UnitOfTemperature.FAHRENHEIT _attr_precision = PRECISION_HALVES _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator: RadioThermUpdateCoordinator) -> None: """Initialize the thermostat.""" @@ -113,7 +114,10 @@ class RadioThermostat(RadioThermostatEntity, ClimateEntity): self._attr_unique_id = self.init_data.mac self._attr_fan_modes = CT30_FAN_OPERATION_LIST self._attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) if not isinstance(self.device, radiotherm.thermostat.CT80): return From 79bcf60c737e2ca54f2f9091c3f498e55aac86df Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:56:57 +0100 Subject: [PATCH 0101/1367] Adds migrated ClimateEntityFeature to Netatmo (#109418) --- homeassistant/components/netatmo/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 721e453e834..db12efb2f01 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -190,6 +190,7 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): _attr_supported_features = SUPPORT_FLAGS _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, netatmo_device: NetatmoRoom) -> None: """Initialize the sensor.""" From 5c3707ec9ce9d5dda2abb9cdb0151ba8cdc90a12 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 04:02:19 +0100 Subject: [PATCH 0102/1367] Add new climate feature flags to airzone (#109423) --- homeassistant/components/airzone/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index f5a0e1b109e..2b4cae18086 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -117,6 +117,7 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): _attr_name = None _speeds: dict[int, str] = {} _speeds_reverse: dict[str, int] = {} + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -129,7 +130,11 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): super().__init__(coordinator, entry, system_zone_id, zone_data) self._attr_unique_id = f"{self._attr_unique_id}_{system_zone_id}" - self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) self._attr_target_temperature_step = API_TEMPERATURE_STEP self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[ self.get_airzone_value(AZD_TEMP_UNIT) From ab5163fb5e3dc1b741ec355e94fd7bb1224496a7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:32:47 -0500 Subject: [PATCH 0103/1367] Add migrated climate entity features to flexit (#109430) --- homeassistant/components/flexit/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index b833617f2ca..85d5e9f4eac 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -69,6 +69,7 @@ class Flexit(ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__( self, hub: ModbusHub, modbus_slave: int | None, name: str | None From 974cee2349d5a90af990446e9c2f9ea80f33687f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:33:03 -0500 Subject: [PATCH 0104/1367] Add migrated feature flags to vera (#109438) --- homeassistant/components/vera/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 85c1851b20e..93d0fbf2aee 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -53,6 +53,7 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ) + _enable_turn_on_off_backwards_compatibility = False def __init__( self, vera_device: veraApi.VeraThermostat, controller_data: ControllerData From fe25975cab332552e8496c2d13c6c304eb7a062e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:33:42 -0500 Subject: [PATCH 0105/1367] Adds migrated climate entity feature for velbus (#109435) --- homeassistant/components/velbus/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index ecdddd19289..9afbfc683a8 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -41,6 +41,7 @@ class VelbusClimate(VelbusEntity, ClimateEntity): _attr_hvac_mode = HVACMode.HEAT _attr_hvac_modes = [HVACMode.HEAT] _attr_preset_modes = list(PRESET_MODES) + _enable_turn_on_off_backwards_compatibility = False @property def target_temperature(self) -> float | None: From 63f78dece0078ba843340179c2061e3a2870e638 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:33:55 -0500 Subject: [PATCH 0106/1367] Add new climate feature flags to venstar (#109436) --- homeassistant/components/venstar/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 6359cc19e57..a9ee56c4dbb 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -108,6 +108,7 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): _attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF, HVACMode.AUTO] _attr_precision = PRECISION_HALVES _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -130,6 +131,8 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) if self._client.mode == self._client.MODE_AUTO: From bb6051f9c432412b95319de1bae67639662a2e39 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:35:16 -0500 Subject: [PATCH 0107/1367] Add new climate feature flags to zhong_hong (#109444) --- homeassistant/components/zhong_hong/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index 1364dbe107a..fbada765cde 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -128,9 +128,13 @@ class ZhongHongClimate(ClimateEntity): ] _attr_should_poll = False _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, hub, addr_out, addr_in): """Set up the ZhongHong climate devices.""" From 1d3c5d92ea7267a3ebf1aeeeba944bd85264a9c9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:36:19 -0500 Subject: [PATCH 0108/1367] Add new climate feature flags to whirlpool (#109440) --- homeassistant/components/whirlpool/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index 2d38d713859..48b9b99c1e2 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -103,10 +103,13 @@ class AirConEntity(ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.SWING_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_swing_modes = SUPPORTED_SWING_MODES _attr_target_temperature_step = SUPPORTED_TARGET_TEMPERATURE_STEP _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From 5462badebf2ba29ff49ff7ebedbdf40ca25ce6be Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:36:43 -0500 Subject: [PATCH 0109/1367] Add migrated climate feature flags to zwave_me (#109445) --- homeassistant/components/zwave_me/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zwave_me/climate.py b/homeassistant/components/zwave_me/climate.py index 7d654311213..35e0d745619 100644 --- a/homeassistant/components/zwave_me/climate.py +++ b/homeassistant/components/zwave_me/climate.py @@ -56,6 +56,7 @@ class ZWaveMeClimate(ZWaveMeEntity, ClimateEntity): _attr_hvac_mode = HVACMode.HEAT _attr_hvac_modes = [HVACMode.HEAT] _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _enable_turn_on_off_backwards_compatibility = False def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" From 96a3aac78e9dab112039fc1e93f553e55f877d9a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:37:14 -0500 Subject: [PATCH 0110/1367] Add new climate feature flags to yolink (#109442) --- homeassistant/components/yolink/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/yolink/climate.py b/homeassistant/components/yolink/climate.py index 6e4495ee0b9..a1e2fdd90a2 100644 --- a/homeassistant/components/yolink/climate.py +++ b/homeassistant/components/yolink/climate.py @@ -62,6 +62,7 @@ class YoLinkClimateEntity(YoLinkEntity, ClimateEntity): """YoLink Climate Entity.""" _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -86,6 +87,8 @@ class YoLinkClimateEntity(YoLinkEntity, ClimateEntity): ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) @callback From 04ce480d652b473b6a3b75a71687ecbb7cd5cc87 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:37:49 -0500 Subject: [PATCH 0111/1367] Add migrated climate feature flags to teslemetry (#109446) --- homeassistant/components/teslemetry/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/teslemetry/climate.py b/homeassistant/components/teslemetry/climate.py index b626d3ef759..748acbb8552 100644 --- a/homeassistant/components/teslemetry/climate.py +++ b/homeassistant/components/teslemetry/climate.py @@ -45,6 +45,7 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): | ClimateEntityFeature.PRESET_MODE ) _attr_preset_modes = ["off", "keep", "dog", "camp"] + _enable_turn_on_off_backwards_compatibility = False @property def hvac_mode(self) -> HVACMode | None: From f40e2ecb95a6a987040e94cf34fe26b6854d446c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:38:40 -0500 Subject: [PATCH 0112/1367] Add migrated climate feature flags for tessie (#109447) --- homeassistant/components/tessie/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tessie/climate.py b/homeassistant/components/tessie/climate.py index d143771ee2c..8eb69d619ff 100644 --- a/homeassistant/components/tessie/climate.py +++ b/homeassistant/components/tessie/climate.py @@ -56,6 +56,7 @@ class TessieClimateEntity(TessieEntity, ClimateEntity): TessieClimateKeeper.DOG, TessieClimateKeeper.CAMP, ] + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From e308dcf398ec9493fbe2564cfbe0036219a9e480 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:39:18 -0500 Subject: [PATCH 0113/1367] Add new climate feature flags to tfiac (#109448) --- homeassistant/components/tfiac/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/tfiac/climate.py b/homeassistant/components/tfiac/climate.py index 2e764b5c637..7e5999b7f02 100644 --- a/homeassistant/components/tfiac/climate.py +++ b/homeassistant/components/tfiac/climate.py @@ -83,8 +83,11 @@ class TfiacClimate(ClimateEntity): ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.SWING_MODE | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_temperature_unit = UnitOfTemperature.FAHRENHEIT + _enable_turn_on_off_backwards_compatibility = False def __init__(self, hass, client): """Init class.""" From 26caa85179ef2a27ba562b1e27df10a647867c0b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:39:37 -0500 Subject: [PATCH 0114/1367] Add migrated climate feature flags to tolo (#109449) --- homeassistant/components/tolo/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tolo/climate.py b/homeassistant/components/tolo/climate.py index 05afce41ff3..033a4c5b51c 100644 --- a/homeassistant/components/tolo/climate.py +++ b/homeassistant/components/tolo/climate.py @@ -58,6 +58,7 @@ class SaunaClimate(ToloSaunaCoordinatorEntity, ClimateEntity): ) _attr_target_temperature_step = 1 _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__( self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry From 3c8bfce3a4fa2238d362617f5816b2a4cf52c6ce Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:40:17 -0500 Subject: [PATCH 0115/1367] Add migrated climate feature flags to toon (#109450) --- homeassistant/components/toon/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index cc51bb03fec..16fbdbdd356 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -51,6 +51,7 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From a6b912b282e91495101e06146dbaebf3d0bf3e45 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:40:50 -0500 Subject: [PATCH 0116/1367] Add migrated climate feature flags to xs1 (#109441) --- homeassistant/components/xs1/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py index 4c4f6682ffa..949d2330347 100644 --- a/homeassistant/components/xs1/climate.py +++ b/homeassistant/components/xs1/climate.py @@ -54,6 +54,7 @@ class XS1ThermostatEntity(XS1DeviceEntity, ClimateEntity): _attr_hvac_mode = HVACMode.HEAT _attr_hvac_modes = [HVACMode.HEAT] _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device, sensor): """Initialize the actuator.""" From a69fe882ff202a616361789d3c8900c53682259a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:41:08 -0500 Subject: [PATCH 0117/1367] Add migrated climate feature flags to touchline (#109451) --- homeassistant/components/touchline/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/touchline/climate.py b/homeassistant/components/touchline/climate.py index ed3d4500db1..5004646a667 100644 --- a/homeassistant/components/touchline/climate.py +++ b/homeassistant/components/touchline/climate.py @@ -69,6 +69,7 @@ class Touchline(ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, touchline_thermostat): """Initialize the Touchline device.""" From b7f2ae4e3ae10ae9e11a55ec46cb7ff60dac2337 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:41:28 -0500 Subject: [PATCH 0118/1367] Add migrated climate feature flags to schluter (#109452) --- homeassistant/components/schluter/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py index c8c0d76690d..5d747c8f345 100644 --- a/homeassistant/components/schluter/climate.py +++ b/homeassistant/components/schluter/climate.py @@ -81,6 +81,7 @@ class SchluterThermostat(CoordinatorEntity, ClimateEntity): _attr_hvac_modes = [HVACMode.HEAT] _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator, serial_number, api, session_id): """Initialize the thermostat.""" From 88aec4af72a7c58e9d72f96f70c972dfaec8bc7e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:42:37 -0500 Subject: [PATCH 0119/1367] Add new climate feature flags to screenlogic (#109454) --- homeassistant/components/screenlogic/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index 7cdfbba10c0..6d95f06a49c 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -81,8 +81,12 @@ class ScreenLogicClimate(ScreenLogicPushEntity, ClimateEntity, RestoreEntity): entity_description: ScreenLogicClimateDescription _attr_hvac_modes = SUPPORTED_MODES _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator, entity_description) -> None: """Initialize a ScreenLogic climate entity.""" From de308fbd55b6bb714e6a505e477febe645506842 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:44:04 -0500 Subject: [PATCH 0120/1367] Add migrated climate feature flags to senz (#109455) --- homeassistant/components/senz/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/senz/climate.py b/homeassistant/components/senz/climate.py index a94941ac642..c921e1ac1da 100644 --- a/homeassistant/components/senz/climate.py +++ b/homeassistant/components/senz/climate.py @@ -45,6 +45,7 @@ class SENZClimate(CoordinatorEntity, ClimateEntity): _attr_min_temp = 5 _attr_has_entity_name = True _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From 9325243ef28d6752aa7868ccd4c97a81d65f375b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:45:06 -0500 Subject: [PATCH 0121/1367] Add new climate feature flags to stiebel_eltron (#109457) --- homeassistant/components/stiebel_eltron/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stiebel_eltron/climate.py b/homeassistant/components/stiebel_eltron/climate.py index 88cce6c52d7..cedd1b3dd90 100644 --- a/homeassistant/components/stiebel_eltron/climate.py +++ b/homeassistant/components/stiebel_eltron/climate.py @@ -73,9 +73,13 @@ class StiebelEltron(ClimateEntity): _attr_hvac_modes = SUPPORT_HVAC _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, name, ste_data): """Initialize the unit.""" From 7b132dc1897530690c37f668bec344f8c11a9574 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:51:38 -0500 Subject: [PATCH 0122/1367] Add new climate feature flags to oem (#109461) --- homeassistant/components/oem/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/oem/climate.py b/homeassistant/components/oem/climate.py index 1b600b25d94..86c770ec82d 100644 --- a/homeassistant/components/oem/climate.py +++ b/homeassistant/components/oem/climate.py @@ -66,8 +66,13 @@ class ThermostatDevice(ClimateEntity): """Interface class for the oemthermostat module.""" _attr_hvac_modes = SUPPORT_HVAC - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, thermostat, name): """Initialize the device.""" From 9360165ba70243c1238da8044787aef63a1b2d0f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:54:01 -0500 Subject: [PATCH 0123/1367] Add migrated climate feature flags to opentherm_gw (#109462) --- homeassistant/components/opentherm_gw/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index bcad621eb82..0b9cd1862be 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -84,6 +84,7 @@ class OpenThermClimate(ClimateEntity): _away_state_a = False _away_state_b = False _current_operation: HVACAction | None = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, gw_dev, options): """Initialize the device.""" From c0fd709b3e07c5f4414d6fd5d6b25c0a1cd38cce Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 00:42:10 -0500 Subject: [PATCH 0124/1367] Add migrated climate feature flags to smarttub (#109427) --- homeassistant/components/smarttub/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py index 9f1802e7327..4921fca022d 100644 --- a/homeassistant/components/smarttub/climate.py +++ b/homeassistant/components/smarttub/climate.py @@ -67,6 +67,7 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_preset_modes = list(PRESET_MODES.values()) + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator, spa): """Initialize the entity.""" From 3039616133dba5217daf5b54d04de3a64c7912e3 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:03:14 -0500 Subject: [PATCH 0125/1367] Adds migrated climate feature flags to nobo_hub (#109473) --- homeassistant/components/nobo_hub/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py index 7041d097f3e..ca8ee08885d 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -81,6 +81,7 @@ class NoboZone(ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_target_temperature_step = 1 # Need to poll to get preset change when in HVACMode.AUTO, so can't set _attr_should_poll = False + _enable_turn_on_off_backwards_compatibility = False def __init__(self, zone_id, hub: nobo, override_type) -> None: """Initialize the climate device.""" From 69f5b5e78e5b685a63bc285b331f0d685094fa05 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:03:34 -0500 Subject: [PATCH 0126/1367] Adds migrated climate feature flags in nuheat (#109474) --- homeassistant/components/nuheat/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index 13a46c0b32f..b2ebbfa8485 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -78,6 +78,7 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): _attr_has_entity_name = True _attr_name = None _attr_preset_modes = PRESET_MODES + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator, thermostat, temperature_unit): """Initialize the thermostat.""" From b990e9663648c2f11c22958a93cd2d89e209449a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:03:44 -0500 Subject: [PATCH 0127/1367] Adds new climate feature flags to ambiclimate (#109475) --- homeassistant/components/ambiclimate/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index fc192d8658f..58b2334260e 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -153,10 +153,15 @@ class AmbiclimateEntity(ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_target_temperature_step = 1 - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] _attr_has_entity_name = True _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, heater: AmbiclimateDevice, store: Store[dict[str, Any]]) -> None: """Initialize the thermostat.""" From 3e903495fa5bb0a5e11ae164bfbea16a0534a912 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:03:59 -0500 Subject: [PATCH 0128/1367] Adds migrated climate feature flags in balboa (#109477) --- homeassistant/components/balboa/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/balboa/climate.py b/homeassistant/components/balboa/climate.py index 0ca8b1a3acc..b9cce73de75 100644 --- a/homeassistant/components/balboa/climate.py +++ b/homeassistant/components/balboa/climate.py @@ -63,6 +63,7 @@ class BalboaClimateEntity(BalboaEntity, ClimateEntity): ) _attr_translation_key = DOMAIN _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, client: SpaClient) -> None: """Initialize the climate entity.""" From d4c0a9a847c71886f62b8fe64ab493399faceb83 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:04:11 -0500 Subject: [PATCH 0129/1367] Adds new climate feature flags to broadlink (#109479) --- homeassistant/components/broadlink/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/broadlink/climate.py b/homeassistant/components/broadlink/climate.py index 6937d6bb0da..dd37d270f9e 100644 --- a/homeassistant/components/broadlink/climate.py +++ b/homeassistant/components/broadlink/climate.py @@ -35,9 +35,14 @@ class BroadlinkThermostat(ClimateEntity, BroadlinkEntity): _attr_has_entity_name = True _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF, HVACMode.AUTO] - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device: BroadlinkDevice) -> None: """Initialize the climate entity.""" From 366da3e01f421874f7118447096982708ac668a0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:04:26 -0500 Subject: [PATCH 0130/1367] Adds new climate feature flags to bsblan (#109480) --- homeassistant/components/bsblan/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 609d5ab6e83..511701cb538 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -73,12 +73,16 @@ class BSBLANClimate( _attr_name = None # Determine preset modes _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_preset_modes = PRESET_MODES # Determine hvac modes _attr_hvac_modes = HVAC_MODES + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From eb7d12597648ceb6862030c2e006532494a6c694 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:05:16 -0500 Subject: [PATCH 0131/1367] Add migrated climate feature flags to moehlenhoff (#109470) --- homeassistant/components/moehlenhoff_alpha2/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/moehlenhoff_alpha2/climate.py b/homeassistant/components/moehlenhoff_alpha2/climate.py index 23a39084f9f..063628d6d32 100644 --- a/homeassistant/components/moehlenhoff_alpha2/climate.py +++ b/homeassistant/components/moehlenhoff_alpha2/climate.py @@ -46,6 +46,7 @@ class Alpha2Climate(CoordinatorEntity[Alpha2BaseCoordinator], ClimateEntity): _attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL] _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_preset_modes = [PRESET_AUTO, PRESET_DAY, PRESET_NIGHT] + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator: Alpha2BaseCoordinator, heat_area_id: str) -> None: """Initialize Alpha2 ClimateEntity.""" From 82a2980cbdb682111acee500a275f9af9042d745 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:05:33 -0500 Subject: [PATCH 0132/1367] Adds new climate feature flags to melissa (#109469) --- homeassistant/components/melissa/climate.py | 6 +++++- tests/components/melissa/test_climate.py | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index 9facb18ed05..f94c3af6d9a 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -57,9 +57,13 @@ class MelissaClimate(ClimateEntity): _attr_hvac_modes = OP_MODES _attr_supported_features = ( - ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.TARGET_TEMPERATURE + ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, api, serial_number, init_data): """Initialize the climate device.""" diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index 4568eaf2e77..dc2ca4391f1 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -223,7 +223,10 @@ async def test_supported_features(hass: HomeAssistant) -> None: device = (await api.async_fetch_devices())[_SERIAL] thermostat = MelissaClimate(api, _SERIAL, device) features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) assert thermostat.supported_features == features From 7666c432e48a3a023d75ef3176bdb9111b07f28e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:05:46 -0500 Subject: [PATCH 0133/1367] Adds new climate feature flags to maxcube (#109467) --- homeassistant/components/maxcube/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index 2ef451b04a7..f3d302fc209 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -68,8 +68,12 @@ class MaxCubeClimate(ClimateEntity): _attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO, HVACMode.HEAT] _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) + _enable_turn_on_off_backwards_compatibility = False def __init__(self, handler, device): """Initialize MAX! Cube ClimateEntity.""" From 8f637d3ca7bd95e4fdeb5321acb52af96ff7c0b3 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:06:21 -0500 Subject: [PATCH 0134/1367] Adds migrated climate feature flags for proliphix (#109465) --- homeassistant/components/proliphix/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index 5f841441d59..797fd751197 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -60,6 +60,7 @@ class ProliphixThermostat(ClimateEntity): _attr_precision = PRECISION_TENTHS _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE _attr_temperature_unit = UnitOfTemperature.FAHRENHEIT + _enable_turn_on_off_backwards_compatibility = False def __init__(self, pdp): """Initialize the thermostat.""" From 0d881dfc12059d8d47a85f37e3482298e5299ff6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:06:53 -0500 Subject: [PATCH 0135/1367] Adds new climate feature flags for airzone_cloud (#109424) --- homeassistant/components/airzone_cloud/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/airzone_cloud/climate.py b/homeassistant/components/airzone_cloud/climate.py index e076edc1f5b..73333d346c5 100644 --- a/homeassistant/components/airzone_cloud/climate.py +++ b/homeassistant/components/airzone_cloud/climate.py @@ -144,8 +144,13 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity): """Define an Airzone Cloud climate.""" _attr_name = None - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False @callback def _handle_coordinator_update(self) -> None: From 0884215130472fd6138938c692c16b35682defae Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 3 Feb 2024 06:39:35 +0000 Subject: [PATCH 0136/1367] Bump aiohomekit to 3.1.4 (#109414) --- 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 799058b0e20..1617b907a26 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.1.3"], + "requirements": ["aiohomekit==3.1.4"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 154cecbff64..7c4e1045123 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -257,7 +257,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.1.3 +aiohomekit==3.1.4 # homeassistant.components.http aiohttp-fast-url-dispatcher==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49c46f237ab..14cd5e5f8a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -233,7 +233,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.1.3 +aiohomekit==3.1.4 # homeassistant.components.http aiohttp-fast-url-dispatcher==0.3.0 From 53db392150682f44b5171bddf6dbd1ea7dbd52a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Feb 2024 00:47:07 -0600 Subject: [PATCH 0137/1367] Convert auth token removal websocket api to normal functions (#109432) There was nothing being awaited here anymore --- homeassistant/components/auth/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index f97647fff0e..dd07e137e5e 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -578,6 +578,7 @@ def websocket_refresh_tokens( connection.send_result(msg["id"], tokens) +@callback @websocket_api.websocket_command( { vol.Required("type"): "auth/delete_refresh_token", @@ -585,8 +586,7 @@ def websocket_refresh_tokens( } ) @websocket_api.ws_require_user() -@websocket_api.async_response -async def websocket_delete_refresh_token( +def websocket_delete_refresh_token( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Handle a delete refresh token request.""" @@ -601,6 +601,7 @@ async def websocket_delete_refresh_token( connection.send_result(msg["id"], {}) +@callback @websocket_api.websocket_command( { vol.Required("type"): "auth/delete_all_refresh_tokens", @@ -609,8 +610,7 @@ async def websocket_delete_refresh_token( } ) @websocket_api.ws_require_user() -@websocket_api.async_response -async def websocket_delete_all_refresh_tokens( +def websocket_delete_all_refresh_tokens( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Handle delete all refresh tokens request.""" From 5f1d20c5e23d44b0a64550ab4ab95ab8dcc93236 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sat, 3 Feb 2024 07:50:33 +0100 Subject: [PATCH 0138/1367] Add new OUIs for tplink (#109437) --- homeassistant/components/tplink/manifest.json | 66 ++++++++++++++- homeassistant/generated/dhcp.py | 82 ++++++++++++++++++- 2 files changed, 146 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index a479314d649..a91e7e5a46f 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -156,6 +156,10 @@ "hostname": "k[lps]*", "macaddress": "54AF97*" }, + { + "hostname": "l[59]*", + "macaddress": "54AF97*" + }, { "hostname": "k[lps]*", "macaddress": "AC15A2*" @@ -177,21 +181,41 @@ "macaddress": "5CE931*" }, { - "hostname": "l5*", + "hostname": "l[59]*", "macaddress": "3C52A1*" }, { "hostname": "l5*", "macaddress": "5C628B*" }, + { + "hostname": "tp*", + "macaddress": "5C628B*" + }, { "hostname": "p1*", "macaddress": "482254*" }, + { + "hostname": "s5*", + "macaddress": "482254*" + }, { "hostname": "p1*", "macaddress": "30DE4B*" }, + { + "hostname": "p1*", + "macaddress": "3C52A1*" + }, + { + "hostname": "tp*", + "macaddress": "3C52A1*" + }, + { + "hostname": "s5*", + "macaddress": "3C52A1*" + }, { "hostname": "l9*", "macaddress": "A842A1*" @@ -199,6 +223,46 @@ { "hostname": "l9*", "macaddress": "3460F9*" + }, + { + "hostname": "hs*", + "macaddress": "704F57*" + }, + { + "hostname": "k[lps]*", + "macaddress": "74DA88*" + }, + { + "hostname": "p3*", + "macaddress": "788CB5*" + }, + { + "hostname": "p1*", + "macaddress": "CC32E5*" + }, + { + "hostname": "k[lps]*", + "macaddress": "CC32E5*" + }, + { + "hostname": "hs*", + "macaddress": "CC32E5*" + }, + { + "hostname": "k[lps]*", + "macaddress": "D80D17*" + }, + { + "hostname": "k[lps]*", + "macaddress": "D84732*" + }, + { + "hostname": "p1*", + "macaddress": "F0A731*" + }, + { + "hostname": "l9*", + "macaddress": "F0A731*" } ], "documentation": "https://www.home-assistant.io/integrations/tplink", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index a087c8ac483..a6722282e35 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -788,6 +788,11 @@ DHCP: list[dict[str, str | bool]] = [ "hostname": "k[lps]*", "macaddress": "54AF97*", }, + { + "domain": "tplink", + "hostname": "l[59]*", + "macaddress": "54AF97*", + }, { "domain": "tplink", "hostname": "k[lps]*", @@ -815,7 +820,7 @@ DHCP: list[dict[str, str | bool]] = [ }, { "domain": "tplink", - "hostname": "l5*", + "hostname": "l[59]*", "macaddress": "3C52A1*", }, { @@ -823,16 +828,41 @@ DHCP: list[dict[str, str | bool]] = [ "hostname": "l5*", "macaddress": "5C628B*", }, + { + "domain": "tplink", + "hostname": "tp*", + "macaddress": "5C628B*", + }, { "domain": "tplink", "hostname": "p1*", "macaddress": "482254*", }, + { + "domain": "tplink", + "hostname": "s5*", + "macaddress": "482254*", + }, { "domain": "tplink", "hostname": "p1*", "macaddress": "30DE4B*", }, + { + "domain": "tplink", + "hostname": "p1*", + "macaddress": "3C52A1*", + }, + { + "domain": "tplink", + "hostname": "tp*", + "macaddress": "3C52A1*", + }, + { + "domain": "tplink", + "hostname": "s5*", + "macaddress": "3C52A1*", + }, { "domain": "tplink", "hostname": "l9*", @@ -843,6 +873,56 @@ DHCP: list[dict[str, str | bool]] = [ "hostname": "l9*", "macaddress": "3460F9*", }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "704F57*", + }, + { + "domain": "tplink", + "hostname": "k[lps]*", + "macaddress": "74DA88*", + }, + { + "domain": "tplink", + "hostname": "p3*", + "macaddress": "788CB5*", + }, + { + "domain": "tplink", + "hostname": "p1*", + "macaddress": "CC32E5*", + }, + { + "domain": "tplink", + "hostname": "k[lps]*", + "macaddress": "CC32E5*", + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "CC32E5*", + }, + { + "domain": "tplink", + "hostname": "k[lps]*", + "macaddress": "D80D17*", + }, + { + "domain": "tplink", + "hostname": "k[lps]*", + "macaddress": "D84732*", + }, + { + "domain": "tplink", + "hostname": "p1*", + "macaddress": "F0A731*", + }, + { + "domain": "tplink", + "hostname": "l9*", + "macaddress": "F0A731*", + }, { "domain": "tuya", "macaddress": "105A17*", From a1cbc62ddc44fdc61d9a7be37df61faa0a4c2e0a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 02:03:38 -0500 Subject: [PATCH 0139/1367] Add new climate feature flags to mysensors (#109471) Adds new climate feature flags to mysensors --- homeassistant/components/mysensors/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index d532135304a..0058fca021e 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -70,11 +70,12 @@ class MySensorsHVAC(mysensors.device.MySensorsChildEntity, ClimateEntity): """Representation of a MySensors HVAC.""" _attr_hvac_modes = OPERATION_LIST + _enable_turn_on_off_backwards_compatibility = False @property def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" - features = ClimateEntityFeature(0) + features = ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON set_req = self.gateway.const.SetReq if set_req.V_HVAC_SPEED in self._values: features = features | ClimateEntityFeature.FAN_MODE From 6f9876d5e0e2b5136d2ce4f1e9d91b65fed8fbe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 3 Feb 2024 08:16:28 +0100 Subject: [PATCH 0140/1367] Extend the history of Elvia history to 3 years (#109490) Extend the history of Elvia data to 3 years --- homeassistant/components/elvia/config_flow.py | 4 ++- homeassistant/components/elvia/importer.py | 36 +++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/elvia/config_flow.py b/homeassistant/components/elvia/config_flow.py index e65c93b09a6..fb50842e39b 100644 --- a/homeassistant/components/elvia/config_flow.py +++ b/homeassistant/components/elvia/config_flow.py @@ -35,8 +35,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._api_token = api_token = user_input[CONF_API_TOKEN] client = Elvia(meter_value_token=api_token).meter_value() try: + end_time = dt_util.utcnow() results = await client.get_meter_values( - start_time=(dt_util.now() - timedelta(hours=1)).isoformat() + start_time=(end_time - timedelta(hours=1)).isoformat(), + end_time=end_time.isoformat(), ) except ElviaError.AuthError as exception: diff --git a/homeassistant/components/elvia/importer.py b/homeassistant/components/elvia/importer.py index 69e3d64d09d..097db51cab8 100644 --- a/homeassistant/components/elvia/importer.py +++ b/homeassistant/components/elvia/importer.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import datetime, timedelta from typing import TYPE_CHECKING, cast -from elvia import Elvia +from elvia import Elvia, error as ElviaError from homeassistant.components.recorder.models import StatisticData, StatisticMetaData from homeassistant.components.recorder.statistics import ( @@ -68,21 +68,37 @@ class ElviaImporter: ) if not last_stats: - # First time we insert 1 years of data (if available) + # First time we insert 3 years of data (if available) + hourly_data: list[MeterValueTimeSeries] = [] until = dt_util.utcnow() - hourly_data = await self._fetch_hourly_data( - since=until - timedelta(days=365), - until=until, - ) + for year in (3, 2, 1): + try: + year_hours = await self._fetch_hourly_data( + since=until - timedelta(days=365 * year), + until=until - timedelta(days=365 * (year - 1)), + ) + except ElviaError.ElviaException: + # This will raise if the contract have no data for the + # year, we can safely ignore this + continue + hourly_data.extend(year_hours) + if hourly_data is None or len(hourly_data) == 0: + LOGGER.error("No data available for the metering point") return last_stats_time = None _sum = 0.0 else: - hourly_data = await self._fetch_hourly_data( - since=dt_util.utc_from_timestamp(last_stats[statistic_id][0]["end"]), - until=dt_util.utcnow(), - ) + try: + hourly_data = await self._fetch_hourly_data( + since=dt_util.utc_from_timestamp( + last_stats[statistic_id][0]["end"] + ), + until=dt_util.utcnow(), + ) + except ElviaError.ElviaException as err: + LOGGER.error("Error fetching data: %s", err) + return if ( hourly_data is None From fe4dd2cb93d91a6d29e55524e3488f7c2e3d7f96 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 3 Feb 2024 09:00:00 +0100 Subject: [PATCH 0141/1367] Improve color mode handling in light groups (#109390) * Improve color mode handling in light groups * Update config flow test --- homeassistant/components/group/light.py | 42 ++++++++++++++-------- tests/components/group/test_config_flow.py | 2 +- tests/components/group/test_light.py | 40 ++++++++------------- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 5a113491891..c8689cdaa1c 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -30,6 +30,7 @@ from homeassistant.components.light import ( ColorMode, LightEntity, LightEntityFeature, + filter_supported_color_modes, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -162,6 +163,9 @@ class LightGroup(GroupEntity, LightEntity): if mode: self.mode = all + self._attr_color_mode = ColorMode.UNKNOWN + self._attr_supported_color_modes = {ColorMode.ONOFF} + async def async_turn_on(self, **kwargs: Any) -> None: """Forward the turn_on command to all lights in the light group.""" data = { @@ -261,26 +265,36 @@ class LightGroup(GroupEntity, LightEntity): effects_count = Counter(itertools.chain(all_effects)) self._attr_effect = effects_count.most_common(1)[0][0] - self._attr_color_mode = None - all_color_modes = list(find_state_attributes(on_states, ATTR_COLOR_MODE)) - if all_color_modes: - # Report the most common color mode, select brightness and onoff last - color_mode_count = Counter(itertools.chain(all_color_modes)) - if ColorMode.ONOFF in color_mode_count: - color_mode_count[ColorMode.ONOFF] = -1 - if ColorMode.BRIGHTNESS in color_mode_count: - color_mode_count[ColorMode.BRIGHTNESS] = 0 - self._attr_color_mode = color_mode_count.most_common(1)[0][0] - - self._attr_supported_color_modes = None + supported_color_modes = {ColorMode.ONOFF} all_supported_color_modes = list( find_state_attributes(states, ATTR_SUPPORTED_COLOR_MODES) ) if all_supported_color_modes: # Merge all color modes. - self._attr_supported_color_modes = cast( - set[str], set().union(*all_supported_color_modes) + supported_color_modes = filter_supported_color_modes( + cast(set[ColorMode], set().union(*all_supported_color_modes)) ) + self._attr_supported_color_modes = supported_color_modes + + self._attr_color_mode = ColorMode.UNKNOWN + all_color_modes = list(find_state_attributes(on_states, ATTR_COLOR_MODE)) + if all_color_modes: + # Report the most common color mode, select brightness and onoff last + color_mode_count = Counter(itertools.chain(all_color_modes)) + if ColorMode.ONOFF in color_mode_count: + if ColorMode.ONOFF in supported_color_modes: + color_mode_count[ColorMode.ONOFF] = -1 + else: + color_mode_count.pop(ColorMode.ONOFF) + if ColorMode.BRIGHTNESS in color_mode_count: + if ColorMode.BRIGHTNESS in supported_color_modes: + color_mode_count[ColorMode.BRIGHTNESS] = 0 + else: + color_mode_count.pop(ColorMode.BRIGHTNESS) + if color_mode_count: + self._attr_color_mode = color_mode_count.most_common(1)[0][0] + else: + self._attr_color_mode = next(iter(supported_color_modes)) self._attr_supported_features = LightEntityFeature(0) for support in find_state_attributes(states, ATTR_SUPPORTED_FEATURES): diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 7b83ed9eb0d..9db70ca80d1 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -479,7 +479,7 @@ LIGHT_ATTRS = [ "supported_color_modes": ["onoff"], "supported_features": 0, }, - {"color_mode": "onoff"}, + {"color_mode": "unknown"}, ] LOCK_ATTRS = [{"supported_features": 1}, {}] MEDIA_PLAYER_ATTRS = [{"supported_features": 0}, {}] diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 59f0a5b7d55..63f21456066 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -28,9 +28,6 @@ from homeassistant.components.light import ( SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, ColorMode, ) from homeassistant.const import ( @@ -278,10 +275,8 @@ async def test_brightness( entity0.brightness = 255 entity1 = platform.ENTITIES[1] - entity1.supported_features = SUPPORT_BRIGHTNESS - # Set color modes to none to trigger backwards compatibility in LightEntity - entity1.supported_color_modes = None - entity1.color_mode = None + entity1.supported_color_modes = {ColorMode.BRIGHTNESS} + entity1.color_mode = ColorMode.BRIGHTNESS assert await async_setup_component( hass, @@ -352,10 +347,8 @@ async def test_color_hs(hass: HomeAssistant, enable_custom_integrations: None) - entity0.hs_color = (0, 100) entity1 = platform.ENTITIES[1] - entity1.supported_features = SUPPORT_COLOR - # Set color modes to none to trigger backwards compatibility in LightEntity - entity1.supported_color_modes = None - entity1.color_mode = None + entity1.supported_color_modes = {ColorMode.HS} + entity1.color_mode = ColorMode.HS assert await async_setup_component( hass, @@ -703,10 +696,8 @@ async def test_color_temp( entity0.color_temp_kelvin = 2 entity1 = platform.ENTITIES[1] - entity1.supported_features = SUPPORT_COLOR_TEMP - # Set color modes to none to trigger backwards compatibility in LightEntity - entity1.supported_color_modes = None - entity1.color_mode = None + entity1.supported_color_modes = {ColorMode.COLOR_TEMP} + entity1.color_mode = ColorMode.COLOR_TEMP assert await async_setup_component( hass, @@ -846,10 +837,8 @@ async def test_min_max_mireds( entity0._attr_max_color_temp_kelvin = 5 entity1 = platform.ENTITIES[1] - entity1.supported_features = SUPPORT_COLOR_TEMP - # Set color modes to none to trigger backwards compatibility in LightEntity - entity1.supported_color_modes = None - entity1.color_mode = None + entity1.supported_color_modes = {ColorMode.COLOR_TEMP} + entity1.color_mode = ColorMode.COLOR_TEMP entity1._attr_min_color_temp_kelvin = 1 entity1._attr_max_color_temp_kelvin = 1234567890 @@ -1021,15 +1010,15 @@ async def test_supported_color_modes( entity0 = platform.ENTITIES[0] entity0.supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} + entity0.color_mode = ColorMode.UNKNOWN entity1 = platform.ENTITIES[1] entity1.supported_color_modes = {ColorMode.RGBW, ColorMode.RGBWW} + entity1.color_mode = ColorMode.UNKNOWN entity2 = platform.ENTITIES[2] - entity2.supported_features = SUPPORT_BRIGHTNESS - # Set color modes to none to trigger backwards compatibility in LightEntity - entity2.supported_color_modes = None - entity2.color_mode = None + entity2.supported_color_modes = {ColorMode.BRIGHTNESS} + entity2.color_mode = ColorMode.UNKNOWN assert await async_setup_component( hass, @@ -1051,7 +1040,6 @@ async def test_supported_color_modes( state = hass.states.get("light.light_group") assert set(state.attributes[ATTR_SUPPORTED_COLOR_MODES]) == { - "brightness", "color_temp", "hs", "rgbw", @@ -1198,6 +1186,7 @@ async def test_color_mode2( await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.COLOR_TEMP] assert state.attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP await hass.services.async_call( @@ -1208,7 +1197,8 @@ async def test_color_mode2( ) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.COLOR_TEMP] + assert state.attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP async def test_supported_features(hass: HomeAssistant) -> None: From d44b00f8513b24f844095748e073bdd7fecb067d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 3 Feb 2024 09:14:52 +0100 Subject: [PATCH 0142/1367] Change IoT class for Traccar Client (#109493) --- homeassistant/components/traccar/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 978a0b2f507..c3b9e540ab6 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -5,7 +5,7 @@ "config_flow": true, "dependencies": ["webhook"], "documentation": "https://www.home-assistant.io/integrations/traccar", - "iot_class": "local_polling", + "iot_class": "cloud_push", "loggers": ["pytraccar"], "requirements": ["pytraccar==2.0.0", "stringcase==1.2.0"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 9ae87cbd706..c49882f4394 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6173,7 +6173,7 @@ "traccar": { "integration_type": "hub", "config_flow": true, - "iot_class": "local_polling", + "iot_class": "cloud_push", "name": "Traccar Client" }, "traccar_server": { From 68797feac5442ca6b9f4730b771d9da31a3ceaa9 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 3 Feb 2024 02:20:10 -0600 Subject: [PATCH 0143/1367] Do not suggest area for portable Sonos speakers (#109350) * Do not suggest area for portable speakers * Update tests * Improve readability, update tests --- homeassistant/components/sonos/entity.py | 6 +++- tests/components/sonos/test_media_player.py | 31 +++++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 90cadcdad37..05b69c54c50 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -76,6 +76,10 @@ class SonosEntity(Entity): @property def device_info(self) -> DeviceInfo: """Return information about the device.""" + suggested_area: str | None = None + if not self.speaker.battery_info: + # Only set suggested area for non-portable devices + suggested_area = self.speaker.zone_name return DeviceInfo( identifiers={(DOMAIN, self.soco.uid)}, name=self.speaker.zone_name, @@ -86,7 +90,7 @@ class SonosEntity(Entity): (dr.CONNECTION_UPNP, f"uuid:{self.speaker.uid}"), }, manufacturer="Sonos", - suggested_area=self.speaker.zone_name, + suggested_area=suggested_area, configuration_url=f"http://{self.soco.ip_address}:1400/support/review", ) diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index fa37b2210e7..ddf550dc376 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,26 +1,45 @@ """Tests for the Sonos Media Player platform.""" from homeassistant.const import STATE_IDLE from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + CONNECTION_UPNP, + DeviceRegistry, +) async def test_device_registry( - hass: HomeAssistant, async_autosetup_sonos, soco + hass: HomeAssistant, device_registry: DeviceRegistry, async_autosetup_sonos, soco ) -> None: """Test sonos device registered in the device registry.""" - device_registry = dr.async_get(hass) reg_device = device_registry.async_get_device( identifiers={("sonos", "RINCON_test")} ) + assert reg_device is not None assert reg_device.model == "Model Name" assert reg_device.sw_version == "13.1" assert reg_device.connections == { - (dr.CONNECTION_NETWORK_MAC, "00:11:22:33:44:55"), - (dr.CONNECTION_UPNP, "uuid:RINCON_test"), + (CONNECTION_NETWORK_MAC, "00:11:22:33:44:55"), + (CONNECTION_UPNP, "uuid:RINCON_test"), } assert reg_device.manufacturer == "Sonos" - assert reg_device.suggested_area == "Zone A" assert reg_device.name == "Zone A" + # Default device provides battery info, area should not be suggested + assert reg_device.suggested_area is None + + +async def test_device_registry_not_portable( + hass: HomeAssistant, device_registry: DeviceRegistry, async_setup_sonos, soco +) -> None: + """Test non-portable sonos device registered in the device registry to ensure area suggested.""" + soco.get_battery_info.return_value = {} + await async_setup_sonos() + + reg_device = device_registry.async_get_device( + identifiers={("sonos", "RINCON_test")} + ) + assert reg_device is not None + assert reg_device.suggested_area == "Zone A" async def test_entity_basic( From 28337fb941fd4d83d3cb35afa24174224224bc1f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 05:07:56 -0500 Subject: [PATCH 0144/1367] Add new climate feature flags to demo (#109481) --- homeassistant/components/demo/climate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index b857f98e2da..745a2473939 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -97,6 +97,7 @@ class DemoClimate(ClimateEntity): _attr_name = None _attr_should_poll = False _attr_translation_key = "ubercool" + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -137,6 +138,9 @@ class DemoClimate(ClimateEntity): self._attr_supported_features |= ( ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) self._target_temperature = target_temperature self._target_humidity = target_humidity self._unit_of_measurement = unit_of_measurement From f15aa037ef71dacb3c0b1827d572fdebdb7f33cc Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 05:17:20 -0500 Subject: [PATCH 0145/1367] Add new climate feature flags to deconz (#109482) --- homeassistant/components/deconz/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index eb1d0d6b672..35a0e810c9e 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -100,6 +100,7 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity): TYPE = DOMAIN _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device: Thermostat, gateway: DeconzGateway) -> None: """Set up thermostat device.""" @@ -119,7 +120,11 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity): HVAC_MODE_TO_DECONZ[item]: item for item in self._attr_hvac_modes } - self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) if device.fan_mode: self._attr_supported_features |= ClimateEntityFeature.FAN_MODE From e0f0159304a5b7d08fb08dd27ea657f11a1a5399 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 05:18:00 -0500 Subject: [PATCH 0146/1367] Add new climate feature flags to blebox (#109478) --- homeassistant/components/blebox/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py index e4ac8985ebd..1350f1f29a2 100644 --- a/homeassistant/components/blebox/climate.py +++ b/homeassistant/components/blebox/climate.py @@ -53,8 +53,13 @@ async def async_setup_entry( class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEntity): """Representation of a BleBox climate feature (saunaBox).""" - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False @property def hvac_modes(self): From 98892f5b41684a663baa294ffeb95a8aa7c5621e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 05:18:40 -0500 Subject: [PATCH 0147/1367] Add new feature flags to melcloud (#109468) --- homeassistant/components/melcloud/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index 9d2a4f08257..ed37ff76b76 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -114,6 +114,7 @@ class MelCloudClimate(ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_has_entity_name = True _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device: MelCloudDevice) -> None: """Initialize the climate.""" @@ -137,6 +138,8 @@ class AtaDeviceClimate(MelCloudClimate): ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.SWING_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) def __init__(self, device: MelCloudDevice, ata_device: AtaDevice) -> None: From c233a12946ef2479f1d99d7ca4ac8b0db87030fe Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sat, 3 Feb 2024 11:51:23 +0100 Subject: [PATCH 0148/1367] Fix Tankerkoenig diagnostics file to use right format (#109494) Fix tankerkoenig diagnostics file --- homeassistant/components/tankerkoenig/diagnostics.py | 6 +++++- .../components/tankerkoenig/snapshots/test_diagnostics.ambr | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tankerkoenig/diagnostics.py b/homeassistant/components/tankerkoenig/diagnostics.py index 811ec07ef19..d5fd7c8cada 100644 --- a/homeassistant/components/tankerkoenig/diagnostics.py +++ b/homeassistant/components/tankerkoenig/diagnostics.py @@ -1,6 +1,7 @@ """Diagnostics support for Tankerkoenig.""" from __future__ import annotations +from dataclasses import asdict from typing import Any from homeassistant.components.diagnostics import async_redact_data @@ -27,6 +28,9 @@ async def async_get_config_entry_diagnostics( diag_data = { "entry": async_redact_data(entry.as_dict(), TO_REDACT), - "data": coordinator.data, + "data": { + station_id: asdict(price_info) + for station_id, price_info in coordinator.data.items() + }, } return diag_data diff --git a/tests/components/tankerkoenig/snapshots/test_diagnostics.ambr b/tests/components/tankerkoenig/snapshots/test_diagnostics.ambr index a27a210c46e..f52cb3a88a5 100644 --- a/tests/components/tankerkoenig/snapshots/test_diagnostics.ambr +++ b/tests/components/tankerkoenig/snapshots/test_diagnostics.ambr @@ -3,8 +3,10 @@ dict({ 'data': dict({ '3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8': dict({ - '__type': "", - 'repr': "PriceInfo(status=, e5=1.719, e10=1.659, diesel=1.659)", + 'diesel': 1.659, + 'e10': 1.659, + 'e5': 1.719, + 'status': 'open', }), }), 'entry': dict({ From 5b9a3d5bd5cd00344dca28a96b558781edebe552 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:08:58 -0500 Subject: [PATCH 0149/1367] Add migrated ClimateEntityFeature to MQTT (#109419) --- homeassistant/components/mqtt/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 3df9db0d5d0..94311eeda61 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -610,6 +610,7 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): _attributes_extra_blocked = MQTT_CLIMATE_ATTRIBUTES_BLOCKED _attr_target_temperature_low: float | None = None _attr_target_temperature_high: float | None = None + _enable_turn_on_off_backwards_compatibility = False @staticmethod def config_schema() -> vol.Schema: From 8bec20ffa702304de4aca8a95f5b5a3c00dc35e4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:11:19 -0500 Subject: [PATCH 0150/1367] Add ClimateEntityFeatures to Nest (#109417) --- homeassistant/components/nest/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 03fb79eb78e..2d0186b2bfd 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -100,6 +100,7 @@ class ThermostatEntity(ClimateEntity): _attr_has_entity_name = True _attr_should_poll = False _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device: Device) -> None: """Initialize ThermostatEntity.""" @@ -246,7 +247,7 @@ class ThermostatEntity(ClimateEntity): def _get_supported_features(self) -> ClimateEntityFeature: """Compute the bitmap of supported features from the current state.""" - features = ClimateEntityFeature(0) + features = ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON 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: From 96feec9cbf9b09a32cd8c0851ea1d76b5c613ce7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:11:39 -0500 Subject: [PATCH 0151/1367] Add migrated climate feature flags to smartthings (#109426) --- homeassistant/components/smartthings/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 656a198f42b..4c2afa45b7f 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -162,6 +162,8 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: class SmartThingsThermostat(SmartThingsEntity, ClimateEntity): """Define a SmartThings climate entities.""" + _enable_turn_on_off_backwards_compatibility = False + def __init__(self, device): """Init the class.""" super().__init__(device) @@ -343,6 +345,7 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): """Define a SmartThings Air Conditioner.""" _hvac_modes: list[HVACMode] + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device) -> None: """Init the class.""" From 6c8636ae7bf29ba1916ed1479c26012ec42b6cc9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:13:20 -0500 Subject: [PATCH 0152/1367] Add new climate feature flags to evohome (#109429) --- homeassistant/components/evohome/climate.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index a1c46f3d331..8b74d31cc0d 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -156,6 +156,7 @@ class EvoClimateEntity(EvoDevice, ClimateEntity): """Base for an evohome Climate device.""" _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False @property def hvac_modes(self) -> list[HVACMode]: @@ -190,7 +191,10 @@ class EvoZone(EvoChild, EvoClimateEntity): ] self._attr_supported_features = ( - ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE + ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) async def async_zone_svc_request(self, service: str, data: dict[str, Any]) -> None: @@ -372,6 +376,9 @@ class EvoController(EvoClimateEntity): ] if self._attr_preset_modes: self._attr_supported_features = ClimateEntityFeature.PRESET_MODE + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) async def async_tcs_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (system mode) for a controller. From 29556465de5094ef90e21de8d89409801c62ef78 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:13:52 -0500 Subject: [PATCH 0153/1367] Add migrated climate feature flags to vicare (#109439) --- homeassistant/components/vicare/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 7c47629530a..ba2665ac083 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -157,6 +157,7 @@ class ViCareClimate(ViCareEntity, ClimateEntity): _attr_preset_modes = list(HA_TO_VICARE_PRESET_HEATING) _current_action: bool | None = None _current_mode: str | None = None + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From c6ea57458c1f7898e99a6e914eb531c906225ebd Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Sat, 3 Feb 2024 05:14:33 -0600 Subject: [PATCH 0154/1367] Pass slots to error messages instead of IDs [rework] (#109410) Co-authored-by: tetele --- .../components/conversation/__init__.py | 2 +- .../components/conversation/default_agent.py | 28 ++++--- .../components/conversation/manifest.json | 2 +- homeassistant/helpers/intent.py | 23 +++-- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../conversation/snapshots/test_init.ambr | 12 +-- .../conversation/test_default_agent.py | 84 ++++++++++++------- 9 files changed, 95 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 7ca7fec115f..09b0e8e2310 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -349,7 +349,7 @@ async def websocket_hass_agent_debug( }, # Slot values that would be received by the intent "slots": { # direct access to values - entity_key: entity.value + entity_key: entity.text or entity.value for entity_key, entity in result.entities.items() }, # Extra slot details, such as the originally matched text diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index a2cb3b68041..fb33d87e107 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -1,4 +1,5 @@ """Standard conversation implementation for Home Assistant.""" + from __future__ import annotations import asyncio @@ -264,9 +265,11 @@ class DefaultAgent(AbstractConversationAgent): _LOGGER.debug( "Recognized intent '%s' for template '%s' but had unmatched: %s", result.intent.name, - result.intent_sentence.text - if result.intent_sentence is not None - else "", + ( + result.intent_sentence.text + if result.intent_sentence is not None + else "" + ), result.unmatched_entities_list, ) error_response_type, error_response_args = _get_unmatched_response(result) @@ -285,7 +288,8 @@ class DefaultAgent(AbstractConversationAgent): # Slot values to pass to the intent slots = { - entity.name: {"value": entity.value} for entity in result.entities_list + entity.name: {"value": entity.value, "text": entity.text or entity.value} + for entity in result.entities_list } try: @@ -474,9 +478,11 @@ class DefaultAgent(AbstractConversationAgent): for entity_name, entity_value in recognize_result.entities.items() }, # First matched or unmatched state - "state": template.TemplateState(self.hass, state1) - if state1 is not None - else None, + "state": ( + template.TemplateState(self.hass, state1) + if state1 is not None + else None + ), "query": { # Entity states that matched the query (e.g, "on") "matched": [ @@ -734,7 +740,7 @@ class DefaultAgent(AbstractConversationAgent): if not entity: # Default name - entity_names.append((state.name, state.name, context)) + entity_names.append((state.name, state.entity_id, context)) continue if entity.aliases: @@ -742,10 +748,10 @@ class DefaultAgent(AbstractConversationAgent): if not alias.strip(): continue - entity_names.append((alias, alias, context)) + entity_names.append((alias, state.entity_id, context)) # Default name - entity_names.append((state.name, state.name, context)) + entity_names.append((state.name, state.entity_id, context)) # Expose all areas areas = ar.async_get(self.hass) @@ -785,7 +791,7 @@ class DefaultAgent(AbstractConversationAgent): if device_area is None: return None - return {"area": device_area.id} + return {"area": {"value": device_area.id, "text": device_area.name}} def _get_error_text( self, diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 1e46170024c..e4317052b04 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.6.0", "home-assistant-intents==2024.2.2"] + "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.2.2"] } diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 26468f1fdb7..fe399659a56 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -1,4 +1,5 @@ """Module to coordinate user intentions.""" + from __future__ import annotations import asyncio @@ -401,17 +402,21 @@ class ServiceIntentHandler(IntentHandler): hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - name: str | None = slots.get("name", {}).get("value") - if name == "all": + name_slot = slots.get("name", {}) + entity_id: str | None = name_slot.get("value") + entity_name: str | None = name_slot.get("text") + if entity_id == "all": # Don't match on name if targeting all entities - name = None + entity_id = None # Look up area first to fail early - area_name = slots.get("area", {}).get("value") + area_slot = slots.get("area", {}) + area_id = area_slot.get("value") + area_name = area_slot.get("text") area: area_registry.AreaEntry | None = None - if area_name is not None: + if area_id is not None: areas = area_registry.async_get(hass) - area = areas.async_get_area(area_name) or areas.async_get_area_by_name( + area = areas.async_get_area(area_id) or areas.async_get_area_by_name( area_name ) if area is None: @@ -431,7 +436,7 @@ class ServiceIntentHandler(IntentHandler): states = list( async_match_states( hass, - name=name, + name=entity_id, area=area, domains=domains, device_classes=device_classes, @@ -442,8 +447,8 @@ class ServiceIntentHandler(IntentHandler): if not states: # No states matched constraints raise NoStatesMatchedError( - name=name, - area=area_name, + name=entity_name or entity_id, + area=area_name or area_id, domains=domains, device_classes=device_classes, ) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 96efd08bc1f..7746745da6b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,7 +26,7 @@ ha-av==10.1.1 ha-ffmpeg==3.1.0 habluetooth==2.4.0 hass-nabucasa==0.76.0 -hassil==1.6.0 +hassil==1.6.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240202.0 home-assistant-intents==2024.2.2 diff --git a/requirements_all.txt b/requirements_all.txt index 7c4e1045123..6b87dd3d5b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1025,7 +1025,7 @@ hass-nabucasa==0.76.0 hass-splunk==0.1.1 # homeassistant.components.conversation -hassil==1.6.0 +hassil==1.6.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 14cd5e5f8a1..f3674dd283c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -830,7 +830,7 @@ habluetooth==2.4.0 hass-nabucasa==0.76.0 # homeassistant.components.conversation -hassil==1.6.0 +hassil==1.6.1 # 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 468f3215cb7..034bfafc1f5 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -1397,7 +1397,7 @@ 'name': dict({ 'name': 'name', 'text': 'my cool light', - 'value': 'my cool light', + 'value': 'light.kitchen', }), }), 'intent': dict({ @@ -1422,7 +1422,7 @@ 'name': dict({ 'name': 'name', 'text': 'my cool light', - 'value': 'my cool light', + 'value': 'light.kitchen', }), }), 'intent': dict({ @@ -1498,7 +1498,7 @@ 'sentence_template': '[tell me] how many {on_off_domains:domain} (is|are) {on_off_states:state} [in ]', 'slots': dict({ 'area': 'kitchen', - 'domain': 'light', + 'domain': 'lights', 'state': 'on', }), 'source': 'builtin', @@ -1572,7 +1572,7 @@ 'name': dict({ 'name': 'name', 'text': 'test light', - 'value': 'test light', + 'value': 'light.demo_1234', }), }), 'intent': dict({ @@ -1581,7 +1581,7 @@ 'match': True, 'sentence_template': '[] brightness [to] ', 'slots': dict({ - 'brightness': 100, + 'brightness': '100%', 'name': 'test light', }), 'source': 'builtin', @@ -1604,7 +1604,7 @@ 'name': dict({ 'name': 'name', 'text': 'test light', - 'value': 'test light', + 'value': 'light.demo_1234', }), }), 'intent': dict({ diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index d7182aa3c2f..0cf343a3e20 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -1,4 +1,5 @@ """Test for the default agent.""" + from collections import defaultdict from unittest.mock import AsyncMock, patch @@ -85,8 +86,10 @@ async def test_exposed_areas( entity_registry: er.EntityRegistry, ) -> None: """Test that all areas are exposed.""" - area_kitchen = area_registry.async_get_or_create("kitchen") - area_bedroom = area_registry.async_get_or_create("bedroom") + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") + area_bedroom = area_registry.async_get_or_create("bedroom_id") + area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom") entry = MockConfigEntry() entry.add_to_hass(hass) @@ -122,6 +125,9 @@ async def test_exposed_areas( # All is well for the exposed kitchen light assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.intent is not None + assert result.response.intent.slots["area"]["value"] == area_kitchen.id + assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name # Bedroom has no exposed entities result = await conversation.async_converse( @@ -195,7 +201,8 @@ async def test_unexposed_entities_skipped( entity_registry: er.EntityRegistry, ) -> None: """Test that unexposed entities are skipped in exposed areas.""" - area_kitchen = area_registry.async_get_or_create("kitchen") + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") # Both lights are in the kitchen exposed_light = entity_registry.async_get_or_create("light", "demo", "1234") @@ -224,6 +231,9 @@ async def test_unexposed_entities_skipped( assert len(calls) == 1 assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.intent is not None + assert result.response.intent.slots["area"]["value"] == area_kitchen.id + assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name # Only one light should be returned hass.states.async_set(exposed_light.entity_id, "on") @@ -314,8 +324,10 @@ async def test_device_area_context( turn_on_calls = async_mock_service(hass, "light", "turn_on") turn_off_calls = async_mock_service(hass, "light", "turn_off") - area_kitchen = area_registry.async_get_or_create("Kitchen") - area_bedroom = area_registry.async_get_or_create("Bedroom") + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") + area_bedroom = area_registry.async_get_or_create("bedroom_id") + area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom") # Create 2 lights in each area area_lights = defaultdict(list) @@ -363,13 +375,14 @@ async def test_device_area_context( assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert result.response.intent is not None assert result.response.intent.slots["area"]["value"] == area_kitchen.id + assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name # Verify only kitchen lights were targeted assert {s.entity_id for s in result.response.matched_states} == { - e.entity_id for e in area_lights["kitchen"] + e.entity_id for e in area_lights[area_kitchen.id] } assert {c.data["entity_id"][0] for c in turn_on_calls} == { - e.entity_id for e in area_lights["kitchen"] + e.entity_id for e in area_lights[area_kitchen.id] } turn_on_calls.clear() @@ -386,13 +399,14 @@ async def test_device_area_context( assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert result.response.intent is not None assert result.response.intent.slots["area"]["value"] == area_bedroom.id + assert result.response.intent.slots["area"]["text"] == area_bedroom.normalized_name # Verify only bedroom lights were targeted assert {s.entity_id for s in result.response.matched_states} == { - e.entity_id for e in area_lights["bedroom"] + e.entity_id for e in area_lights[area_bedroom.id] } assert {c.data["entity_id"][0] for c in turn_on_calls} == { - e.entity_id for e in area_lights["bedroom"] + e.entity_id for e in area_lights[area_bedroom.id] } turn_on_calls.clear() @@ -409,13 +423,14 @@ async def test_device_area_context( assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert result.response.intent is not None assert result.response.intent.slots["area"]["value"] == area_bedroom.id + assert result.response.intent.slots["area"]["text"] == area_bedroom.normalized_name # Verify only bedroom lights were targeted assert {s.entity_id for s in result.response.matched_states} == { - e.entity_id for e in area_lights["bedroom"] + e.entity_id for e in area_lights[area_bedroom.id] } assert {c.data["entity_id"][0] for c in turn_off_calls} == { - e.entity_id for e in area_lights["bedroom"] + e.entity_id for e in area_lights[area_bedroom.id] } turn_off_calls.clear() @@ -463,7 +478,8 @@ async def test_error_no_device_in_area( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: """Test error message when area is missing a device/entity.""" - area_registry.async_get_or_create("kitchen") + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") result = await conversation.async_converse( hass, "turn on missing entity in the kitchen", None, Context(), None ) @@ -482,7 +498,7 @@ async def test_error_no_domain( """Test error message when no devices/entities exist for a domain.""" # We don't have a sentence for turning on all fans - fan_domain = MatchEntity(name="domain", value="fan", text="") + fan_domain = MatchEntity(name="domain", value="fan", text="fans") recognize_result = RecognizeResult( intent=Intent("HassTurnOn"), intent_data=IntentData([]), @@ -513,7 +529,8 @@ async def test_error_no_domain_in_area( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: """Test error message when no devices/entities for a domain exist in an area.""" - area_registry.async_get_or_create("kitchen") + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") result = await conversation.async_converse( hass, "turn on the lights in the kitchen", None, Context(), None ) @@ -526,13 +543,11 @@ async def test_error_no_domain_in_area( ) -async def test_error_no_device_class( - hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry -) -> None: +async def test_error_no_device_class(hass: HomeAssistant, init_components) -> None: """Test error message when no entities of a device class exist.""" # We don't have a sentence for opening all windows - window_class = MatchEntity(name="device_class", value="window", text="") + window_class = MatchEntity(name="device_class", value="window", text="windows") recognize_result = RecognizeResult( intent=Intent("HassTurnOn"), intent_data=IntentData([]), @@ -563,7 +578,8 @@ async def test_error_no_device_class_in_area( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: """Test error message when no entities of a device class exist in an area.""" - area_registry.async_get_or_create("bedroom") + area_bedroom = area_registry.async_get_or_create("bedroom_id") + area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom") result = await conversation.async_converse( hass, "open bedroom windows", None, Context(), None ) @@ -600,7 +616,8 @@ async def test_no_states_matched_default_error( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: """Test default response when no states match and slots are missing.""" - area_registry.async_get_or_create("kitchen") + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") with patch( "homeassistant.components.conversation.default_agent.intent.async_handle", @@ -629,9 +646,9 @@ async def test_empty_aliases( entity_registry: er.EntityRegistry, ) -> None: """Test that empty aliases are not added to slot lists.""" - area_kitchen = area_registry.async_get_or_create("kitchen") - assert area_kitchen.id is not None - area_registry.async_update(area_kitchen.id, aliases={" "}) + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") + area_kitchen = area_registry.async_update(area_kitchen.id, aliases={" "}) entry = MockConfigEntry() entry.add_to_hass(hass) @@ -643,11 +660,16 @@ async def test_empty_aliases( device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id) kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234") - entity_registry.async_update_entity( - kitchen_light.entity_id, device_id=kitchen_device.id, aliases={" "} + kitchen_light = entity_registry.async_update_entity( + kitchen_light.entity_id, + device_id=kitchen_device.id, + name="kitchen light", + aliases={" "}, ) hass.states.async_set( - kitchen_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} + kitchen_light.entity_id, + "on", + attributes={ATTR_FRIENDLY_NAME: kitchen_light.name}, ) with patch( @@ -665,16 +687,16 @@ async def test_empty_aliases( assert slot_lists.keys() == {"area", "name"} areas = slot_lists["area"] assert len(areas.values) == 1 - assert areas.values[0].value_out == "kitchen" + assert areas.values[0].value_out == area_kitchen.id + assert areas.values[0].text_in.text == area_kitchen.normalized_name names = slot_lists["name"] assert len(names.values) == 1 - assert names.values[0].value_out == "kitchen light" + assert names.values[0].value_out == kitchen_light.entity_id + assert names.values[0].text_in.text == kitchen_light.name -async def test_all_domains_loaded( - hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry -) -> None: +async def test_all_domains_loaded(hass: HomeAssistant, init_components) -> None: """Test that sentences for all domains are always loaded.""" # light domain is not loaded From 33f3fb32d8c9cc248d0359e2a5f814687eae8ab6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:25:52 -0500 Subject: [PATCH 0155/1367] Add migrated climate feature flags to overkiz (#109463) --- .../overkiz/climate_entities/atlantic_electrical_heater.py | 1 + ...tic_electrical_heater_with_adjustable_temperature_setpoint.py | 1 + .../overkiz/climate_entities/atlantic_electrical_towel_dryer.py | 1 + .../climate_entities/atlantic_heat_recovery_ventilation.py | 1 + .../overkiz/climate_entities/atlantic_pass_apc_heating_zone.py | 1 + .../overkiz/climate_entities/atlantic_pass_apc_zone_control.py | 1 + .../climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py | 1 + .../climate_entities/somfy_heating_temperature_interface.py | 1 + .../components/overkiz/climate_entities/somfy_thermostat.py | 1 + .../climate_entities/valve_heating_temperature_interface.py | 1 + 10 files changed, 10 insertions(+) diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py index 867e977276d..2678986574d 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py @@ -53,6 +53,7 @@ class AtlanticElectricalHeater(OverkizEntity, ClimateEntity): ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False @property def hvac_mode(self) -> HVACMode: diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py index 14237b4601b..36e958fb49c 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py @@ -75,6 +75,7 @@ class AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint( | ClimateEntityFeature.TURN_ON ) _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py index b053611de9b..fefaa75a114 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py @@ -45,6 +45,7 @@ class AtlanticElectricalTowelDryer(OverkizEntity, ClimateEntity): _attr_preset_modes = [*PRESET_MODE_TO_OVERKIZ] _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py b/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py index 115a30a7c36..5876f7df4a7 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py @@ -54,6 +54,7 @@ class AtlanticHeatRecoveryVentilation(OverkizEntity, ClimateEntity): | ClimateEntityFeature.TURN_ON ) _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py index 90bc3e40404..25dab7c1d7e 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py @@ -83,6 +83,7 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity): ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py index 1ef0f9bf400..fe9f20b05fc 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py @@ -30,6 +30,7 @@ class AtlanticPassAPCZoneControl(OverkizEntity, ClimateEntity): _attr_supported_features = ( ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ) + _enable_turn_on_off_backwards_compatibility = False @property def hvac_mode(self) -> HVACMode: diff --git a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py index 162b9b4fce6..9b956acd014 100644 --- a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py +++ b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py @@ -90,6 +90,7 @@ class HitachiAirToAirHeatPumpHLRRWIFI(OverkizEntity, ClimateEntity): _attr_target_temperature_step = 1.0 _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator diff --git a/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py b/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py index cc470dee032..f98865456e1 100644 --- a/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py +++ b/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py @@ -81,6 +81,7 @@ class SomfyHeatingTemperatureInterface(OverkizEntity, ClimateEntity): # Both min and max temp values have been retrieved from the Somfy Application. _attr_min_temp = 15.0 _attr_max_temp = 26.0 + _enable_turn_on_off_backwards_compatibility = False def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator diff --git a/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py index 9a81b6d5bd3..2b6840b463d 100644 --- a/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py +++ b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py @@ -64,6 +64,7 @@ class SomfyThermostat(OverkizEntity, ClimateEntity): _attr_hvac_modes = [*HVAC_MODES_TO_OVERKIZ] _attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ] _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False # Both min and max temp values have been retrieved from the Somfy Application. _attr_min_temp = 15.0 diff --git a/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py b/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py index b58c29a6121..79c360a5f93 100644 --- a/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py +++ b/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py @@ -58,6 +58,7 @@ class ValveHeatingTemperatureInterface(OverkizEntity, ClimateEntity): ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator From 3f7d2da35c1877075078f9322f6ad7d09c8a8658 Mon Sep 17 00:00:00 2001 From: Jurriaan Pruis Date: Sat, 3 Feb 2024 12:31:48 +0100 Subject: [PATCH 0156/1367] Bump matrix-nio to 0.24.0 (#109403) Update matrix-nio to 0.24.0 --- homeassistant/components/matrix/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index a0eb7f3cb5b..0838bcc3764 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/matrix", "iot_class": "cloud_push", "loggers": ["matrix_client"], - "requirements": ["matrix-nio==0.22.1", "Pillow==10.2.0"] + "requirements": ["matrix-nio==0.24.0", "Pillow==10.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6b87dd3d5b0..a501710a518 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1256,7 +1256,7 @@ lxml==5.1.0 mac-vendor-lookup==0.1.12 # homeassistant.components.matrix -matrix-nio==0.22.1 +matrix-nio==0.24.0 # homeassistant.components.maxcube maxcube-api==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3674dd283c..b64fcb944eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -998,7 +998,7 @@ lxml==5.1.0 mac-vendor-lookup==0.1.12 # homeassistant.components.matrix -matrix-nio==0.22.1 +matrix-nio==0.24.0 # homeassistant.components.maxcube maxcube-api==0.4.3 From bb8d7424236b670b2cabdd5132967e4bd7ebb338 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Sat, 3 Feb 2024 12:53:22 +0100 Subject: [PATCH 0157/1367] Ignore gateway devices in ViCare integration (#106477) * filter unsupported devices * Update __init__.py * use debug * remove dead code --- homeassistant/components/vicare/__init__.py | 23 +++++++++--- .../components/vicare/config_flow.py | 2 +- homeassistant/components/vicare/sensor.py | 35 ------------------- 3 files changed, 19 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 603a42bae41..a2b2f3ac769 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -10,6 +10,7 @@ from typing import Any from PyViCare.PyViCare import PyViCare from PyViCare.PyViCareDevice import Device +from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig from PyViCare.PyViCareUtils import ( PyViCareInvalidConfigurationError, PyViCareInvalidCredentialsError, @@ -85,15 +86,16 @@ def setup_vicare_api(hass: HomeAssistant, entry: ConfigEntry) -> None: """Set up PyVicare API.""" vicare_api = vicare_login(hass, entry.data) - for device in vicare_api.devices: - _LOGGER.info( + device_config_list = get_supported_devices(vicare_api.devices) + + for device in device_config_list: + _LOGGER.debug( "Found device: %s (online: %s)", device.getModel(), str(device.isOnline()) ) # Currently we only support a single device - device_list = vicare_api.devices - device = device_list[0] - hass.data[DOMAIN][entry.entry_id][VICARE_DEVICE_CONFIG_LIST] = device_list + device = device_config_list[0] + hass.data[DOMAIN][entry.entry_id][VICARE_DEVICE_CONFIG_LIST] = device_config_list hass.data[DOMAIN][entry.entry_id][VICARE_DEVICE_CONFIG] = device hass.data[DOMAIN][entry.entry_id][VICARE_API] = getattr( device, @@ -113,3 +115,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return unload_ok + + +def get_supported_devices( + devices: list[PyViCareDeviceConfig], +) -> list[PyViCareDeviceConfig]: + """Remove unsupported devices from the list.""" + return [ + device_config + for device_config in devices + if device_config.getModel() not in ["Heatbox1", "Heatbox2_SRC"] + ] diff --git a/homeassistant/components/vicare/config_flow.py b/homeassistant/components/vicare/config_flow.py index 87bfcf7b146..32ae4af0fe7 100644 --- a/homeassistant/components/vicare/config_flow.py +++ b/homeassistant/components/vicare/config_flow.py @@ -118,7 +118,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Invoke when a Viessmann MAC address is discovered on the network.""" formatted_mac = format_mac(discovery_info.macaddress) - _LOGGER.info("Found device with mac %s", formatted_mac) + _LOGGER.debug("Found device with mac %s", formatted_mac) await self.async_set_unique_id(formatted_mac) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 6c794b548ad..a8a21c7e787 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -692,41 +692,6 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ) -def _build_entity( - vicare_api, - device_config: PyViCareDeviceConfig, - entity_description: ViCareSensorEntityDescription, -): - """Create a ViCare sensor entity.""" - if is_supported(entity_description.key, entity_description, vicare_api): - return ViCareSensor( - vicare_api, - device_config, - entity_description, - ) - return None - - -async def _entities_from_descriptions( - hass: HomeAssistant, - entities: list[ViCareSensor], - sensor_descriptions: tuple[ViCareSensorEntityDescription, ...], - iterables, - config_entry: ConfigEntry, -) -> None: - """Create entities from descriptions and list of burners/circuits.""" - for description in sensor_descriptions: - for current in iterables: - entity = await hass.async_add_executor_job( - _build_entity, - current, - hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], - description, - ) - if entity: - entities.append(entity) - - def _build_entities( device: PyViCareDevice, device_config: PyViCareDeviceConfig, From 897ea272d6565cf17b8a6515a4f0511b39a0fc91 Mon Sep 17 00:00:00 2001 From: Cody C <50791984+codyc1515@users.noreply.github.com> Date: Sun, 4 Feb 2024 02:12:55 +1300 Subject: [PATCH 0158/1367] Update Twinkly DHCP discovery addresses (#109495) --- homeassistant/components/twinkly/manifest.json | 3 +++ homeassistant/generated/dhcp.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json index c6ab0bab893..6ec89261b3d 100644 --- a/homeassistant/components/twinkly/manifest.json +++ b/homeassistant/components/twinkly/manifest.json @@ -6,6 +6,9 @@ "dhcp": [ { "hostname": "twinkly_*" + }, + { + "hostname": "twinkly-*" } ], "documentation": "https://www.home-assistant.io/integrations/twinkly", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index a6722282e35..4f9f822e85e 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -971,6 +971,10 @@ DHCP: list[dict[str, str | bool]] = [ "domain": "twinkly", "hostname": "twinkly_*", }, + { + "domain": "twinkly", + "hostname": "twinkly-*", + }, { "domain": "unifiprotect", "macaddress": "B4FBE4*", From 5bda4be88f42927bc4acb048f07ff949b7e04069 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Feb 2024 07:15:56 -0600 Subject: [PATCH 0159/1367] Remove useless _handle_pipeline_event function in ESPHome (#109484) --- homeassistant/components/esphome/manager.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 59f37d3a078..9d52c8eddea 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -21,7 +21,6 @@ from aioesphomeapi import ( UserService, UserServiceArgType, VoiceAssistantAudioSettings, - VoiceAssistantEventType, ) from awesomeversion import AwesomeVersion import voluptuous as vol @@ -330,11 +329,6 @@ class ESPHomeManager: ) ) - 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) @@ -358,7 +352,7 @@ class ESPHomeManager: self.voice_assistant_udp_server = VoiceAssistantUDPServer( hass, self.entry_data, - self._handle_pipeline_event, + self.cli.send_voice_assistant_event, self._handle_pipeline_finished, ) port = await self.voice_assistant_udp_server.start_server() From ad0ee7d781c78755b743324056004180e6796b2e Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 3 Feb 2024 16:34:50 +0100 Subject: [PATCH 0160/1367] Update pyfronius to 0.7.3 (#109507) --- homeassistant/components/fronius/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index 1ec62c54b6c..c2f635119aa 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -12,5 +12,5 @@ "iot_class": "local_polling", "loggers": ["pyfronius"], "quality_scale": "platinum", - "requirements": ["PyFronius==0.7.2"] + "requirements": ["PyFronius==0.7.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index a501710a518..0ea193ddd48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -63,7 +63,7 @@ PyFlick==0.0.2 PyFlume==0.6.5 # homeassistant.components.fronius -PyFronius==0.7.2 +PyFronius==0.7.3 # homeassistant.components.mvglive PyMVGLive==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b64fcb944eb..2adcb052c99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -54,7 +54,7 @@ PyFlick==0.0.2 PyFlume==0.6.5 # homeassistant.components.fronius -PyFronius==0.7.2 +PyFronius==0.7.3 # homeassistant.components.met_eireann PyMetEireann==2021.8.0 From 978d2a79f66a81c0e247d109705839ed1cac5746 Mon Sep 17 00:00:00 2001 From: Aidas Klimas Date: Sat, 3 Feb 2024 19:23:19 +0200 Subject: [PATCH 0161/1367] Bump qingping_ble to 0.10.0 - Add support for GCP22C (Qingping CO2 Temp RH) (#108567) Co-authored-by: J. Nick Koston --- homeassistant/components/qingping/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json index 5cde039c5ce..c25652ca91e 100644 --- a/homeassistant/components/qingping/manifest.json +++ b/homeassistant/components/qingping/manifest.json @@ -20,5 +20,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/qingping", "iot_class": "local_push", - "requirements": ["qingping-ble==0.9.0"] + "requirements": ["qingping-ble==0.10.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0ea193ddd48..0a4ddd55d79 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2384,7 +2384,7 @@ pyzbar==0.1.7 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.9.0 +qingping-ble==0.10.0 # homeassistant.components.qnap qnapstats==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2adcb052c99..868193758bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1827,7 +1827,7 @@ pyyardian==1.1.1 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.9.0 +qingping-ble==0.10.0 # homeassistant.components.qnap qnapstats==0.4.0 From 1c268b172481135ac65ad83a9775046e2960b60b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Feb 2024 12:11:40 -0600 Subject: [PATCH 0162/1367] Bump mopeka-iot-ble to 0.7.0 (#109516) --- homeassistant/components/mopeka/manifest.json | 38 ++++++++++++- homeassistant/generated/bluetooth.py | 54 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 93 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mopeka/manifest.json b/homeassistant/components/mopeka/manifest.json index 69452bf1fec..82afd4d2057 100644 --- a/homeassistant/components/mopeka/manifest.json +++ b/homeassistant/components/mopeka/manifest.json @@ -8,12 +8,48 @@ "manufacturer_data_start": [3], "connectable": false }, + { + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 89, + "manufacturer_data_start": [4], + "connectable": false + }, + { + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 89, + "manufacturer_data_start": [5], + "connectable": false + }, + { + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 89, + "manufacturer_data_start": [6], + "connectable": false + }, { "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", "manufacturer_id": 89, "manufacturer_data_start": [8], "connectable": false }, + { + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 89, + "manufacturer_data_start": [9], + "connectable": false + }, + { + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 89, + "manufacturer_data_start": [10], + "connectable": false + }, + { + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 89, + "manufacturer_data_start": [11], + "connectable": false + }, { "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", "manufacturer_id": 89, @@ -27,5 +63,5 @@ "documentation": "https://www.home-assistant.io/integrations/mopeka", "integration_type": "device", "iot_class": "local_push", - "requirements": ["mopeka-iot-ble==0.5.0"] + "requirements": ["mopeka-iot-ble==0.7.0"] } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 7d32dbfe963..405803ee1c4 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -340,6 +340,33 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "manufacturer_id": 89, "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", }, + { + "connectable": False, + "domain": "mopeka", + "manufacturer_data_start": [ + 4, + ], + "manufacturer_id": 89, + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + }, + { + "connectable": False, + "domain": "mopeka", + "manufacturer_data_start": [ + 5, + ], + "manufacturer_id": 89, + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + }, + { + "connectable": False, + "domain": "mopeka", + "manufacturer_data_start": [ + 6, + ], + "manufacturer_id": 89, + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + }, { "connectable": False, "domain": "mopeka", @@ -349,6 +376,33 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "manufacturer_id": 89, "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", }, + { + "connectable": False, + "domain": "mopeka", + "manufacturer_data_start": [ + 9, + ], + "manufacturer_id": 89, + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + }, + { + "connectable": False, + "domain": "mopeka", + "manufacturer_data_start": [ + 10, + ], + "manufacturer_id": 89, + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + }, + { + "connectable": False, + "domain": "mopeka", + "manufacturer_data_start": [ + 11, + ], + "manufacturer_id": 89, + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + }, { "connectable": False, "domain": "mopeka", diff --git a/requirements_all.txt b/requirements_all.txt index 0a4ddd55d79..accb746d170 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1307,7 +1307,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.3.0 # homeassistant.components.mopeka -mopeka-iot-ble==0.5.0 +mopeka-iot-ble==0.7.0 # homeassistant.components.motion_blinds motionblinds==0.6.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 868193758bd..b63ef4bb8b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1043,7 +1043,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.3.0 # homeassistant.components.mopeka -mopeka-iot-ble==0.5.0 +mopeka-iot-ble==0.7.0 # homeassistant.components.motion_blinds motionblinds==0.6.19 From 7e299c21422e3d69c6c18450e7c6607ff9c64227 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sat, 3 Feb 2024 20:20:17 +0100 Subject: [PATCH 0163/1367] Bump easyenergy lib to v2.1.1 (#109510) --- homeassistant/components/easyenergy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/easyenergy/manifest.json b/homeassistant/components/easyenergy/manifest.json index 6f57ea6ed5f..4dcce0fd705 100644 --- a/homeassistant/components/easyenergy/manifest.json +++ b/homeassistant/components/easyenergy/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/easyenergy", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["easyenergy==2.1.0"] + "requirements": ["easyenergy==2.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index accb746d170..fffa4ba0073 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -746,7 +746,7 @@ dynalite-panel==0.0.4 eagle100==0.1.1 # homeassistant.components.easyenergy -easyenergy==2.1.0 +easyenergy==2.1.1 # homeassistant.components.ebusd ebusdpy==0.0.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b63ef4bb8b1..fbdf60ab4b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -612,7 +612,7 @@ dynalite-panel==0.0.4 eagle100==0.1.1 # homeassistant.components.easyenergy -easyenergy==2.1.0 +easyenergy==2.1.1 # homeassistant.components.electric_kiwi electrickiwi-api==0.8.5 From da29b4ef167e413bfa541467dec85952e3142ecc Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 4 Feb 2024 07:21:19 +1000 Subject: [PATCH 0164/1367] Add Speed Limit to Tessie lock platform (#106527) * Add speed limit * Make regex more readable * Add tests * Add test * Ruff * Remove extra line * Update snapshot * Remove bad snapshot --- homeassistant/components/tessie/lock.py | 44 ++++++++++++++++-- homeassistant/components/tessie/strings.json | 3 ++ .../tessie/snapshots/test_lock.ambr | 45 +++++++++++++++++++ tests/components/tessie/test_lock.py | 40 +++++++++++++++-- 4 files changed, 126 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tessie/lock.py b/homeassistant/components/tessie/lock.py index 1a0d879cd79..9a27e95c73e 100644 --- a/homeassistant/components/tessie/lock.py +++ b/homeassistant/components/tessie/lock.py @@ -3,9 +3,15 @@ from __future__ import annotations from typing import Any -from tessie_api import lock, open_unlock_charge_port, unlock +from tessie_api import ( + disable_speed_limit, + enable_speed_limit, + lock, + open_unlock_charge_port, + unlock, +) -from homeassistant.components.lock import LockEntity +from homeassistant.components.lock import ATTR_CODE, LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError @@ -24,7 +30,7 @@ async def async_setup_entry( async_add_entities( klass(vehicle.state_coordinator) - for klass in (TessieLockEntity, TessieCableLockEntity) + for klass in (TessieLockEntity, TessieCableLockEntity, TessieSpeedLimitEntity) for vehicle in data ) @@ -55,6 +61,38 @@ class TessieLockEntity(TessieEntity, LockEntity): self.set((self.key, False)) +class TessieSpeedLimitEntity(TessieEntity, LockEntity): + """Speed Limit with PIN entity for Tessie.""" + + _attr_code_format = r"^\d\d\d\d$" + + def __init__( + self, + coordinator: TessieStateUpdateCoordinator, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, "vehicle_state_speed_limit_mode_active") + + @property + def is_locked(self) -> bool | None: + """Return the state of the Lock.""" + return self._value + + async def async_lock(self, **kwargs: Any) -> None: + """Enable speed limit with pin.""" + code: str | None = kwargs.get(ATTR_CODE) + if code: + await self.run(enable_speed_limit, pin=code) + self.set((self.key, True)) + + async def async_unlock(self, **kwargs: Any) -> None: + """Disable speed limit with pin.""" + code: str | None = kwargs.get(ATTR_CODE) + if code: + await self.run(disable_speed_limit, pin=code) + self.set((self.key, False)) + + class TessieCableLockEntity(TessieEntity, LockEntity): """Cable Lock entity for Tessie.""" diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 01e6a654163..f5900095836 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -59,6 +59,9 @@ }, "charge_state_charge_port_latch": { "name": "Charge cable lock" + }, + "vehicle_state_speed_limit_mode_active": { + "name": "Speed limit" } }, "media_player": { diff --git a/tests/components/tessie/snapshots/test_lock.ambr b/tests/components/tessie/snapshots/test_lock.ambr index cef92a1226f..026e02603c9 100644 --- a/tests/components/tessie/snapshots/test_lock.ambr +++ b/tests/components/tessie/snapshots/test_lock.ambr @@ -87,3 +87,48 @@ 'state': 'locked', }) # --- +# name: test_locks[lock.test_speed_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'lock', + 'entity_category': None, + 'entity_id': 'lock.test_speed_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Speed limit', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_speed_limit_mode_active', + 'unique_id': 'VINVINVIN-vehicle_state_speed_limit_mode_active', + 'unit_of_measurement': None, + }) +# --- +# name: test_locks[lock.test_speed_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'code_format': '^\\d\\d\\d\\d$', + 'friendly_name': 'Test Speed limit', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lock.test_speed_limit', + 'last_changed': , + 'last_updated': , + 'state': 'unlocked', + }) +# --- diff --git a/tests/components/tessie/test_lock.py b/tests/components/tessie/test_lock.py index b1e4f24ac59..ca921583d97 100644 --- a/tests/components/tessie/test_lock.py +++ b/tests/components/tessie/test_lock.py @@ -6,6 +6,7 @@ import pytest from syrupy import SnapshotAssertion from homeassistant.components.lock import ( + ATTR_CODE, DOMAIN as LOCK_DOMAIN, SERVICE_LOCK, SERVICE_UNLOCK, @@ -27,9 +28,8 @@ async def test_locks( assert_entities(hass, entry.entry_id, entity_registry, snapshot) - entity_id = "lock.test_lock" - # Test lock set value functions + entity_id = "lock.test_lock" with patch("homeassistant.components.tessie.lock.lock") as mock_run: await hass.services.async_call( LOCK_DOMAIN, @@ -47,8 +47,8 @@ async def test_locks( {ATTR_ENTITY_ID: [entity_id]}, blocking=True, ) - mock_run.assert_called_once() + assert hass.states.get(entity_id).state == STATE_UNLOCKED # Test charge cable lock set value functions entity_id = "lock.test_charge_cable_lock" @@ -71,3 +71,37 @@ async def test_locks( ) assert hass.states.get(entity_id).state == STATE_UNLOCKED mock_run.assert_called_once() + + # Test lock set value functions + entity_id = "lock.test_speed_limit" + with patch( + "homeassistant.components.tessie.lock.enable_speed_limit" + ) as mock_enable_speed_limit: + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + {ATTR_ENTITY_ID: [entity_id], ATTR_CODE: "1234"}, + blocking=True, + ) + assert hass.states.get(entity_id).state == STATE_LOCKED + mock_enable_speed_limit.assert_called_once() + + with patch( + "homeassistant.components.tessie.lock.disable_speed_limit" + ) as mock_disable_speed_limit: + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: [entity_id], ATTR_CODE: "1234"}, + blocking=True, + ) + assert hass.states.get(entity_id).state == STATE_UNLOCKED + mock_disable_speed_limit.assert_called_once() + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: [entity_id], ATTR_CODE: "abc"}, + blocking=True, + ) From 63da42f394ce282133f589b0ac8e3ba7d187df23 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 3 Feb 2024 18:30:00 -0500 Subject: [PATCH 0165/1367] Prevent Flo devices and entities from going unavailable when a single refresh fails (#109522) * Prevent Flo devices and entities from going unavailable when a single refresh fails * review comment --- homeassistant/components/flo/device.py | 7 ++++++- tests/components/flo/test_device.py | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index 7aacb1b262a..3b7469686b4 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -18,6 +18,8 @@ from .const import DOMAIN as FLO_DOMAIN, LOGGER class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=hass-enforce-coordinator-module """Flo device object.""" + _failure_count: int = 0 + def __init__( self, hass: HomeAssistant, api_client: API, location_id: str, device_id: str ) -> None: @@ -43,8 +45,11 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable= await self.send_presence_ping() await self._update_device() await self._update_consumption_data() + self._failure_count = 0 except RequestError as error: - raise UpdateFailed(error) from error + self._failure_count += 1 + if self._failure_count > 3: + raise UpdateFailed(error) from error @property def location_id(self) -> str: diff --git a/tests/components/flo/test_device.py b/tests/components/flo/test_device.py index 5d619f9e91f..6a633c774ed 100644 --- a/tests/components/flo/test_device.py +++ b/tests/components/flo/test_device.py @@ -93,4 +93,8 @@ async def test_device( "homeassistant.components.flo.device.FloDeviceDataUpdateCoordinator.send_presence_ping", side_effect=RequestError, ), pytest.raises(UpdateFailed): + # simulate 4 updates failing + await valve._async_update_data() + await valve._async_update_data() + await valve._async_update_data() await valve._async_update_data() From a7b52c8dd7f1f5cf0ec78c86f8cd3a37e98db5fe Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 18:46:47 -0500 Subject: [PATCH 0166/1367] Adds new climate feature flags to elkm1 (#109543) --- homeassistant/components/elkm1/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index c1e6dc7b034..97b16b14954 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -79,6 +79,8 @@ class ElkThermostat(ElkEntity, ClimateEntity): ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.AUX_HEAT | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_min_temp = 1 _attr_max_temp = 99 @@ -87,6 +89,7 @@ class ElkThermostat(ElkEntity, ClimateEntity): _attr_target_temperature_step = 1 _attr_fan_modes = [FAN_AUTO, FAN_ON] _element: Thermostat + _enable_turn_on_off_backwards_compatibility = False @property def temperature_unit(self) -> str: From 96f0fc9ae573b2c3285a93bf18da692067dd1a97 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 18:47:05 -0500 Subject: [PATCH 0167/1367] Add ClimateEntityFeatures to airtouch4 (#109421) * Add ClimateEntityFeatures to airtouch4 * adapt --- homeassistant/components/airtouch4/climate.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index bd1c481ce65..89afddad76e 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -88,9 +88,13 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity): _attr_name = None _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator, ac_number, info): """Initialize the climate device.""" @@ -192,9 +196,14 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): _attr_has_entity_name = True _attr_name = None - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_hvac_modes = AT_GROUP_MODES + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator, group_number, info): """Initialize the climate device.""" From ebf533f0ffa9e76b0f584a9be2b3b15d730e6bfe Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 19:18:25 -0500 Subject: [PATCH 0168/1367] Add new climate feature flags to ccm15 (#109534) Adds new climate feature flags to ccm15 --- homeassistant/components/ccm15/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/ccm15/climate.py b/homeassistant/components/ccm15/climate.py index 30896d12299..1f90f317fe0 100644 --- a/homeassistant/components/ccm15/climate.py +++ b/homeassistant/components/ccm15/climate.py @@ -64,8 +64,11 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.SWING_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__( self, ac_host: str, ac_index: int, coordinator: CCM15Coordinator From dd587d6fe5689bd1fa066ff21087a9a63d40d3a7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:10:34 -0500 Subject: [PATCH 0169/1367] Add new climate feature flags to isy994 (#109564) --- homeassistant/components/isy994/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 3ac2fd18473..06b73978456 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -82,9 +82,12 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_target_temperature_step = 1.0 _attr_fan_modes = [FAN_AUTO, FAN_ON] + _enable_turn_on_off_backwards_compatibility = False def __init__(self, node: Node, device_info: DeviceInfo | None = None) -> None: """Initialize the ISY Thermostat entity.""" From b49193caf7820a876405034285134499fbe71fd8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:11:02 -0500 Subject: [PATCH 0170/1367] Add new climate feature flags to lookin (#109570) --- homeassistant/components/lookin/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/lookin/climate.py b/homeassistant/components/lookin/climate.py index f09bedab201..1bee2d14295 100644 --- a/homeassistant/components/lookin/climate.py +++ b/homeassistant/components/lookin/climate.py @@ -97,6 +97,8 @@ class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.SWING_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_fan_modes: list[str] = LOOKIN_FAN_MODE_IDX_TO_HASS _attr_swing_modes: list[str] = LOOKIN_SWING_MODE_IDX_TO_HASS @@ -104,6 +106,7 @@ class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity): _attr_min_temp = MIN_TEMP _attr_max_temp = MAX_TEMP _attr_target_temperature_step = PRECISION_WHOLE + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From e649fe938034febf525daed74ed6b2353e22390d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:22:43 -0500 Subject: [PATCH 0171/1367] Add migrated climate feature flags to coolmaster (#109536) Adds migrated climate feature flags to coolmaster --- homeassistant/components/coolmaster/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index de0a7029ac6..ecb604a14cc 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -54,6 +54,7 @@ class CoolmasterClimate(CoolmasterEntity, ClimateEntity): """Representation of a coolmaster climate device.""" _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator, unit_id, info, supported_modes): """Initialize the climate device.""" From 6661b535306f8a513e623b9d9f13b79ed57ab267 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:23:06 -0500 Subject: [PATCH 0172/1367] Add new climate feature flags for airtouch5 (#109422) * Add new climate feature flags for airtouch5 * adapt --- homeassistant/components/airtouch5/climate.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/airtouch5/climate.py b/homeassistant/components/airtouch5/climate.py index 829915ce6d1..ee92f68c0ed 100644 --- a/homeassistant/components/airtouch5/climate.py +++ b/homeassistant/components/airtouch5/climate.py @@ -120,15 +120,12 @@ class Airtouch5ClimateEntity(ClimateEntity, Airtouch5Entity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_target_temperature_step = 1 _attr_name = None + _enable_turn_on_off_backwards_compatibility = False class Airtouch5AC(Airtouch5ClimateEntity): """Representation of the AC unit. Used to control the overall HVAC Mode.""" - _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE - ) - def __init__(self, client: Airtouch5SimpleClient, ability: AcAbility) -> None: """Initialise the Climate Entity.""" super().__init__(client) @@ -152,6 +149,14 @@ class Airtouch5AC(Airtouch5ClimateEntity): if ability.supports_mode_heat: self._attr_hvac_modes.append(HVACMode.HEAT) + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ) + if len(self.hvac_modes) > 1: + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) + self._attr_fan_modes = [] if ability.supports_fan_speed_quiet: self._attr_fan_modes.append(FAN_DIFFUSE) @@ -262,7 +267,10 @@ class Airtouch5Zone(Airtouch5ClimateEntity): _attr_hvac_modes = [HVACMode.OFF, HVACMode.FAN_ONLY] _attr_preset_modes = [PRESET_NONE, PRESET_BOOST] _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) def __init__( From ec6122abd51cc1ea06126c153a6508dff9ea80ac Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:23:23 -0500 Subject: [PATCH 0173/1367] Add migrated climate feature flags to daikin (#109537) Adds migrated climate feature flags to daikin --- homeassistant/components/daikin/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 047acd3cccf..c6bab19aa8a 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -128,6 +128,7 @@ class DaikinClimate(ClimateEntity): _attr_target_temperature_step = 1 _attr_fan_modes: list[str] _attr_swing_modes: list[str] + _enable_turn_on_off_backwards_compatibility = False def __init__(self, api: DaikinApi) -> None: """Initialize the climate device.""" From 760cbaa939e7f00307e63d1359966699bc8faf64 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:23:59 -0500 Subject: [PATCH 0174/1367] Add new climate feature flags to geniushub (#109549) Adds new climate feature flags to geniushub --- homeassistant/components/geniushub/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index bafda44501b..cb817c64930 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -50,8 +50,12 @@ class GeniusClimateZone(GeniusHeatingZone, ClimateEntity): """Representation of a Genius Hub climate device.""" _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) + _enable_turn_on_off_backwards_compatibility = False def __init__(self, broker, zone) -> None: """Initialize the climate device.""" From 39bd9cf6a2bebf800b974c3b75d8da1f2b6e34e6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:24:18 -0500 Subject: [PATCH 0175/1367] Add new climate feature flags to freedompro (#109546) Adds new climate feature flags to freedompro --- homeassistant/components/freedompro/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/freedompro/climate.py b/homeassistant/components/freedompro/climate.py index 7a4b0473600..3bb62cb23fb 100644 --- a/homeassistant/components/freedompro/climate.py +++ b/homeassistant/components/freedompro/climate.py @@ -64,10 +64,15 @@ class Device(CoordinatorEntity[FreedomproDataUpdateCoordinator], ClimateEntity): _attr_hvac_modes = SUPPORTED_HVAC_MODES _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_name = None - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_current_temperature = 0 _attr_target_temperature = 0 _attr_hvac_mode = HVACMode.OFF + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From 3429a4733d220d780690abb54dc8f386015a7866 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:24:28 -0500 Subject: [PATCH 0176/1367] Add new climate feature flags to escea (#109545) Adds new climate feature flags to escea --- homeassistant/components/escea/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/escea/climate.py b/homeassistant/components/escea/climate.py index 71c8a403f8f..021cfd26764 100644 --- a/homeassistant/components/escea/climate.py +++ b/homeassistant/components/escea/climate.py @@ -82,10 +82,14 @@ class ControllerEntity(ClimateEntity): _attr_precision = PRECISION_WHOLE _attr_should_poll = False _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, controller: Controller) -> None: """Initialise ControllerDevice.""" From 45d8581bf196fe9a13cdc03c7f4c04993001e121 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:26:44 -0500 Subject: [PATCH 0177/1367] Add new climate feature flags to gree (#109550) Adds new climate feature flags to gree --- homeassistant/components/gree/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 8d50cdf2aed..1d061c06901 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -113,6 +113,8 @@ class GreeClimateEntity(GreeEntity, ClimateEntity): | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.SWING_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_target_temperature_step = TARGET_TEMPERATURE_STEP _attr_hvac_modes = [*HVAC_MODES_REVERSE, HVACMode.OFF] @@ -120,6 +122,7 @@ class GreeClimateEntity(GreeEntity, ClimateEntity): _attr_fan_modes = [*FAN_MODES_REVERSE] _attr_swing_modes = SWING_MODES _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator: DeviceDataUpdateCoordinator) -> None: """Initialize the Gree device.""" From f5ca82923dc010912cdbccb018fd1934123dca0a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:26:53 -0500 Subject: [PATCH 0178/1367] Add migrated climate feature flags to devolo home control (#109538) Adds migrated climate feature flags to devolo home control --- homeassistant/components/devolo_home_control/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index e27d5a315a5..9f17a653673 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -56,6 +56,7 @@ class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntit _attr_precision = PRECISION_TENTHS _attr_hvac_mode = HVACMode.HEAT _attr_hvac_modes = [HVACMode.HEAT] + _enable_turn_on_off_backwards_compatibility = False def __init__( self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str From 5f02d178731d309025ff805569a42ad731ae6a07 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:27:04 -0500 Subject: [PATCH 0179/1367] Add new climate feature flags to duotecno (#109539) Adds new climate feature flags to duotecno --- homeassistant/components/duotecno/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/duotecno/climate.py b/homeassistant/components/duotecno/climate.py index 22be40a812e..3df80721af4 100644 --- a/homeassistant/components/duotecno/climate.py +++ b/homeassistant/components/duotecno/climate.py @@ -47,12 +47,16 @@ class DuotecnoClimate(DuotecnoEntity, ClimateEntity): _unit: SensUnit _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_hvac_modes = list(HVACMODE_REVERSE) _attr_preset_modes = list(PRESETMODES) _attr_translation_key = "duotecno" + _enable_turn_on_off_backwards_compatibility = False @property def current_temperature(self) -> float | None: From b5055df374a75dd8caf9f8ce30568c2af5506225 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:27:30 -0500 Subject: [PATCH 0180/1367] Add new climate feature flags to hisense (#109552) Adds new climate feature flags to hisense --- homeassistant/components/hisense_aehw4a1/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/hisense_aehw4a1/climate.py b/homeassistant/components/hisense_aehw4a1/climate.py index ca5ec694eab..0e3fa9981c1 100644 --- a/homeassistant/components/hisense_aehw4a1/climate.py +++ b/homeassistant/components/hisense_aehw4a1/climate.py @@ -144,6 +144,8 @@ class ClimateAehW4a1(ClimateEntity): | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.SWING_MODE | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_fan_modes = FAN_MODES _attr_swing_modes = SWING_MODES @@ -152,6 +154,7 @@ class ClimateAehW4a1(ClimateEntity): _attr_target_temperature_step = 1 _previous_state: HVACMode | str | None = None _on: str | None = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device): """Initialize the climate device.""" From a1736550508c9dd18097b6e11d46988434552ff3 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:27:42 -0500 Subject: [PATCH 0181/1367] Add new climate feature flags to hive (#109553) Adds new climate feature flags to hive --- homeassistant/components/hive/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 99de8b99675..8085719d8c5 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -92,8 +92,12 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): _attr_hvac_modes = [HVACMode.AUTO, HVACMode.HEAT, HVACMode.OFF] _attr_preset_modes = [PRESET_BOOST, PRESET_NONE] _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) + _enable_turn_on_off_backwards_compatibility = False def __init__(self, hive_session, hive_device): """Initialize the Climate device.""" From 97db23fe82426e5e8a4833babfeee932099bbfbd Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:28:15 -0500 Subject: [PATCH 0182/1367] Add new climate feature flags to intellifire (#109562) --- homeassistant/components/intellifire/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/intellifire/climate.py b/homeassistant/components/intellifire/climate.py index 5d305db8feb..9fed9c08bb6 100644 --- a/homeassistant/components/intellifire/climate.py +++ b/homeassistant/components/intellifire/climate.py @@ -49,10 +49,15 @@ class IntellifireClimate(IntellifireEntity, ClimateEntity): _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] _attr_min_temp = 0 _attr_max_temp = 37 - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_target_temperature_step = 1.0 _attr_temperature_unit = UnitOfTemperature.CELSIUS last_temp = DEFAULT_THERMOSTAT_TEMP + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From cfeafb410cc1fbe56f6a198f44ef64e6ee13e2df Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:28:33 -0500 Subject: [PATCH 0183/1367] Add new climate feature flags to huum (#109557) Adds new climate feature flags to huum --- homeassistant/components/huum/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/huum/climate.py b/homeassistant/components/huum/climate.py index dcf025082cc..2bc3c626deb 100644 --- a/homeassistant/components/huum/climate.py +++ b/homeassistant/components/huum/climate.py @@ -41,7 +41,11 @@ class HuumDevice(ClimateEntity): """Representation of a heater.""" _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_max_temp = 110 @@ -51,6 +55,7 @@ class HuumDevice(ClimateEntity): _target_temperature: int | None = None _status: HuumStatusResponse | None = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, huum_handler: Huum, unique_id: str) -> None: """Initialize the heater.""" From b376f93fe690fba3c1d4491601598e9ebb680aaf Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:28:45 -0500 Subject: [PATCH 0184/1367] Add migrated climate feature flags to homematicip_cloud (#109555) Adds migrated climate feature flags to homematicip_cloud --- homeassistant/components/homematicip_cloud/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 09d00e9bee1..63b78e91a2f 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -70,6 +70,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, hap: HomematicipHAP, device: AsyncHeatingGroup) -> None: """Initialize heating group.""" From 24be51b223bf09f13980f0b043553c5ae160fbfc Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:29:40 -0500 Subject: [PATCH 0185/1367] Add new climate feature flags to iaqualink (#109558) --- homeassistant/components/iaqualink/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index b7dbe43fca9..5a81ad3d681 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -42,7 +42,12 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity): """Representation of a thermostat.""" _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) + _enable_turn_on_off_backwards_compatibility = False def __init__(self, dev: AqualinkThermostat) -> None: """Initialize AquaLink thermostat.""" From c94eb436fb1df893663be6396616bb2633f5425b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:29:56 -0500 Subject: [PATCH 0186/1367] Add new climate feature flags to heatmiser (#109551) Adds new climate feature flags to heatmiser --- homeassistant/components/heatmiser/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index 24a0c88b45a..566a4696a73 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -76,7 +76,12 @@ class HeatmiserV3Thermostat(ClimateEntity): """Representation of a HeatmiserV3 thermostat.""" _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) + _enable_turn_on_off_backwards_compatibility = False def __init__(self, therm, device, uh1): """Initialize the thermostat.""" From 470aef7483c2d205379dcf411970e33ef9079d0a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:30:08 -0500 Subject: [PATCH 0187/1367] Add new climate feature flags to lightwave (#109568) --- homeassistant/components/lightwave/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lightwave/climate.py b/homeassistant/components/lightwave/climate.py index 60108aba024..5e89e4f8145 100644 --- a/homeassistant/components/lightwave/climate.py +++ b/homeassistant/components/lightwave/climate.py @@ -47,9 +47,14 @@ class LightwaveTrv(ClimateEntity): _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] _attr_min_temp = DEFAULT_MIN_TEMP _attr_max_temp = DEFAULT_MAX_TEMP - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_target_temperature_step = 0.5 _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, name, device_id, lwlink, serial): """Initialize LightwaveTrv entity.""" From a2d574355bd5579dbcb5159c8e84cf5c2865ec42 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:30:55 -0500 Subject: [PATCH 0188/1367] Add new climate feature flags to izone (#109565) --- homeassistant/components/izone/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 75eb19b2978..e85b7ef4d56 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -144,12 +144,17 @@ class ControllerDevice(ClimateEntity): _attr_has_entity_name = True _attr_name = None _attr_target_temperature_step = 0.5 + _enable_turn_on_off_backwards_compatibility = False def __init__(self, controller: Controller) -> None: """Initialise ControllerDevice.""" self._controller = controller - self._attr_supported_features = ClimateEntityFeature.FAN_MODE + self._attr_supported_features = ( + ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) # If mode RAS, or mode master with CtrlZone 13 then can set master temperature, # otherwise the unit determines which zone to use as target. See interface manual p. 8 From 810e5f9f67c728b2857333bd2459909092980e81 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:31:05 -0500 Subject: [PATCH 0189/1367] Add migrated climate feature flags to livisi (#109569) --- homeassistant/components/livisi/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/livisi/climate.py b/homeassistant/components/livisi/climate.py index 952363650d6..6990dabff1d 100644 --- a/homeassistant/components/livisi/climate.py +++ b/homeassistant/components/livisi/climate.py @@ -67,6 +67,7 @@ class LivisiClimate(LivisiEntity, ClimateEntity): _attr_hvac_mode = HVACMode.HEAT _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From 5f8248dc77e77282d9666ff539c11c7b663afeb5 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:32:35 -0500 Subject: [PATCH 0190/1367] Add migrated climate feature flags to fritzbox (#109547) Adds migrated climate feature flags to fritzbox --- homeassistant/components/fritzbox/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index b76e0fda18a..8dc19c199a3 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -80,6 +80,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): | ClimateEntityFeature.TURN_ON ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False @property def current_temperature(self) -> float: From 5041ee3c281d66267c7abd266d0d400147c67fcd Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 21:29:29 -0500 Subject: [PATCH 0191/1367] Add new climate feature flags to ephember (#109544) Adds new climate feature flags to ephember --- homeassistant/components/ephember/climate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index 3735b4d16c2..047b9234b82 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -83,6 +83,7 @@ class EphEmberThermostat(ClimateEntity): _attr_hvac_modes = OPERATION_LIST _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, ember, zone): """Initialize the thermostat.""" @@ -100,6 +101,9 @@ class EphEmberThermostat(ClimateEntity): if self._hot_water: self._attr_supported_features = ClimateEntityFeature.AUX_HEAT self._attr_target_temperature_step = None + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) @property def current_temperature(self): From f6d2f6117a0a4067fd3ff707a34c7dea0a63c84b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 21:52:33 -0500 Subject: [PATCH 0192/1367] Fix overkiz climate feature flags for valve heating (#109582) * Fix overkiz climate feature flags for valve heating * Update homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py --- .../climate_entities/valve_heating_temperature_interface.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py b/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py index 79c360a5f93..7b7493a37bb 100644 --- a/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py +++ b/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py @@ -51,10 +51,7 @@ class ValveHeatingTemperatureInterface(OverkizEntity, ClimateEntity): _attr_hvac_modes = [HVACMode.HEAT] _attr_preset_modes = [*PRESET_MODE_TO_OVERKIZ] _attr_supported_features = ( - ClimateEntityFeature.PRESET_MODE - | ClimateEntityFeature.TARGET_TEMPERATURE - | ClimateEntityFeature.TURN_OFF - | ClimateEntityFeature.TURN_ON + ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN From 630c8b28ca4d4343a040aa72bd9b88f630467187 Mon Sep 17 00:00:00 2001 From: Matrix Date: Sun, 4 Feb 2024 15:27:57 +0800 Subject: [PATCH 0193/1367] Fix yolink abnormal status when LeakSensor detection mode changes to "no water detect" (#109575) Add no water detect support --- homeassistant/components/yolink/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index 0650cc3a203..0762a3b5c60 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -63,7 +63,7 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( YoLinkBinarySensorEntityDescription( key="leak_state", device_class=BinarySensorDeviceClass.MOISTURE, - value=lambda value: value == "alert" if value is not None else None, + value=lambda value: value in ("alert", "full") if value is not None else None, exists_fn=lambda device: device.device_type == ATTR_DEVICE_LEAK_SENSOR, ), YoLinkBinarySensorEntityDescription( From ac20d49c8fbd16ad013e5d59d1549e22faf46565 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 4 Feb 2024 01:13:35 -0800 Subject: [PATCH 0194/1367] Fix Google generative AI service example (#109594) Update strings.json --- .../components/google_generative_ai_conversation/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_generative_ai_conversation/strings.json b/homeassistant/components/google_generative_ai_conversation/strings.json index 76e6135b14d..306072f33a8 100644 --- a/homeassistant/components/google_generative_ai_conversation/strings.json +++ b/homeassistant/components/google_generative_ai_conversation/strings.json @@ -35,7 +35,7 @@ "prompt": { "name": "Prompt", "description": "The prompt", - "example": "Describe what you see in these images:" + "example": "Describe what you see in these images" }, "image_filename": { "name": "Image filename", From 0a248d0f87fec39461887c4fe2d86b1f34a3f0de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 03:17:13 -0600 Subject: [PATCH 0195/1367] Bump yalexs-ble to 2.4.1 (#109585) changelog: https://github.com/bdraco/yalexs-ble/compare/v2.4.0...v2.4.1 --- 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 d0f2a27522d..97963b19378 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.10.0", "yalexs-ble==2.4.0"] + "requirements": ["yalexs==1.10.0", "yalexs-ble==2.4.1"] } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index dcd7e57ce1f..c9ed4bc6a8f 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.4.0"] + "requirements": ["yalexs-ble==2.4.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index fffa4ba0073..eb37e1a123f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2880,7 +2880,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.4.0 +yalexs-ble==2.4.1 # homeassistant.components.august yalexs==1.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fbdf60ab4b8..70dfc68f58b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2206,7 +2206,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.4.0 +yalexs-ble==2.4.1 # homeassistant.components.august yalexs==1.10.0 From 153c82c2d23b2589a0c68caeaca6947f831eb685 Mon Sep 17 00:00:00 2001 From: Cody C <50791984+codyc1515@users.noreply.github.com> Date: Sun, 4 Feb 2024 22:25:31 +1300 Subject: [PATCH 0196/1367] Fix empty error modal when adding duplicate Thread integration (#109530) --- homeassistant/components/thread/strings.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/thread/strings.json b/homeassistant/components/thread/strings.json index 0a9cf0004bc..474999b06bd 100644 --- a/homeassistant/components/thread/strings.json +++ b/homeassistant/components/thread/strings.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, "step": { "confirm": { "description": "[%key:common::config_flow::description::confirm_setup%]" From d7c6e85cc4f522a56ba69f22ef83f28bcb7a537a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:28:10 -0500 Subject: [PATCH 0197/1367] Add new climate feature flags to comelit (#109535) Adds new climate feature flags to comelit --- homeassistant/components/comelit/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/comelit/climate.py b/homeassistant/components/comelit/climate.py index 2c2d31514a5..877afd1414e 100644 --- a/homeassistant/components/comelit/climate.py +++ b/homeassistant/components/comelit/climate.py @@ -91,11 +91,16 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity _attr_hvac_modes = [HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT, HVACMode.OFF] _attr_max_temp = 30 _attr_min_temp = 5 - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_target_temperature_step = PRECISION_TENTHS _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_has_entity_name = True _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__( self, From 7730efdaa278ffd8b8c2be05062b069292a3d6d2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:28:49 -0500 Subject: [PATCH 0198/1367] Add new climate feature flags to ecobee (#109540) Adds new climate feature flags to ecobee --- homeassistant/components/ecobee/climate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index e15a8e1d3d8..58a3cb09997 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -323,6 +323,7 @@ class Thermostat(ClimateEntity): _attr_fan_modes = [FAN_AUTO, FAN_ON] _attr_name = None _attr_has_entity_name = True + _enable_turn_on_off_backwards_compatibility = False def __init__( self, data: EcobeeData, thermostat_index: int, thermostat: dict @@ -375,6 +376,10 @@ class Thermostat(ClimateEntity): supported = supported | ClimateEntityFeature.TARGET_HUMIDITY if self.has_aux_heat: supported = supported | ClimateEntityFeature.AUX_HEAT + if len(self.hvac_modes) > 1 and HVACMode.OFF in self.hvac_modes: + supported = ( + supported | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) return supported @property From f154b7f2d9dfd49725ccd602e48e2e33b0a68515 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:30:09 -0500 Subject: [PATCH 0199/1367] Add new climate feature flags to econet (#109541) Adds new climate feature flags to econet --- homeassistant/components/econet/climate.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/econet/climate.py b/homeassistant/components/econet/climate.py index f5328da4776..ac812a07566 100644 --- a/homeassistant/components/econet/climate.py +++ b/homeassistant/components/econet/climate.py @@ -66,6 +66,7 @@ class EcoNetThermostat(EcoNetEntity, ClimateEntity): _attr_should_poll = True _attr_temperature_unit = UnitOfTemperature.FAHRENHEIT + _enable_turn_on_off_backwards_compatibility = False def __init__(self, thermostat): """Initialize.""" @@ -79,12 +80,13 @@ class EcoNetThermostat(EcoNetEntity, ClimateEntity): ha_mode = ECONET_STATE_TO_HA[mode] self._attr_hvac_modes.append(ha_mode) - @property - def supported_features(self) -> ClimateEntityFeature: - """Return the list of supported features.""" - if self._econet.supports_humidifier: - return SUPPORT_FLAGS_THERMOSTAT | ClimateEntityFeature.TARGET_HUMIDITY - return SUPPORT_FLAGS_THERMOSTAT + self._attr_supported_features |= SUPPORT_FLAGS_THERMOSTAT + if thermostat.supports_humidifier: + self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY + if len(self.hvac_modes) > 1 and HVACMode.OFF in self.hvac_modes: + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) @property def current_temperature(self): From 6b6d0606ac34228c7d5a817bbd1d18078ce011c3 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:31:23 -0500 Subject: [PATCH 0200/1367] Add new climate feature flags to electrasmart (#109542) Adds new climate feature flags to electrasmart --- homeassistant/components/electrasmart/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/electrasmart/climate.py b/homeassistant/components/electrasmart/climate.py index 086a5288f77..9f6e7cbddf5 100644 --- a/homeassistant/components/electrasmart/climate.py +++ b/homeassistant/components/electrasmart/climate.py @@ -111,6 +111,7 @@ class ElectraClimateEntity(ClimateEntity): _attr_hvac_modes = ELECTRA_MODES _attr_has_entity_name = True _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__(self, device: ElectraAirConditioner, api: ElectraAPI) -> None: """Initialize Electra climate entity.""" @@ -121,6 +122,8 @@ class ElectraClimateEntity(ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) swing_modes: list = [] From b6555077389efb57f7071fe5c467cca354711218 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:38:09 -0500 Subject: [PATCH 0201/1367] Add new climate feature flags to generic_thermostat (#109548) Adds new climate feature flags to generic_thermostat --- homeassistant/components/generic_thermostat/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 7bc6c63697c..3a964204b70 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -178,6 +178,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): """Representation of a Generic Thermostat device.""" _attr_should_poll = False + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -225,7 +226,11 @@ class GenericThermostat(ClimateEntity, RestoreEntity): self._target_temp = target_temp self._attr_temperature_unit = unit self._attr_unique_id = unique_id - self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) if len(presets): self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE self._attr_preset_modes = [PRESET_NONE] + list(presets.keys()) From 552b14c5e8877ca363a1ece7ce71478f4ebab4e0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:40:02 -0500 Subject: [PATCH 0202/1367] Add new climate feature flags to homematic (#109554) Adds new climate feature flags to homematic --- homeassistant/components/homematic/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index c1dead1835e..76d9dff4d46 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -56,9 +56,13 @@ class HMThermostat(HMDevice, ClimateEntity): """Representation of a Homematic thermostat.""" _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False @property def hvac_mode(self) -> HVACMode: From 0b3f4f1720f7a0cb3ecdf5bccb7d4aa36cfbb185 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:41:07 -0500 Subject: [PATCH 0203/1367] Add new climate feature flags to honeywell (#109556) Adds new climate feature flags to honeywell --- homeassistant/components/honeywell/climate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 803ca1da1aa..efd06ba2905 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -143,6 +143,7 @@ class HoneywellUSThermostat(ClimateEntity): _attr_has_entity_name = True _attr_name = None _attr_translation_key = "honeywell" + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -187,6 +188,10 @@ class HoneywellUSThermostat(ClimateEntity): | ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) + if len(self.hvac_modes) > 1 and HVACMode.OFF in self.hvac_modes: + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) if device._data.get("canControlHumidification"): self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY From 6e38da15f0fd398efd6ff893d1197a3703266370 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:41:53 -0500 Subject: [PATCH 0204/1367] Add migrated climate feature flags to incomfort (#109559) --- homeassistant/components/incomfort/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index cae73495438..0dba00ff416 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -42,6 +42,7 @@ class InComfortClimate(IncomfortChild, ClimateEntity): _attr_hvac_modes = [HVACMode.HEAT] _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, client, heater, room) -> None: """Initialize the climate device.""" From 83380c0f7d2da3abd2b0aadd5016af7e72a6af36 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:42:32 -0500 Subject: [PATCH 0205/1367] Add new climate feature flags to insteon (#109560) --- homeassistant/components/insteon/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index 74fb11491c0..22bd776e1c8 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -87,10 +87,13 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity): | ClimateEntityFeature.TARGET_HUMIDITY | ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) _attr_hvac_modes = list(HVAC_MODES.values()) _attr_fan_modes = list(FAN_MODES.values()) _attr_min_humidity = 1 + _enable_turn_on_off_backwards_compatibility = False @property def temperature_unit(self) -> str: From 6c41540ad8e10391d6591f36c2bfd4b1d3e0b0b6 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Sun, 4 Feb 2024 11:29:32 +0100 Subject: [PATCH 0206/1367] Bugfix lamarzocco issue (#109596) --- homeassistant/components/lamarzocco/number.py | 2 +- homeassistant/components/lamarzocco/sensor.py | 2 ++ tests/components/lamarzocco/fixtures/current_status.json | 7 ++----- .../components/lamarzocco/snapshots/test_diagnostics.ambr | 5 +---- tests/components/lamarzocco/snapshots/test_sensor.ambr | 6 ++++++ 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py index 76632d4a5b8..bf866872f5b 100644 --- a/homeassistant/components/lamarzocco/number.py +++ b/homeassistant/components/lamarzocco/number.py @@ -79,7 +79,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( set_value_fn=lambda coordinator, value: coordinator.lm.set_dose_hot_water( value=int(value) ), - native_value_fn=lambda lm: lm.current_status["dose_k5"], + native_value_fn=lambda lm: lm.current_status["dose_hot_water"], supported_fn=lambda coordinator: coordinator.lm.model_name in ( LaMarzoccoModel.GS3_AV, diff --git a/homeassistant/components/lamarzocco/sensor.py b/homeassistant/components/lamarzocco/sensor.py index c46b965850c..ea5a5e184e1 100644 --- a/homeassistant/components/lamarzocco/sensor.py +++ b/homeassistant/components/lamarzocco/sensor.py @@ -62,6 +62,7 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( key="current_temp_coffee", translation_key="current_temp_coffee", native_unit_of_measurement=UnitOfTemperature.CELSIUS, + suggested_display_precision=1, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, value_fn=lambda lm: lm.current_status.get("coffee_temp", 0), @@ -70,6 +71,7 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( key="current_temp_steam", translation_key="current_temp_steam", native_unit_of_measurement=UnitOfTemperature.CELSIUS, + suggested_display_precision=1, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, value_fn=lambda lm: lm.current_status.get("steam_temp", 0), diff --git a/tests/components/lamarzocco/fixtures/current_status.json b/tests/components/lamarzocco/fixtures/current_status.json index 4f208607c17..f99c3d5c331 100644 --- a/tests/components/lamarzocco/fixtures/current_status.json +++ b/tests/components/lamarzocco/fixtures/current_status.json @@ -43,7 +43,7 @@ "dose_k2": 1023, "dose_k3": 1023, "dose_k4": 1023, - "dose_k5": 1023, + "dose_hot_water": 1023, "prebrewing_ton_k1": 3, "prebrewing_toff_k1": 5, "prebrewing_ton_k2": 3, @@ -52,11 +52,8 @@ "prebrewing_toff_k3": 5, "prebrewing_ton_k4": 3, "prebrewing_toff_k4": 5, - "prebrewing_ton_k5": 3, - "prebrewing_toff_k5": 5, "preinfusion_k1": 4, "preinfusion_k2": 4, "preinfusion_k3": 4, - "preinfusion_k4": 4, - "preinfusion_k5": 4 + "preinfusion_k4": 4 } diff --git a/tests/components/lamarzocco/snapshots/test_diagnostics.ambr b/tests/components/lamarzocco/snapshots/test_diagnostics.ambr index 2462d4a125d..ec44100fe1e 100644 --- a/tests/components/lamarzocco/snapshots/test_diagnostics.ambr +++ b/tests/components/lamarzocco/snapshots/test_diagnostics.ambr @@ -198,11 +198,11 @@ 'coffee_boiler_on': True, 'coffee_set_temp': 95, 'coffee_temp': 93, + 'dose_hot_water': 1023, 'dose_k1': 1023, 'dose_k2': 1023, 'dose_k3': 1023, 'dose_k4': 1023, - 'dose_k5': 1023, 'drinks_k1': 13, 'drinks_k2': 2, 'drinks_k3': 42, @@ -221,17 +221,14 @@ 'prebrewing_toff_k2': 5, 'prebrewing_toff_k3': 5, 'prebrewing_toff_k4': 5, - 'prebrewing_toff_k5': 5, 'prebrewing_ton_k1': 3, 'prebrewing_ton_k2': 3, 'prebrewing_ton_k3': 3, 'prebrewing_ton_k4': 3, - 'prebrewing_ton_k5': 3, 'preinfusion_k1': 4, 'preinfusion_k2': 4, 'preinfusion_k3': 4, 'preinfusion_k4': 4, - 'preinfusion_k5': 4, 'sat_auto': 'Disabled', 'sat_off_time': '00:00', 'sat_on_time': '00:00', diff --git a/tests/components/lamarzocco/snapshots/test_sensor.ambr b/tests/components/lamarzocco/snapshots/test_sensor.ambr index 4228252f526..e0b04289f7c 100644 --- a/tests/components/lamarzocco/snapshots/test_sensor.ambr +++ b/tests/components/lamarzocco/snapshots/test_sensor.ambr @@ -20,6 +20,9 @@ 'id': , 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -68,6 +71,9 @@ 'id': , 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, From 9fbf00bdd314918cf9e0a16cf0109558efa49d4d Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Sun, 4 Feb 2024 11:30:30 +0100 Subject: [PATCH 0207/1367] Move climate feature flags to child classes for airzone_cloud (#109515) --- .../components/airzone_cloud/climate.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/airzone_cloud/climate.py b/homeassistant/components/airzone_cloud/climate.py index 73333d346c5..1bab9dd6c33 100644 --- a/homeassistant/components/airzone_cloud/climate.py +++ b/homeassistant/components/airzone_cloud/climate.py @@ -144,11 +144,6 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity): """Define an Airzone Cloud climate.""" _attr_name = None - _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE - | ClimateEntityFeature.TURN_OFF - | ClimateEntityFeature.TURN_ON - ) _attr_temperature_unit = UnitOfTemperature.CELSIUS _enable_turn_on_off_backwards_compatibility = False @@ -180,6 +175,12 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity): class AirzoneDeviceClimate(AirzoneClimate): """Define an Airzone Cloud Device base class.""" + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) + async def async_turn_on(self) -> None: """Turn the entity on.""" params = { @@ -217,6 +218,12 @@ class AirzoneDeviceClimate(AirzoneClimate): class AirzoneDeviceGroupClimate(AirzoneClimate): """Define an Airzone Cloud DeviceGroup base class.""" + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) + async def async_turn_on(self) -> None: """Turn the entity on.""" params = { From 7d9935d24bc405c4dd4f66653f4bbbc6ca7c863c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 04:37:14 -0600 Subject: [PATCH 0208/1367] Reduce overhead to convert history to float states (#109526) --- homeassistant/components/sensor/recorder.py | 33 ++++++++------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 1aba934aba4..04dd69349e4 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -146,31 +146,22 @@ def _equivalent_units(units: set[str | None]) -> bool: return len(units) == 1 -def _parse_float(state: str) -> float: - """Parse a float string, throw on inf or nan.""" - fstate = float(state) - if not math.isfinite(fstate): - raise ValueError - return fstate - - -def _float_or_none(state: str) -> float | None: - """Return a float or None.""" - try: - return _parse_float(state) - except (ValueError, TypeError): - return None - - def _entity_history_to_float_and_state( entity_history: Iterable[State], ) -> list[tuple[float, State]]: """Return a list of (float, state) tuples for the given entity.""" - return [ - (fstate, state) - for state in entity_history - if (fstate := _float_or_none(state.state)) is not None - ] + float_states: list[tuple[float, State]] = [] + append = float_states.append + isfinite = math.isfinite + for state in entity_history: + try: + if (float_state := float(state.state)) is not None and isfinite( + float_state + ): + append((float_state, state)) + except (ValueError, TypeError): + pass + return float_states def _normalize_states( From 89d77ee8ab693ff96e2e6cc46e64f2bdf7e650f2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 05:37:52 -0500 Subject: [PATCH 0209/1367] Add new climate feature flags for plugwise (#109464) --- homeassistant/components/plugwise/climate.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 84e0619773b..8e4dccb9e05 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -45,6 +45,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): _attr_name = None _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False _previous_mode: str = "heating" @@ -62,6 +63,11 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): self.gateway_data = coordinator.data.devices[gateway_id] # Determine supported features self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + if HVACMode.OFF in self.hvac_modes: + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) + if ( self.cdr_gateway["cooling_present"] and self.cdr_gateway["smile_name"] != "Adam" From 37f3fcbdafca4740ab24995b7c50880289226dc2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 05:39:04 -0500 Subject: [PATCH 0210/1367] Add climate feature flags to spider (#109456) --- homeassistant/components/spider/climate.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index 1498c4b0039..15ba19e9b3a 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -43,6 +43,7 @@ class SpiderThermostat(ClimateEntity): _attr_has_entity_name = True _attr_name = None _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, api, thermostat): """Initialize the thermostat.""" @@ -53,6 +54,13 @@ class SpiderThermostat(ClimateEntity): for operation_value in thermostat.operation_values: if operation_value in SPIDER_STATE_TO_HA: self.support_hvac.append(SPIDER_STATE_TO_HA[operation_value]) + self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE + if len(self.hvac_modes) > 1 and HVACMode.OFF in self.hvac_modes: + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) + if thermostat.has_fan_mode: + self._attr_supported_features |= ClimateEntityFeature.FAN_MODE @property def device_info(self) -> DeviceInfo: @@ -65,15 +73,6 @@ class SpiderThermostat(ClimateEntity): name=self.thermostat.name, ) - @property - def supported_features(self) -> ClimateEntityFeature: - """Return the list of supported features.""" - if self.thermostat.has_fan_mode: - return ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE - ) - return ClimateEntityFeature.TARGET_TEMPERATURE - @property def unique_id(self): """Return the id of the thermostat, if any.""" From 6e5a085413e36a945e4d3461dcafad75da6de9e7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 05:40:37 -0500 Subject: [PATCH 0211/1367] Add new climate feature flags to tuya (#109434) --- homeassistant/components/tuya/climate.py | 25 ++++++++---------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 9f20df98370..45adb532705 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -127,6 +127,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): _set_temperature: IntegerTypeData | None = None entity_description: TuyaClimateEntityDescription _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -277,6 +278,11 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): if self.find_dpcode(DPCode.SWITCH_VERTICAL, prefer_function=True): self._attr_swing_modes.append(SWING_VERTICAL) + if DPCode.SWITCH in self.device.function: + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" await super().async_added_to_hass() @@ -476,23 +482,8 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): def turn_on(self) -> None: """Turn the device on, retaining current HVAC (if supported).""" - if DPCode.SWITCH in self.device.function: - self._send_command([{"code": DPCode.SWITCH, "value": True}]) - return - - # Fake turn on - for mode in (HVACMode.HEAT_COOL, HVACMode.HEAT, HVACMode.COOL): - if mode not in self.hvac_modes: - continue - self.set_hvac_mode(mode) - break + self._send_command([{"code": DPCode.SWITCH, "value": True}]) def turn_off(self) -> None: """Turn the device on, retaining current HVAC (if supported).""" - if DPCode.SWITCH in self.device.function: - self._send_command([{"code": DPCode.SWITCH, "value": False}]) - return - - # Fake turn off - if HVACMode.OFF in self.hvac_modes: - self.set_hvac_mode(HVACMode.OFF) + self._send_command([{"code": DPCode.SWITCH, "value": False}]) From 17f1aa644b496539d3d5b874208095d9391017e6 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 4 Feb 2024 13:26:51 +0100 Subject: [PATCH 0212/1367] Avoid duplicate entity names in proximity (#109413) * avoid duplicate config entry title * consecutive range 2..10 * use existing logic --- .../components/proximity/config_flow.py | 17 +++-- .../components/proximity/test_config_flow.py | 64 +++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/proximity/config_flow.py b/homeassistant/components/proximity/config_flow.py index 231a50c6c00..f3306bebf39 100644 --- a/homeassistant/components/proximity/config_flow.py +++ b/homeassistant/components/proximity/config_flow.py @@ -18,6 +18,7 @@ from homeassistant.helpers.selector import ( NumberSelector, NumberSelectorConfig, ) +from homeassistant.util import slugify from .const import ( CONF_IGNORED_ZONES, @@ -89,11 +90,19 @@ class ProximityConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: self._async_abort_entries_match(user_input) - zone = self.hass.states.get(user_input[CONF_ZONE]) + title = cast(State, self.hass.states.get(user_input[CONF_ZONE])).name - return self.async_create_entry( - title=cast(State, zone).name, data=user_input - ) + slugified_existing_entry_titles = [ + slugify(e.title) for e in self._async_current_entries() + ] + + possible_title = title + tries = 1 + while slugify(possible_title) in slugified_existing_entry_titles: + tries += 1 + possible_title = f"{title} {tries}" + + return self.async_create_entry(title=possible_title, data=user_input) return self.async_show_form( step_id="user", diff --git a/tests/components/proximity/test_config_flow.py b/tests/components/proximity/test_config_flow.py index 92b924be1ce..3c94e941227 100644 --- a/tests/components/proximity/test_config_flow.py +++ b/tests/components/proximity/test_config_flow.py @@ -185,3 +185,67 @@ async def test_abort_duplicated_entry(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" await hass.async_block_till_done() + + +async def test_avoid_duplicated_title(hass: HomeAssistant) -> None: + """Test if we avoid duplicate titles.""" + MockConfigEntry( + domain=DOMAIN, + title="home", + data={ + CONF_ZONE: "zone.home", + CONF_TRACKED_ENTITIES: ["device_tracker.test1"], + CONF_IGNORED_ZONES: ["zone.work"], + CONF_TOLERANCE: 10, + }, + unique_id=f"{DOMAIN}_home", + ).add_to_hass(hass) + + MockConfigEntry( + domain=DOMAIN, + title="home 3", + data={ + CONF_ZONE: "zone.home", + CONF_TRACKED_ENTITIES: ["device_tracker.test2"], + CONF_IGNORED_ZONES: ["zone.work"], + CONF_TOLERANCE: 10, + }, + unique_id=f"{DOMAIN}_home", + ).add_to_hass(hass) + + with patch( + "homeassistant.components.proximity.async_setup_entry", return_value=True + ): + 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_ZONE: "zone.home", + CONF_TRACKED_ENTITIES: ["device_tracker.test3"], + CONF_IGNORED_ZONES: [], + CONF_TOLERANCE: 10, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "home 2" + + await hass.async_block_till_done() + + 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_ZONE: "zone.home", + CONF_TRACKED_ENTITIES: ["device_tracker.test4"], + CONF_IGNORED_ZONES: [], + CONF_TOLERANCE: 10, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "home 4" + + await hass.async_block_till_done() From baa511b80873fe3217e0c5542310088241a7bb57 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 08:56:23 -0500 Subject: [PATCH 0213/1367] Fix group sensor uom's in not convertable device classes (#109580) --- homeassistant/components/group/sensor.py | 37 +++++++++- tests/components/group/test_sensor.py | 86 ++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index 3c8f7059901..47695a275fc 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -1,4 +1,5 @@ """Platform allowing several sensors to be grouped into one sensor to provide numeric combinations.""" + from __future__ import annotations from collections.abc import Callable @@ -13,6 +14,7 @@ from homeassistant.components.input_number import DOMAIN as INPUT_NUMBER_DOMAIN from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN from homeassistant.components.sensor import ( CONF_STATE_CLASS, + DEVICE_CLASS_UNITS, DEVICE_CLASSES_SCHEMA, DOMAIN, PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, @@ -313,6 +315,7 @@ class SensorGroup(GroupEntity, SensorEntity): self._device_class = device_class self._native_unit_of_measurement = unit_of_measurement self._valid_units: set[str | None] = set() + self._can_convert: bool = False self._attr_name = name if name == DEFAULT_NAME: self._attr_name = f"{DEFAULT_NAME} {sensor_type}".capitalize() @@ -352,10 +355,18 @@ class SensorGroup(GroupEntity, SensorEntity): self._valid_units and (uom := state.attributes["unit_of_measurement"]) in self._valid_units + and self._can_convert is True ): numeric_state = UNIT_CONVERTERS[self.device_class].convert( numeric_state, uom, self.native_unit_of_measurement ) + if ( + self._valid_units + and (uom := state.attributes["unit_of_measurement"]) + not in self._valid_units + ): + raise HomeAssistantError("Not a valid unit") + sensor_values.append((entity_id, numeric_state, state)) if entity_id in self._state_incorrect: self._state_incorrect.remove(entity_id) @@ -536,8 +547,21 @@ class SensorGroup(GroupEntity, SensorEntity): unit_of_measurements.append(_unit_of_measurement) # Ensure only valid unit of measurements for the specific device class can be used - if (device_class := self.device_class) in UNIT_CONVERTERS and all( - x in UNIT_CONVERTERS[device_class].VALID_UNITS for x in unit_of_measurements + if ( + # Test if uom's in device class is convertible + (device_class := self.device_class) in UNIT_CONVERTERS + and all( + uom in UNIT_CONVERTERS[device_class].VALID_UNITS + for uom in unit_of_measurements + ) + ) or ( + # Test if uom's in device class is not convertible + device_class + and device_class not in UNIT_CONVERTERS + and device_class in DEVICE_CLASS_UNITS + and all( + uom in DEVICE_CLASS_UNITS[device_class] for uom in unit_of_measurements + ) ): async_delete_issue( self.hass, DOMAIN, f"{self.entity_id}_uoms_not_matching_device_class" @@ -546,6 +570,7 @@ class SensorGroup(GroupEntity, SensorEntity): self.hass, DOMAIN, f"{self.entity_id}_uoms_not_matching_no_device_class" ) return unit_of_measurements[0] + if device_class: async_create_issue( self.hass, @@ -587,5 +612,13 @@ class SensorGroup(GroupEntity, SensorEntity): if ( device_class := self.device_class ) in UNIT_CONVERTERS and self.native_unit_of_measurement: + self._can_convert = True return UNIT_CONVERTERS[device_class].VALID_UNITS + if ( + device_class + and (device_class) in DEVICE_CLASS_UNITS + and self.native_unit_of_measurement + ): + valid_uoms: set = DEVICE_CLASS_UNITS[device_class] + return valid_uoms return set() diff --git a/tests/components/group/test_sensor.py b/tests/components/group/test_sensor.py index aa4901e689c..ec6905a500f 100644 --- a/tests/components/group/test_sensor.py +++ b/tests/components/group/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Group Sensor platform.""" + from __future__ import annotations from math import prod @@ -27,6 +28,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -557,6 +559,90 @@ async def test_sensor_calculated_result_fails_on_uom(hass: HomeAssistant) -> Non assert state.attributes.get("unit_of_measurement") == "kWh" +async def test_sensor_calculated_properties_not_convertible_device_class( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the sensor calculating device_class, state_class and unit of measurement when device class not convertible.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": "test_sum", + "type": "sum", + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id_sum_sensor", + } + } + + entity_ids = config["sensor"]["entities"] + + hass.states.async_set( + entity_ids[0], + VALUES[0], + { + "device_class": SensorDeviceClass.HUMIDITY, + "state_class": SensorStateClass.MEASUREMENT, + "unit_of_measurement": PERCENTAGE, + }, + ) + hass.states.async_set( + entity_ids[1], + VALUES[1], + { + "device_class": SensorDeviceClass.HUMIDITY, + "state_class": SensorStateClass.MEASUREMENT, + "unit_of_measurement": PERCENTAGE, + }, + ) + hass.states.async_set( + entity_ids[2], + VALUES[2], + { + "device_class": SensorDeviceClass.HUMIDITY, + "state_class": SensorStateClass.MEASUREMENT, + "unit_of_measurement": PERCENTAGE, + }, + ) + await hass.async_block_till_done() + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_sum") + assert state.state == str(sum(VALUES)) + assert state.attributes.get("device_class") == "humidity" + assert state.attributes.get("state_class") == "measurement" + assert state.attributes.get("unit_of_measurement") == "%" + + assert ( + "Unable to use state. Only entities with correct unit of measurement is" + " supported when having a device class" + ) not in caplog.text + + hass.states.async_set( + entity_ids[2], + VALUES[2], + { + "device_class": SensorDeviceClass.HUMIDITY, + "state_class": SensorStateClass.MEASUREMENT, + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_sum") + assert state.state == STATE_UNKNOWN + assert state.attributes.get("device_class") == "humidity" + assert state.attributes.get("state_class") == "measurement" + assert state.attributes.get("unit_of_measurement") == "%" + + assert ( + "Unable to use state. Only entities with correct unit of measurement is" + " supported when having a device class, entity sensor.test_3, value 15.3 with" + " device class humidity and unit of measurement None excluded from calculation" + " in sensor.test_sum" + ) in caplog.text + + async def test_last_sensor(hass: HomeAssistant) -> None: """Test the last sensor.""" config = { From 9831da34cea3ea070be2c0f584a1133bd002004b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 08:57:26 -0500 Subject: [PATCH 0214/1367] Add new climate feature flags to esphome (#109428) --- homeassistant/components/esphome/climate.py | 3 + .../esphome/snapshots/test_climate.ambr | 38 +++++++++++++ tests/components/esphome/test_climate.py | 57 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 tests/components/esphome/snapshots/test_climate.ambr diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 5c265068216..9c2177800f3 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -137,6 +137,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = "climate" + _enable_turn_on_off_backwards_compatibility = False @callback def _on_static_info_update(self, static_info: EntityInfo) -> None: @@ -179,6 +180,8 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti features |= ClimateEntityFeature.FAN_MODE if self.swing_modes: features |= ClimateEntityFeature.SWING_MODE + if len(self.hvac_modes) > 1 and HVACMode.OFF in self.hvac_modes: + features |= ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON self._attr_supported_features = features def _get_precision(self) -> float: diff --git a/tests/components/esphome/snapshots/test_climate.ambr b/tests/components/esphome/snapshots/test_climate.ambr new file mode 100644 index 00000000000..69d721ecb94 --- /dev/null +++ b/tests/components/esphome/snapshots/test_climate.ambr @@ -0,0 +1,38 @@ +# serializer version: 1 +# name: test_climate_entity_attributes[climate-entity-attributes] + ReadOnlyDict({ + 'current_temperature': 30, + 'fan_mode': 'auto', + 'fan_modes': list([ + 'low', + 'high', + 'fan1', + 'fan2', + ]), + 'friendly_name': 'Test my climate', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 30, + 'min_temp': 10, + 'preset_mode': 'none', + 'preset_modes': list([ + 'away', + 'activity', + 'preset1', + 'preset2', + ]), + 'supported_features': , + 'swing_mode': 'both', + 'swing_modes': list([ + 'both', + 'off', + ]), + 'target_temp_step': 2, + 'temperature': 20, + }) +# --- diff --git a/tests/components/esphome/test_climate.py b/tests/components/esphome/test_climate.py index cb9a084d094..dbdee826137 100644 --- a/tests/components/esphome/test_climate.py +++ b/tests/components/esphome/test_climate.py @@ -14,6 +14,7 @@ from aioesphomeapi import ( ClimateState, ClimateSwingMode, ) +from syrupy import SnapshotAssertion from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, @@ -432,3 +433,59 @@ async def test_climate_entity_with_inf_value( assert attributes[ATTR_MIN_HUMIDITY] == 10 assert ATTR_TEMPERATURE not in attributes assert attributes[ATTR_CURRENT_TEMPERATURE] is None + + +async def test_climate_entity_attributes( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, + snapshot: SnapshotAssertion, +) -> None: + """Test a climate entity sets correct attributes.""" + entity_info = [ + ClimateInfo( + object_id="myclimate", + key=1, + name="my climate", + unique_id="my_climate", + supports_current_temperature=True, + visual_target_temperature_step=2, + visual_current_temperature_step=2, + supports_action=True, + visual_min_temperature=10.0, + visual_max_temperature=30.0, + supported_fan_modes=[ClimateFanMode.LOW, ClimateFanMode.HIGH], + supported_modes=[ + ClimateMode.COOL, + ClimateMode.HEAT, + ClimateMode.AUTO, + ClimateMode.OFF, + ], + supported_presets=[ClimatePreset.AWAY, ClimatePreset.ACTIVITY], + supported_custom_presets=["preset1", "preset2"], + supported_custom_fan_modes=["fan1", "fan2"], + supported_swing_modes=[ClimateSwingMode.BOTH, ClimateSwingMode.OFF], + ) + ] + states = [ + ClimateState( + key=1, + mode=ClimateMode.COOL, + action=ClimateAction.COOLING, + current_temperature=30, + target_temperature=20, + fan_mode=ClimateFanMode.AUTO, + swing_mode=ClimateSwingMode.BOTH, + ) + ] + 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("climate.test_myclimate") + assert state is not None + assert state.state == HVACMode.COOL + assert state.attributes == snapshot(name="climate-entity-attributes") From c732668d6e70c89ad55dbe8c4169823b57d3d060 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 08:59:34 -0500 Subject: [PATCH 0215/1367] Add migrated climate feature flag to switchbee (#109458) --- homeassistant/components/switchbee/climate.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbee/climate.py b/homeassistant/components/switchbee/climate.py index 8dd740262f9..1fc5cfcba12 100644 --- a/homeassistant/components/switchbee/climate.py +++ b/homeassistant/components/switchbee/climate.py @@ -1,4 +1,5 @@ """Support for SwitchBee climate.""" + from __future__ import annotations from typing import Any @@ -87,11 +88,9 @@ async def async_setup_entry( class SwitchBeeClimateEntity(SwitchBeeDeviceEntity[SwitchBeeThermostat], ClimateEntity): """Representation of a SwitchBee climate.""" - _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE - ) _attr_fan_modes = SUPPORTED_FAN_MODES _attr_target_temperature_step = 1 + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -106,6 +105,13 @@ class SwitchBeeClimateEntity(SwitchBeeDeviceEntity[SwitchBeeThermostat], Climate self._attr_temperature_unit = HVAC_UNIT_SB_TO_HASS[device.temperature_unit] self._attr_hvac_modes = [HVAC_MODE_SB_TO_HASS[mode] for mode in device.modes] self._attr_hvac_modes.append(HVACMode.OFF) + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ) + if len(self.hvac_modes) > 1: + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) self._update_attrs_from_coordinator() @callback From e109ed53eb113eb940eff4d188ea7fb1270b2046 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 09:00:31 -0500 Subject: [PATCH 0216/1367] Add new climate feature flags to switcher_kis (#109459) --- homeassistant/components/switcher_kis/climate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/switcher_kis/climate.py b/homeassistant/components/switcher_kis/climate.py index 272d3ccf6ef..01c4814f985 100644 --- a/homeassistant/components/switcher_kis/climate.py +++ b/homeassistant/components/switcher_kis/climate.py @@ -86,6 +86,7 @@ class SwitcherClimateEntity( _attr_has_entity_name = True _attr_name = None + _enable_turn_on_off_backwards_compatibility = False def __init__( self, coordinator: SwitcherDataUpdateCoordinator, remote: SwitcherBreezeRemote @@ -118,6 +119,10 @@ class SwitcherClimateEntity( if features["swing"] and not remote.separated_swing_command: self._attr_supported_features |= ClimateEntityFeature.SWING_MODE + # There is always support for off + minimum one other mode so no need to check + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) self._update_data(True) @callback From eefc6cd50ab44b56a629ac00e2592ca3180123c9 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 4 Feb 2024 15:01:06 +0100 Subject: [PATCH 0217/1367] Correct flow rate conversion review after merge (#109501) --- homeassistant/components/sensor/recorder.py | 9 ++++++++- tests/components/number/test_init.py | 4 ++-- tests/components/sensor/test_recorder.py | 13 +++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 04dd69349e4..2edd5b0e103 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -36,7 +36,13 @@ from homeassistant.loader import async_suggest_report_issue from homeassistant.util import dt as dt_util from homeassistant.util.enum import try_parse_enum -from .const import ATTR_LAST_RESET, ATTR_STATE_CLASS, DOMAIN, SensorStateClass +from .const import ( + ATTR_LAST_RESET, + ATTR_STATE_CLASS, + DOMAIN, + SensorStateClass, + UnitOfVolumeFlowRate, +) _LOGGER = logging.getLogger(__name__) @@ -52,6 +58,7 @@ EQUIVALENT_UNITS = { "RPM": REVOLUTIONS_PER_MINUTE, "ft3": UnitOfVolume.CUBIC_FEET, "m3": UnitOfVolume.CUBIC_METERS, + "ft³/m": UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, } # Keep track of entities for which a warning about decreasing value has been logged diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 9c66b45df25..279ffbfbbaa 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -688,7 +688,7 @@ async def test_restore_number_restore_state( 38.0, ), ( - SensorDeviceClass.VOLUME_FLOW_RATE, + NumberDeviceClass.VOLUME_FLOW_RATE, UnitOfVolumeFlowRate.LITERS_PER_MINUTE, UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, @@ -696,7 +696,7 @@ async def test_restore_number_restore_state( "13.2", ), ( - SensorDeviceClass.VOLUME_FLOW_RATE, + NumberDeviceClass.VOLUME_FLOW_RATE, UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, UnitOfVolumeFlowRate.LITERS_PER_MINUTE, UnitOfVolumeFlowRate.LITERS_PER_MINUTE, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 34aaeda6740..2dcc873ca8b 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -2422,6 +2422,7 @@ def test_list_statistic_ids_unsupported( (None, "kW", "Wh", "power", 13.050847, -10, 30), # Can't downgrade from ft³ to ft3 or from m³ to m3 (None, "ft³", "ft3", "volume", 13.050847, -10, 30), + (None, "ft³/min", "ft³/m", "volume_flow_rate", 13.050847, -10, 30), (None, "m³", "m3", "volume", 13.050847, -10, 30), ], ) @@ -2887,6 +2888,17 @@ def test_compile_hourly_statistics_convert_units_1( (None, "RPM", "rpm", None, None, 13.050847, 13.333333, -10, 30), (None, "rpm", "RPM", None, None, 13.050847, 13.333333, -10, 30), (None, "ft3", "ft³", None, "volume", 13.050847, 13.333333, -10, 30), + ( + None, + "ft³/m", + "ft³/min", + None, + "volume_flow_rate", + 13.050847, + 13.333333, + -10, + 30, + ), (None, "m3", "m³", None, "volume", 13.050847, 13.333333, -10, 30), ], ) @@ -3010,6 +3022,7 @@ def test_compile_hourly_statistics_equivalent_units_1( (None, "RPM", "rpm", None, 13.333333, -10, 30), (None, "rpm", "RPM", None, 13.333333, -10, 30), (None, "ft3", "ft³", None, 13.333333, -10, 30), + (None, "ft³/m", "ft³/min", None, 13.333333, -10, 30), (None, "m3", "m³", None, 13.333333, -10, 30), ], ) From ae707299320df6dbfa3f1cfb642afc05fdf93aeb Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Sun, 4 Feb 2024 15:02:21 +0100 Subject: [PATCH 0218/1367] Revert "Add webhook support to tedee integration (#106846)" (#109408) --- homeassistant/components/tedee/__init__.py | 80 +------------- homeassistant/components/tedee/config_flow.py | 8 +- homeassistant/components/tedee/coordinator.py | 26 +---- homeassistant/components/tedee/manifest.json | 2 +- tests/components/tedee/conftest.py | 7 +- tests/components/tedee/test_config_flow.py | 43 +++----- tests/components/tedee/test_init.py | 104 +----------------- tests/components/tedee/test_lock.py | 34 +----- 8 files changed, 30 insertions(+), 274 deletions(-) diff --git a/homeassistant/components/tedee/__init__.py b/homeassistant/components/tedee/__init__.py index cbc608d03a6..eeb0f8e0d5a 100644 --- a/homeassistant/components/tedee/__init__.py +++ b/homeassistant/components/tedee/__init__.py @@ -1,25 +1,12 @@ """Init the tedee component.""" -from collections.abc import Awaitable, Callable -from http import HTTPStatus import logging -from typing import Any -from aiohttp.hdrs import METH_POST -from aiohttp.web import Request, Response -from pytedee_async.exception import TedeeWebhookException - -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.webhook import ( - async_generate_url as webhook_generate_url, - async_register as webhook_register, - async_unregister as webhook_unregister, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_WEBHOOK_ID, EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from .const import DOMAIN, NAME +from .const import DOMAIN from .coordinator import TedeeApiCoordinator PLATFORMS = [ @@ -50,38 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - async def unregister_webhook(_: Any) -> None: - await coordinator.async_unregister_webhook() - webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) - - async def register_webhook() -> None: - webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID]) - webhook_name = "Tedee" - if entry.title != NAME: - webhook_name = f"{NAME} {entry.title}" - - webhook_register( - hass, - DOMAIN, - webhook_name, - entry.data[CONF_WEBHOOK_ID], - get_webhook_handler(coordinator), - allowed_methods=[METH_POST], - ) - _LOGGER.debug("Registered Tedee webhook at hass: %s", webhook_url) - - try: - await coordinator.async_register_webhook(webhook_url) - except TedeeWebhookException as ex: - _LOGGER.warning("Failed to register Tedee webhook from bridge: %s", ex) - else: - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) - ) - - entry.async_create_background_task( - hass, register_webhook(), "tedee_register_webhook" - ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -90,34 +45,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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): + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -def get_webhook_handler( - coordinator: TedeeApiCoordinator, -) -> Callable[[HomeAssistant, str, Request], Awaitable[Response | None]]: - """Return webhook handler.""" - - async def async_webhook_handler( - hass: HomeAssistant, webhook_id: str, request: Request - ) -> Response | None: - # Handle http post calls to the path. - if not request.body_exists: - return HomeAssistantView.json( - result="No Body", status_code=HTTPStatus.BAD_REQUEST - ) - - body = await request.json() - try: - coordinator.webhook_received(body) - except TedeeWebhookException as ex: - return HomeAssistantView.json( - result=str(ex), status_code=HTTPStatus.BAD_REQUEST - ) - - return HomeAssistantView.json(result="OK", status_code=HTTPStatus.OK) - - return async_webhook_handler diff --git a/homeassistant/components/tedee/config_flow.py b/homeassistant/components/tedee/config_flow.py index 8bd9efd2b17..075a4c998ea 100644 --- a/homeassistant/components/tedee/config_flow.py +++ b/homeassistant/components/tedee/config_flow.py @@ -11,9 +11,8 @@ from pytedee_async import ( ) import voluptuous as vol -from homeassistant.components.webhook import async_generate_id as webhook_generate_id from homeassistant.config_entries import ConfigEntry, ConfigFlow -from homeassistant.const import CONF_HOST, CONF_WEBHOOK_ID +from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -62,10 +61,7 @@ class TedeeConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="reauth_successful") await self.async_set_unique_id(local_bridge.serial) self._abort_if_unique_id_configured() - return self.async_create_entry( - title=NAME, - data={**user_input, CONF_WEBHOOK_ID: webhook_generate_id()}, - ) + return self.async_create_entry(title=NAME, data=user_input) return self.async_show_form( step_id="user", diff --git a/homeassistant/components/tedee/coordinator.py b/homeassistant/components/tedee/coordinator.py index cdd907b2e58..c846f2a8d9a 100644 --- a/homeassistant/components/tedee/coordinator.py +++ b/homeassistant/components/tedee/coordinator.py @@ -3,7 +3,6 @@ from collections.abc import Awaitable, Callable from datetime import timedelta import logging import time -from typing import Any from pytedee_async import ( TedeeClient, @@ -11,7 +10,6 @@ from pytedee_async import ( TedeeDataUpdateException, TedeeLocalAuthException, TedeeLock, - TedeeWebhookException, ) from pytedee_async.bridge import TedeeBridge @@ -25,7 +23,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN -SCAN_INTERVAL = timedelta(seconds=30) +SCAN_INTERVAL = timedelta(seconds=20) GET_LOCKS_INTERVAL_SECONDS = 3600 _LOGGER = logging.getLogger(__name__) @@ -55,7 +53,6 @@ class TedeeApiCoordinator(DataUpdateCoordinator[dict[int, TedeeLock]]): self._next_get_locks = time.time() self._locks_last_update: set[int] = set() self.new_lock_callbacks: list[Callable[[int], None]] = [] - self.tedee_webhook_id: int | None = None @property def bridge(self) -> TedeeBridge: @@ -106,27 +103,6 @@ class TedeeApiCoordinator(DataUpdateCoordinator[dict[int, TedeeLock]]): except (TedeeClientException, TimeoutError) as ex: raise UpdateFailed("Querying API failed. Error: %s" % str(ex)) from ex - def webhook_received(self, message: dict[str, Any]) -> None: - """Handle webhook message.""" - self.tedee_client.parse_webhook_message(message) - self.async_set_updated_data(self.tedee_client.locks_dict) - - async def async_register_webhook(self, webhook_url: str) -> None: - """Register the webhook at the Tedee bridge.""" - self.tedee_webhook_id = await self.tedee_client.register_webhook(webhook_url) - - async def async_unregister_webhook(self) -> None: - """Unregister the webhook at the Tedee bridge.""" - if self.tedee_webhook_id is not None: - try: - await self.tedee_client.delete_webhook(self.tedee_webhook_id) - except TedeeWebhookException as ex: - _LOGGER.warning( - "Failed to unregister Tedee webhook from bridge: %s", ex - ) - else: - _LOGGER.debug("Unregistered Tedee webhook") - def _async_add_remove_locks(self) -> None: """Add new locks, remove non-existing locks.""" if not self._locks_last_update: diff --git a/homeassistant/components/tedee/manifest.json b/homeassistant/components/tedee/manifest.json index 0a13b2266fa..1776e3b7ab2 100644 --- a/homeassistant/components/tedee/manifest.json +++ b/homeassistant/components/tedee/manifest.json @@ -3,7 +3,7 @@ "name": "Tedee", "codeowners": ["@patrickhilker", "@zweckj"], "config_flow": true, - "dependencies": ["http", "webhook"], + "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/tedee", "iot_class": "local_push", "requirements": ["pytedee-async==0.2.13"] diff --git a/tests/components/tedee/conftest.py b/tests/components/tedee/conftest.py index a633b1642ea..21fb4047ab3 100644 --- a/tests/components/tedee/conftest.py +++ b/tests/components/tedee/conftest.py @@ -10,13 +10,11 @@ from pytedee_async.lock import TedeeLock import pytest from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN -from homeassistant.const import CONF_HOST, CONF_WEBHOOK_ID +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture -WEBHOOK_ID = "bq33efxmdi3vxy55q2wbnudbra7iv8mjrq9x0gea33g4zqtd87093pwveg8xcb33" - @pytest.fixture def mock_config_entry() -> MockConfigEntry: @@ -27,7 +25,6 @@ def mock_config_entry() -> MockConfigEntry: data={ CONF_LOCAL_ACCESS_TOKEN: "api_token", CONF_HOST: "192.168.1.42", - CONF_WEBHOOK_ID: WEBHOOK_ID, }, unique_id="0000-0000", ) @@ -62,8 +59,6 @@ def mock_tedee(request) -> Generator[MagicMock, None, None]: tedee.get_local_bridge.return_value = TedeeBridge(0, "0000-0000", "Bridge-AB1C") tedee.parse_webhook_message.return_value = None - tedee.register_webhook.return_value = 1 - tedee.delete_webhooks.return_value = None locks_json = json.loads(load_fixture("locks.json", DOMAIN)) diff --git a/tests/components/tedee/test_config_flow.py b/tests/components/tedee/test_config_flow.py index 68a61842fc3..bc5b73aa4a9 100644 --- a/tests/components/tedee/test_config_flow.py +++ b/tests/components/tedee/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Tedee config flow.""" -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock from pytedee_async import ( TedeeClientException, @@ -10,12 +10,10 @@ import pytest from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_WEBHOOK_ID +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .conftest import WEBHOOK_ID - from tests.common import MockConfigEntry FLOW_UNIQUE_ID = "112233445566778899" @@ -24,30 +22,25 @@ LOCAL_ACCESS_TOKEN = "api_token" async def test_flow(hass: HomeAssistant, mock_tedee: MagicMock) -> None: """Test config flow with one bridge.""" - with patch( - "homeassistant.components.tedee.config_flow.webhook_generate_id", - return_value=WEBHOOK_ID, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOST: "192.168.1.62", - CONF_LOCAL_ACCESS_TOKEN: "token", - }, - ) - - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["data"] == { + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { CONF_HOST: "192.168.1.62", CONF_LOCAL_ACCESS_TOKEN: "token", - CONF_WEBHOOK_ID: WEBHOOK_ID, - } + }, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == { + CONF_HOST: "192.168.1.62", + CONF_LOCAL_ACCESS_TOKEN: "token", + } async def test_flow_already_configured( diff --git a/tests/components/tedee/test_init.py b/tests/components/tedee/test_init.py index 05fb2c1d6eb..ca64c01a983 100644 --- a/tests/components/tedee/test_init.py +++ b/tests/components/tedee/test_init.py @@ -1,27 +1,15 @@ """Test initialization of tedee.""" -from http import HTTPStatus -from typing import Any from unittest.mock import MagicMock -from urllib.parse import urlparse -from pytedee_async.exception import ( - TedeeAuthException, - TedeeClientException, - TedeeWebhookException, -) +from pytedee_async.exception import TedeeAuthException, TedeeClientException import pytest from syrupy import SnapshotAssertion -from homeassistant.components.webhook import async_generate_url from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from .conftest import WEBHOOK_ID - from tests.common import MockConfigEntry -from tests.typing import ClientSessionGenerator async def test_load_unload_config_entry( @@ -62,62 +50,6 @@ async def test_config_entry_not_ready( assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY -async def test_cleanup_on_shutdown( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_tedee: MagicMock, -) -> None: - """Test the webhook is cleaned up on shutdown.""" - 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.state is ConfigEntryState.LOADED - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - mock_tedee.delete_webhook.assert_called_once() - - -async def test_webhook_cleanup_errors( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_tedee: MagicMock, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the webhook is cleaned up on shutdown.""" - 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.state is ConfigEntryState.LOADED - - mock_tedee.delete_webhook.side_effect = TedeeWebhookException("") - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - mock_tedee.delete_webhook.assert_called_once() - assert "Failed to unregister Tedee webhook from bridge" in caplog.text - - -async def test_webhook_registration_errors( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_tedee: MagicMock, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the webhook is cleaned up on shutdown.""" - mock_tedee.register_webhook.side_effect = TedeeWebhookException("") - 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.state is ConfigEntryState.LOADED - - mock_tedee.register_webhook.assert_called_once() - assert "Failed to register Tedee webhook from bridge" in caplog.text - - async def test_bridge_device( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -135,37 +67,3 @@ async def test_bridge_device( ) assert device assert device == snapshot - - -@pytest.mark.parametrize( - ("body", "expected_code", "side_effect"), - [ - ({"hello": "world"}, HTTPStatus.OK, None), # Success - (None, HTTPStatus.BAD_REQUEST, None), # Missing data - ({}, HTTPStatus.BAD_REQUEST, TedeeWebhookException), # Error - ], -) -async def test_webhook_post( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_tedee: MagicMock, - hass_client_no_auth: ClientSessionGenerator, - body: dict[str, Any], - expected_code: HTTPStatus, - side_effect: Exception, -) -> None: - """Test webhook callback.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - client = await hass_client_no_auth() - webhook_url = async_generate_url(hass, WEBHOOK_ID) - mock_tedee.parse_webhook_message.side_effect = side_effect - resp = await client.post(urlparse(webhook_url).path, json=body) - - # Wait for remaining tasks to complete. - await hass.async_block_till_done() - - assert resp.status == expected_code diff --git a/tests/components/tedee/test_lock.py b/tests/components/tedee/test_lock.py index 2f8b1e2b36d..fca1ae2b07f 100644 --- a/tests/components/tedee/test_lock.py +++ b/tests/components/tedee/test_lock.py @@ -1,10 +1,9 @@ """Tests for tedee lock.""" from datetime import timedelta from unittest.mock import MagicMock -from urllib.parse import urlparse from freezegun.api import FrozenDateTimeFactory -from pytedee_async import TedeeLock, TedeeLockState +from pytedee_async import TedeeLock from pytedee_async.exception import ( TedeeClientException, TedeeDataUpdateException, @@ -18,21 +17,15 @@ from homeassistant.components.lock import ( SERVICE_LOCK, SERVICE_OPEN, SERVICE_UNLOCK, - STATE_LOCKED, STATE_LOCKING, - STATE_UNLOCKED, STATE_UNLOCKING, ) -from homeassistant.components.webhook import async_generate_url from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -from .conftest import WEBHOOK_ID - from tests.common import MockConfigEntry, async_fire_time_changed -from tests.typing import ClientSessionGenerator pytestmark = pytest.mark.usefixtures("init_integration") @@ -273,28 +266,3 @@ async def test_new_lock( assert state state = hass.states.get("lock.lock_6g7h") assert state - - -async def test_webhook_update( - hass: HomeAssistant, - mock_tedee: MagicMock, - hass_client_no_auth: ClientSessionGenerator, -) -> None: - """Test updated data set through webhook.""" - - state = hass.states.get("lock.lock_1a2b") - assert state - assert state.state == STATE_UNLOCKED - - webhook_data = {"dummystate": 6} - mock_tedee.locks_dict[ - 12345 - ].state = TedeeLockState.LOCKED # is updated in the lib, so mock and assert in L296 - client = await hass_client_no_auth() - webhook_url = async_generate_url(hass, WEBHOOK_ID) - await client.post(urlparse(webhook_url).path, json=webhook_data) - mock_tedee.parse_webhook_message.assert_called_once_with(webhook_data) - - state = hass.states.get("lock.lock_1a2b") - assert state - assert state.state == STATE_LOCKED From 2950c402d7eb27f1481727304c669638c938fa78 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 09:02:52 -0500 Subject: [PATCH 0219/1367] Fix new climate feature flags in intesishome (#109563) --- homeassistant/components/intesishome/climate.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index 285be2c9cea..64f52fae0a6 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -146,6 +146,7 @@ class IntesisAC(ClimateEntity): _attr_should_poll = False _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, ih_device_id, ih_device, controller): """Initialize the thermostat.""" @@ -175,10 +176,6 @@ class IntesisAC(ClimateEntity): self._power_consumption_heat = None self._power_consumption_cool = None - self._attr_supported_features |= ( - ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON - ) - # Setpoint support if controller.has_setpoint_control(ih_device_id): self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE @@ -208,6 +205,11 @@ class IntesisAC(ClimateEntity): self._attr_hvac_modes.extend(mode_list) self._attr_hvac_modes.append(HVACMode.OFF) + if len(self.hvac_modes) > 1: + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) + async def async_added_to_hass(self) -> None: """Subscribe to event updates.""" _LOGGER.debug("Added climate device with state: %s", repr(self._ih_device)) From 846dae675c2d617dccb9e6021526f19d3bd09257 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 09:03:14 -0500 Subject: [PATCH 0220/1367] Add new climate feature flags to lcn (#109566) --- homeassistant/components/lcn/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index 4f40bcd25cd..d1e92d54fb1 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -69,7 +69,7 @@ async def async_setup_entry( class LcnClimate(LcnEntity, ClimateEntity): """Representation of a LCN climate device.""" - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _enable_turn_on_off_backwards_compatibility = False def __init__( self, config: ConfigType, entry_id: str, device_connection: DeviceConnectionType @@ -95,6 +95,11 @@ class LcnClimate(LcnEntity, ClimateEntity): self._attr_hvac_modes = [HVACMode.HEAT] if self.is_lockable: self._attr_hvac_modes.append(HVACMode.OFF) + self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + if len(self.hvac_modes) > 1: + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" From 8ef2bece59530c36395d79c2be7f6c8a7397d106 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 09:03:48 -0500 Subject: [PATCH 0221/1367] Add new climate feature flags to lyric (#109571) --- homeassistant/components/lyric/climate.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index 90d9e407cb2..ecf9b50474d 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -173,6 +173,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): PRESET_TEMPORARY_HOLD, PRESET_VACATION_HOLD, ] + _enable_turn_on_off_backwards_compatibility = False def __init__( self, @@ -231,6 +232,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): self._attr_supported_features | ClimateEntityFeature.FAN_MODE ) + if len(self.hvac_modes) > 1: + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) + super().__init__( coordinator, location, From ac8a4f4b054c67b73c3dc24eb704a82acc5a5635 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 4 Feb 2024 15:12:08 +0100 Subject: [PATCH 0222/1367] Redact location names in proximity diagnostics (#109600) --- .../components/proximity/coordinator.py | 17 +++++------ .../components/proximity/diagnostics.py | 28 ++++++++++++++++--- .../proximity/snapshots/test_diagnostics.ambr | 22 +++++++++++++++ .../components/proximity/test_diagnostics.py | 11 ++++++++ 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/proximity/coordinator.py b/homeassistant/components/proximity/coordinator.py index 4ae923276cc..53c1180e832 100644 --- a/homeassistant/components/proximity/coordinator.py +++ b/homeassistant/components/proximity/coordinator.py @@ -56,14 +56,11 @@ class ProximityData: entities: dict[str, dict[str, str | int | None]] -DEFAULT_DATA = ProximityData( - { - ATTR_DIST_TO: DEFAULT_DIST_TO_ZONE, - ATTR_DIR_OF_TRAVEL: DEFAULT_DIR_OF_TRAVEL, - ATTR_NEAREST: DEFAULT_NEAREST, - }, - {}, -) +DEFAULT_PROXIMITY_DATA: dict[str, str | float] = { + ATTR_DIST_TO: DEFAULT_DIST_TO_ZONE, + ATTR_DIR_OF_TRAVEL: DEFAULT_DIR_OF_TRAVEL, + ATTR_NEAREST: DEFAULT_NEAREST, +} class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]): @@ -92,7 +89,7 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]): update_interval=None, ) - self.data = DEFAULT_DATA + self.data = ProximityData(DEFAULT_PROXIMITY_DATA, {}) self.state_change_data: StateChangedData | None = None @@ -238,7 +235,7 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]): self.name, self.proximity_zone_id, ) - return DEFAULT_DATA + return ProximityData(DEFAULT_PROXIMITY_DATA, {}) entities_data = self.data.entities diff --git a/homeassistant/components/proximity/diagnostics.py b/homeassistant/components/proximity/diagnostics.py index ba5e1f53722..3ccecbe1f19 100644 --- a/homeassistant/components/proximity/diagnostics.py +++ b/homeassistant/components/proximity/diagnostics.py @@ -4,10 +4,18 @@ from __future__ import annotations from typing import Any from homeassistant.components.device_tracker import ATTR_GPS, ATTR_IP, ATTR_MAC -from homeassistant.components.diagnostics import async_redact_data +from homeassistant.components.diagnostics import REDACTED, async_redact_data from homeassistant.components.person import ATTR_USER_ID +from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + STATE_HOME, + STATE_NOT_HOME, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) from homeassistant.core import HomeAssistant from .const import DOMAIN @@ -21,6 +29,7 @@ TO_REDACT = { ATTR_MAC, ATTR_USER_ID, "context", + "location_name", } @@ -34,16 +43,27 @@ async def async_get_config_entry_diagnostics( "entry": entry.as_dict(), } + non_sensitiv_states = [ + STATE_HOME, + STATE_NOT_HOME, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ] + [z.name for z in hass.states.async_all(ZONE_DOMAIN)] + tracked_states: dict[str, dict] = {} for tracked_entity_id in coordinator.tracked_entities: if (state := hass.states.get(tracked_entity_id)) is None: continue - tracked_states[tracked_entity_id] = state.as_dict() + tracked_states[tracked_entity_id] = async_redact_data( + state.as_dict(), TO_REDACT + ) + if state.state not in non_sensitiv_states: + tracked_states[tracked_entity_id]["state"] = REDACTED diag_data["data"] = { "proximity": coordinator.data.proximity, "entities": coordinator.data.entities, "entity_mapping": coordinator.entity_mapping, - "tracked_states": async_redact_data(tracked_states, TO_REDACT), + "tracked_states": tracked_states, } return diag_data diff --git a/tests/components/proximity/snapshots/test_diagnostics.ambr b/tests/components/proximity/snapshots/test_diagnostics.ambr index f8f7d9b014e..a93ff33f443 100644 --- a/tests/components/proximity/snapshots/test_diagnostics.ambr +++ b/tests/components/proximity/snapshots/test_diagnostics.ambr @@ -15,6 +15,12 @@ 'is_in_ignored_zone': False, 'name': 'test2', }), + 'device_tracker.test3': dict({ + 'dir_of_travel': None, + 'dist_to_zone': 4077309, + 'is_in_ignored_zone': False, + 'name': 'test3', + }), }), 'entity_mapping': dict({ 'device_tracker.test1': list([ @@ -29,6 +35,10 @@ 'sensor.home_test3_distance', 'sensor.home_test3_direction_of_travel', ]), + 'device_tracker.test4': list([ + 'sensor.home_test4_distance', + 'sensor.home_test4_direction_of_travel', + ]), }), 'proximity': dict({ 'dir_of_travel': 'unknown', @@ -56,6 +66,17 @@ 'entity_id': 'device_tracker.test2', 'state': 'not_home', }), + 'device_tracker.test3': dict({ + 'attributes': dict({ + 'friendly_name': 'test3', + 'latitude': '**REDACTED**', + 'location_name': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'context': '**REDACTED**', + 'entity_id': 'device_tracker.test3', + 'state': '**REDACTED**', + }), }), }), 'entry': dict({ @@ -67,6 +88,7 @@ 'device_tracker.test1', 'device_tracker.test2', 'device_tracker.test3', + 'device_tracker.test4', ]), 'zone': 'zone.home', }), diff --git a/tests/components/proximity/test_diagnostics.py b/tests/components/proximity/test_diagnostics.py index 35ecd152a06..e23d8180672 100644 --- a/tests/components/proximity/test_diagnostics.py +++ b/tests/components/proximity/test_diagnostics.py @@ -35,6 +35,16 @@ async def test_entry_diagnostics( "not_home", {"friendly_name": "test2", "latitude": 150.1, "longitude": 20.1}, ) + hass.states.async_set( + "device_tracker.test3", + "my secret address", + { + "friendly_name": "test3", + "latitude": 150.1, + "longitude": 20.1, + "location_name": "my secret address", + }, + ) mock_entry = MockConfigEntry( domain=DOMAIN, @@ -45,6 +55,7 @@ async def test_entry_diagnostics( "device_tracker.test1", "device_tracker.test2", "device_tracker.test3", + "device_tracker.test4", ], CONF_IGNORED_ZONES: [], CONF_TOLERANCE: 1, From a74516a80d332c6ad28a473ec93e12bd2aaf8621 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 08:13:58 -0600 Subject: [PATCH 0223/1367] Cache path for Store helper (#109587) --- homeassistant/helpers/storage.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index f789aeb37e4..2a175f76182 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -9,7 +9,7 @@ import inspect from json import JSONDecodeError, JSONEncoder import logging import os -from typing import Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypeVar from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE from homeassistant.core import ( @@ -28,6 +28,12 @@ from homeassistant.util.file import WriteError from . import json as json_helper +if TYPE_CHECKING: + from functools import cached_property +else: + from ..backports.functools import cached_property + + # mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any # mypy: no-check-untyped-defs @@ -110,7 +116,7 @@ class Store(Generic[_T]): self._atomic_writes = atomic_writes self._read_only = read_only - @property + @cached_property def path(self): """Return the config path.""" return self.hass.config.path(STORAGE_DIR, self.key) From e877113b21c442a11c267bc58a6443214496f557 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 09:15:00 -0500 Subject: [PATCH 0224/1367] Add back logging for core for feature flags in climate (#109572) --- homeassistant/components/climate/__init__.py | 3 - tests/components/climate/test_init.py | 62 -------------------- 2 files changed, 65 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index bf663fac365..43d98ad6bbd 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -339,9 +339,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def _report_turn_on_off(feature: str, method: str) -> None: """Log warning not implemented turn on/off feature.""" - module = type(self).__module__ - if module and "custom_components" not in module: - return report_issue = self._suggest_report_issue() if feature.startswith("TURN"): message = ( diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index f764ad77aa9..0e4e70796f0 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -684,65 +684,3 @@ async def test_no_warning_integration_has_migrated( " implements HVACMode(s): off, heat and therefore implicitly supports the off, heat methods" not in caplog.text ) - - -async def test_no_warning_on_core_integrations_for_on_off_feature_flags( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, config_flow_fixture: None -) -> None: - """Test we don't warn on core integration on new turn_on/off feature flags.""" - - class MockClimateEntityTest(MockClimateEntity): - """Mock Climate device.""" - - def turn_on(self) -> None: - """Turn on.""" - - def turn_off(self) -> None: - """Turn off.""" - - async def async_setup_entry_init( - hass: HomeAssistant, config_entry: ConfigEntry - ) -> bool: - """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) - return True - - async def async_setup_entry_climate_platform( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, - ) -> None: - """Set up test climate platform via config entry.""" - async_add_entities( - [MockClimateEntityTest(name="test", entity_id="climate.test")] - ) - - mock_integration( - hass, - MockModule( - "test", - async_setup_entry=async_setup_entry_init, - ), - built_in=False, - ) - mock_platform( - hass, - "test.climate", - MockPlatform(async_setup_entry=async_setup_entry_climate_platform), - ) - - with patch.object( - MockClimateEntityTest, "__module__", "homeassistant.components.test.climate" - ): - config_entry = MockConfigEntry(domain="test") - 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("climate.test") - assert state is not None - - assert ( - "does not set ClimateEntityFeature.TURN_OFF but implements the turn_off method." - not in caplog.text - ) From 2f724b042b71ae098a0411a76fb2954fa4172033 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 08:15:51 -0600 Subject: [PATCH 0225/1367] Avoid looking up services to check if they support responses (#109588) We already have the Service object as its the value in the services_map so there is not need to look it up again --- homeassistant/helpers/service.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 30516e3a099..3fe0c0eb086 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -640,7 +640,7 @@ async def async_get_all_descriptions( descriptions[domain] = {} domain_descriptions = descriptions[domain] - for service_name in services_map: + for service_name, service in services_map.items(): cache_key = (domain, service_name) description = descriptions_cache.get(cache_key) if description is not None: @@ -695,11 +695,10 @@ async def async_get_all_descriptions( if "target" in yaml_description: description["target"] = yaml_description["target"] - if ( - response := hass.services.supports_response(domain, service_name) - ) != SupportsResponse.NONE: + response = service.supports_response + if response is not SupportsResponse.NONE: description["response"] = { - "optional": response == SupportsResponse.OPTIONAL, + "optional": response is SupportsResponse.OPTIONAL, } descriptions_cache[cache_key] = description From 7042ae05961da03f2e1c69612134ef589399bd75 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sun, 4 Feb 2024 15:16:25 +0100 Subject: [PATCH 0226/1367] Add ZonderGas virtual integration (#109601) --- homeassistant/components/zondergas/__init__.py | 1 + homeassistant/components/zondergas/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 homeassistant/components/zondergas/__init__.py create mode 100644 homeassistant/components/zondergas/manifest.json diff --git a/homeassistant/components/zondergas/__init__.py b/homeassistant/components/zondergas/__init__.py new file mode 100644 index 00000000000..150414e001f --- /dev/null +++ b/homeassistant/components/zondergas/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: ZonderGas.""" diff --git a/homeassistant/components/zondergas/manifest.json b/homeassistant/components/zondergas/manifest.json new file mode 100644 index 00000000000..09292e9d330 --- /dev/null +++ b/homeassistant/components/zondergas/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "zondergas", + "name": "ZonderGas", + "integration_type": "virtual", + "supported_by": "energyzero" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index c49882f4394..336a0a37475 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6952,6 +6952,11 @@ "config_flow": true, "iot_class": "calculated" }, + "zondergas": { + "name": "ZonderGas", + "integration_type": "virtual", + "supported_by": "energyzero" + }, "zoneminder": { "name": "ZoneMinder", "integration_type": "hub", From 9de9852c943c4c8a43e07fba1964ac0fa9eab606 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sun, 4 Feb 2024 15:18:24 +0100 Subject: [PATCH 0227/1367] Add SamSam virtual integration (#109602) --- homeassistant/components/samsam/__init__.py | 1 + homeassistant/components/samsam/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 homeassistant/components/samsam/__init__.py create mode 100644 homeassistant/components/samsam/manifest.json diff --git a/homeassistant/components/samsam/__init__.py b/homeassistant/components/samsam/__init__.py new file mode 100644 index 00000000000..a7109c35339 --- /dev/null +++ b/homeassistant/components/samsam/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: SamSam.""" diff --git a/homeassistant/components/samsam/manifest.json b/homeassistant/components/samsam/manifest.json new file mode 100644 index 00000000000..61078e6c432 --- /dev/null +++ b/homeassistant/components/samsam/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "samsam", + "name": "SamSam", + "integration_type": "virtual", + "supported_by": "energyzero" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 336a0a37475..d4ff080c9fc 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5080,6 +5080,11 @@ "config_flow": false, "iot_class": "local_polling" }, + "samsam": { + "name": "SamSam", + "integration_type": "virtual", + "supported_by": "energyzero" + }, "samsung": { "name": "Samsung", "integrations": { From 3db033137850b328eb46ba897bdb5c3c8d5128f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 08:21:05 -0600 Subject: [PATCH 0228/1367] Avoid converting to same units when compiling stats (#109531) --- homeassistant/components/sensor/recorder.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 2edd5b0e103..ebf138d39e6 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -222,13 +222,14 @@ def _normalize_states( converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER[statistics_unit] valid_fstates: list[tuple[float, State]] = [] - convert: Callable[[float], float] + convert: Callable[[float], float] | None = None last_unit: str | None | object = object() + valid_units = converter.VALID_UNITS for fstate, state in fstates: state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) # Exclude states with unsupported unit from statistics - if state_unit not in converter.VALID_UNITS: + if state_unit not in valid_units: if WARN_UNSUPPORTED_UNIT not in hass.data: hass.data[WARN_UNSUPPORTED_UNIT] = set() if entity_id not in hass.data[WARN_UNSUPPORTED_UNIT]: @@ -247,13 +248,20 @@ 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) + if state_unit == statistics_unit: + convert = None + else: + convert = converter.converter_factory(state_unit, statistics_unit) last_unit = state_unit - valid_fstates.append((convert(fstate), state)) + if convert is not None: + fstate = convert(fstate) + + valid_fstates.append((fstate, state)) return statistics_unit, valid_fstates From 8a9478b71400e8be84948b5fa959f109fb27c342 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 4 Feb 2024 11:05:51 -0500 Subject: [PATCH 0229/1367] Update Flo test to address review comment (#109604) * Update Flo test to address review comment * update comment * clean up * cleanup * change mock * remove unnecessary assert * review comment --- tests/components/flo/test_device.py | 52 ++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/tests/components/flo/test_device.py b/tests/components/flo/test_device.py index 6a633c774ed..884c9e3ca7e 100644 --- a/tests/components/flo/test_device.py +++ b/tests/components/flo/test_device.py @@ -3,15 +3,13 @@ from datetime import timedelta from unittest.mock import patch from aioflo.errors import RequestError -import pytest +from freezegun.api import FrozenDateTimeFactory from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN from homeassistant.components.flo.device import FloDeviceDataUpdateCoordinator -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.setup import async_setup_component -from homeassistant.util import dt as dt_util from .common import TEST_PASSWORD, TEST_USER_ID @@ -24,6 +22,7 @@ async def test_device( config_entry, aioclient_mock_fixture, aioclient_mock: AiohttpClientMocker, + freezer: FrozenDateTimeFactory, ) -> None: """Test Flo by Moen devices.""" config_entry.add_to_hass(hass) @@ -83,18 +82,47 @@ async def test_device( call_count = aioclient_mock.call_count - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=90)) + freezer.tick(timedelta(seconds=90)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert aioclient_mock.call_count == call_count + 6 - # test error sending device ping + +async def test_device_failures( + hass: HomeAssistant, + config_entry, + aioclient_mock_fixture, + aioclient_mock: AiohttpClientMocker, + freezer: FrozenDateTimeFactory, +) -> None: + """Test Flo by Moen devices buffer API failures.""" + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + def assert_state(state: str) -> None: + assert ( + hass.states.get("sensor.smart_water_shutoff_current_system_mode").state + == state + ) + + assert_state("home") + + async def move_time_and_assert_state(state: str) -> None: + freezer.tick(timedelta(seconds=65)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert_state(state) + + aioclient_mock.clear_requests() with patch( "homeassistant.components.flo.device.FloDeviceDataUpdateCoordinator.send_presence_ping", side_effect=RequestError, - ), pytest.raises(UpdateFailed): - # simulate 4 updates failing - await valve._async_update_data() - await valve._async_update_data() - await valve._async_update_data() - await valve._async_update_data() + ): + # simulate 4 updates failing. The failures should be buffered so that it takes 4 + # consecutive failures to mark the device and entities as unavailable. + await move_time_and_assert_state("home") + await move_time_and_assert_state("home") + await move_time_and_assert_state("home") + await move_time_and_assert_state(STATE_UNAVAILABLE) From 2c91b312331ce76a078da2c30c60ca0fa7cf82f6 Mon Sep 17 00:00:00 2001 From: Cody C <50791984+codyc1515@users.noreply.github.com> Date: Mon, 5 Feb 2024 05:22:22 +1300 Subject: [PATCH 0230/1367] Remove default name prefix of HomePods from Suggested Area in Apple TV integration (#109489) --- homeassistant/components/apple_tv/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 8f52db13cfa..273acf12329 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -40,7 +40,8 @@ from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "Apple TV" +DEFAULT_NAME_TV = "Apple TV" +DEFAULT_NAME_HP = "HomePod" BACKOFF_TIME_LOWER_LIMIT = 15 # seconds BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes @@ -358,7 +359,11 @@ class AppleTVManager(DeviceListener): ATTR_MANUFACTURER: "Apple", ATTR_NAME: self.config_entry.data[CONF_NAME], } - attrs[ATTR_SUGGESTED_AREA] = attrs[ATTR_NAME].removesuffix(f" {DEFAULT_NAME}") + attrs[ATTR_SUGGESTED_AREA] = ( + attrs[ATTR_NAME] + .removesuffix(f" {DEFAULT_NAME_TV}") + .removesuffix(f" {DEFAULT_NAME_HP}") + ) if self.atv: dev_info = self.atv.device_info From e35c7fde891d3ceaa0320fba2df7383a72e98793 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 10:35:35 -0600 Subject: [PATCH 0231/1367] Avoid many string lowers in the state machine (#109607) --- homeassistant/core.py | 14 +++++++++++--- tests/test_core.py | 13 +++++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 4c59e88e840..0f2a28b2fa4 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1757,7 +1757,9 @@ class StateMachine: Async friendly. """ - return self._states_data.get(entity_id.lower()) + return self._states_data.get(entity_id) or self._states_data.get( + entity_id.lower() + ) def is_state(self, entity_id: str, state: str) -> bool: """Test if entity exists and is in specified state. @@ -1870,10 +1872,16 @@ class StateMachine: This method must be run in the event loop. """ - entity_id = entity_id.lower() new_state = str(new_state) attributes = attributes or {} - if (old_state := self._states_data.get(entity_id)) is None: + old_state = self._states_data.get(entity_id) + if old_state is None: + # If the state is missing, try to convert the entity_id to lowercase + # and try again. + entity_id = entity_id.lower() + old_state = self._states_data.get(entity_id) + + if old_state is None: same_state = False same_attr = False last_changed = None diff --git a/tests/test_core.py b/tests/test_core.py index 4136249f993..efc0b875b4f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1194,8 +1194,8 @@ async def test_statemachine_remove(hass: HomeAssistant) -> None: assert len(events) == 1 -async def test_statemachine_case_insensitivty(hass: HomeAssistant) -> None: - """Test insensitivty.""" +async def test_state_machine_case_insensitivity(hass: HomeAssistant) -> None: + """Test setting and getting states entity_id insensitivity.""" events = async_capture_events(hass, EVENT_STATE_CHANGED) hass.states.async_set("light.BOWL", "off") @@ -1204,6 +1204,15 @@ async def test_statemachine_case_insensitivty(hass: HomeAssistant) -> None: assert hass.states.is_state("light.bowl", "off") assert len(events) == 1 + hass.states.async_set("ligHT.Bowl", "on") + assert hass.states.get("light.bowl").state == "on" + + hass.states.async_set("light.BOWL", "off") + assert hass.states.get("light.BoWL").state == "off" + + hass.states.async_set("light.bowl", "on") + assert hass.states.get("light.bowl").state == "on" + async def test_statemachine_last_changed_not_updated_on_same_state( hass: HomeAssistant, From 8d4f32645dc1cb604dc131816f5c214ffdc44878 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 11:23:33 -0600 Subject: [PATCH 0232/1367] Add tests for shelly switch unique ids (#109617) --- tests/components/shelly/test_switch.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index 9a99116e66c..555533cc817 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -54,6 +54,16 @@ async def test_block_device_services(hass: HomeAssistant, mock_block_device) -> assert hass.states.get("switch.test_name_channel_1").state == STATE_OFF +async def test_block_device_unique_ids(hass: HomeAssistant, mock_block_device) -> None: + """Test block device unique_ids.""" + await init_integration(hass, 1) + + registry = er.async_get(hass) + entry = registry.async_get("switch.test_name_channel_1") + assert entry + assert entry.unique_id == "123456789ABC-relay_0" + + async def test_block_set_state_connection_error( hass: HomeAssistant, mock_block_device, monkeypatch ) -> None: @@ -176,6 +186,18 @@ async def test_rpc_device_services( assert hass.states.get("switch.test_switch_0").state == STATE_OFF +async def test_rpc_device_unique_ids( + hass: HomeAssistant, mock_rpc_device, monkeypatch +) -> None: + """Test RPC device unique_ids.""" + await init_integration(hass, 2) + + registry = er.async_get(hass) + entry = registry.async_get("switch.test_switch_0") + assert entry + assert entry.unique_id == "123456789ABC-switch:0" + + async def test_rpc_device_switch_type_lights_mode( hass: HomeAssistant, mock_rpc_device, monkeypatch ) -> None: From c14ad6455fbd7423d6f7c41c575f16bbb1b64c25 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 4 Feb 2024 18:37:16 +0100 Subject: [PATCH 0233/1367] Update orjson to 3.9.13 (#109614) --- 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 7746745da6b..4ff23275e8f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -36,7 +36,7 @@ janus==1.0.0 Jinja2==3.1.3 lru-dict==1.3.0 mutagen==1.47.0 -orjson==3.9.12 +orjson==3.9.13 packaging>=23.1 paho-mqtt==1.6.1 Pillow==10.2.0 diff --git a/pyproject.toml b/pyproject.toml index 24a50508722..910670c6caa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dependencies = [ "cryptography==42.0.2", # pyOpenSSL 23.2.0 is required to work with cryptography 41+ "pyOpenSSL==24.0.0", - "orjson==3.9.12", + "orjson==3.9.13", "packaging>=23.1", "pip>=21.3.1", "python-slugify==8.0.1", diff --git a/requirements.txt b/requirements.txt index 066855e718b..44c11281517 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ lru-dict==1.3.0 PyJWT==2.8.0 cryptography==42.0.2 pyOpenSSL==24.0.0 -orjson==3.9.12 +orjson==3.9.13 packaging>=23.1 pip>=21.3.1 python-slugify==8.0.1 From e96f574a79f0352172b1407b304813f6426d9cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 4 Feb 2024 18:40:44 +0100 Subject: [PATCH 0234/1367] Add diagnostics platform to Traccar Server (#109521) --- .../components/traccar_server/diagnostics.py | 79 +++++++ tests/components/traccar_server/common.py | 11 + .../snapshots/test_diagnostics.ambr | 199 ++++++++++++++++++ .../traccar_server/test_diagnostics.py | 64 ++++++ 4 files changed, 353 insertions(+) create mode 100644 homeassistant/components/traccar_server/diagnostics.py create mode 100644 tests/components/traccar_server/common.py create mode 100644 tests/components/traccar_server/snapshots/test_diagnostics.ambr create mode 100644 tests/components/traccar_server/test_diagnostics.py diff --git a/homeassistant/components/traccar_server/diagnostics.py b/homeassistant/components/traccar_server/diagnostics.py new file mode 100644 index 00000000000..ce296499398 --- /dev/null +++ b/homeassistant/components/traccar_server/diagnostics.py @@ -0,0 +1,79 @@ +"""Diagnostics platform for Traccar Server.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .const import DOMAIN +from .coordinator import TraccarServerCoordinator + +TO_REDACT = {CONF_ADDRESS, CONF_LATITUDE, CONF_LONGITUDE} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: TraccarServerCoordinator = hass.data[DOMAIN][config_entry.entry_id] + entity_registry = er.async_get(hass) + + entities = er.async_entries_for_config_entry( + entity_registry, + config_entry_id=config_entry.entry_id, + ) + + return async_redact_data( + { + "config_entry_options": dict(config_entry.options), + "coordinator_data": coordinator.data, + "entities": [ + { + "enity_id": entity.entity_id, + "disabled": entity.disabled, + "state": {"state": state.state, "attributes": state.attributes}, + } + for entity in entities + if (state := hass.states.get(entity.entity_id)) is not None + ], + }, + TO_REDACT, + ) + + +async def async_get_device_diagnostics( + hass: HomeAssistant, + entry: ConfigEntry, + device: dr.DeviceEntry, +) -> dict[str, Any]: + """Return device diagnostics.""" + coordinator: TraccarServerCoordinator = hass.data[DOMAIN][entry.entry_id] + entity_registry = er.async_get(hass) + + entities = er.async_entries_for_device( + entity_registry, + device_id=device.id, + include_disabled_entities=True, + ) + + return async_redact_data( + { + "config_entry_options": dict(entry.options), + "coordinator_data": coordinator.data, + "entities": [ + { + "enity_id": entity.entity_id, + "disabled": entity.disabled, + "state": {"state": state.state, "attributes": state.attributes}, + } + for entity in entities + if (state := hass.states.get(entity.entity_id)) is not None + ], + }, + TO_REDACT, + ) diff --git a/tests/components/traccar_server/common.py b/tests/components/traccar_server/common.py new file mode 100644 index 00000000000..b85f7b672f8 --- /dev/null +++ b/tests/components/traccar_server/common.py @@ -0,0 +1,11 @@ +"""Common test tools for Traccar Server.""" +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Set up the integration.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/traccar_server/snapshots/test_diagnostics.ambr b/tests/components/traccar_server/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..0492291384d --- /dev/null +++ b/tests/components/traccar_server/snapshots/test_diagnostics.ambr @@ -0,0 +1,199 @@ +# serializer version: 1 +# name: test_device_diagnostics[X-Wing] + dict({ + 'config_entry_options': dict({ + 'custom_attributes': list([ + 'custom_attr_1', + ]), + 'events': list([ + 'device_moving', + ]), + 'max_accuracy': 5.0, + 'skip_accuracy_filter_for': list([ + ]), + }), + 'coordinator_data': dict({ + 'abc123': dict({ + 'attributes': dict({ + 'custom_attr_1': 'custom_attr_1_value', + }), + 'device': dict({ + 'attributes': dict({ + }), + 'category': 'starfighter', + 'contact': None, + 'disabled': False, + 'groupId': 0, + 'id': 0, + 'lastUpdate': '1970-01-01T00:00:00Z', + 'model': '1337', + 'name': 'X-Wing', + 'phone': None, + 'positionId': 0, + 'status': 'unknown', + 'uniqueId': 'abc123', + }), + 'geofence': dict({ + 'area': 'string', + 'attributes': dict({ + }), + 'calendarId': 0, + 'description': "A harsh desert world orbiting twin suns in the galaxy's Outer Rim", + 'id': 0, + 'name': 'Tatooine', + }), + 'position': dict({ + 'accuracy': 3.5, + 'address': '**REDACTED**', + 'altitude': 546841384638, + 'attributes': dict({ + 'custom_attr_1': 'custom_attr_1_value', + }), + 'course': 360, + 'deviceId': 0, + 'deviceTime': '1970-01-01T00:00:00Z', + 'fixTime': '1970-01-01T00:00:00Z', + 'geofenceIds': list([ + 0, + ]), + 'id': 0, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'network': dict({ + }), + 'outdated': True, + 'protocol': 'C-3PO', + 'serverTime': '1970-01-01T00:00:00Z', + 'speed': 4568795, + 'valid': True, + }), + }), + }), + 'entities': list([ + dict({ + 'disabled': False, + 'enity_id': 'device_tracker.x_wing', + 'state': dict({ + 'attributes': dict({ + 'address': '**REDACTED**', + 'altitude': 546841384638, + 'battery_level': -1, + 'category': 'starfighter', + 'custom_attr_1': 'custom_attr_1_value', + 'friendly_name': 'X-Wing', + 'geofence': 'Tatooine', + 'gps_accuracy': 3.5, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'motion': False, + 'source_type': 'gps', + 'speed': 4568795, + 'status': 'unknown', + 'traccar_id': 0, + 'tracker': 'traccar_server', + }), + 'state': 'not_home', + }), + }), + ]), + }) +# --- +# name: test_entry_diagnostics[entry] + dict({ + 'config_entry_options': dict({ + 'custom_attributes': list([ + 'custom_attr_1', + ]), + 'events': list([ + 'device_moving', + ]), + 'max_accuracy': 5.0, + 'skip_accuracy_filter_for': list([ + ]), + }), + 'coordinator_data': dict({ + 'abc123': dict({ + 'attributes': dict({ + 'custom_attr_1': 'custom_attr_1_value', + }), + 'device': dict({ + 'attributes': dict({ + }), + 'category': 'starfighter', + 'contact': None, + 'disabled': False, + 'groupId': 0, + 'id': 0, + 'lastUpdate': '1970-01-01T00:00:00Z', + 'model': '1337', + 'name': 'X-Wing', + 'phone': None, + 'positionId': 0, + 'status': 'unknown', + 'uniqueId': 'abc123', + }), + 'geofence': dict({ + 'area': 'string', + 'attributes': dict({ + }), + 'calendarId': 0, + 'description': "A harsh desert world orbiting twin suns in the galaxy's Outer Rim", + 'id': 0, + 'name': 'Tatooine', + }), + 'position': dict({ + 'accuracy': 3.5, + 'address': '**REDACTED**', + 'altitude': 546841384638, + 'attributes': dict({ + 'custom_attr_1': 'custom_attr_1_value', + }), + 'course': 360, + 'deviceId': 0, + 'deviceTime': '1970-01-01T00:00:00Z', + 'fixTime': '1970-01-01T00:00:00Z', + 'geofenceIds': list([ + 0, + ]), + 'id': 0, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'network': dict({ + }), + 'outdated': True, + 'protocol': 'C-3PO', + 'serverTime': '1970-01-01T00:00:00Z', + 'speed': 4568795, + 'valid': True, + }), + }), + }), + 'entities': list([ + dict({ + 'disabled': False, + 'enity_id': 'device_tracker.x_wing', + 'state': dict({ + 'attributes': dict({ + 'address': '**REDACTED**', + 'altitude': 546841384638, + 'battery_level': -1, + 'category': 'starfighter', + 'custom_attr_1': 'custom_attr_1_value', + 'friendly_name': 'X-Wing', + 'geofence': 'Tatooine', + 'gps_accuracy': 3.5, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'motion': False, + 'source_type': 'gps', + 'speed': 4568795, + 'status': 'unknown', + 'traccar_id': 0, + 'tracker': 'traccar_server', + }), + 'state': 'not_home', + }), + }), + ]), + }) +# --- diff --git a/tests/components/traccar_server/test_diagnostics.py b/tests/components/traccar_server/test_diagnostics.py new file mode 100644 index 00000000000..ebefaab6df8 --- /dev/null +++ b/tests/components/traccar_server/test_diagnostics.py @@ -0,0 +1,64 @@ +"""Test Traccar Server diagnostics.""" +from collections.abc import Generator +from unittest.mock import AsyncMock + +from syrupy import SnapshotAssertion + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .common import setup_integration + +from tests.common import MockConfigEntry +from tests.components.diagnostics import ( + get_diagnostics_for_config_entry, + get_diagnostics_for_device, +) +from tests.typing import ClientSessionGenerator + + +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_traccar_api_client: Generator[AsyncMock, None, None], + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + await setup_integration(hass, mock_config_entry) + + result = await get_diagnostics_for_config_entry( + hass, + hass_client, + mock_config_entry, + ) + + assert result == snapshot(name="entry") + + +async def test_device_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_traccar_api_client: Generator[AsyncMock, None, None], + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device diagnostics.""" + await setup_integration(hass, mock_config_entry) + + devices = dr.async_entries_for_config_entry( + hass.helpers.device_registry.async_get(hass), + mock_config_entry.entry_id, + ) + + assert len(devices) == 1 + + for device in dr.async_entries_for_config_entry( + hass.helpers.device_registry.async_get(hass), mock_config_entry.entry_id + ): + result = await get_diagnostics_for_device( + hass, hass_client, mock_config_entry, device=device + ) + + assert result == snapshot(name=device.name) From b553bb71e67ae979b4a5afcf7c6296b046d410f1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 4 Feb 2024 18:58:09 +0100 Subject: [PATCH 0235/1367] Fix Tuya QR code expiry, use native QR selector (#109615) * Fix Tuya QR code expiry, use native QR selector * Adjust tests --- homeassistant/components/tuya/config_flow.py | 52 ++++++++++---------- homeassistant/components/tuya/manifest.json | 2 +- homeassistant/components/tuya/strings.json | 2 +- requirements_all.txt | 3 -- requirements_test_all.txt | 3 -- tests/components/tuya/test_config_flow.py | 5 +- 6 files changed, 28 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index 3577a6d6b06..e0ac5375b00 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -2,15 +2,14 @@ from __future__ import annotations from collections.abc import Mapping -from io import BytesIO from typing import Any -import segno from tuya_sharing import LoginControl import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import selector from .const import ( CONF_ENDPOINT, @@ -33,7 +32,6 @@ class TuyaConfigFlow(ConfigFlow, domain=DOMAIN): __user_code: str __qr_code: str - __qr_image: str __reauth_entry: ConfigEntry | None = None def __init__(self) -> None: @@ -82,9 +80,17 @@ class TuyaConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is None: return self.async_show_form( step_id="scan", - description_placeholders={ - TUYA_RESPONSE_QR_CODE: self.__qr_image, - }, + data_schema=vol.Schema( + { + vol.Optional("QR"): selector.QrCodeSelector( + config=selector.QrCodeSelectorConfig( + data=f"tuyaSmart--qrLogin?token={self.__qr_code}", + scale=5, + error_correction_level=selector.QrErrorCorrectionLevel.QUARTILE, + ) + ) + } + ), ) ret, info = await self.hass.async_add_executor_job( @@ -94,11 +100,23 @@ class TuyaConfigFlow(ConfigFlow, domain=DOMAIN): self.__user_code, ) if not ret: + # Try to get a new QR code on failure + await self.__async_get_qr_code(self.__user_code) return self.async_show_form( step_id="scan", errors={"base": "login_error"}, + data_schema=vol.Schema( + { + vol.Optional("QR"): selector.QrCodeSelector( + config=selector.QrCodeSelectorConfig( + data=f"tuyaSmart--qrLogin?token={self.__qr_code}", + scale=5, + error_correction_level=selector.QrErrorCorrectionLevel.QUARTILE, + ) + ) + } + ), description_placeholders={ - TUYA_RESPONSE_QR_CODE: self.__qr_image, TUYA_RESPONSE_MSG: info.get(TUYA_RESPONSE_MSG, "Unknown error"), TUYA_RESPONSE_CODE: info.get(TUYA_RESPONSE_CODE, 0), }, @@ -189,24 +207,4 @@ class TuyaConfigFlow(ConfigFlow, domain=DOMAIN): if success := response.get(TUYA_RESPONSE_SUCCESS, False): self.__user_code = user_code self.__qr_code = response[TUYA_RESPONSE_RESULT][TUYA_RESPONSE_QR_CODE] - self.__qr_image = _generate_qr_code(self.__qr_code) return success, response - - -def _generate_qr_code(data: str) -> str: - """Create an SVG QR code that can be scanned with the Smart Life app.""" - qr_code = segno.make(f"tuyaSmart--qrLogin?token={data}", error="h") - with BytesIO() as buffer: - qr_code.save( - buffer, - kind="svg", - border=5, - scale=5, - xmldecl=False, - svgns=False, - svgclass=None, - lineclass=None, - svgversion=2, - dark="#1abcf2", - ) - return str(buffer.getvalue().decode("ascii")) diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 71e43c8d445..305a74160de 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -43,5 +43,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["tuya_iot"], - "requirements": ["tuya-device-sharing-sdk==0.1.9", "segno==1.5.3"] + "requirements": ["tuya-device-sharing-sdk==0.1.9"] } diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 6e4848d9cc0..693f799e6e9 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -14,7 +14,7 @@ } }, "scan": { - "description": "Use Smart Life app or Tuya Smart app to scan the following QR-code to complete the login:\n\n {qrcode} \n\nContinue to the next step once you have completed this step in the app." + "description": "Use Smart Life app or Tuya Smart app to scan the following QR-code to complete the login.\n\nContinue to the next step once you have completed this step in the app." } }, "error": { diff --git a/requirements_all.txt b/requirements_all.txt index eb37e1a123f..fb563af478f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2494,9 +2494,6 @@ scsgate==0.1.0 # homeassistant.components.backup securetar==2023.3.0 -# homeassistant.components.tuya -segno==1.5.3 - # homeassistant.components.sendgrid sendgrid==6.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70dfc68f58b..3cb24852bac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1901,9 +1901,6 @@ screenlogicpy==0.10.0 # homeassistant.components.backup securetar==2023.3.0 -# homeassistant.components.tuya -segno==1.5.3 - # homeassistant.components.emulated_kasa # homeassistant.components.sense sense-energy==0.12.2 diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 66a5d1d226d..c38d8e5f8b5 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -11,7 +11,7 @@ from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from tests.common import ANY, MockConfigEntry +from tests.common import MockConfigEntry pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -37,7 +37,6 @@ async def test_user_flow( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "scan" - assert result2.get("description_placeholders") == {"qrcode": ANY} result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -157,7 +156,6 @@ async def test_reauth_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "scan" - assert result.get("description_placeholders") == {"qrcode": ANY} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -206,7 +204,6 @@ async def test_reauth_flow_migration( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "scan" - assert result2.get("description_placeholders") == {"qrcode": ANY} result3 = await hass.config_entries.flow.async_configure( result["flow_id"], From 7572a73c165a7990a017901d36dacd6d31272e15 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 14:11:28 -0500 Subject: [PATCH 0236/1367] Add debug logger for cpu temp in System Monitor (#109627) --- homeassistant/components/systemmonitor/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/systemmonitor/util.py b/homeassistant/components/systemmonitor/util.py index 293492b90e8..11d8fa9c062 100644 --- a/homeassistant/components/systemmonitor/util.py +++ b/homeassistant/components/systemmonitor/util.py @@ -1,4 +1,5 @@ """Utils for System Monitor.""" + import logging import os @@ -71,6 +72,7 @@ def read_cpu_temperature(temps: dict[str, list[shwtemp]] | None = None) -> float temps = psutil.sensors_temperatures() entry: shwtemp + _LOGGER.debug("CPU Temperatures: %s", temps) for name, entries in temps.items(): for i, entry in enumerate(entries, start=1): # In case the label is empty (e.g. on Raspberry PI 4), From a7c074e388073c911917e287818b9f3b99f9a034 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 13:38:36 -0600 Subject: [PATCH 0237/1367] Reduce complexity of shelly button setup (#109625) --- homeassistant/components/shelly/button.py | 41 +++++++++-------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 17f60f566aa..6dd97731ab5 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass +from functools import partial from typing import TYPE_CHECKING, Any, Final, Generic, TypeVar from aioshelly.const import RPC_GENERATIONS @@ -83,8 +84,8 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [ @callback def async_migrate_unique_ids( - entity_entry: er.RegistryEntry, coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator, + entity_entry: er.RegistryEntry, ) -> dict[str, Any] | None: """Migrate button unique IDs.""" if not entity_entry.entity_id.startswith("button"): @@ -117,35 +118,25 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set buttons for device.""" - - @callback - def _async_migrate_unique_ids( - entity_entry: er.RegistryEntry, - ) -> dict[str, Any] | None: - """Migrate button unique IDs.""" - if TYPE_CHECKING: - assert coordinator is not None - return async_migrate_unique_ids(entity_entry, coordinator) - - coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator | None = None + entry_data = get_entry_data(hass)[config_entry.entry_id] + coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator | None if get_device_entry_gen(config_entry) in RPC_GENERATIONS: - coordinator = get_entry_data(hass)[config_entry.entry_id].rpc + coordinator = entry_data.rpc else: - coordinator = get_entry_data(hass)[config_entry.entry_id].block + coordinator = entry_data.block - if coordinator is not None: - await er.async_migrate_entries( - hass, config_entry.entry_id, _async_migrate_unique_ids - ) + if TYPE_CHECKING: + assert coordinator is not None - entities: list[ShellyButton] = [] + await er.async_migrate_entries( + hass, config_entry.entry_id, partial(async_migrate_unique_ids, coordinator) + ) - for button in BUTTONS: - if not button.supported(coordinator): - continue - entities.append(ShellyButton(coordinator, button)) - - async_add_entities(entities) + async_add_entities( + ShellyButton(coordinator, button) + for button in BUTTONS + if button.supported(coordinator) + ) class ShellyButton( From 7e3001f843b3fa0bfc09ea1aba1f645f35fff2e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 13:49:42 -0600 Subject: [PATCH 0238/1367] Remove default values from calls to async_fire (#109613) There were a few places were we passing unnecessary default values. Since this code tends to get copied, remove them --- homeassistant/core.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 0f2a28b2fa4..f3ef4bc598e 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1797,7 +1797,6 @@ class StateMachine: self._bus.async_fire( EVENT_STATE_CHANGED, {"entity_id": entity_id, "old_state": old_state, "new_state": None}, - EventOrigin.local, context=context, ) return True @@ -1932,8 +1931,7 @@ class StateMachine: self._bus.async_fire( EVENT_STATE_CHANGED, {"entity_id": entity_id, "old_state": old_state, "new_state": state}, - EventOrigin.local, - context, + context=context, time_fired=now, ) From b24b4fc23745b4758a8bd5c41a0100e8eb3077ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 14:05:48 -0600 Subject: [PATCH 0239/1367] Use get_entries_for_config_entry_id helper in async_migrate_entries (#109629) --- homeassistant/helpers/entity_registry.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index b6790ff0dc3..8ae9256754d 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -1379,16 +1379,12 @@ async def async_migrate_entries( Can also be used to remove duplicated entity registry entries. """ ent_reg = async_get(hass) - - for entry in list(ent_reg.entities.values()): - if entry.config_entry_id != config_entry_id: - continue - if not ent_reg.entities.get_entry(entry.id): - continue - - updates = entry_callback(entry) - - if updates is not None: + entities = ent_reg.entities + for entry in entities.get_entries_for_config_entry_id(config_entry_id): + if ( + entities.get_entry(entry.id) + and (updates := entry_callback(entry)) is not None + ): ent_reg.async_update_entity(entry.entity_id, **updates) From c988d3d427e829d6d24f40c1ad4d7e3f6dbddb56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 14:11:56 -0600 Subject: [PATCH 0240/1367] Avoid linear search of entity registry in guardian (#109634) --- homeassistant/components/guardian/util.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index ffa57322551..a5e91dce813 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -48,9 +48,10 @@ def async_finish_entity_domain_replacements( try: [registry_entry] = [ registry_entry - for registry_entry in ent_reg.entities.values() - if registry_entry.config_entry_id == entry.entry_id - and registry_entry.domain == strategy.old_domain + for registry_entry in er.async_entries_for_config_entry( + ent_reg, entry.entry_id + ) + if registry_entry.domain == strategy.old_domain and registry_entry.unique_id == strategy.old_unique_id ] except ValueError: From 6ccf82d7b1385d902382b7e05c5bb38a528768e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 14:12:24 -0600 Subject: [PATCH 0241/1367] Avoid linear search of entity registry in keenetic_ndms2 (#109635) --- homeassistant/components/keenetic_ndms2/__init__.py | 9 ++++----- .../components/keenetic_ndms2/device_tracker.py | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index 207c9e353a1..6f33b11742a 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -75,11 +75,10 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> for mac, device in router.last_devices.items() if device.interface in new_tracked_interfaces } - for entity_entry in list(ent_reg.entities.values()): - if ( - entity_entry.config_entry_id == config_entry.entry_id - and entity_entry.domain == Platform.DEVICE_TRACKER - ): + for entity_entry in ent_reg.entities.get_entries_for_config_entry_id( + config_entry.entry_id + ): + if entity_entry.domain == Platform.DEVICE_TRACKER: mac = entity_entry.unique_id.partition("_")[0] if mac not in keep_devices: _LOGGER.debug("Removing entity %s", entity_entry.entity_id) diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index c51d30431be..c9e81071ad7 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -43,11 +43,10 @@ async def async_setup_entry( registry = er.async_get(hass) # Restore devices that are not a part of active clients list. restored = [] - for entity_entry in registry.entities.values(): - if ( - entity_entry.config_entry_id == config_entry.entry_id - and entity_entry.domain == DEVICE_TRACKER_DOMAIN - ): + for entity_entry in registry.entities.get_entries_for_config_entry_id( + config_entry.entry_id + ): + if entity_entry.domain == DEVICE_TRACKER_DOMAIN: mac = entity_entry.unique_id.partition("_")[0] if mac not in tracked: tracked.add(mac) From 13a5038c1720996b1dd9282852629aa5c340a3f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 14:12:43 -0600 Subject: [PATCH 0242/1367] Avoid linear search of entity registry in lcn (#109638) --- homeassistant/components/lcn/helpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 64a789f3a34..8cb0201033e 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -286,7 +286,8 @@ def purge_device_registry( # Find all devices that are referenced in the entity registry. references_entities = { - entry.device_id for entry in entity_registry.entities.values() + entry.device_id + for entry in entity_registry.entities.get_entries_for_config_entry_id(entry_id) } # Find device that references the host. From 3531444e2e3f2a64b7ce4c6eae3101826e7e7ceb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 14:13:06 -0600 Subject: [PATCH 0243/1367] Avoid linear search of entity registry in huawei_lte (#109637) --- homeassistant/components/huawei_lte/device_tracker.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index fd1b9850054..1bb5077a2b4 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -70,11 +70,10 @@ async def async_setup_entry( track_wired_clients = router.config_entry.options.get( CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS ) - for entity in registry.entities.values(): - if ( - entity.domain == DEVICE_TRACKER_DOMAIN - and entity.config_entry_id == config_entry.entry_id - ): + for entity in registry.entities.get_entries_for_config_entry_id( + config_entry.entry_id + ): + if entity.domain == DEVICE_TRACKER_DOMAIN: mac = entity.unique_id.partition("-")[2] # Do not add known wired clients if not tracking them (any more) skip = False From babfdaac548ffb843822b846e226527a7db0c132 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 14:13:23 -0600 Subject: [PATCH 0244/1367] Avoid linear search of entity registry in mikrotik (#109639) --- homeassistant/components/mikrotik/device_tracker.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 14fbb83b61b..8136334514f 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -34,11 +34,10 @@ async def async_setup_entry( registry = er.async_get(hass) # Restore clients that is not a part of active clients list. - for entity in registry.entities.values(): - if ( - entity.config_entry_id == config_entry.entry_id - and entity.domain == DEVICE_TRACKER - ): + for entity in registry.entities.get_entries_for_config_entry_id( + config_entry.entry_id + ): + if entity.domain == DEVICE_TRACKER: if ( entity.unique_id in coordinator.api.devices or entity.unique_id not in coordinator.api.all_devices From bc45b31335d6e05caf662354ca7cb61ae2846f08 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 15:15:17 -0500 Subject: [PATCH 0245/1367] Fix repairs for remove dates in Workday (#109626) --- .../components/workday/binary_sensor.py | 62 ++++++++----- homeassistant/components/workday/repairs.py | 6 +- homeassistant/components/workday/strings.json | 22 ++++- tests/components/workday/__init__.py | 11 +++ tests/components/workday/test_repairs.py | 89 ++++++++++++++++++- 5 files changed, 163 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index bda3a576563..04a3a2544c1 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -1,4 +1,5 @@ """Sensor to indicate whether the current day is a workday.""" + from __future__ import annotations from datetime import date, timedelta @@ -53,7 +54,7 @@ def validate_dates(holiday_list: list[str]) -> list[str]: continue _range: timedelta = d2 - d1 for i in range(_range.days + 1): - day = d1 + timedelta(days=i) + day: date = d1 + timedelta(days=i) calc_holidays.append(day.strftime("%Y-%m-%d")) continue calc_holidays.append(add_date) @@ -123,25 +124,46 @@ async def async_setup_entry( LOGGER.debug("Removed %s by name '%s'", holiday, remove_holiday) except KeyError as unmatched: LOGGER.warning("No holiday found matching %s", unmatched) - async_create_issue( - hass, - DOMAIN, - f"bad_named_holiday-{entry.entry_id}-{slugify(remove_holiday)}", - is_fixable=True, - is_persistent=False, - severity=IssueSeverity.WARNING, - translation_key="bad_named_holiday", - translation_placeholders={ - CONF_COUNTRY: country if country else "-", - "title": entry.title, - CONF_REMOVE_HOLIDAYS: remove_holiday, - }, - data={ - "entry_id": entry.entry_id, - "country": country, - "named_holiday": remove_holiday, - }, - ) + if dt_util.parse_date(remove_holiday): + async_create_issue( + hass, + DOMAIN, + f"bad_date_holiday-{entry.entry_id}-{slugify(remove_holiday)}", + is_fixable=True, + is_persistent=False, + severity=IssueSeverity.WARNING, + translation_key="bad_date_holiday", + translation_placeholders={ + CONF_COUNTRY: country if country else "-", + "title": entry.title, + CONF_REMOVE_HOLIDAYS: remove_holiday, + }, + data={ + "entry_id": entry.entry_id, + "country": country, + "named_holiday": remove_holiday, + }, + ) + else: + async_create_issue( + hass, + DOMAIN, + f"bad_named_holiday-{entry.entry_id}-{slugify(remove_holiday)}", + is_fixable=True, + is_persistent=False, + severity=IssueSeverity.WARNING, + translation_key="bad_named_holiday", + translation_placeholders={ + CONF_COUNTRY: country if country else "-", + "title": entry.title, + CONF_REMOVE_HOLIDAYS: remove_holiday, + }, + data={ + "entry_id": entry.entry_id, + "country": country, + "named_holiday": remove_holiday, + }, + ) LOGGER.debug("Found the following holidays for your configuration:") for holiday_date, name in sorted(obj_holidays.items()): diff --git a/homeassistant/components/workday/repairs.py b/homeassistant/components/workday/repairs.py index 905434f76ac..1221514da42 100644 --- a/homeassistant/components/workday/repairs.py +++ b/homeassistant/components/workday/repairs.py @@ -125,9 +125,9 @@ class HolidayFixFlow(RepairsFlow): self, user_input: dict[str, str] | None = None ) -> data_entry_flow.FlowResult: """Handle the first step of a fix flow.""" - return await self.async_step_named_holiday() + return await self.async_step_fix_remove_holiday() - async def async_step_named_holiday( + async def async_step_fix_remove_holiday( self, user_input: dict[str, Any] | None = None ) -> data_entry_flow.FlowResult: """Handle the options step of a fix flow.""" @@ -168,7 +168,7 @@ class HolidayFixFlow(RepairsFlow): {CONF_REMOVE_HOLIDAYS: removed_named_holiday}, ) return self.async_show_form( - step_id="named_holiday", + step_id="fix_remove_holiday", data_schema=new_schema, description_placeholders={ CONF_COUNTRY: self.country if self.country else "-", diff --git a/homeassistant/components/workday/strings.json b/homeassistant/components/workday/strings.json index bbb76676f96..0e618beaf82 100644 --- a/homeassistant/components/workday/strings.json +++ b/homeassistant/components/workday/strings.json @@ -137,7 +137,7 @@ "title": "Configured named holiday {remove_holidays} for {title} does not exist", "fix_flow": { "step": { - "named_holiday": { + "fix_remove_holiday": { "title": "[%key:component::workday::issues::bad_named_holiday::title%]", "description": "Remove named holiday `{remove_holidays}` as it can't be found in country {country}.", "data": { @@ -152,6 +152,26 @@ "remove_holiday_error": "[%key:component::workday::config::error::remove_holiday_error%]" } } + }, + "bad_date_holiday": { + "title": "Configured holiday date {remove_holidays} for {title} does not exist", + "fix_flow": { + "step": { + "fix_remove_holiday": { + "title": "[%key:component::workday::issues::bad_date_holiday::title%]", + "description": "Remove holiday date `{remove_holidays}` as it can't be found in country {country}.", + "data": { + "remove_holidays": "[%key:component::workday::config::step::options::data::remove_holidays%]" + }, + "data_description": { + "remove_holidays": "[%key:component::workday::config::step::options::data_description::remove_holidays%]" + } + } + }, + "error": { + "remove_holiday_error": "[%key:component::workday::config::error::remove_holiday_error%]" + } + } } }, "entity": { diff --git a/tests/components/workday/__init__.py b/tests/components/workday/__init__.py index fb436a57e5c..a7e26765643 100644 --- a/tests/components/workday/__init__.py +++ b/tests/components/workday/__init__.py @@ -1,4 +1,5 @@ """Tests the Home Assistant workday binary sensor.""" + from __future__ import annotations from typing import Any @@ -181,6 +182,16 @@ TEST_CONFIG_REMOVE_NAMED = { "remove_holidays": ["Not a Holiday", "Christmas", "Thanksgiving"], "language": "en_US", } +TEST_CONFIG_REMOVE_DATE = { + "name": DEFAULT_NAME, + "country": "US", + "excludes": DEFAULT_EXCLUDES, + "days_offset": DEFAULT_OFFSET, + "workdays": DEFAULT_WORKDAYS, + "add_holidays": [], + "remove_holidays": ["2024-02-05", "2024-02-06"], + "language": "en_US", +} TEST_CONFIG_TOMORROW = { "name": DEFAULT_NAME, "country": "DE", diff --git a/tests/components/workday/test_repairs.py b/tests/components/workday/test_repairs.py index fc7bfeb1b0e..60a55e1a347 100644 --- a/tests/components/workday/test_repairs.py +++ b/tests/components/workday/test_repairs.py @@ -1,4 +1,5 @@ """Test repairs for unifiprotect.""" + from __future__ import annotations from http import HTTPStatus @@ -10,12 +11,13 @@ from homeassistant.components.repairs.websocket_api import ( from homeassistant.components.workday.const import CONF_REMOVE_HOLIDAYS, DOMAIN from homeassistant.const import CONF_COUNTRY from homeassistant.core import HomeAssistant -from homeassistant.helpers.issue_registry import async_create_issue +from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component from . import ( TEST_CONFIG_INCORRECT_COUNTRY, TEST_CONFIG_INCORRECT_PROVINCE, + TEST_CONFIG_REMOVE_DATE, TEST_CONFIG_REMOVE_NAMED, init_integration, ) @@ -329,6 +331,7 @@ async def test_bad_named_holiday( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_ws_client: WebSocketGenerator, + issue_registry: ir.IssueRegistry, ) -> None: """Test fixing bad province selecting none.""" assert await async_setup_component(hass, "repairs", {}) @@ -337,6 +340,11 @@ async def test_bad_named_holiday( state = hass.states.get("binary_sensor.workday_sensor") assert state + issues = issue_registry.issues.keys() + for issue in issues: + if issue[0] == DOMAIN: + assert issue[1].startswith("bad_named") + ws_client = await hass_ws_client(hass) client = await hass_client() @@ -365,7 +373,7 @@ async def test_bad_named_holiday( CONF_REMOVE_HOLIDAYS: "Not a Holiday", "title": entry.title, } - assert data["step_id"] == "named_holiday" + assert data["step_id"] == "fix_remove_holiday" url = RepairsFlowResourceView.url.format(flow_id=flow_id) resp = await client.post( @@ -402,6 +410,81 @@ async def test_bad_named_holiday( assert not issue +async def test_bad_date_holiday( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, + issue_registry: ir.IssueRegistry, +) -> None: + """Test fixing bad province selecting none.""" + assert await async_setup_component(hass, "repairs", {}) + entry = await init_integration(hass, TEST_CONFIG_REMOVE_DATE) + + state = hass.states.get("binary_sensor.workday_sensor") + assert state + + issues = issue_registry.issues.keys() + for issue in issues: + if issue[0] == DOMAIN: + assert issue[1].startswith("bad_date") + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert len(msg["result"]["issues"]) > 0 + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "bad_date_holiday-1-2024_02_05": + issue = i + assert issue is not None + + url = RepairsFlowIndexView.url + resp = await client.post( + url, + json={"handler": DOMAIN, "issue_id": "bad_date_holiday-1-2024_02_05"}, + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["description_placeholders"] == { + CONF_COUNTRY: "US", + CONF_REMOVE_HOLIDAYS: "2024-02-05", + "title": entry.title, + } + assert data["step_id"] == "fix_remove_holiday" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url, json={"remove_holidays": ["2024-02-06"]}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.workday_sensor") + assert state + + await ws_client.send_json({"id": 2, "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"] == "bad_date_holiday-1-2024_02_05": + issue = i + assert not issue + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "bad_date_holiday-1-2024_02_06": + issue = i + assert issue + + async def test_other_fixable_issues( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -428,7 +511,7 @@ async def test_other_fixable_issues( "severity": "error", "translation_key": "issue_1", } - async_create_issue( + ir.async_create_issue( hass, issue["domain"], issue["issue_id"], From 113d38361df9cf7aa4076df0b7228a88292a2b55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 14:16:31 -0600 Subject: [PATCH 0246/1367] Avoid linear search of entity registry in nmap_tracker (#109640) --- homeassistant/components/nmap_tracker/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index 726b3fa3db8..3ebbce8361c 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -191,8 +191,9 @@ class NmapDeviceScanner: registry = er.async_get(self._hass) self._known_mac_addresses = { entry.unique_id: entry.original_name - for entry in registry.entities.values() - if entry.config_entry_id == self._entry_id + for entry in registry.entities.get_entries_for_config_entry_id( + self._entry_id + ) } @property From 74812261d4df665669d92ad753833b58a2b81692 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 14:17:40 -0600 Subject: [PATCH 0247/1367] Avoid linear search of entity registry in rainmachine (#109642) --- homeassistant/components/rainmachine/util.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/util.py b/homeassistant/components/rainmachine/util.py index dfb03b11b5d..77a91c627a9 100644 --- a/homeassistant/components/rainmachine/util.py +++ b/homeassistant/components/rainmachine/util.py @@ -60,9 +60,10 @@ def async_finish_entity_domain_replacements( try: [registry_entry] = [ registry_entry - for registry_entry in ent_reg.entities.values() - if registry_entry.config_entry_id == entry.entry_id - and registry_entry.domain == strategy.old_domain + for registry_entry in ent_reg.entities.get_entries_for_config_entry_id( + entry.entry_id + ) + if registry_entry.domain == strategy.old_domain and registry_entry.unique_id == strategy.old_unique_id ] except ValueError: From 6003ae149a582d5ed0fe3ecaee3c61672e5fd1ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 14:20:55 -0600 Subject: [PATCH 0248/1367] Avoid linear search of entity registry in ruckus_unleashed (#109643) --- .../ruckus_unleashed/device_tracker.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py index df5027ebaa8..89cc22ef766 100644 --- a/homeassistant/components/ruckus_unleashed/device_tracker.py +++ b/homeassistant/components/ruckus_unleashed/device_tracker.py @@ -18,6 +18,7 @@ from .const import ( KEY_SYS_CLIENTS, UNDO_UPDATE_LISTENERS, ) +from .coordinator import RuckusUnleashedDataUpdateCoordinator _LOGGER = logging.getLogger(__package__) @@ -65,14 +66,19 @@ def add_new_entities(coordinator, async_add_entities, tracked): @callback -def restore_entities(registry, coordinator, entry, async_add_entities, tracked): +def restore_entities( + registry: er.EntityRegistry, + coordinator: RuckusUnleashedDataUpdateCoordinator, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + tracked: set[str], +) -> None: """Restore clients that are not a part of active clients list.""" - missing = [] + missing: list[RuckusUnleashedDevice] = [] - for entity in registry.entities.values(): + for entity in registry.entities.get_entries_for_config_entry_id(entry.entry_id): if ( - entity.config_entry_id == entry.entry_id - and entity.platform == DOMAIN + entity.platform == DOMAIN and entity.unique_id not in coordinator.data[KEY_SYS_CLIENTS] ): missing.append( From f37db94f23756478ea9eac975bc53092f76a2a64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 14:22:12 -0600 Subject: [PATCH 0249/1367] Avoid linear search of entity registry in async_get_device_automations (#109633) --- homeassistant/components/device_automation/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 68d05c19f67..979c82acfe2 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -247,9 +247,10 @@ async def async_get_device_automations( match_device_ids = set(device_ids or device_registry.devices) combined_results: dict[str, list[dict[str, Any]]] = {} - for entry in entity_registry.entities.values(): - if not entry.disabled_by and entry.device_id in match_device_ids: - device_entities_domains.setdefault(entry.device_id, set()).add(entry.domain) + for device_id in match_device_ids: + for entry in entity_registry.entities.get_entries_for_device_id(device_id): + if not entry.disabled_by: + device_entities_domains.setdefault(device_id, set()).add(entry.domain) for device_id in match_device_ids: combined_results[device_id] = [] From 7cc0b8a2fe8953b7824af38d9e639df8448b847e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 4 Feb 2024 21:25:14 +0100 Subject: [PATCH 0250/1367] Fix imap message part decoding (#109523) --- homeassistant/components/imap/coordinator.py | 18 ++++++++++-------- tests/components/imap/test_init.py | 8 ++++++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index 5591980b2f1..49938eaaa0a 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -9,7 +9,7 @@ from email.header import decode_header, make_header from email.message import Message from email.utils import parseaddr, parsedate_to_datetime import logging -from typing import Any +from typing import TYPE_CHECKING, Any from aioimaplib import AUTH, IMAP4_SSL, NONAUTH, SELECTED, AioImapException @@ -97,9 +97,8 @@ async def connect_to_server(data: Mapping[str, Any]) -> IMAP4_SSL: class ImapMessage: """Class to parse an RFC822 email message.""" - def __init__(self, raw_message: bytes, charset: str = "utf-8") -> None: + def __init__(self, raw_message: bytes) -> None: """Initialize IMAP message.""" - self._charset = charset self.email_message = email.message_from_bytes(raw_message) @property @@ -153,7 +152,7 @@ class ImapMessage: def text(self) -> str: """Get the message text from the email. - Will look for text/plain or use text/html if not found. + Will look for text/plain or use/ text/html if not found. """ message_text: str | None = None message_html: str | None = None @@ -166,8 +165,13 @@ class ImapMessage: Falls back to the raw content part if decoding fails. """ try: - return str(part.get_payload(decode=True).decode(self._charset)) + decoded_payload: Any = part.get_payload(decode=True) + if TYPE_CHECKING: + assert isinstance(decoded_payload, bytes) + content_charset = part.get_content_charset() or "utf-8" + return decoded_payload.decode(content_charset) except ValueError: + # return undecoded payload return str(part.get_payload()) part: Message @@ -237,9 +241,7 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): """Send a event for the last message if the last message was changed.""" response = await self.imap_client.fetch(last_message_uid, "BODY.PEEK[]") if response.result == "OK": - message = ImapMessage( - response.lines[1], charset=self.config_entry.data[CONF_CHARSET] - ) + message = ImapMessage(response.lines[1]) # Set `initial` to `False` if the last message is triggered again initial: bool = True if (message_id := message.message_id) == self._last_message_id: diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index a00f9d9c25d..8a8ac88c8aa 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -8,6 +8,7 @@ from aioimaplib import AUTH, NONAUTH, SELECTED, AioImapException, Response import pytest from homeassistant.components.imap import DOMAIN +from homeassistant.components.imap.const import CONF_CHARSET from homeassistant.components.imap.errors import InvalidAuth, InvalidFolder from homeassistant.components.sensor.const import SensorStateClass from homeassistant.const import STATE_UNAVAILABLE @@ -131,13 +132,16 @@ async def test_entry_startup_fails( ], ) @pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) +@pytest.mark.parametrize("charset", ["utf-8", "us-ascii"], ids=["utf-8", "us-ascii"]) async def test_receiving_message_successfully( - hass: HomeAssistant, mock_imap_protocol: MagicMock, valid_date: bool + hass: HomeAssistant, mock_imap_protocol: MagicMock, valid_date: bool, charset: str ) -> None: """Test receiving a message successfully.""" event_called = async_capture_events(hass, "imap_content") - config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) + config = MOCK_CONFIG.copy() + config[CONF_CHARSET] = charset + 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() From 770119c8addbf04922bb28d7acc0e46d0cf07aa7 Mon Sep 17 00:00:00 2001 From: Cody C <50791984+codyc1515@users.noreply.github.com> Date: Mon, 5 Feb 2024 09:26:05 +1300 Subject: [PATCH 0251/1367] Handle GeoJSON int to str conversion when the name is an int (#108937) Co-authored-by: Chris Roberts --- .../components/geo_json_events/geo_location.py | 3 ++- tests/components/geo_json_events/test_geo_location.py | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index 8915962c4ff..134f6a0e943 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -105,7 +105,8 @@ class GeoJsonLocationEvent(GeolocationEvent): def _update_from_feed(self, feed_entry: GenericFeedEntry) -> None: """Update the internal state from the provided feed entry.""" if feed_entry.properties and "name" in feed_entry.properties: - self._attr_name = feed_entry.properties.get("name") + # The entry name's type can vary, but our own name must be a string + self._attr_name = str(feed_entry.properties["name"]) else: self._attr_name = feed_entry.title self._attr_distance = feed_entry.distance_to_home diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py index 3176a37ab74..2f3b12ed554 100644 --- a/tests/components/geo_json_events/test_geo_location.py +++ b/tests/components/geo_json_events/test_geo_location.py @@ -58,7 +58,9 @@ async def test_entity_lifecycle( (-31.0, 150.0), {ATTR_NAME: "Properties 1"}, ) - mock_entry_2 = _generate_mock_feed_entry("2345", "Title 2", 20.5, (-31.1, 150.1)) + mock_entry_2 = _generate_mock_feed_entry( + "2345", "271310188", 20.5, (-31.1, 150.1), {ATTR_NAME: 271310188} + ) mock_entry_3 = _generate_mock_feed_entry("3456", "Title 3", 25.5, (-31.2, 150.2)) mock_entry_4 = _generate_mock_feed_entry("4567", "Title 4", 12.5, (-31.3, 150.3)) @@ -89,14 +91,14 @@ async def test_entity_lifecycle( } assert round(abs(float(state.state) - 15.5), 7) == 0 - state = hass.states.get(f"{GEO_LOCATION_DOMAIN}.title_2") + state = hass.states.get(f"{GEO_LOCATION_DOMAIN}.271310188") assert state is not None - assert state.name == "Title 2" + assert state.name == "271310188" assert state.attributes == { ATTR_EXTERNAL_ID: "2345", ATTR_LATITUDE: -31.1, ATTR_LONGITUDE: 150.1, - ATTR_FRIENDLY_NAME: "Title 2", + ATTR_FRIENDLY_NAME: "271310188", ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "geo_json_events", } From edc6e3e2f9e74739f38e2160eeeac37f9ffe97b1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 4 Feb 2024 14:35:08 -0700 Subject: [PATCH 0252/1367] Bump `aionotion` to 2024.02.0 (#109577) --- homeassistant/components/notion/__init__.py | 102 +++++++++--------- .../components/notion/binary_sensor.py | 6 +- .../components/notion/config_flow.py | 4 +- homeassistant/components/notion/manifest.json | 2 +- homeassistant/components/notion/model.py | 2 +- homeassistant/components/notion/sensor.py | 8 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/notion/conftest.py | 34 ++++-- .../notion/fixtures/bridge_data.json | 32 +----- .../notion/fixtures/listener_data.json | 49 ++------- .../notion/fixtures/sensor_data.json | 10 +- tests/components/notion/test_diagnostics.py | 78 +++++--------- 13 files changed, 134 insertions(+), 197 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 406acd6aabd..bd83b192a69 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -4,22 +4,15 @@ from __future__ import annotations import asyncio from dataclasses import dataclass, field from datetime import timedelta -import logging -import traceback from typing import Any from uuid import UUID from aionotion import async_get_client -from aionotion.bridge.models import Bridge, BridgeAllResponse +from aionotion.bridge.models import Bridge from aionotion.errors import InvalidCredentialsError, NotionError -from aionotion.sensor.models import ( - Listener, - ListenerAllResponse, - ListenerKind, - Sensor, - SensorAllResponse, -) -from aionotion.user.models import UserPreferences, UserPreferencesResponse +from aionotion.listener.models import Listener, ListenerKind +from aionotion.sensor.models import Sensor +from aionotion.user.models import UserPreferences from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform @@ -112,36 +105,35 @@ class NotionData: # Define a user preferences response object: user_preferences: UserPreferences | None = field(default=None) - def update_data_from_response( - self, - response: BridgeAllResponse - | ListenerAllResponse - | SensorAllResponse - | UserPreferencesResponse, - ) -> None: - """Update data from an aionotion response.""" - if isinstance(response, BridgeAllResponse): - for bridge in response.bridges: - # If a new bridge is discovered, register it: - if bridge.id not in self.bridges: - _async_register_new_bridge(self.hass, self.entry, bridge) - self.bridges[bridge.id] = bridge - elif isinstance(response, ListenerAllResponse): - self.listeners = {listener.id: listener for listener in response.listeners} - elif isinstance(response, SensorAllResponse): - self.sensors = {sensor.uuid: sensor for sensor in response.sensors} - elif isinstance(response, UserPreferencesResponse): - self.user_preferences = response.user_preferences + def update_bridges(self, bridges: list[Bridge]) -> None: + """Update the bridges.""" + for bridge in bridges: + # If a new bridge is discovered, register it: + if bridge.id not in self.bridges: + _async_register_new_bridge(self.hass, self.entry, bridge) + self.bridges[bridge.id] = bridge + + def update_listeners(self, listeners: list[Listener]) -> None: + """Update the listeners.""" + self.listeners = {listener.id: listener for listener in listeners} + + def update_sensors(self, sensors: list[Sensor]) -> None: + """Update the sensors.""" + self.sensors = {sensor.uuid: sensor for sensor in sensors} + + def update_user_preferences(self, user_preferences: UserPreferences) -> None: + """Update the user preferences.""" + self.user_preferences = user_preferences def asdict(self) -> dict[str, Any]: """Represent this dataclass (and its Pydantic contents) as a dict.""" data: dict[str, Any] = { - DATA_BRIDGES: [bridge.dict() for bridge in self.bridges.values()], - DATA_LISTENERS: [listener.dict() for listener in self.listeners.values()], - DATA_SENSORS: [sensor.dict() for sensor in self.sensors.values()], + DATA_BRIDGES: [item.to_dict() for item in self.bridges.values()], + DATA_LISTENERS: [item.to_dict() for item in self.listeners.values()], + DATA_SENSORS: [item.to_dict() for item in self.sensors.values()], } if self.user_preferences: - data[DATA_USER_PREFERENCES] = self.user_preferences.dict() + data[DATA_USER_PREFERENCES] = self.user_preferences.to_dict() return data @@ -156,7 +148,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: client = await async_get_client( - entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session=session + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + session=session, + use_legacy_auth=True, ) except InvalidCredentialsError as err: raise ConfigEntryAuthFailed("Invalid username and/or password") from err @@ -166,34 +161,39 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update() -> NotionData: """Get the latest data from the Notion API.""" data = NotionData(hass=hass, entry=entry) - tasks = { - DATA_BRIDGES: client.bridge.async_all(), - DATA_LISTENERS: client.sensor.async_listeners(), - DATA_SENSORS: client.sensor.async_all(), - DATA_USER_PREFERENCES: client.user.async_preferences(), - } - results = await asyncio.gather(*tasks.values(), return_exceptions=True) - for attr, result in zip(tasks, results): + try: + async with asyncio.TaskGroup() as tg: + bridges = tg.create_task(client.bridge.async_all()) + listeners = tg.create_task(client.listener.async_all()) + sensors = tg.create_task(client.sensor.async_all()) + user_preferences = tg.create_task(client.user.async_preferences()) + except BaseExceptionGroup as err: + result = err.exceptions[0] if isinstance(result, InvalidCredentialsError): raise ConfigEntryAuthFailed( "Invalid username and/or password" ) from result if isinstance(result, NotionError): raise UpdateFailed( - f"There was a Notion error while updating {attr}: {result}" + f"There was a Notion error while updating: {result}" ) from result if isinstance(result, Exception): - if LOGGER.isEnabledFor(logging.DEBUG): - LOGGER.debug("".join(traceback.format_tb(result.__traceback__))) + LOGGER.debug( + "There was an unknown error while updating: %s", + result, + exc_info=result, + ) raise UpdateFailed( - f"There was an unknown error while updating {attr}: {result}" + f"There was an unknown error while updating: {result}" ) from result if isinstance(result, BaseException): raise result from None - data.update_data_from_response(result) # type: ignore[arg-type] - + data.update_bridges(bridges.result()) + data.update_listeners(listeners.result()) + data.update_sensors(sensors.result()) + data.update_user_preferences(user_preferences.result()) return data coordinator = DataUpdateCoordinator( @@ -232,7 +232,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: listener for listener in coordinator.data.listeners.values() if listener.sensor_id == sensor.uuid - and listener.listener_kind == TASK_TYPE_TO_LISTENER_MAP[task_type] + and listener.definition_id == TASK_TYPE_TO_LISTENER_MAP[task_type].value ) return {"new_unique_id": listener.id} diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 8e4d5927152..dfa6dc5ec06 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import Literal -from aionotion.sensor.models import ListenerKind +from aionotion.listener.models import ListenerKind from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -123,7 +123,7 @@ async def async_setup_entry( ) for listener_id, listener in coordinator.data.listeners.items() for description in BINARY_SENSOR_DESCRIPTIONS - if description.listener_kind == listener.listener_kind + if description.listener_kind.value == listener.definition_id and (sensor := coordinator.data.sensors[listener.sensor_id]) ] ) @@ -138,6 +138,6 @@ class NotionBinarySensor(NotionEntity, BinarySensorEntity): def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" if not self.listener.insights.primary.value: - LOGGER.warning("Unknown listener structure: %s", self.listener.dict()) + LOGGER.warning("Unknown listener structure: %s", self.listener) return False return self.listener.insights.primary.value == self.entity_description.on_state diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py index 1e4adab2910..2ed83adeb08 100644 --- a/homeassistant/components/notion/config_flow.py +++ b/homeassistant/components/notion/config_flow.py @@ -38,7 +38,9 @@ async def async_validate_credentials( errors = {} try: - await async_get_client(username, password, session=session) + await async_get_client( + username, password, session=session, use_legacy_auth=True + ) except InvalidCredentialsError: errors["base"] = "invalid_auth" except NotionError as err: diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json index f23a082df35..662114742bd 100644 --- a/homeassistant/components/notion/manifest.json +++ b/homeassistant/components/notion/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["aionotion"], - "requirements": ["aionotion==2023.05.5"] + "requirements": ["aionotion==2024.02.0"] } diff --git a/homeassistant/components/notion/model.py b/homeassistant/components/notion/model.py index a774bfdfad3..059ea551b09 100644 --- a/homeassistant/components/notion/model.py +++ b/homeassistant/components/notion/model.py @@ -1,7 +1,7 @@ """Define Notion model mixins.""" from dataclasses import dataclass -from aionotion.sensor.models import ListenerKind +from aionotion.listener.models import ListenerKind @dataclass(frozen=True, kw_only=True) diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 1d2c81addfa..f5439895ac9 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -1,7 +1,7 @@ """Support for Notion sensors.""" from dataclasses import dataclass -from aionotion.sensor.models import ListenerKind +from aionotion.listener.models import ListenerKind from homeassistant.components.sensor import ( SensorDeviceClass, @@ -59,7 +59,7 @@ async def async_setup_entry( ) for listener_id, listener in coordinator.data.listeners.items() for description in SENSOR_DESCRIPTIONS - if description.listener_kind == listener.listener_kind + if description.listener_kind.value == listener.definition_id and (sensor := coordinator.data.sensors[listener.sensor_id]) ] ) @@ -71,7 +71,7 @@ class NotionSensor(NotionEntity, SensorEntity): @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of the sensor.""" - if self.listener.listener_kind == ListenerKind.TEMPERATURE: + if self.listener.definition_id == ListenerKind.TEMPERATURE.value: if not self.coordinator.data.user_preferences: return None if self.coordinator.data.user_preferences.celsius_enabled: @@ -84,7 +84,7 @@ class NotionSensor(NotionEntity, SensorEntity): """Return the value reported by the sensor.""" if not self.listener.status_localized: return None - if self.listener.listener_kind == ListenerKind.TEMPERATURE: + if self.listener.definition_id == ListenerKind.TEMPERATURE.value: # 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] diff --git a/requirements_all.txt b/requirements_all.txt index fb563af478f..3cda8a4f523 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -309,7 +309,7 @@ aiomusiccast==0.14.8 aionanoleaf==0.2.1 # homeassistant.components.notion -aionotion==2023.05.5 +aionotion==2024.02.0 # homeassistant.components.oncue aiooncue==0.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3cb24852bac..086133cbb25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ aiomusiccast==0.14.8 aionanoleaf==0.2.1 # homeassistant.components.notion -aionotion==2023.05.5 +aionotion==2024.02.0 # homeassistant.components.oncue aiooncue==0.3.5 diff --git a/tests/components/notion/conftest.py b/tests/components/notion/conftest.py index 81d69158e82..3623782429f 100644 --- a/tests/components/notion/conftest.py +++ b/tests/components/notion/conftest.py @@ -3,9 +3,10 @@ from collections.abc import Generator import json from unittest.mock import AsyncMock, Mock, patch -from aionotion.bridge.models import BridgeAllResponse -from aionotion.sensor.models import ListenerAllResponse, SensorAllResponse -from aionotion.user.models import UserPreferencesResponse +from aionotion.bridge.models import Bridge +from aionotion.listener.models import Listener +from aionotion.sensor.models import Sensor +from aionotion.user.models import UserPreferences import pytest from homeassistant.components.notion import DOMAIN @@ -32,17 +33,32 @@ def client_fixture(data_bridge, data_listener, data_sensor, data_user_preference """Define a fixture for an aionotion client.""" return Mock( bridge=Mock( - async_all=AsyncMock(return_value=BridgeAllResponse.parse_obj(data_bridge)) + async_all=AsyncMock( + return_value=[ + Bridge.from_dict(bridge) for bridge in data_bridge["base_stations"] + ] + ) + ), + listener=Mock( + async_all=AsyncMock( + return_value=[ + Listener.from_dict(listener) + for listener in data_listener["listeners"] + ] + ) ), sensor=Mock( - async_all=AsyncMock(return_value=SensorAllResponse.parse_obj(data_sensor)), - async_listeners=AsyncMock( - return_value=ListenerAllResponse.parse_obj(data_listener) - ), + async_all=AsyncMock( + return_value=[ + Sensor.from_dict(sensor) for sensor in data_sensor["sensors"] + ] + ) ), user=Mock( async_preferences=AsyncMock( - return_value=UserPreferencesResponse.parse_obj(data_user_preferences) + return_value=UserPreferences.from_dict( + data_user_preferences["user_preferences"] + ) ) ), ) diff --git a/tests/components/notion/fixtures/bridge_data.json b/tests/components/notion/fixtures/bridge_data.json index 05bd8859e7e..d8a0feead69 100644 --- a/tests/components/notion/fixtures/bridge_data.json +++ b/tests/components/notion/fixtures/bridge_data.json @@ -2,31 +2,7 @@ "base_stations": [ { "id": 12345, - "name": "Bridge 1", - "mode": "home", - "hardware_id": "0x0000000000000000", - "hardware_revision": 4, - "firmware_version": { - "silabs": "1.1.2", - "wifi": "0.121.0", - "wifi_app": "3.3.0" - }, - "missing_at": null, - "created_at": "2019-06-27T00:18:44.337Z", - "updated_at": "2023-03-19T03:20:16.061Z", - "system_id": 11111, - "firmware": { - "silabs": "1.1.2", - "wifi": "0.121.0", - "wifi_app": "3.3.0" - }, - "links": { - "system": 11111 - } - }, - { - "id": 67890, - "name": "Bridge 2", + "name": "Laundry Closet", "mode": "home", "hardware_id": "0x0000000000000000", "hardware_revision": 4, @@ -37,15 +13,15 @@ }, "missing_at": null, "created_at": "2019-04-30T01:43:50.497Z", - "updated_at": "2023-01-02T19:09:58.251Z", - "system_id": 11111, + "updated_at": "2023-12-12T22:33:01.073Z", + "system_id": 12345, "firmware": { "wifi": "0.121.0", "wifi_app": "3.3.0", "silabs": "1.1.2" }, "links": { - "system": 11111 + "system": 12345 } } ] diff --git a/tests/components/notion/fixtures/listener_data.json b/tests/components/notion/fixtures/listener_data.json index 6d59dde76df..af692795f1b 100644 --- a/tests/components/notion/fixtures/listener_data.json +++ b/tests/components/notion/fixtures/listener_data.json @@ -2,56 +2,27 @@ "listeners": [ { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "definition_id": 4, - "created_at": "2019-06-28T22:12:49.651Z", + "definition_id": 24, + "created_at": "2019-06-17T03:29:45.722Z", "type": "sensor", - "model_version": "2.1", + "model_version": "1.0", "sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "status": { - "trigger_value": "no_leak", - "data_received_at": "2022-03-20T08:00:29.763Z" - }, "status_localized": { - "state": "No Leak", - "description": "Mar 20 at 2:00am" + "state": "Idle", + "description": "Jun 18 at 12:17am" }, "insights": { "primary": { "origin": { - "type": "Sensor", - "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "type": "Sensor" }, - "value": "no_leak", - "data_received_at": "2022-03-20T08:00:29.763Z" + "value": "idle", + "data_received_at": "2023-06-18T06:17:00.697Z" } }, "configuration": {}, - "pro_monitoring_status": "eligible" - }, - { - "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "definition_id": 7, - "created_at": "2019-07-10T22:40:48.847Z", - "type": "sensor", - "model_version": "3.1", - "sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "status": { - "trigger_value": "no_alarm", - "data_received_at": "2019-06-28T22:12:49.516Z" - }, - "status_localized": { - "state": "No Sound", - "description": "Jun 28 at 4:12pm" - }, - "insights": { - "primary": { - "origin": {}, - "value": "no_alarm", - "data_received_at": "2019-06-28T22:12:49.516Z" - } - }, - "configuration": {}, - "pro_monitoring_status": "eligible" + "pro_monitoring_status": "ineligible" } ] } diff --git a/tests/components/notion/fixtures/sensor_data.json b/tests/components/notion/fixtures/sensor_data.json index 9f0d0fe2e03..56989cbb157 100644 --- a/tests/components/notion/fixtures/sensor_data.json +++ b/tests/components/notion/fixtures/sensor_data.json @@ -20,12 +20,12 @@ "firmware_version": "1.1.2", "device_key": "0x0000000000000000", "encryption_key": true, - "installed_at": "2019-06-28T22:12:51.209Z", - "calibrated_at": "2023-03-07T19:51:56.838Z", - "last_reported_at": "2023-04-19T18:09:40.479Z", + "installed_at": "2019-06-17T03:30:27.766Z", + "calibrated_at": "2024-01-19T00:38:15.372Z", + "last_reported_at": "2024-01-21T00:00:46.705Z", "missing_at": null, - "updated_at": "2023-03-28T13:33:33.801Z", - "created_at": "2019-06-28T22:12:20.256Z", + "updated_at": "2024-01-19T00:38:16.856Z", + "created_at": "2019-06-17T03:29:45.506Z", "signal_strength": 4, "firmware": { "status": "valid" diff --git a/tests/components/notion/test_diagnostics.py b/tests/components/notion/test_diagnostics.py index 07a67cb1429..a2b829281f8 100644 --- a/tests/components/notion/test_diagnostics.py +++ b/tests/components/notion/test_diagnostics.py @@ -33,31 +33,7 @@ async def test_entry_diagnostics( "bridges": [ { "id": 12345, - "name": "Bridge 1", - "mode": "home", - "hardware_id": REDACTED, - "hardware_revision": 4, - "firmware_version": { - "wifi": "0.121.0", - "wifi_app": "3.3.0", - "silabs": "1.1.2", - "ti": None, - }, - "missing_at": None, - "created_at": "2019-06-27T00:18:44.337000+00:00", - "updated_at": "2023-03-19T03:20:16.061000+00:00", - "system_id": 11111, - "firmware": { - "wifi": "0.121.0", - "wifi_app": "3.3.0", - "silabs": "1.1.2", - "ti": None, - }, - "links": {"system": 11111}, - }, - { - "id": 67890, - "name": "Bridge 2", + "name": "Laundry Closet", "mode": "home", "hardware_id": REDACTED, "hardware_revision": 4, @@ -69,45 +45,41 @@ async def test_entry_diagnostics( }, "missing_at": None, "created_at": "2019-04-30T01:43:50.497000+00:00", - "updated_at": "2023-01-02T19:09:58.251000+00:00", - "system_id": 11111, + "updated_at": "2023-12-12T22:33:01.073000+00:00", + "system_id": 12345, "firmware": { "wifi": "0.121.0", "wifi_app": "3.3.0", "silabs": "1.1.2", "ti": None, }, - "links": {"system": 11111}, - }, + "links": {"system": 12345}, + } ], "listeners": [ { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "listener_kind": { - "__type": "", - "repr": "", - }, - "created_at": "2019-07-10T22:40:48.847000+00:00", - "device_type": "sensor", - "model_version": "3.1", + "definition_id": 24, + "created_at": "2019-06-17T03:29:45.722000+00:00", + "model_version": "1.0", "sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "status_localized": { + "state": "Idle", + "description": "Jun 18 at 12:17am", + }, "insights": { "primary": { - "origin": {"type": None, "id": None}, - "value": "no_alarm", - "data_received_at": "2019-06-28T22:12:49.516000+00:00", + "origin": { + "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "type": "Sensor", + }, + "value": "idle", + "data_received_at": "2023-06-18T06:17:00.697000+00:00", } }, "configuration": {}, - "pro_monitoring_status": "eligible", - "status": { - "trigger_value": "no_alarm", - "data_received_at": "2019-06-28T22:12:49.516000+00:00", - }, - "status_localized": { - "state": "No Sound", - "description": "Jun 28 at 4:12pm", - }, + "pro_monitoring_status": "ineligible", + "device_type": "sensor", } ], "sensors": [ @@ -125,12 +97,12 @@ async def test_entry_diagnostics( "firmware_version": "1.1.2", "device_key": REDACTED, "encryption_key": True, - "installed_at": "2019-06-28T22:12:51.209000+00:00", - "calibrated_at": "2023-03-07T19:51:56.838000+00:00", - "last_reported_at": "2023-04-19T18:09:40.479000+00:00", + "installed_at": "2019-06-17T03:30:27.766000+00:00", + "calibrated_at": "2024-01-19T00:38:15.372000+00:00", + "last_reported_at": "2024-01-21T00:00:46.705000+00:00", "missing_at": None, - "updated_at": "2023-03-28T13:33:33.801000+00:00", - "created_at": "2019-06-28T22:12:20.256000+00:00", + "updated_at": "2024-01-19T00:38:16.856000+00:00", + "created_at": "2019-06-17T03:29:45.506000+00:00", "signal_strength": 4, "firmware": {"status": "valid"}, "surface_type": None, From 63ec20ab5d4e835f8a57ed0137907d47750db540 Mon Sep 17 00:00:00 2001 From: spycle <48740594+spycle@users.noreply.github.com> Date: Sun, 4 Feb 2024 21:50:28 +0000 Subject: [PATCH 0253/1367] Fix keymitt_ble config-flow (#109644) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/keymitt_ble/config_flow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/keymitt_ble/config_flow.py b/homeassistant/components/keymitt_ble/config_flow.py index e8176b152a6..5665dc27d17 100644 --- a/homeassistant/components/keymitt_ble/config_flow.py +++ b/homeassistant/components/keymitt_ble/config_flow.py @@ -138,6 +138,8 @@ class MicroBotConfigFlow(ConfigFlow, domain=DOMAIN): await self._client.connect(init=True) return self.async_show_form(step_id="link") + if not await self._client.is_connected(): + await self._client.connect(init=False) if not await self._client.is_connected(): errors["base"] = "linking" else: From 10d3b10da47b47f38ccee07b87056834cdd3c781 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 4 Feb 2024 22:56:22 +0100 Subject: [PATCH 0254/1367] Improve Tuya token/reauth handling (#109653) --- homeassistant/components/tuya/__init__.py | 32 ++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index ea38c117af7..5a6874fb352 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -59,12 +59,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: listener = DeviceListener(hass, manager) manager.add_device_listener(listener) + + # Get all devices from Tuya + try: + await hass.async_add_executor_job(manager.update_device_cache) + except Exception as exc: # pylint: disable=broad-except + # While in general, we should avoid catching broad exceptions, + # we have no other way of detecting this case. + if "sign invalid" in str(exc): + msg = "Authentication failed. Please re-authenticate" + raise ConfigEntryAuthFailed(msg) from exc + raise + + # Connection is successful, store the manager & listener hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantTuyaData( manager=manager, listener=listener ) - # Get devices & clean up device entities - await hass.async_add_executor_job(manager.update_device_cache) + # Cleanup device registry await cleanup_device_registry(hass, manager) # Register known device IDs @@ -102,11 +114,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if tuya.manager.mq is not None: tuya.manager.mq.stop() tuya.manager.remove_device_listener(tuya.listener) - await hass.async_add_executor_job(tuya.manager.unload) del hass.data[DOMAIN][entry.entry_id] return unload_ok +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove a config entry. + + This will revoke the credentials from Tuya. + """ + manager = Manager( + TUYA_CLIENT_ID, + entry.data[CONF_USER_CODE], + entry.data[CONF_TERMINAL_ID], + entry.data[CONF_ENDPOINT], + entry.data[CONF_TOKEN_INFO], + ) + await hass.async_add_executor_job(manager.unload) + + class DeviceListener(SharingDeviceListener): """Device Update Listener.""" From 2c0b8976586597646e91629575c38d255e57562f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 4 Feb 2024 22:57:11 +0100 Subject: [PATCH 0255/1367] Allow the helper integrations to omit icon translation field (#109648) --- script/hassfest/icons.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/script/hassfest/icons.py b/script/hassfest/icons.py index 2b28312284a..60dc2b79f56 100644 --- a/script/hassfest/icons.py +++ b/script/hassfest/icons.py @@ -77,9 +77,13 @@ def icon_schema(integration_type: str) -> vol.Schema: ) if integration_type in ("entity", "helper", "system"): + if integration_type != "entity": + field = vol.Optional("entity_component") + else: + field = vol.Required("entity_component") schema = schema.extend( { - vol.Required("entity_component"): vol.All( + field: vol.All( cv.schema_with_slug_keys( icon_schema_slug(vol.Required), slug_validator=vol.Any("_", cv.slug), From 9fef1938b425d06b47443bb4521a0cb3a7da6c28 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 16:11:56 -0600 Subject: [PATCH 0256/1367] Make get_entries_for_device_id skip disabled devices by default (#109645) --- .../components/device_automation/__init__.py | 3 +-- homeassistant/helpers/entity_registry.py | 23 +++++++++++-------- tests/helpers/test_entity_registry.py | 7 ++++++ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 979c82acfe2..2bf87343c72 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -249,8 +249,7 @@ async def async_get_device_automations( for device_id in match_device_ids: for entry in entity_registry.entities.get_entries_for_device_id(device_id): - if not entry.disabled_by: - device_entities_domains.setdefault(device_id, set()).add(entry.domain) + device_entities_domains.setdefault(device_id, set()).add(entry.domain) for device_id in match_device_ids: combined_results[device_id] = [] diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 8ae9256754d..3f2e8a94b7c 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -498,17 +498,24 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): """Get entry from id.""" return self._entry_ids.get(key) - def get_entries_for_device_id(self, device_id: str) -> list[RegistryEntry]: + def get_entries_for_device_id( + self, device_id: str, include_disabled_entities: bool = False + ) -> list[RegistryEntry]: """Get entries for device.""" - return [self.data[key] for key in self._device_id_index.get(device_id, ())] + data = self.data + return [ + entry + for key in self._device_id_index.get(device_id, ()) + if not (entry := data[key]).disabled_by or include_disabled_entities + ] def get_entries_for_config_entry_id( self, config_entry_id: str ) -> list[RegistryEntry]: """Get entries for config entry.""" + data = self.data return [ - self.data[key] - for key in self._config_entry_id_index.get(config_entry_id, ()) + data[key] for key in self._config_entry_id_index.get(config_entry_id, ()) ] @@ -1249,11 +1256,9 @@ def async_entries_for_device( registry: EntityRegistry, device_id: str, include_disabled_entities: bool = False ) -> list[RegistryEntry]: """Return entries that match a device.""" - return [ - entry - for entry in registry.entities.get_entries_for_device_id(device_id) - if (not entry.disabled_by or include_disabled_entities) - ] + return registry.entities.get_entries_for_device_id( + device_id, include_disabled_entities + ) @callback diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 1c13da1192f..9e86b0279de 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1370,6 +1370,13 @@ async def test_disabled_entities_excluded_from_entity_list( ) assert entries == [entry1, entry2] + ent_reg = er.async_get(hass) + assert ent_reg.entities.get_entries_for_device_id(device_entry.id) == [entry1] + + assert ent_reg.entities.get_entries_for_device_id( + device_entry.id, include_disabled_entities=True + ) == [entry1, entry2] + async def test_entity_max_length_exceeded(entity_registry: er.EntityRegistry) -> None: """Test that an exception is raised when the max character length is exceeded.""" From c3690e74c523002226826fdfcdba2a1a6ed8944d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 4 Feb 2024 23:20:14 +0100 Subject: [PATCH 0257/1367] Add icon translations to Counter (#109651) --- homeassistant/components/counter/icons.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 homeassistant/components/counter/icons.json diff --git a/homeassistant/components/counter/icons.json b/homeassistant/components/counter/icons.json new file mode 100644 index 00000000000..1e0ef54bbb7 --- /dev/null +++ b/homeassistant/components/counter/icons.json @@ -0,0 +1,8 @@ +{ + "services": { + "decrement": "mdi:numeric-negative-1", + "increment": "mdi:numeric-positive-1", + "reset": "mdi:refresh", + "set_value": "mdi:counter" + } +} From 0b3ed92a6e7fea8ed68cd7a48244221f07520468 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 4 Feb 2024 23:20:46 +0100 Subject: [PATCH 0258/1367] Add icon translations to derivative (#109650) --- homeassistant/components/derivative/icons.json | 9 +++++++++ homeassistant/components/derivative/sensor.py | 6 ++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/derivative/icons.json diff --git a/homeassistant/components/derivative/icons.json b/homeassistant/components/derivative/icons.json new file mode 100644 index 00000000000..d8f2a961c3a --- /dev/null +++ b/homeassistant/components/derivative/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "derivative": { + "default": "mdi:chart-line" + } + } + } +} diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 73d297d7541..cd912ceb24e 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -64,8 +64,6 @@ UNIT_TIME = { UnitOfTime.DAYS: 24 * 60 * 60, } -ICON = "mdi:chart-line" - DEFAULT_ROUND = 3 DEFAULT_TIME_WINDOW = 0 @@ -157,9 +155,9 @@ async def async_setup_platform( class DerivativeSensor(RestoreSensor, SensorEntity): - """Representation of an derivative sensor.""" + """Representation of a derivative sensor.""" - _attr_icon = ICON + _attr_translation_key = "derivative" _attr_should_poll = False def __init__( From 945aff605057c034aeb392ccfc04817b878bbccf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 4 Feb 2024 23:21:18 +0100 Subject: [PATCH 0259/1367] Fix Tuya reauth_successful translation string (#109659) --- homeassistant/components/tuya/strings.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 693f799e6e9..cfce12273a0 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -19,7 +19,9 @@ }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "login_error": "Login error ({code}): {msg}", + "login_error": "Login error ({code}): {msg}" + }, + "abort": { "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, From f18448950cf61e47fa37d0e325600b6c29f4568c Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sun, 4 Feb 2024 23:21:57 +0100 Subject: [PATCH 0260/1367] Add icon translations to Utility meter helper (#109656) --- homeassistant/components/utility_meter/const.py | 2 -- homeassistant/components/utility_meter/icons.json | 14 ++++++++++++++ homeassistant/components/utility_meter/select.py | 11 +++-------- homeassistant/components/utility_meter/sensor.py | 2 +- 4 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/utility_meter/icons.json diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py index 6e1cabac509..4f62925069d 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -1,8 +1,6 @@ """Constants for the utility meter component.""" DOMAIN = "utility_meter" -TARIFF_ICON = "mdi:clock-outline" - QUARTER_HOURLY = "quarter-hourly" HOURLY = "hourly" DAILY = "daily" diff --git a/homeassistant/components/utility_meter/icons.json b/homeassistant/components/utility_meter/icons.json new file mode 100644 index 00000000000..7260fbfbe96 --- /dev/null +++ b/homeassistant/components/utility_meter/icons.json @@ -0,0 +1,14 @@ +{ + "entity": { + "sensor": { + "utility_meter": { + "default": "mdi:counter" + } + }, + "select": { + "tariff": { + "default": "mdi:clock-outline" + } + } + } +} diff --git a/homeassistant/components/utility_meter/select.py b/homeassistant/components/utility_meter/select.py index 64b271d4200..86433ca77f8 100644 --- a/homeassistant/components/utility_meter/select.py +++ b/homeassistant/components/utility_meter/select.py @@ -13,13 +13,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import ( - CONF_METER, - CONF_SOURCE_SENSOR, - CONF_TARIFFS, - DATA_UTILITY, - TARIFF_ICON, -) +from .const import CONF_METER, CONF_SOURCE_SENSOR, CONF_TARIFFS, DATA_UTILITY _LOGGER = logging.getLogger(__name__) @@ -100,6 +94,8 @@ async def async_setup_platform( class TariffSelect(SelectEntity, RestoreEntity): """Representation of a Tariff selector.""" + _attr_translation_key = "tariff" + def __init__( self, name, @@ -113,7 +109,6 @@ class TariffSelect(SelectEntity, RestoreEntity): self._attr_device_info = device_info self._current_tariff: str | None = None self._tariffs = tariffs - self._attr_icon = TARIFF_ICON self._attr_should_poll = False @property diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index ee0d5f85b3b..e9ad7a1ba30 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -362,7 +362,7 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData): class UtilityMeterSensor(RestoreSensor): """Representation of an utility meter sensor.""" - _attr_icon = "mdi:counter" + _attr_translation_key = "utility_meter" _attr_should_poll = False def __init__( From 52d27230bce239017722d8ce9dd6f5386f63aba2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 4 Feb 2024 23:23:10 +0100 Subject: [PATCH 0261/1367] Add icon translations to Random (#109652) --- homeassistant/components/random/binary_sensor.py | 2 ++ homeassistant/components/random/icons.json | 14 ++++++++++++++ homeassistant/components/random/sensor.py | 2 ++ 3 files changed, 18 insertions(+) create mode 100644 homeassistant/components/random/icons.json diff --git a/homeassistant/components/random/binary_sensor.py b/homeassistant/components/random/binary_sensor.py index 0c5b4a8b0dd..a6d330e6151 100644 --- a/homeassistant/components/random/binary_sensor.py +++ b/homeassistant/components/random/binary_sensor.py @@ -54,6 +54,8 @@ async def async_setup_entry( class RandomBinarySensor(BinarySensorEntity): """Representation of a Random binary sensor.""" + _attr_translation_key = "random" + def __init__(self, config: Mapping[str, Any], entry_id: str | None = None) -> None: """Initialize the Random binary sensor.""" self._attr_name = config.get(CONF_NAME) diff --git a/homeassistant/components/random/icons.json b/homeassistant/components/random/icons.json new file mode 100644 index 00000000000..83d5ecd0688 --- /dev/null +++ b/homeassistant/components/random/icons.json @@ -0,0 +1,14 @@ +{ + "entity": { + "binary_sensor": { + "random": { + "default": "mdi:dice-multiple" + } + }, + "sensor": { + "random": { + "default": "mdi:dice-multiple" + } + } + } +} diff --git a/homeassistant/components/random/sensor.py b/homeassistant/components/random/sensor.py index f1ca4290d83..8cc21e34ce9 100644 --- a/homeassistant/components/random/sensor.py +++ b/homeassistant/components/random/sensor.py @@ -65,6 +65,8 @@ async def async_setup_entry( class RandomSensor(SensorEntity): """Representation of a Random number sensor.""" + _attr_translation_key = "random" + def __init__(self, config: Mapping[str, Any], entry_id: str | None = None) -> None: """Initialize the Random sensor.""" self._attr_name = config.get(CONF_NAME) From 73589015c3a4a07b7308277255543083d153a510 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 16:50:08 -0600 Subject: [PATCH 0262/1367] Improve scalability of DHCP matchers (#109406) --- homeassistant/components/dhcp/__init__.py | 86 ++++++++--- tests/components/dhcp/test_init.py | 178 +++++++++++++++++----- 2 files changed, 202 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index b8a12a937e3..ad0446543db 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -9,6 +9,7 @@ from dataclasses import dataclass from datetime import timedelta from fnmatch import translate from functools import lru_cache +import itertools import logging import os import re @@ -89,11 +90,55 @@ class DhcpServiceInfo(BaseServiceInfo): macaddress: str +@dataclass(slots=True) +class DhcpMatchers: + """Prepared info from dhcp entries.""" + + registered_devices_domains: set[str] + no_oui_matchers: dict[str, list[DHCPMatcher]] + oui_matchers: dict[str, list[DHCPMatcher]] + + +def async_index_integration_matchers( + integration_matchers: list[DHCPMatcher], +) -> DhcpMatchers: + """Index the integration matchers. + + We have three types of matchers: + + 1. Registered devices + 2. Devices with no OUI - index by first char of lower() hostname + 3. Devices with OUI - index by OUI + """ + registered_devices_domains: set[str] = set() + no_oui_matchers: dict[str, list[DHCPMatcher]] = {} + oui_matchers: dict[str, list[DHCPMatcher]] = {} + for matcher in integration_matchers: + domain = matcher["domain"] + if REGISTERED_DEVICES in matcher: + registered_devices_domains.add(domain) + continue + + if mac_address := matcher.get(MAC_ADDRESS): + oui_matchers.setdefault(mac_address[:6], []).append(matcher) + continue + + if hostname := matcher.get(HOSTNAME): + first_char = hostname[0].lower() + no_oui_matchers.setdefault(first_char, []).append(matcher) + + return DhcpMatchers( + registered_devices_domains=registered_devices_domains, + no_oui_matchers=no_oui_matchers, + oui_matchers=oui_matchers, + ) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the dhcp component.""" watchers: list[WatcherBase] = [] address_data: dict[str, dict[str, str]] = {} - integration_matchers = await async_get_dhcp(hass) + integration_matchers = async_index_integration_matchers(await async_get_dhcp(hass)) # For the passive classes we need to start listening # for state changes and connect the dispatchers before # everything else starts up or we will miss events @@ -125,7 +170,7 @@ class WatcherBase(ABC): self, hass: HomeAssistant, address_data: dict[str, dict[str, str]], - integration_matchers: list[DHCPMatcher], + integration_matchers: DhcpMatchers, ) -> None: """Initialize class.""" super().__init__() @@ -189,28 +234,29 @@ class WatcherBase(ABC): lowercase_hostname, ) - matched_domains = set() - device_domains = set() + matched_domains: set[str] = set() + matchers = self._integration_matchers + registered_devices_domains = matchers.registered_devices_domains dev_reg: DeviceRegistry = async_get(self.hass) if device := dev_reg.async_get_device( connections={(CONNECTION_NETWORK_MAC, uppercase_mac)} ): for entry_id in device.config_entries: - if entry := self.hass.config_entries.async_get_entry(entry_id): - device_domains.add(entry.domain) + if ( + entry := self.hass.config_entries.async_get_entry(entry_id) + ) and entry.domain in registered_devices_domains: + matched_domains.add(entry.domain) - for matcher in self._integration_matchers: + oui = uppercase_mac[:6] + lowercase_hostname_first_char = ( + lowercase_hostname[0] if len(lowercase_hostname) else "" + ) + for matcher in itertools.chain( + matchers.no_oui_matchers.get(lowercase_hostname_first_char, ()), + matchers.oui_matchers.get(oui, ()), + ): domain = matcher["domain"] - - if matcher.get(REGISTERED_DEVICES) and domain not in device_domains: - continue - - if ( - matcher_mac := matcher.get(MAC_ADDRESS) - ) is not None and not _memorized_fnmatch(uppercase_mac, matcher_mac): - continue - if ( matcher_hostname := matcher.get(HOSTNAME) ) is not None and not _memorized_fnmatch( @@ -241,7 +287,7 @@ class NetworkWatcher(WatcherBase): self, hass: HomeAssistant, address_data: dict[str, dict[str, str]], - integration_matchers: list[DHCPMatcher], + integration_matchers: DhcpMatchers, ) -> None: """Initialize class.""" super().__init__(hass, address_data, integration_matchers) @@ -294,7 +340,7 @@ class DeviceTrackerWatcher(WatcherBase): self, hass: HomeAssistant, address_data: dict[str, dict[str, str]], - integration_matchers: list[DHCPMatcher], + integration_matchers: DhcpMatchers, ) -> None: """Initialize class.""" super().__init__(hass, address_data, integration_matchers) @@ -349,7 +395,7 @@ class DeviceTrackerRegisteredWatcher(WatcherBase): self, hass: HomeAssistant, address_data: dict[str, dict[str, str]], - integration_matchers: list[DHCPMatcher], + integration_matchers: DhcpMatchers, ) -> None: """Initialize class.""" super().__init__(hass, address_data, integration_matchers) @@ -387,7 +433,7 @@ class DHCPWatcher(WatcherBase): self, hass: HomeAssistant, address_data: dict[str, dict[str, str]], - integration_matchers: list[DHCPMatcher], + integration_matchers: DhcpMatchers, ) -> None: """Initialize class.""" super().__init__(hass, address_data, integration_matchers) diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index a63300b1ea2..18d213a7029 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -1,6 +1,8 @@ """Test the DHCP discovery integration.""" +from collections.abc import Awaitable, Callable import datetime import threading +from typing import Any, cast from unittest.mock import MagicMock, patch import pytest @@ -130,13 +132,15 @@ RAW_DHCP_REQUEST_WITHOUT_HOSTNAME = ( ) -async def _async_get_handle_dhcp_packet(hass, integration_matchers): +async def _async_get_handle_dhcp_packet( + hass: HomeAssistant, integration_matchers: dhcp.DhcpMatchers +) -> Callable[[Any], Awaitable[None]]: dhcp_watcher = dhcp.DHCPWatcher( hass, {}, integration_matchers, ) - async_handle_dhcp_packet = None + async_handle_dhcp_packet: Callable[[Any], Awaitable[None]] | None = None def _mock_sniffer(*args, **kwargs): nonlocal async_handle_dhcp_packet @@ -158,14 +162,14 @@ async def _async_get_handle_dhcp_packet(hass, integration_matchers): ): await dhcp_watcher.async_start() - return async_handle_dhcp_packet + return cast("Callable[[Any], Awaitable[None]]", async_handle_dhcp_packet) async def test_dhcp_match_hostname_and_macaddress(hass: HomeAssistant) -> None: """Test matching based on hostname and macaddress.""" - integration_matchers = [ - {"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"} - ] + integration_matchers = dhcp.async_index_integration_matchers( + [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}] + ) packet = Ether(RAW_DHCP_REQUEST) async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( @@ -190,9 +194,9 @@ async def test_dhcp_match_hostname_and_macaddress(hass: HomeAssistant) -> None: async def test_dhcp_renewal_match_hostname_and_macaddress(hass: HomeAssistant) -> None: """Test renewal matching based on hostname and macaddress.""" - integration_matchers = [ - {"domain": "mock-domain", "hostname": "irobot-*", "macaddress": "501479*"} - ] + integration_matchers = dhcp.async_index_integration_matchers( + [{"domain": "mock-domain", "hostname": "irobot-*", "macaddress": "501479*"}] + ) packet = Ether(RAW_DHCP_RENEWAL) @@ -220,10 +224,12 @@ async def test_registered_devices( hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: """Test discovery flows are created for registered devices.""" - integration_matchers = [ - {"domain": "not-matching", "registered_devices": True}, - {"domain": "mock-domain", "registered_devices": True}, - ] + integration_matchers = dhcp.async_index_integration_matchers( + [ + {"domain": "not-matching", "registered_devices": True}, + {"domain": "mock-domain", "registered_devices": True}, + ] + ) packet = Ether(RAW_DHCP_RENEWAL) @@ -265,7 +271,9 @@ async def test_registered_devices( async def test_dhcp_match_hostname(hass: HomeAssistant) -> None: """Test matching based on hostname only.""" - integration_matchers = [{"domain": "mock-domain", "hostname": "connect"}] + integration_matchers = dhcp.async_index_integration_matchers( + [{"domain": "mock-domain", "hostname": "connect"}] + ) packet = Ether(RAW_DHCP_REQUEST) @@ -289,7 +297,9 @@ async def test_dhcp_match_hostname(hass: HomeAssistant) -> None: async def test_dhcp_match_macaddress(hass: HomeAssistant) -> None: """Test matching based on macaddress only.""" - integration_matchers = [{"domain": "mock-domain", "macaddress": "B8B7F1*"}] + integration_matchers = dhcp.async_index_integration_matchers( + [{"domain": "mock-domain", "macaddress": "B8B7F1*"}] + ) packet = Ether(RAW_DHCP_REQUEST) @@ -313,10 +323,12 @@ async def test_dhcp_match_macaddress(hass: HomeAssistant) -> None: async def test_dhcp_multiple_match_only_one_flow(hass: HomeAssistant) -> None: """Test matching the domain multiple times only generates one flow.""" - integration_matchers = [ - {"domain": "mock-domain", "macaddress": "B8B7F1*"}, - {"domain": "mock-domain", "hostname": "connect"}, - ] + integration_matchers = dhcp.async_index_integration_matchers( + [ + {"domain": "mock-domain", "macaddress": "B8B7F1*"}, + {"domain": "mock-domain", "hostname": "connect"}, + ] + ) packet = Ether(RAW_DHCP_REQUEST) @@ -340,7 +352,9 @@ async def test_dhcp_multiple_match_only_one_flow(hass: HomeAssistant) -> None: async def test_dhcp_match_macaddress_without_hostname(hass: HomeAssistant) -> None: """Test matching based on macaddress only.""" - integration_matchers = [{"domain": "mock-domain", "macaddress": "606BBD*"}] + integration_matchers = dhcp.async_index_integration_matchers( + [{"domain": "mock-domain", "macaddress": "606BBD*"}] + ) packet = Ether(RAW_DHCP_REQUEST_WITHOUT_HOSTNAME) @@ -364,7 +378,9 @@ async def test_dhcp_match_macaddress_without_hostname(hass: HomeAssistant) -> No async def test_dhcp_nomatch(hass: HomeAssistant) -> None: """Test not matching based on macaddress only.""" - integration_matchers = [{"domain": "mock-domain", "macaddress": "ABC123*"}] + integration_matchers = dhcp.async_index_integration_matchers( + [{"domain": "mock-domain", "macaddress": "ABC123*"}] + ) packet = Ether(RAW_DHCP_REQUEST) @@ -379,7 +395,9 @@ async def test_dhcp_nomatch(hass: HomeAssistant) -> None: async def test_dhcp_nomatch_hostname(hass: HomeAssistant) -> None: """Test not matching based on hostname only.""" - integration_matchers = [{"domain": "mock-domain", "hostname": "nomatch*"}] + integration_matchers = dhcp.async_index_integration_matchers( + [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) packet = Ether(RAW_DHCP_REQUEST) @@ -394,7 +412,9 @@ async def test_dhcp_nomatch_hostname(hass: HomeAssistant) -> None: async def test_dhcp_nomatch_non_dhcp_packet(hass: HomeAssistant) -> None: """Test matching does not throw on a non-dhcp packet.""" - integration_matchers = [{"domain": "mock-domain", "hostname": "nomatch*"}] + integration_matchers = dhcp.async_index_integration_matchers( + [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) packet = Ether(b"") @@ -409,7 +429,9 @@ async def test_dhcp_nomatch_non_dhcp_packet(hass: HomeAssistant) -> None: async def test_dhcp_nomatch_non_dhcp_request_packet(hass: HomeAssistant) -> None: """Test nothing happens with the wrong message-type.""" - integration_matchers = [{"domain": "mock-domain", "hostname": "nomatch*"}] + integration_matchers = dhcp.async_index_integration_matchers( + [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) packet = Ether(RAW_DHCP_REQUEST) @@ -433,7 +455,9 @@ async def test_dhcp_nomatch_non_dhcp_request_packet(hass: HomeAssistant) -> None async def test_dhcp_invalid_hostname(hass: HomeAssistant) -> None: """Test we ignore invalid hostnames.""" - integration_matchers = [{"domain": "mock-domain", "hostname": "nomatch*"}] + integration_matchers = dhcp.async_index_integration_matchers( + [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) packet = Ether(RAW_DHCP_REQUEST) @@ -457,7 +481,9 @@ async def test_dhcp_invalid_hostname(hass: HomeAssistant) -> None: async def test_dhcp_missing_hostname(hass: HomeAssistant) -> None: """Test we ignore missing hostnames.""" - integration_matchers = [{"domain": "mock-domain", "hostname": "nomatch*"}] + integration_matchers = dhcp.async_index_integration_matchers( + [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) packet = Ether(RAW_DHCP_REQUEST) @@ -481,7 +507,9 @@ async def test_dhcp_missing_hostname(hass: HomeAssistant) -> None: async def test_dhcp_invalid_option(hass: HomeAssistant) -> None: """Test we ignore invalid hostname option.""" - integration_matchers = [{"domain": "mock-domain", "hostname": "nomatch*"}] + integration_matchers = dhcp.async_index_integration_matchers( + [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) packet = Ether(RAW_DHCP_REQUEST) @@ -628,7 +656,15 @@ async def test_device_tracker_hostname_and_macaddress_exists_before_start( device_tracker_watcher = dhcp.DeviceTrackerWatcher( hass, {}, - [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + dhcp.async_index_integration_matchers( + [ + { + "domain": "mock-domain", + "hostname": "connect", + "macaddress": "B8B7F1*", + } + ] + ), ) await device_tracker_watcher.async_start() await hass.async_block_till_done() @@ -653,7 +689,15 @@ async def test_device_tracker_registered(hass: HomeAssistant) -> None: device_tracker_watcher = dhcp.DeviceTrackerRegisteredWatcher( hass, {}, - [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + dhcp.async_index_integration_matchers( + [ + { + "domain": "mock-domain", + "hostname": "connect", + "macaddress": "B8B7F1*", + } + ] + ), ) await device_tracker_watcher.async_start() await hass.async_block_till_done() @@ -684,7 +728,15 @@ async def test_device_tracker_registered_hostname_none(hass: HomeAssistant) -> N device_tracker_watcher = dhcp.DeviceTrackerRegisteredWatcher( hass, {}, - [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + dhcp.async_index_integration_matchers( + [ + { + "domain": "mock-domain", + "hostname": "connect", + "macaddress": "B8B7F1*", + } + ] + ), ) await device_tracker_watcher.async_start() await hass.async_block_till_done() @@ -709,7 +761,15 @@ async def test_device_tracker_hostname_and_macaddress_after_start( device_tracker_watcher = dhcp.DeviceTrackerWatcher( hass, {}, - [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + dhcp.async_index_integration_matchers( + [ + { + "domain": "mock-domain", + "hostname": "connect", + "macaddress": "B8B7F1*", + } + ] + ), ) await device_tracker_watcher.async_start() await hass.async_block_till_done() @@ -748,7 +808,15 @@ async def test_device_tracker_hostname_and_macaddress_after_start_not_home( device_tracker_watcher = dhcp.DeviceTrackerWatcher( hass, {}, - [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + dhcp.async_index_integration_matchers( + [ + { + "domain": "mock-domain", + "hostname": "connect", + "macaddress": "B8B7F1*", + } + ] + ), ) await device_tracker_watcher.async_start() await hass.async_block_till_done() @@ -877,7 +945,15 @@ async def test_device_tracker_ignore_self_assigned_ips_before_start( device_tracker_watcher = dhcp.DeviceTrackerWatcher( hass, {}, - [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + dhcp.async_index_integration_matchers( + [ + { + "domain": "mock-domain", + "hostname": "connect", + "macaddress": "B8B7F1*", + } + ] + ), ) await device_tracker_watcher.async_start() await hass.async_block_till_done() @@ -902,7 +978,15 @@ async def test_aiodiscover_finds_new_hosts(hass: HomeAssistant) -> None: device_tracker_watcher = dhcp.NetworkWatcher( hass, {}, - [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + dhcp.async_index_integration_matchers( + [ + { + "domain": "mock-domain", + "hostname": "connect", + "macaddress": "B8B7F1*", + } + ] + ), ) await device_tracker_watcher.async_start() await hass.async_block_till_done() @@ -953,13 +1037,15 @@ async def test_aiodiscover_does_not_call_again_on_shorter_hostname( device_tracker_watcher = dhcp.NetworkWatcher( hass, {}, - [ - { - "domain": "mock-domain", - "hostname": "irobot-*", - "macaddress": "B8B7F1*", - } - ], + dhcp.async_index_integration_matchers( + [ + { + "domain": "mock-domain", + "hostname": "irobot-*", + "macaddress": "B8B7F1*", + } + ] + ), ) await device_tracker_watcher.async_start() await hass.async_block_till_done() @@ -996,7 +1082,15 @@ async def test_aiodiscover_finds_new_hosts_after_interval(hass: HomeAssistant) - device_tracker_watcher = dhcp.NetworkWatcher( hass, {}, - [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + dhcp.async_index_integration_matchers( + [ + { + "domain": "mock-domain", + "hostname": "connect", + "macaddress": "B8B7F1*", + } + ] + ), ) await device_tracker_watcher.async_start() await hass.async_block_till_done() From 02fb60b33e62aafbf0a4258a7c74e9f4abb9130c Mon Sep 17 00:00:00 2001 From: Leah Oswald Date: Sun, 4 Feb 2024 23:56:12 +0100 Subject: [PATCH 0263/1367] Fix home connect remaining progress time (#109525) * fix remaining progress time for home connect component The home connect API is sending some default values (on dishwashers) for the remaining progress time after the program finished. This is a problem because this value is stored and on every API event, for example opening the door of a dishwasher, the value for remaining progress time is updated with this wrong value. So I see a wrong value the whole time the dishwasher is not running and therefore has no remaining progress time. This coming fixes this problem and adds a check if the appliance is in running, pause or finished state, because there we have valid data. In the other states the new code just returns none like on other edge cases. Now there is no value if there is no program running. * fix some formating according to the ruff rules * fix some formating according to the ruff rules again * fix alphabetic order of imports * add check if keys exist in dict before accessing them check if BSH_OPERATION_STATE and ATTR_VALUE key values exist before accessing them later in the elif statement * fix formating because forgotten local ruff run --- .../components/home_connect/const.py | 6 ++++- .../components/home_connect/sensor.py | 22 +++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/home_connect/const.py b/homeassistant/components/home_connect/const.py index 9eabc9b5d43..5b0a9e3e9d8 100644 --- a/homeassistant/components/home_connect/const.py +++ b/homeassistant/components/home_connect/const.py @@ -10,10 +10,14 @@ BSH_POWER_ON = "BSH.Common.EnumType.PowerState.On" BSH_POWER_OFF = "BSH.Common.EnumType.PowerState.Off" BSH_POWER_STANDBY = "BSH.Common.EnumType.PowerState.Standby" BSH_ACTIVE_PROGRAM = "BSH.Common.Root.ActiveProgram" -BSH_OPERATION_STATE = "BSH.Common.Status.OperationState" BSH_REMOTE_CONTROL_ACTIVATION_STATE = "BSH.Common.Status.RemoteControlActive" BSH_REMOTE_START_ALLOWANCE_STATE = "BSH.Common.Status.RemoteControlStartAllowed" +BSH_OPERATION_STATE = "BSH.Common.Status.OperationState" +BSH_OPERATION_STATE_RUN = "BSH.Common.EnumType.OperationState.Run" +BSH_OPERATION_STATE_PAUSE = "BSH.Common.EnumType.OperationState.Pause" +BSH_OPERATION_STATE_FINISHED = "BSH.Common.EnumType.OperationState.Finished" + COOKING_LIGHTING = "Cooking.Common.Setting.Lighting" COOKING_LIGHTING_BRIGHTNESS = "Cooking.Common.Setting.LightingBrightness" diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index 07edfb4bd4b..a01cae5862a 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -10,7 +10,14 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import ATTR_VALUE, BSH_OPERATION_STATE, DOMAIN +from .const import ( + ATTR_VALUE, + BSH_OPERATION_STATE, + BSH_OPERATION_STATE_FINISHED, + BSH_OPERATION_STATE_PAUSE, + BSH_OPERATION_STATE_RUN, + DOMAIN, +) from .entity import HomeConnectEntity _LOGGER = logging.getLogger(__name__) @@ -69,9 +76,20 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): # if the date is supposed to be in the future but we're # already past it, set state to None. self._attr_native_value = None - else: + elif ( + BSH_OPERATION_STATE in status + and ATTR_VALUE in status[BSH_OPERATION_STATE] + and status[BSH_OPERATION_STATE][ATTR_VALUE] + in [ + BSH_OPERATION_STATE_RUN, + BSH_OPERATION_STATE_PAUSE, + BSH_OPERATION_STATE_FINISHED, + ] + ): seconds = self._sign * float(status[self._key][ATTR_VALUE]) self._attr_native_value = dt_util.utcnow() + timedelta(seconds=seconds) + else: + self._attr_native_value = None else: self._attr_native_value = status[self._key].get(ATTR_VALUE) if self._key == BSH_OPERATION_STATE: From 3abc48b7c1444611f7b9c173b4d4959f29fae7a9 Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Sun, 4 Feb 2024 15:58:44 -0700 Subject: [PATCH 0264/1367] Add icons for fan preset modes (#109334) Co-authored-by: Franck Nijhof --- homeassistant/components/demo/fan.py | 1 + homeassistant/components/demo/icons.json | 15 +++++++++++++++ homeassistant/components/demo/strings.json | 14 ++++++++++++++ homeassistant/components/fan/icons.json | 4 ++++ 4 files changed, 34 insertions(+) diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 73cae4a64b1..644c4cb7860 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -90,6 +90,7 @@ class BaseDemoFan(FanEntity): """A demonstration fan component that uses legacy fan speeds.""" _attr_should_poll = False + _attr_translation_key = "demo" def __init__( self, diff --git a/homeassistant/components/demo/icons.json b/homeassistant/components/demo/icons.json index 79c18bc0a2e..9c746c633d4 100644 --- a/homeassistant/components/demo/icons.json +++ b/homeassistant/components/demo/icons.json @@ -23,6 +23,21 @@ } } }, + "fan": { + "demo": { + "state_attributes": { + "preset_mode": { + "default": "mdi:circle-medium", + "state": { + "auto": "mdi:fan-auto", + "sleep": "mdi:bed", + "smart": "mdi:brain", + "on": "mdi:power" + } + } + } + } + }, "number": { "volume": { "default": "mdi:volume-high" diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json index 555760a5af9..aa5554e9fcc 100644 --- a/homeassistant/components/demo/strings.json +++ b/homeassistant/components/demo/strings.json @@ -46,6 +46,20 @@ } } }, + "fan": { + "demo": { + "state_attributes": { + "preset_mode": { + "state": { + "auto": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::auto%]", + "sleep": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::sleep%]", + "smart": "Smart", + "on": "[%key:common::state::on%]" + } + } + } + } + }, "event": { "push": { "state_attributes": { diff --git a/homeassistant/components/fan/icons.json b/homeassistant/components/fan/icons.json index ebc4988e87f..f962d1e7c1a 100644 --- a/homeassistant/components/fan/icons.json +++ b/homeassistant/components/fan/icons.json @@ -11,6 +11,10 @@ "state": { "reverse": "mdi:rotate-left" } + }, + "preset_mode": { + "default": "mdi:circle-medium", + "state": {} } } } From 3def42726a977ef1327219bb9185d50eaea71918 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Mon, 5 Feb 2024 10:27:49 +1100 Subject: [PATCH 0265/1367] Bump georss-generic-client to 0.8 (#109658) --- homeassistant/components/geo_rss_events/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index bdf8f126680..17640e37278 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", "iot_class": "cloud_polling", "loggers": ["georss_client", "georss_generic_client"], - "requirements": ["georss-generic-client==0.6"] + "requirements": ["georss-generic-client==0.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3cda8a4f523..dbcf81495e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -905,7 +905,7 @@ geocachingapi==0.2.1 geopy==2.3.0 # homeassistant.components.geo_rss_events -georss-generic-client==0.6 +georss-generic-client==0.8 # homeassistant.components.ign_sismologia georss-ign-sismologia-client==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 086133cbb25..851db4ed511 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -731,7 +731,7 @@ geocachingapi==0.2.1 geopy==2.3.0 # homeassistant.components.geo_rss_events -georss-generic-client==0.6 +georss-generic-client==0.8 # homeassistant.components.ign_sismologia georss-ign-sismologia-client==0.6 From a95a51da058a67ef5b84b4e8326287bb309f3fca Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Mon, 5 Feb 2024 00:30:47 +0100 Subject: [PATCH 0266/1367] Add Xiaomi-ble remotes and dimmers (#109327) --- .../components/xiaomi_ble/binary_sensor.py | 8 ++ homeassistant/components/xiaomi_ble/const.py | 9 ++ .../components/xiaomi_ble/device_trigger.py | 92 +++++++++++++++++++ homeassistant/components/xiaomi_ble/event.py | 23 +++++ .../components/xiaomi_ble/manifest.json | 2 +- .../components/xiaomi_ble/strings.json | 57 +++++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../xiaomi_ble/test_binary_sensor.py | 31 +++++++ .../xiaomi_ble/test_device_trigger.py | 26 ++++++ tests/components/xiaomi_ble/test_event.py | 77 ++++++++++++++++ 11 files changed, 324 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index 2894b8d2f3f..cd6f7b453bb 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -29,6 +29,10 @@ from .coordinator import ( from .device import device_key_to_bluetooth_entity_key BINARY_SENSOR_DESCRIPTIONS = { + XiaomiBinarySensorDeviceClass.BATTERY: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.BATTERY, + device_class=BinarySensorDeviceClass.BATTERY, + ), XiaomiBinarySensorDeviceClass.DOOR: BinarySensorEntityDescription( key=XiaomiBinarySensorDeviceClass.DOOR, device_class=BinarySensorDeviceClass.DOOR, @@ -49,6 +53,10 @@ BINARY_SENSOR_DESCRIPTIONS = { key=XiaomiBinarySensorDeviceClass.OPENING, device_class=BinarySensorDeviceClass.OPENING, ), + XiaomiBinarySensorDeviceClass.POWER: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.POWER, + device_class=BinarySensorDeviceClass.POWER, + ), XiaomiBinarySensorDeviceClass.SMOKE: BinarySensorEntityDescription( key=XiaomiBinarySensorDeviceClass.SMOKE, device_class=BinarySensorDeviceClass.SMOKE, diff --git a/homeassistant/components/xiaomi_ble/const.py b/homeassistant/components/xiaomi_ble/const.py index 1accfd9dc55..5f9dea9eb45 100644 --- a/homeassistant/components/xiaomi_ble/const.py +++ b/homeassistant/components/xiaomi_ble/const.py @@ -19,14 +19,23 @@ EVENT_PROPERTIES: Final = "event_properties" XIAOMI_BLE_EVENT: Final = "xiaomi_ble_event" EVENT_CLASS_BUTTON: Final = "button" +EVENT_CLASS_DIMMER: Final = "dimmer" EVENT_CLASS_MOTION: Final = "motion" +EVENT_CLASS_CUBE: Final = "cube" BUTTON: Final = "button" +CUBE: Final = "cube" +DIMMER: Final = "dimmer" DOUBLE_BUTTON: Final = "double_button" TRIPPLE_BUTTON: Final = "tripple_button" +REMOTE: Final = "remote" +REMOTE_FAN: Final = "remote_fan" +REMOTE_VENFAN: Final = "remote_ventilator_fan" +REMOTE_BATHROOM: Final = "remote_bathroom" MOTION: Final = "motion" BUTTON_PRESS: Final = "button_press" +BUTTON_PRESS_LONG: Final = "button_press_long" BUTTON_PRESS_DOUBLE_LONG: Final = "button_press_double_long" DOUBLE_BUTTON_PRESS_DOUBLE_LONG: Final = "double_button_press_double_long" TRIPPLE_BUTTON_PRESS_DOUBLE_LONG: Final = "tripple_button_press_double_long" diff --git a/homeassistant/components/xiaomi_ble/device_trigger.py b/homeassistant/components/xiaomi_ble/device_trigger.py index 6d29af9ac11..8d281ddc8a9 100644 --- a/homeassistant/components/xiaomi_ble/device_trigger.py +++ b/homeassistant/components/xiaomi_ble/device_trigger.py @@ -24,16 +24,25 @@ from .const import ( BUTTON, BUTTON_PRESS, BUTTON_PRESS_DOUBLE_LONG, + BUTTON_PRESS_LONG, CONF_SUBTYPE, + CUBE, + DIMMER, DOMAIN, DOUBLE_BUTTON, DOUBLE_BUTTON_PRESS_DOUBLE_LONG, EVENT_CLASS, EVENT_CLASS_BUTTON, + EVENT_CLASS_CUBE, + EVENT_CLASS_DIMMER, EVENT_CLASS_MOTION, EVENT_TYPE, MOTION, MOTION_DEVICE, + REMOTE, + REMOTE_BATHROOM, + REMOTE_FAN, + REMOTE_VENFAN, TRIPPLE_BUTTON, TRIPPLE_BUTTON_PRESS_DOUBLE_LONG, XIAOMI_BLE_EVENT, @@ -41,14 +50,61 @@ from .const import ( TRIGGERS_BY_TYPE = { BUTTON_PRESS: ["press"], + BUTTON_PRESS_LONG: ["press", "long_press"], BUTTON_PRESS_DOUBLE_LONG: ["press", "double_press", "long_press"], + CUBE: ["rotate_left", "rotate_right"], + DIMMER: [ + "press", + "long_press", + "rotate_left", + "rotate_right", + "rotate_left_pressed", + "rotate_right_pressed", + ], MOTION_DEVICE: ["motion_detected"], } EVENT_TYPES = { BUTTON: ["button"], + CUBE: ["cube"], + DIMMER: ["dimmer"], DOUBLE_BUTTON: ["button_left", "button_right"], TRIPPLE_BUTTON: ["button_left", "button_middle", "button_right"], + REMOTE: [ + "button_on", + "button_off", + "button_brightness", + "button_plus", + "button_min", + "button_m", + ], + REMOTE_BATHROOM: [ + "button_heat", + "button_air_exchange", + "button_dry", + "button_fan", + "button_swing", + "button_decrease_speed", + "button_increase_speed", + "button_stop", + "button_light", + ], + REMOTE_FAN: [ + "button_fan", + "button_light", + "button_wind_speed", + "button_wind_mode", + "button_brightness", + "button_color_temperature", + ], + REMOTE_VENFAN: [ + "button_swing", + "button_power", + "button_timer_30_minutes", + "button_timer_60_minutes", + "button_increase_wind_speed", + "button_decrease_wind_speed", + ], MOTION: ["motion"], } @@ -78,11 +134,41 @@ TRIGGER_MODEL_DATA = { event_types=EVENT_TYPES[DOUBLE_BUTTON], triggers=TRIGGERS_BY_TYPE[BUTTON_PRESS_DOUBLE_LONG], ), + CUBE: TriggerModelData( + event_class=EVENT_CLASS_CUBE, + event_types=EVENT_TYPES[CUBE], + triggers=TRIGGERS_BY_TYPE[CUBE], + ), + DIMMER: TriggerModelData( + event_class=EVENT_CLASS_DIMMER, + event_types=EVENT_TYPES[DIMMER], + triggers=TRIGGERS_BY_TYPE[DIMMER], + ), TRIPPLE_BUTTON_PRESS_DOUBLE_LONG: TriggerModelData( event_class=EVENT_CLASS_BUTTON, event_types=EVENT_TYPES[TRIPPLE_BUTTON], triggers=TRIGGERS_BY_TYPE[BUTTON_PRESS_DOUBLE_LONG], ), + REMOTE: TriggerModelData( + event_class=EVENT_CLASS_BUTTON, + event_types=EVENT_TYPES[REMOTE], + triggers=TRIGGERS_BY_TYPE[BUTTON_PRESS_LONG], + ), + REMOTE_BATHROOM: TriggerModelData( + event_class=EVENT_CLASS_BUTTON, + event_types=EVENT_TYPES[REMOTE_BATHROOM], + triggers=TRIGGERS_BY_TYPE[BUTTON_PRESS_LONG], + ), + REMOTE_FAN: TriggerModelData( + event_class=EVENT_CLASS_BUTTON, + event_types=EVENT_TYPES[REMOTE_FAN], + triggers=TRIGGERS_BY_TYPE[BUTTON_PRESS_LONG], + ), + REMOTE_VENFAN: TriggerModelData( + event_class=EVENT_CLASS_BUTTON, + event_types=EVENT_TYPES[REMOTE_VENFAN], + triggers=TRIGGERS_BY_TYPE[BUTTON_PRESS_LONG], + ), MOTION_DEVICE: TriggerModelData( event_class=EVENT_CLASS_MOTION, event_types=EVENT_TYPES[MOTION], @@ -103,7 +189,13 @@ MODEL_DATA = { "XMWXKG01YL": TRIGGER_MODEL_DATA[DOUBLE_BUTTON_PRESS_DOUBLE_LONG], "K9B-2BTN": TRIGGER_MODEL_DATA[DOUBLE_BUTTON_PRESS_DOUBLE_LONG], "K9B-3BTN": TRIGGER_MODEL_DATA[TRIPPLE_BUTTON_PRESS_DOUBLE_LONG], + "YLYK01YL": TRIGGER_MODEL_DATA[REMOTE], + "YLYK01YL-FANRC": TRIGGER_MODEL_DATA[REMOTE_FAN], + "YLYK01YL-VENFAN": TRIGGER_MODEL_DATA[REMOTE_VENFAN], + "YLYK01YL-BHFRC": TRIGGER_MODEL_DATA[REMOTE_BATHROOM], "MUE4094RT": TRIGGER_MODEL_DATA[MOTION_DEVICE], + "XMMF01JQD": TRIGGER_MODEL_DATA[CUBE], + "YLKG07YL/YLKG08YL": TRIGGER_MODEL_DATA[DIMMER], } diff --git a/homeassistant/components/xiaomi_ble/event.py b/homeassistant/components/xiaomi_ble/event.py index 1d5b08fb8f9..2c1550dc5d7 100644 --- a/homeassistant/components/xiaomi_ble/event.py +++ b/homeassistant/components/xiaomi_ble/event.py @@ -18,6 +18,8 @@ from . import format_discovered_event_class, format_event_dispatcher_name from .const import ( DOMAIN, EVENT_CLASS_BUTTON, + EVENT_CLASS_CUBE, + EVENT_CLASS_DIMMER, EVENT_CLASS_MOTION, EVENT_PROPERTIES, EVENT_TYPE, @@ -36,10 +38,31 @@ DESCRIPTIONS_BY_EVENT_CLASS = { ], device_class=EventDeviceClass.BUTTON, ), + EVENT_CLASS_CUBE: EventEntityDescription( + key=EVENT_CLASS_CUBE, + translation_key="cube", + event_types=[ + "rotate_left", + "rotate_right", + ], + ), + EVENT_CLASS_DIMMER: EventEntityDescription( + key=EVENT_CLASS_DIMMER, + translation_key="dimmer", + event_types=[ + "press", + "long_press", + "rotate_left", + "rotate_right", + "rotate_left_pressed", + "rotate_right_pressed", + ], + ), EVENT_CLASS_MOTION: EventEntityDescription( key=EVENT_CLASS_MOTION, translation_key="motion", event_types=["motion_detected"], + device_class=EventDeviceClass.MOTION, ), } diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index f11b2426f96..a380ecb8e94 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==0.23.1"] + "requirements": ["xiaomi-ble==0.25.2"] } diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index c7cbe43bd94..d2511869580 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -44,14 +44,43 @@ "press": "Press", "double_press": "Double Press", "long_press": "Long Press", - "motion_detected": "Motion Detected" + "motion_detected": "Motion Detected", + "rotate_left": "Rotate Left", + "rotate_right": "Rotate Right", + "rotate_left_pressed": "Rotate Left (Pressed)", + "rotate_right_pressed": "Rotate Right (Pressed)" }, "trigger_type": { "button": "Button \"{subtype}\"", "button_left": "Button Left \"{subtype}\"", "button_middle": "Button Middle \"{subtype}\"", "button_right": "Button Right \"{subtype}\"", - "motion": "{subtype}" + "button_on": "Button On \"{subtype}\"", + "button_off": "Button Off \"{subtype}\"", + "button_brightness": "Button Brightness \"{subtype}\"", + "button_plus": "Button Plus \"{subtype}\"", + "button_min": "Button Min \"{subtype}\"", + "button_m": "Button M \"{subtype}\"", + "button_heat": "Button Heat \"{subtype}\"", + "button_air_exchange": "Button Air Exchange \"{subtype}\"", + "button_dry": "Button Dry \"{subtype}\"", + "button_fan": "Button Fan \"{subtype}\"", + "button_swing": "Button Swing \"{subtype}\"", + "button_decrease_speed": "Button Decrease Speed \"{subtype}\"", + "button_increase_speed": "Button Inrease Speed \"{subtype}\"", + "button_stop": "Button Stop \"{subtype}\"", + "button_light": "Button Light \"{subtype}\"", + "button_wind_speed": "Button Wind Speed \"{subtype}\"", + "button_wind_mode": "Button Wind Mode \"{subtype}\"", + "button_color_temperature": "Button Color Temperature \"{subtype}\"", + "button_power": "Button Power \"{subtype}\"", + "button_timer_30_minutes": "Button Timer 30 Minutes \"{subtype}\"", + "button_timer_60_minutes": "Button Timer 30 Minutes \"{subtype}\"", + "button_increase_wind_speed": "Button Increase Wind Speed \"{subtype}\"", + "button_decrease_wind_speed": "Button Decrease Wind Speed \"{subtype}\"", + "dimmer": "{subtype}", + "motion": "{subtype}", + "cube": "{subtype}" } }, "entity": { @@ -67,6 +96,30 @@ } } }, + "cube": { + "state_attributes": { + "event_type": { + "state": { + "rotate_left": "Rotate left", + "rotate_right": "Rotate right" + } + } + } + }, + "dimmer": { + "state_attributes": { + "event_type": { + "state": { + "press": "Press", + "long_press": "Long press", + "rotate_left": "Rotate left", + "rotate_right": "Rotate right", + "rotate_left_pressed": "Rotate left (pressed)", + "rotate_right_pressed": "Rotate left (pressed)" + } + } + } + }, "motion": { "state_attributes": { "event_type": { diff --git a/requirements_all.txt b/requirements_all.txt index dbcf81495e7..e8762b76c30 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2853,7 +2853,7 @@ wyoming==1.5.2 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.23.1 +xiaomi-ble==0.25.2 # homeassistant.components.knx xknx==2.11.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 851db4ed511..a6057f4bf7f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2182,7 +2182,7 @@ wyoming==1.5.2 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.23.1 +xiaomi-ble==0.25.2 # homeassistant.components.knx xknx==2.11.2 diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index 14ea3e44af8..b9e0b24a3cf 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -262,6 +262,37 @@ async def test_smoke(hass: HomeAssistant) -> None: await hass.async_block_till_done() +async def test_power(hass: HomeAssistant) -> None: + """Test setting up a power binary sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="F8:24:41:E9:50:74", + ) + 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( + "F8:24:41:E9:50:74", + b"P0S\x01?tP\xe9A$\xf8\x01\x10\x03\x01\x00\x00", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + power_sensor = hass.states.get("binary_sensor.remote_control_5074_power") + power_sensor_attribtes = power_sensor.attributes + assert power_sensor.state == STATE_OFF + assert power_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Remote Control 5074 Power" + + 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() diff --git a/tests/components/xiaomi_ble/test_device_trigger.py b/tests/components/xiaomi_ble/test_device_trigger.py index 31f896680bf..28195f784e8 100644 --- a/tests/components/xiaomi_ble/test_device_trigger.py +++ b/tests/components/xiaomi_ble/test_device_trigger.py @@ -97,6 +97,32 @@ async def test_event_motion_detected(hass: HomeAssistant) -> None: await hass.async_block_till_done() +async def test_event_dimmer_rotate(hass: HomeAssistant) -> None: + """Make sure that a dimmer rotate event is fired.""" + mac = "F8:24:41:C5:98:8B" + data = {"bindkey": "b853075158487ca39a5b5ea9"} + entry = await _async_setup_xiaomi_device(hass, mac, data) + events = async_capture_events(hass, "xiaomi_ble_event") + + # Emit dimmer rotate left with 3 steps event + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + mac, b"X0\xb6\x036\x8b\x98\xc5A$\xf8\x8b\xb8\xf2f" b"\x13Q\x00\x00\x00\xd6" + ), + ) + + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data["address"] == "F8:24:41:C5:98:8B" + assert events[0].data["event_type"] == "rotate_left" + assert events[0].data["event_properties"] == {"steps": 1} + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + async def test_get_triggers_button(hass: HomeAssistant) -> None: """Test that we get the expected triggers from a Xiaomi BLE button sensor.""" mac = "54:EF:44:E3:9C:BC" diff --git a/tests/components/xiaomi_ble/test_event.py b/tests/components/xiaomi_ble/test_event.py index 1d2cf5fb3fc..a0e84c0ac2e 100644 --- a/tests/components/xiaomi_ble/test_event.py +++ b/tests/components/xiaomi_ble/test_event.py @@ -49,6 +49,36 @@ from tests.components.bluetooth import ( } ], ), + ( + "F8:24:41:E9:50:74", + make_advertisement( + "F8:24:41:E9:50:74", + b"P0S\x01?tP\xe9A$\xf8\x01\x10\x03\x04\x00\x02", + ), + None, + [ + { + "entity": "event.remote_control_5074_button_m", + ATTR_FRIENDLY_NAME: "Remote Control 5074 Button M", + ATTR_EVENT_TYPE: "long_press", + } + ], + ), + ( + "F8:24:41:E9:50:74", + make_advertisement( + "F8:24:41:E9:50:74", + b"P0S\x01?tP\xe9A$\xf8\x01\x10\x03\x03\x00\x00", + ), + None, + [ + { + "entity": "event.remote_control_5074_button_plus", + ATTR_FRIENDLY_NAME: "Remote Control 5074 Button plus", + ATTR_EVENT_TYPE: "press", + } + ], + ), ( "DE:70:E8:B2:39:0C", make_advertisement( @@ -64,6 +94,53 @@ from tests.components.bluetooth import ( } ], ), + ( + "E2:53:30:E6:D3:54", + make_advertisement( + "E2:53:30:E6:D3:54", + b"P0\xe1\x04\x8eT\xd3\xe60S\xe2\x01\x10\x03\x01\x00\x00", + ), + None, + [ + { + "entity": "event.magic_cube_d354_cube", + ATTR_FRIENDLY_NAME: "Magic Cube D354 Cube", + ATTR_EVENT_TYPE: "rotate_left", + } + ], + ), + ( + "F8:24:41:C5:98:8B", + make_advertisement( + "F8:24:41:C5:98:8B", + b"X0\xb6\x036\x8b\x98\xc5A$\xf8\x8b\xb8\xf2f" b"\x13Q\x00\x00\x00\xd6", + ), + "b853075158487ca39a5b5ea9", + [ + { + "entity": "event.dimmer_switch_988b_dimmer", + ATTR_FRIENDLY_NAME: "Dimmer Switch 988B Dimmer", + ATTR_EVENT_TYPE: "rotate_left", + "event_properties": {"steps": 1}, + } + ], + ), + ( + "F8:24:41:C5:98:8B", + make_advertisement( + "F8:24:41:C5:98:8B", + b"X0\xb6\x03\xd2\x8b\x98\xc5A$\xf8\xc3I\x14vu~\x00\x00\x00\x99", + ), + "b853075158487ca39a5b5ea9", + [ + { + "entity": "event.dimmer_switch_988b_dimmer", + ATTR_FRIENDLY_NAME: "Dimmer Switch 988B Dimmer", + ATTR_EVENT_TYPE: "press", + "event_properties": {"duration": 2}, + } + ], + ), ], ) async def test_events( From ffe9f0825a42124f466c44deb6de7bfffdeb75ed Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 5 Feb 2024 00:42:07 +0100 Subject: [PATCH 0267/1367] Add zone related sensors in proximity (#109630) * move legacy needed convertions into legacy entity * add zone related sensors * fix test coverage * fix typing * fix entity name translations * rename placeholder to tracked_entity --- .../components/proximity/__init__.py | 19 ++- homeassistant/components/proximity/const.py | 2 + .../components/proximity/coordinator.py | 19 ++- homeassistant/components/proximity/sensor.py | 39 ++++-- .../components/proximity/strings.json | 15 ++- .../proximity/snapshots/test_diagnostics.ambr | 4 +- tests/components/proximity/test_init.py | 126 ++++++++++++++++++ 7 files changed, 193 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index 3f28028d703..349658223f3 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import cast import voluptuous as vol @@ -11,6 +12,7 @@ from homeassistant.const import ( CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_ZONE, + STATE_UNKNOWN, Platform, ) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant @@ -203,16 +205,21 @@ class Proximity(CoordinatorEntity[ProximityDataUpdateCoordinator]): self._attr_unit_of_measurement = self.coordinator.unit_of_measurement @property - def state(self) -> str | int | float: + def data(self) -> dict[str, str | int | None]: + """Get data from coordinator.""" + return self.coordinator.data.proximity + + @property + def state(self) -> str | float: """Return the state.""" - return self.coordinator.data.proximity[ATTR_DIST_TO] + if isinstance(distance := self.data[ATTR_DIST_TO], str): + return distance + return self.coordinator.convert_legacy(cast(int, distance)) @property def extra_state_attributes(self) -> dict[str, str]: """Return the state attributes.""" return { - ATTR_DIR_OF_TRAVEL: str( - self.coordinator.data.proximity[ATTR_DIR_OF_TRAVEL] - ), - ATTR_NEAREST: str(self.coordinator.data.proximity[ATTR_NEAREST]), + ATTR_DIR_OF_TRAVEL: str(self.data[ATTR_DIR_OF_TRAVEL] or STATE_UNKNOWN), + ATTR_NEAREST: str(self.data[ATTR_NEAREST]), } diff --git a/homeassistant/components/proximity/const.py b/homeassistant/components/proximity/const.py index 7627d550e1f..e5b384b2f70 100644 --- a/homeassistant/components/proximity/const.py +++ b/homeassistant/components/proximity/const.py @@ -9,6 +9,8 @@ ATTR_DIST_TO: Final = "dist_to_zone" ATTR_ENTITIES_DATA: Final = "entities_data" ATTR_IN_IGNORED_ZONE: Final = "is_in_ignored_zone" ATTR_NEAREST: Final = "nearest" +ATTR_NEAREST_DIR_OF_TRAVEL: Final = "nearest_dir_of_travel" +ATTR_NEAREST_DIST_TO: Final = "nearest_dist_to_zone" ATTR_PROXIMITY_DATA: Final = "proximity_data" CONF_IGNORED_ZONES = "ignored_zones" diff --git a/homeassistant/components/proximity/coordinator.py b/homeassistant/components/proximity/coordinator.py index 53c1180e832..047ab1b6b3a 100644 --- a/homeassistant/components/proximity/coordinator.py +++ b/homeassistant/components/proximity/coordinator.py @@ -3,6 +3,7 @@ from collections import defaultdict from dataclasses import dataclass import logging +from typing import cast from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.config_entries import ConfigEntry @@ -52,11 +53,11 @@ class StateChangedData: class ProximityData: """ProximityCoordinatorData class.""" - proximity: dict[str, str | float] + proximity: dict[str, str | int | None] entities: dict[str, dict[str, str | int | None]] -DEFAULT_PROXIMITY_DATA: dict[str, str | float] = { +DEFAULT_PROXIMITY_DATA: dict[str, str | int | None] = { ATTR_DIST_TO: DEFAULT_DIST_TO_ZONE, ATTR_DIR_OF_TRAVEL: DEFAULT_DIR_OF_TRAVEL, ATTR_NEAREST: DEFAULT_NEAREST, @@ -130,7 +131,7 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]): }, ) - def _convert(self, value: float | str) -> float | str: + def convert_legacy(self, value: float | str) -> float | str: """Round and convert given distance value.""" if isinstance(value, str): return value @@ -303,7 +304,7 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]): ) # takeover data for legacy proximity entity - proximity_data: dict[str, str | float] = { + proximity_data: dict[str, str | int | None] = { ATTR_DIST_TO: DEFAULT_DIST_TO_ZONE, ATTR_DIR_OF_TRAVEL: DEFAULT_DIR_OF_TRAVEL, ATTR_NEAREST: DEFAULT_NEAREST, @@ -318,28 +319,26 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]): _LOGGER.debug("set first entity_data: %s", entity_data) proximity_data = { ATTR_DIST_TO: distance_to, - ATTR_DIR_OF_TRAVEL: entity_data[ATTR_DIR_OF_TRAVEL] or "unknown", + ATTR_DIR_OF_TRAVEL: entity_data[ATTR_DIR_OF_TRAVEL], ATTR_NEAREST: str(entity_data[ATTR_NAME]), } continue - if float(nearest_distance_to) > float(distance_to): + if cast(int, nearest_distance_to) > int(distance_to): _LOGGER.debug("set closer entity_data: %s", entity_data) proximity_data = { ATTR_DIST_TO: distance_to, - ATTR_DIR_OF_TRAVEL: entity_data[ATTR_DIR_OF_TRAVEL] or "unknown", + ATTR_DIR_OF_TRAVEL: entity_data[ATTR_DIR_OF_TRAVEL], ATTR_NEAREST: str(entity_data[ATTR_NAME]), } continue - if float(nearest_distance_to) == float(distance_to): + if cast(int, nearest_distance_to) == int(distance_to): _LOGGER.debug("set equally close entity_data: %s", entity_data) proximity_data[ ATTR_NEAREST ] = f"{proximity_data[ATTR_NEAREST]}, {str(entity_data[ATTR_NAME])}" - proximity_data[ATTR_DIST_TO] = self._convert(proximity_data[ATTR_DIST_TO]) - return ProximityData(proximity_data, entities_data) def _create_removed_tracked_entity_issue(self, entity_id: str) -> None: diff --git a/homeassistant/components/proximity/sensor.py b/homeassistant/components/proximity/sensor.py index 4b1e1d1f29d..c000aa27683 100644 --- a/homeassistant/components/proximity/sensor.py +++ b/homeassistant/components/proximity/sensor.py @@ -17,38 +17,53 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTR_DIR_OF_TRAVEL, ATTR_DIST_TO, ATTR_NEAREST, DOMAIN +from .const import ( + ATTR_DIR_OF_TRAVEL, + ATTR_DIST_TO, + ATTR_NEAREST, + ATTR_NEAREST_DIR_OF_TRAVEL, + ATTR_NEAREST_DIST_TO, + DOMAIN, +) from .coordinator import ProximityDataUpdateCoordinator +DIRECTIONS = ["arrived", "away_from", "stationary", "towards"] + SENSORS_PER_ENTITY: list[SensorEntityDescription] = [ SensorEntityDescription( key=ATTR_DIST_TO, - name="Distance", + translation_key=ATTR_DIST_TO, device_class=SensorDeviceClass.DISTANCE, native_unit_of_measurement=UnitOfLength.METERS, ), SensorEntityDescription( key=ATTR_DIR_OF_TRAVEL, - name="Direction of travel", translation_key=ATTR_DIR_OF_TRAVEL, icon="mdi:compass-outline", device_class=SensorDeviceClass.ENUM, - options=[ - "arrived", - "away_from", - "stationary", - "towards", - ], + options=DIRECTIONS, ), ] SENSORS_PER_PROXIMITY: list[SensorEntityDescription] = [ SensorEntityDescription( key=ATTR_NEAREST, - name="Nearest", translation_key=ATTR_NEAREST, icon="mdi:near-me", ), + SensorEntityDescription( + key=ATTR_DIST_TO, + translation_key=ATTR_NEAREST_DIST_TO, + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.METERS, + ), + SensorEntityDescription( + key=ATTR_DIR_OF_TRAVEL, + translation_key=ATTR_NEAREST_DIR_OF_TRAVEL, + icon="mdi:compass-outline", + device_class=SensorDeviceClass.ENUM, + options=DIRECTIONS, + ), ] @@ -151,8 +166,10 @@ class ProximityTrackedEntitySensor( self.tracked_entity_id = tracked_entity_descriptor.entity_id self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{tracked_entity_descriptor.identifier}_{description.key}" - self._attr_name = f"{self.tracked_entity_id.split('.')[-1]} {description.name}" self._attr_device_info = _device_info(coordinator) + self._attr_translation_placeholders = { + "tracked_entity": self.tracked_entity_id.split(".")[-1] + } async def async_added_to_hass(self) -> None: """Register entity mapping.""" diff --git a/homeassistant/components/proximity/strings.json b/homeassistant/components/proximity/strings.json index f52f3d03516..72c95eeeeae 100644 --- a/homeassistant/components/proximity/strings.json +++ b/homeassistant/components/proximity/strings.json @@ -32,7 +32,7 @@ "entity": { "sensor": { "dir_of_travel": { - "name": "Direction of travel", + "name": "{tracked_entity} Direction of travel", "state": { "arrived": "Arrived", "away_from": "Away from", @@ -40,7 +40,18 @@ "towards": "Towards" } }, - "nearest": { "name": "Nearest device" } + "dist_to_zone": { "name": "{tracked_entity} Distance" }, + "nearest": { "name": "Nearest device" }, + "nearest_dir_of_travel": { + "name": "Nearest direction of travel", + "state": { + "arrived": "Arrived", + "away_from": "Away from", + "stationary": "Stationary", + "towards": "Towards" + } + }, + "nearest_dist_to_zone": { "name": "Nearest distance" } } }, "issues": { diff --git a/tests/components/proximity/snapshots/test_diagnostics.ambr b/tests/components/proximity/snapshots/test_diagnostics.ambr index a93ff33f443..68270dc3297 100644 --- a/tests/components/proximity/snapshots/test_diagnostics.ambr +++ b/tests/components/proximity/snapshots/test_diagnostics.ambr @@ -41,8 +41,8 @@ ]), }), 'proximity': dict({ - 'dir_of_travel': 'unknown', - 'dist_to_zone': 2219, + 'dir_of_travel': None, + 'dist_to_zone': 2218752, 'nearest': 'test1', }), 'tracked_states': dict({ diff --git a/tests/components/proximity/test_init.py b/tests/components/proximity/test_init.py index 059ba2658ee..bce4c319ce0 100644 --- a/tests/components/proximity/test_init.py +++ b/tests/components/proximity/test_init.py @@ -907,6 +907,132 @@ async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone( assert state.state == "away_from" +async def test_nearest_sensors(hass: HomeAssistant, config_zones) -> None: + """Test for nearest sensors.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + title="home", + data={ + CONF_ZONE: "zone.home", + CONF_TRACKED_ENTITIES: ["device_tracker.test1", "device_tracker.test2"], + CONF_IGNORED_ZONES: [], + CONF_TOLERANCE: 1, + }, + unique_id=f"{DOMAIN}_home", + ) + + mock_config.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config.entry_id) + await hass.async_block_till_done() + + hass.states.async_set( + "device_tracker.test1", + "not_home", + {"friendly_name": "test1", "latitude": 20, "longitude": 10}, + ) + hass.states.async_set( + "device_tracker.test2", + "not_home", + {"friendly_name": "test2", "latitude": 40, "longitude": 20}, + ) + await hass.async_block_till_done() + + hass.states.async_set( + "device_tracker.test1", + "not_home", + {"friendly_name": "test1", "latitude": 15, "longitude": 8}, + ) + hass.states.async_set( + "device_tracker.test2", + "not_home", + {"friendly_name": "test2", "latitude": 45, "longitude": 22}, + ) + await hass.async_block_till_done() + + # sensor entities + state = hass.states.get("sensor.home_nearest_device") + assert state.state == "test1" + state = hass.states.get("sensor.home_nearest_distance") + assert state.state == "1615590" + state = hass.states.get("sensor.home_test1_direction_of_travel") + assert state.state == "towards" + state = hass.states.get("sensor.home_test1_distance") + assert state.state == "1615590" + state = hass.states.get("sensor.home_test1_direction_of_travel") + assert state.state == "towards" + state = hass.states.get("sensor.home_test2_distance") + assert state.state == "5176058" + state = hass.states.get("sensor.home_test2_direction_of_travel") + assert state.state == "away_from" + + # move the far tracker + hass.states.async_set( + "device_tracker.test2", + "not_home", + {"friendly_name": "test2", "latitude": 40, "longitude": 20}, + ) + await hass.async_block_till_done() + state = hass.states.get("sensor.home_nearest_device") + assert state.state == "test1" + state = hass.states.get("sensor.home_nearest_distance") + assert state.state == "1615590" + state = hass.states.get("sensor.home_nearest_direction_of_travel") + assert state.state == "towards" + state = hass.states.get("sensor.home_test1_distance") + assert state.state == "1615590" + state = hass.states.get("sensor.home_test1_direction_of_travel") + assert state.state == "towards" + state = hass.states.get("sensor.home_test2_distance") + assert state.state == "4611404" + state = hass.states.get("sensor.home_test2_direction_of_travel") + assert state.state == "towards" + + # move the near tracker + hass.states.async_set( + "device_tracker.test1", + "not_home", + {"friendly_name": "test1", "latitude": 20, "longitude": 10}, + ) + await hass.async_block_till_done() + state = hass.states.get("sensor.home_nearest_device") + assert state.state == "test1" + state = hass.states.get("sensor.home_nearest_distance") + assert state.state == "2204122" + state = hass.states.get("sensor.home_nearest_direction_of_travel") + assert state.state == "away_from" + state = hass.states.get("sensor.home_test1_distance") + assert state.state == "2204122" + state = hass.states.get("sensor.home_test1_direction_of_travel") + assert state.state == "away_from" + state = hass.states.get("sensor.home_test2_distance") + assert state.state == "4611404" + state = hass.states.get("sensor.home_test2_direction_of_travel") + assert state.state == "towards" + + # get unknown distance and direction + hass.states.async_set( + "device_tracker.test1", "not_home", {"friendly_name": "test1"} + ) + hass.states.async_set( + "device_tracker.test2", "not_home", {"friendly_name": "test2"} + ) + await hass.async_block_till_done() + state = hass.states.get("sensor.home_nearest_device") + assert state.state == STATE_UNKNOWN + state = hass.states.get("sensor.home_nearest_distance") + assert state.state == STATE_UNKNOWN + state = hass.states.get("sensor.home_nearest_direction_of_travel") + assert state.state == STATE_UNKNOWN + state = hass.states.get("sensor.home_test1_distance") + assert state.state == STATE_UNKNOWN + state = hass.states.get("sensor.home_test1_direction_of_travel") + assert state.state == STATE_UNKNOWN + state = hass.states.get("sensor.home_test2_distance") + assert state.state == STATE_UNKNOWN + state = hass.states.get("sensor.home_test2_direction_of_travel") + assert state.state == STATE_UNKNOWN + + async def test_create_deprecated_proximity_issue( hass: HomeAssistant, issue_registry: ir.IssueRegistry, From 31ad48b2b7e6a57936d99e423383cb3d2c6356d2 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 5 Feb 2024 01:27:42 +0100 Subject: [PATCH 0268/1367] Add missing translation string to Home Assistant Analytics Insights (#109666) add missing string --- homeassistant/components/analytics_insights/strings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/analytics_insights/strings.json b/homeassistant/components/analytics_insights/strings.json index 96ec59f299b..96f13243868 100644 --- a/homeassistant/components/analytics_insights/strings.json +++ b/homeassistant/components/analytics_insights/strings.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "tracked_integrations": "Integrations" + "tracked_integrations": "Integrations", + "tracked_custom_integrations": "Custom integrations" } } }, From c34f5dd0b9cd62499c9cc36779a8f33237ece9de Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 5 Feb 2024 01:36:11 +0100 Subject: [PATCH 0269/1367] Add icon translation to proximity (#109664) add icon translations --- homeassistant/components/proximity/icons.json | 27 +++++++++++++++++++ homeassistant/components/proximity/sensor.py | 3 --- 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/proximity/icons.json diff --git a/homeassistant/components/proximity/icons.json b/homeassistant/components/proximity/icons.json new file mode 100644 index 00000000000..2919c73eda0 --- /dev/null +++ b/homeassistant/components/proximity/icons.json @@ -0,0 +1,27 @@ +{ + "entity": { + "sensor": { + "dir_of_travel": { + "default": "mdi:crosshairs-question", + "state": { + "arrived": "mdi:map-marker-radius-outline", + "away_from": "mdi:sign-direction-minus", + "stationary": "mdi:map-marker-outline", + "towards": "mdi:sign-direction-plus" + } + }, + "nearest": { + "default": "mdi:near-me" + }, + "nearest_dir_of_travel": { + "default": "mdi:crosshairs-question", + "state": { + "arrived": "mdi:map-marker-radius-outline", + "away_from": "mdi:sign-direction-minus", + "stationary": "mdi:map-marker-outline", + "towards": "mdi:sign-direction-plus" + } + } + } + } +} diff --git a/homeassistant/components/proximity/sensor.py b/homeassistant/components/proximity/sensor.py index c000aa27683..bd788058869 100644 --- a/homeassistant/components/proximity/sensor.py +++ b/homeassistant/components/proximity/sensor.py @@ -39,7 +39,6 @@ SENSORS_PER_ENTITY: list[SensorEntityDescription] = [ SensorEntityDescription( key=ATTR_DIR_OF_TRAVEL, translation_key=ATTR_DIR_OF_TRAVEL, - icon="mdi:compass-outline", device_class=SensorDeviceClass.ENUM, options=DIRECTIONS, ), @@ -49,7 +48,6 @@ SENSORS_PER_PROXIMITY: list[SensorEntityDescription] = [ SensorEntityDescription( key=ATTR_NEAREST, translation_key=ATTR_NEAREST, - icon="mdi:near-me", ), SensorEntityDescription( key=ATTR_DIST_TO, @@ -60,7 +58,6 @@ SENSORS_PER_PROXIMITY: list[SensorEntityDescription] = [ SensorEntityDescription( key=ATTR_DIR_OF_TRAVEL, translation_key=ATTR_NEAREST_DIR_OF_TRAVEL, - icon="mdi:compass-outline", device_class=SensorDeviceClass.ENUM, options=DIRECTIONS, ), From a5bd0292dac44d9c5a43597d920a8d71f581fb53 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 5 Feb 2024 01:52:37 -0600 Subject: [PATCH 0270/1367] Bump plexapi to 4.15.9 (#109676) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 8fc01140787..ea3f8574415 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["plexapi", "plexwebsocket"], "requirements": [ - "PlexAPI==4.15.7", + "PlexAPI==4.15.9", "plexauth==0.0.6", "plexwebsocket==0.0.14" ], diff --git a/requirements_all.txt b/requirements_all.txt index e8762b76c30..737dc4d9a4e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -45,7 +45,7 @@ Mastodon.py==1.5.1 Pillow==10.2.0 # homeassistant.components.plex -PlexAPI==4.15.7 +PlexAPI==4.15.9 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6057f4bf7f..678b95063cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -39,7 +39,7 @@ HATasmota==0.8.0 Pillow==10.2.0 # homeassistant.components.plex -PlexAPI==4.15.7 +PlexAPI==4.15.9 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 From 57f4f061a6b3f1cc77375dc88cf05abecd2da067 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Feb 2024 01:55:19 -0600 Subject: [PATCH 0271/1367] Use identity check in hassio websocket ingress (#109672) --- homeassistant/components/hassio/ingress.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 4f3933d0f5c..499a83b0444 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -288,13 +288,13 @@ async def _websocket_forward( """Handle websocket message directly.""" try: async for msg in ws_from: - if msg.type == aiohttp.WSMsgType.TEXT: + if msg.type is aiohttp.WSMsgType.TEXT: await ws_to.send_str(msg.data) - elif msg.type == aiohttp.WSMsgType.BINARY: + elif msg.type is aiohttp.WSMsgType.BINARY: await ws_to.send_bytes(msg.data) - elif msg.type == aiohttp.WSMsgType.PING: + elif msg.type is aiohttp.WSMsgType.PING: await ws_to.ping() - elif msg.type == aiohttp.WSMsgType.PONG: + elif msg.type is aiohttp.WSMsgType.PONG: await ws_to.pong() elif ws_to.closed: await ws_to.close(code=ws_to.close_code, message=msg.extra) # type: ignore[arg-type] From 0a8e4b59583330e53c0b5b555bf263c5c116fa1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 08:55:39 +0100 Subject: [PATCH 0272/1367] Bump github/codeql-action from 3.23.2 to 3.24.0 (#109677) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bdec74a3aff..269fb78829d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,11 +29,11 @@ jobs: uses: actions/checkout@v4.1.1 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.23.2 + uses: github/codeql-action/init@v3.24.0 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.23.2 + uses: github/codeql-action/analyze@v3.24.0 with: category: "/language:python" From c9fd97c6a34e147bc37f65d457903f059bbe4ffa Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 5 Feb 2024 02:58:08 -0500 Subject: [PATCH 0273/1367] Buffer TImeoutError in Flo (#109675) --- homeassistant/components/flo/device.py | 2 +- tests/components/flo/test_device.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index 3b7469686b4..0c805b932cb 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -46,7 +46,7 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable= await self._update_device() await self._update_consumption_data() self._failure_count = 0 - except RequestError as error: + except (RequestError, TimeoutError) as error: self._failure_count += 1 if self._failure_count > 3: raise UpdateFailed(error) from error diff --git a/tests/components/flo/test_device.py b/tests/components/flo/test_device.py index 884c9e3ca7e..34abfef0e72 100644 --- a/tests/components/flo/test_device.py +++ b/tests/components/flo/test_device.py @@ -117,7 +117,7 @@ async def test_device_failures( aioclient_mock.clear_requests() with patch( - "homeassistant.components.flo.device.FloDeviceDataUpdateCoordinator.send_presence_ping", + "aioflo.presence.Presence.ping", side_effect=RequestError, ): # simulate 4 updates failing. The failures should be buffered so that it takes 4 From 7ab1cdc2b31558cf8ca5ca0394b99e8c55f31e69 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 5 Feb 2024 08:59:03 +0100 Subject: [PATCH 0274/1367] Move nested code to class level as static method in imap coordinator (#109665) * Move _decode_payload to module level in imap coordinator * Make static method --- homeassistant/components/imap/coordinator.py | 37 ++++++++++---------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index 49938eaaa0a..2941b65be5c 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -101,6 +101,23 @@ class ImapMessage: """Initialize IMAP message.""" self.email_message = email.message_from_bytes(raw_message) + @staticmethod + def _decode_payload(part: Message) -> str: + """Try to decode text payloads. + + Common text encodings are quoted-printable or base64. + Falls back to the raw content part if decoding fails. + """ + try: + decoded_payload: Any = part.get_payload(decode=True) + if TYPE_CHECKING: + assert isinstance(decoded_payload, bytes) + content_charset = part.get_content_charset() or "utf-8" + return decoded_payload.decode(content_charset) + except ValueError: + # return undecoded payload + return str(part.get_payload()) + @property def headers(self) -> dict[str, tuple[str,]]: """Get the email headers.""" @@ -158,30 +175,14 @@ class ImapMessage: message_html: str | None = None message_untyped_text: str | None = None - def _decode_payload(part: Message) -> str: - """Try to decode text payloads. - - Common text encodings are quoted-printable or base64. - Falls back to the raw content part if decoding fails. - """ - try: - decoded_payload: Any = part.get_payload(decode=True) - if TYPE_CHECKING: - assert isinstance(decoded_payload, bytes) - content_charset = part.get_content_charset() or "utf-8" - return decoded_payload.decode(content_charset) - except ValueError: - # return undecoded payload - return str(part.get_payload()) - part: Message for part in self.email_message.walk(): if part.get_content_type() == CONTENT_TYPE_TEXT_PLAIN: if message_text is None: - message_text = _decode_payload(part) + message_text = self._decode_payload(part) elif part.get_content_type() == "text/html": if message_html is None: - message_html = _decode_payload(part) + message_html = self._decode_payload(part) elif ( part.get_content_type().startswith("text") and message_untyped_text is None From 458e1f3a5e56cbf72a24f05a7cf7717373a1fb95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Feb 2024 02:01:43 -0600 Subject: [PATCH 0275/1367] Index area_ids in the entity registry (#109660) --- homeassistant/helpers/entity_registry.py | 40 +++++++++++++++++------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 3f2e8a94b7c..5eb8a37176a 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -451,6 +451,7 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): self._index: dict[tuple[str, str, str], str] = {} self._config_entry_id_index: dict[str, list[str]] = {} self._device_id_index: dict[str, list[str]] = {} + self._area_id_index: dict[str, list[str]] = {} def values(self) -> ValuesView[RegistryEntry]: """Return the underlying values to avoid __iter__ overhead.""" @@ -468,22 +469,34 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): self._config_entry_id_index.setdefault(config_entry_id, []).append(key) if (device_id := entry.device_id) is not None: self._device_id_index.setdefault(device_id, []).append(key) + if (area_id := entry.area_id) is not None: + self._area_id_index.setdefault(area_id, []).append(key) + + def _unindex_entry_value( + self, key: str, value: str, index: dict[str, list[str]] + ) -> None: + """Unindex an entry value. + + key is the entry key + value is the value to unindex such as config_entry_id or device_id. + index is the index to unindex from. + """ + entries = index[value] + entries.remove(key) + if not entries: + del index[value] def _unindex_entry(self, key: str) -> None: """Unindex an entry.""" entry = self.data[key] del self._entry_ids[entry.id] del self._index[(entry.domain, entry.platform, entry.unique_id)] - if (config_entry_id := entry.config_entry_id) is not None: - entries = self._config_entry_id_index[config_entry_id] - entries.remove(key) - if not entries: - del self._config_entry_id_index[config_entry_id] - if (device_id := entry.device_id) is not None: - entries = self._device_id_index[device_id] - entries.remove(key) - if not entries: - del self._device_id_index[device_id] + if config_entry_id := entry.config_entry_id: + self._unindex_entry_value(key, config_entry_id, self._config_entry_id_index) + if device_id := entry.device_id: + self._unindex_entry_value(key, device_id, self._device_id_index) + if area_id := entry.area_id: + self._unindex_entry_value(key, area_id, self._area_id_index) def __delitem__(self, key: str) -> None: """Remove an item.""" @@ -518,6 +531,11 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): data[key] for key in self._config_entry_id_index.get(config_entry_id, ()) ] + def get_entries_for_area_id(self, area_id: str) -> list[RegistryEntry]: + """Get entries for area.""" + data = self.data + return [data[key] for key in self._area_id_index.get(area_id, ())] + class EntityRegistry: """Class to hold a registry of entities.""" @@ -1266,7 +1284,7 @@ def async_entries_for_area( registry: EntityRegistry, area_id: str ) -> list[RegistryEntry]: """Return entries that match an area.""" - return [entry for entry in registry.entities.values() if entry.area_id == area_id] + return registry.entities.get_entries_for_area_id(area_id) @callback From b56dd3f808dc22c4ecf86e871b3189f06879f1e5 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Mon, 5 Feb 2024 09:03:43 +0100 Subject: [PATCH 0276/1367] Don't create AsusWRT loadavg sensors when unavailable (#106790) --- homeassistant/components/asuswrt/bridge.py | 14 +++++++++++- tests/components/asuswrt/test_sensor.py | 25 +++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/asuswrt/bridge.py b/homeassistant/components/asuswrt/bridge.py index 53a0b5d06b5..cc06c225d22 100644 --- a/homeassistant/components/asuswrt/bridge.py +++ b/homeassistant/components/asuswrt/bridge.py @@ -11,6 +11,7 @@ from typing import Any, TypeVar, cast from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy from aiohttp import ClientSession from pyasuswrt import AsusWrtError, AsusWrtHttp +from pyasuswrt.exceptions import AsusWrtNotAvailableInfoError from homeassistant.const import ( CONF_HOST, @@ -354,13 +355,14 @@ class AsusWrtHttpBridge(AsusWrtBridge): 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_loadavg = await self._get_loadavg_sensors_availability() sensors_types = { SENSORS_TYPE_BYTES: { KEY_SENSORS: SENSORS_BYTES, KEY_METHOD: self._get_bytes, }, SENSORS_TYPE_LOAD_AVG: { - KEY_SENSORS: SENSORS_LOAD_AVG, + KEY_SENSORS: sensors_loadavg, KEY_METHOD: self._get_load_avg, }, SENSORS_TYPE_RATES: { @@ -393,6 +395,16 @@ class AsusWrtHttpBridge(AsusWrtBridge): return [] return available_sensors + async def _get_loadavg_sensors_availability(self) -> list[str]: + """Check if load avg is available on the router.""" + try: + await self._api.async_get_loadavg() + except AsusWrtNotAvailableInfoError: + return [] + except AsusWrtError: + pass + return SENSORS_LOAD_AVG + @handle_errors_and_zip(AsusWrtError, SENSORS_BYTES) async def _get_bytes(self) -> Any: """Fetch byte information from the router.""" diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index e3122f1dfef..0ee90b111f5 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -1,7 +1,7 @@ """Tests for the AsusWrt sensor.""" from datetime import timedelta -from pyasuswrt.asuswrt import AsusWrtError +from pyasuswrt.exceptions import AsusWrtError, AsusWrtNotAvailableInfoError import pytest from homeassistant.components import device_tracker, sensor @@ -226,6 +226,29 @@ async def test_loadavg_sensors_http(hass: HomeAssistant, connect_http) -> None: await _test_loadavg_sensors(hass, CONFIG_DATA_HTTP) +async def test_loadavg_sensors_unaivalable_http( + hass: HomeAssistant, connect_http +) -> None: + """Test load average sensors no available using http.""" + config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_HTTP, SENSORS_LOAD_AVG) + config_entry.add_to_hass(hass) + + connect_http.return_value.async_get_loadavg.side_effect = ( + AsusWrtNotAvailableInfoError + ) + + # initial devices setup + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + # assert load average sensors not available + assert not hass.states.get(f"{sensor_prefix}_sensor_load_avg1") + assert not hass.states.get(f"{sensor_prefix}_sensor_load_avg5") + assert not hass.states.get(f"{sensor_prefix}_sensor_load_avg15") + + async def test_temperature_sensors_http_fail( hass: HomeAssistant, connect_http_sens_fail ) -> None: From 0bfef71f1be8ecec198e3d7ea1ce09233cce88bc Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Mon, 5 Feb 2024 00:05:28 -0800 Subject: [PATCH 0277/1367] Screenlogic service refactor (#109041) --- .../components/screenlogic/__init__.py | 19 +- homeassistant/components/screenlogic/const.py | 2 + .../components/screenlogic/services.py | 175 +++++++++++------- .../components/screenlogic/services.yaml | 25 ++- .../components/screenlogic/strings.json | 29 ++- 5 files changed, 170 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 6d066f86072..56c686df6b4 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -9,13 +9,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify from .const import DOMAIN from .coordinator import ScreenlogicDataUpdateCoordinator, async_get_connect_info from .data import ENTITY_MIGRATIONS -from .services import async_load_screenlogic_services, async_unload_screenlogic_services +from .services import async_load_screenlogic_services from .util import generate_unique_id _LOGGER = logging.getLogger(__name__) @@ -36,6 +37,16 @@ PLATFORMS = [ Platform.SWITCH, ] +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Screenlogic.""" + + async_load_screenlogic_services(hass) + + return True + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Screenlogic from a config entry.""" @@ -62,8 +73,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - async_load_screenlogic_services(hass, entry) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -77,8 +86,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.gateway.async_disconnect() hass.data[DOMAIN].pop(entry.entry_id) - async_unload_screenlogic_services(hass) - return unload_ok diff --git a/homeassistant/components/screenlogic/const.py b/homeassistant/components/screenlogic/const.py index 3125f52989e..104736f300b 100644 --- a/homeassistant/components/screenlogic/const.py +++ b/homeassistant/components/screenlogic/const.py @@ -20,6 +20,8 @@ DOMAIN = "screenlogic" DEFAULT_SCAN_INTERVAL = 30 MIN_SCAN_INTERVAL = 10 +ATTR_CONFIG_ENTRY = "config_entry" + SERVICE_SET_COLOR_MODE = "set_color_mode" ATTR_COLOR_MODE = "color_mode" SUPPORTED_COLOR_MODES = {slugify(cm.name): cm.value for cm in COLOR_MODE} diff --git a/homeassistant/components/screenlogic/services.py b/homeassistant/components/screenlogic/services.py index 2c8e786491c..116a66d97df 100644 --- a/homeassistant/components/screenlogic/services.py +++ b/homeassistant/components/screenlogic/services.py @@ -6,14 +6,19 @@ from screenlogicpy import ScreenLogicError from screenlogicpy.device_const.system import EQUIPMENT_FLAG import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import ( + config_validation as cv, + issue_registry as ir, + selector, +) from homeassistant.helpers.service import async_extract_config_entry_ids from .const import ( ATTR_COLOR_MODE, + ATTR_CONFIG_ENTRY, ATTR_RUNTIME, DOMAIN, MAX_RUNTIME, @@ -27,44 +32,103 @@ from .coordinator import ScreenlogicDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -SET_COLOR_MODE_SCHEMA = cv.make_entity_service_schema( +BASE_SERVICE_SCHEMA = vol.Schema( { - vol.Required(ATTR_COLOR_MODE): vol.In(SUPPORTED_COLOR_MODES), - }, + vol.Required(ATTR_CONFIG_ENTRY): selector.ConfigEntrySelector( + { + "integration": DOMAIN, + } + ) + } ) -TURN_ON_SUPER_CHLOR_SCHEMA = cv.make_entity_service_schema( +SET_COLOR_MODE_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(ATTR_CONFIG_ENTRY): selector.ConfigEntrySelector( + { + "integration": DOMAIN, + } + ), + **cv.ENTITY_SERVICE_FIELDS, + vol.Required(ATTR_COLOR_MODE): vol.In(SUPPORTED_COLOR_MODES), + } + ), + cv.has_at_least_one_key(ATTR_CONFIG_ENTRY, *cv.ENTITY_SERVICE_FIELDS), +) + +TURN_ON_SUPER_CHLOR_SCHEMA = BASE_SERVICE_SCHEMA.extend( { - vol.Optional(ATTR_RUNTIME, default=24): vol.Clamp( - min=MIN_RUNTIME, max=MAX_RUNTIME + vol.Optional(ATTR_RUNTIME, default=24): vol.All( + vol.Coerce(int), vol.Clamp(min=MIN_RUNTIME, max=MAX_RUNTIME) ), } ) @callback -def async_load_screenlogic_services(hass: HomeAssistant, entry: ConfigEntry): +def async_load_screenlogic_services(hass: HomeAssistant): """Set up services for the ScreenLogic integration.""" async def extract_screenlogic_config_entry_ids(service_call: ServiceCall): if not ( - screenlogic_entry_ids := [ - entry_id - for entry_id in await async_extract_config_entry_ids(hass, service_call) - if (entry := hass.config_entries.async_get_entry(entry_id)) - and entry.domain == DOMAIN - ] + screenlogic_entry_ids := await async_extract_config_entry_ids( + hass, service_call + ) ): - raise HomeAssistantError( - f"Failed to call service '{service_call.service}'. Config entry for" - " target not found" + raise ServiceValidationError( + f"Failed to call service '{service_call.service}'. Config entry for " + "target not found" ) return screenlogic_entry_ids + async def get_coordinators( + service_call: ServiceCall, + ) -> list[ScreenlogicDataUpdateCoordinator]: + entry_ids: set[str] + if entry_id := service_call.data.get(ATTR_CONFIG_ENTRY): + entry_ids = {entry_id} + else: + ir.async_create_issue( + hass, + DOMAIN, + "service_target_deprecation", + breaks_in_ha_version="2024.8.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_target_deprecation", + ) + entry_ids = await extract_screenlogic_config_entry_ids(service_call) + + coordinators: list[ScreenlogicDataUpdateCoordinator] = [] + for entry_id in entry_ids: + config_entry: ConfigEntry | None = hass.config_entries.async_get_entry( + entry_id + ) + if not config_entry: + raise ServiceValidationError( + f"Failed to call service '{service_call.service}'. Config entry " + f"'{entry_id}' not found" + ) + if not config_entry.domain == DOMAIN: + raise ServiceValidationError( + f"Failed to call service '{service_call.service}'. Config entry " + f"'{entry_id}' is not a {DOMAIN} config" + ) + if not config_entry.state == ConfigEntryState.LOADED: + raise ServiceValidationError( + f"Failed to call service '{service_call.service}'. Config entry " + f"'{entry_id}' not loaded" + ) + coordinators.append(hass.data[DOMAIN][entry_id]) + + return coordinators + async def async_set_color_mode(service_call: ServiceCall) -> None: color_num = SUPPORTED_COLOR_MODES[service_call.data[ATTR_COLOR_MODE]] - for entry_id in await extract_screenlogic_config_entry_ids(service_call): - coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][entry_id] + coordinator: ScreenlogicDataUpdateCoordinator + for coordinator in await get_coordinators(service_call): _LOGGER.debug( "Service %s called on %s with mode %s", SERVICE_SET_COLOR_MODE, @@ -83,13 +147,19 @@ def async_load_screenlogic_services(hass: HomeAssistant, entry: ConfigEntry): is_on: bool, runtime: int | None = None, ) -> None: - for entry_id in await extract_screenlogic_config_entry_ids(service_call): - coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][entry_id] + coordinator: ScreenlogicDataUpdateCoordinator + for coordinator in await get_coordinators(service_call): + if EQUIPMENT_FLAG.CHLORINATOR not in coordinator.gateway.equipment_flags: + raise ServiceValidationError( + f"Equipment configuration for {coordinator.gateway.name} does not" + f" support {service_call.service}" + ) + rt_log = f" with runtime {runtime}" if runtime else "" _LOGGER.debug( - "Service %s called on %s with runtime %s", - SERVICE_START_SUPER_CHLORINATION, + "Service %s called on %s%s", + service_call.service, coordinator.gateway.name, - runtime, + rt_log, ) try: await coordinator.gateway.async_set_scg_config( @@ -107,43 +177,20 @@ def async_load_screenlogic_services(hass: HomeAssistant, entry: ConfigEntry): async def async_stop_super_chlor(service_call: ServiceCall) -> None: await async_set_super_chlor(service_call, False) - if not hass.services.has_service(DOMAIN, SERVICE_SET_COLOR_MODE): - hass.services.async_register( - DOMAIN, SERVICE_SET_COLOR_MODE, async_set_color_mode, SET_COLOR_MODE_SCHEMA - ) + hass.services.async_register( + DOMAIN, SERVICE_SET_COLOR_MODE, async_set_color_mode, SET_COLOR_MODE_SCHEMA + ) - coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - equipment_flags = coordinator.gateway.equipment_flags + hass.services.async_register( + DOMAIN, + SERVICE_START_SUPER_CHLORINATION, + async_start_super_chlor, + TURN_ON_SUPER_CHLOR_SCHEMA, + ) - if EQUIPMENT_FLAG.CHLORINATOR in equipment_flags: - if not hass.services.has_service(DOMAIN, SERVICE_START_SUPER_CHLORINATION): - hass.services.async_register( - DOMAIN, - SERVICE_START_SUPER_CHLORINATION, - async_start_super_chlor, - TURN_ON_SUPER_CHLOR_SCHEMA, - ) - - if not hass.services.has_service(DOMAIN, SERVICE_STOP_SUPER_CHLORINATION): - hass.services.async_register( - DOMAIN, - SERVICE_STOP_SUPER_CHLORINATION, - async_stop_super_chlor, - ) - - -@callback -def async_unload_screenlogic_services(hass: HomeAssistant): - """Unload services for the ScreenLogic integration.""" - - if not hass.data[DOMAIN]: - _LOGGER.debug("Unloading all ScreenLogic services") - for service in hass.services.async_services_for_domain(DOMAIN): - hass.services.async_remove(DOMAIN, service) - elif not any( - EQUIPMENT_FLAG.CHLORINATOR in coordinator.gateway.equipment_flags - for coordinator in hass.data[DOMAIN].values() - ): - _LOGGER.debug("Unloading ScreenLogic chlorination services") - hass.services.async_remove(DOMAIN, SERVICE_START_SUPER_CHLORINATION) - hass.services.async_remove(DOMAIN, SERVICE_STOP_SUPER_CHLORINATION) + hass.services.async_register( + DOMAIN, + SERVICE_STOP_SUPER_CHLORINATION, + async_stop_super_chlor, + BASE_SERVICE_SCHEMA, + ) diff --git a/homeassistant/components/screenlogic/services.yaml b/homeassistant/components/screenlogic/services.yaml index 7b51d1a21db..f05537640ca 100644 --- a/homeassistant/components/screenlogic/services.yaml +++ b/homeassistant/components/screenlogic/services.yaml @@ -1,9 +1,11 @@ # ScreenLogic Services set_color_mode: - target: - device: - integration: screenlogic fields: + config_entry: + required: false + selector: + config_entry: + integration: screenlogic color_mode: required: true selector: @@ -32,10 +34,12 @@ set_color_mode: - thumper - white start_super_chlorination: - target: - device: - integration: screenlogic fields: + config_entry: + required: true + selector: + config_entry: + integration: screenlogic runtime: default: 24 selector: @@ -45,6 +49,9 @@ start_super_chlorination: unit_of_measurement: hours mode: slider stop_super_chlorination: - target: - device: - integration: screenlogic + fields: + config_entry: + required: true + selector: + config_entry: + integration: screenlogic diff --git a/homeassistant/components/screenlogic/strings.json b/homeassistant/components/screenlogic/strings.json index fcddbc1d415..755eeb4ffb2 100644 --- a/homeassistant/components/screenlogic/strings.json +++ b/homeassistant/components/screenlogic/strings.json @@ -41,6 +41,10 @@ "name": "Set Color Mode", "description": "Sets the color mode for all color-capable lights attached to this ScreenLogic gateway.", "fields": { + "config_entry": { + "name": "Config Entry", + "description": "The config entry to use for this service." + }, "color_mode": { "name": "Color Mode", "description": "The ScreenLogic color mode to set." @@ -51,6 +55,10 @@ "name": "Start Super Chlorination", "description": "Begins super chlorination, running for the specified period or 24 hours if none is specified.", "fields": { + "config_entry": { + "name": "Config Entry", + "description": "The config entry to use for this service." + }, "runtime": { "name": "Run Time", "description": "Number of hours for super chlorination to run." @@ -59,7 +67,26 @@ }, "stop_super_chlorination": { "name": "Stop Super Chlorination", - "description": "Stops super chlorination." + "description": "Stops super chlorination.", + "fields": { + "config_entry": { + "name": "Config Entry", + "description": "The config entry to use for this service." + } + } + } + }, + "issues": { + "service_target_deprecation": { + "title": "Deprecating use of target for ScreenLogic services", + "fix_flow": { + "step": { + "confirm": { + "title": "Deprecating target for ScreenLogic services", + "description": "Use of an Area, Device, or Entity as a target for ScreenLogic services is being deprecated. Instead, use `config_entry` with the entry_id of the desired ScreenLogic integration.\n\nPlease update your automations and scripts and select **submit** to fix this issue." + } + } + } } } } From bfebde0f79d9abd099a0e76c27af52968142d907 Mon Sep 17 00:00:00 2001 From: spycle <48740594+spycle@users.noreply.github.com> Date: Mon, 5 Feb 2024 09:34:14 +0000 Subject: [PATCH 0278/1367] Bump pyMicrobot to 0.0.10 (#109628) --- homeassistant/components/keymitt_ble/manifest.json | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/keymitt_ble/manifest.json b/homeassistant/components/keymitt_ble/manifest.json index 760cc67cbd5..ee07881a01e 100644 --- a/homeassistant/components/keymitt_ble/manifest.json +++ b/homeassistant/components/keymitt_ble/manifest.json @@ -13,7 +13,8 @@ "config_flow": true, "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/keymitt_ble", + "integration_type": "hub", "iot_class": "assumed_state", "loggers": ["keymitt_ble"], - "requirements": ["PyMicroBot==0.0.9"] + "requirements": ["PyMicroBot==0.0.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index 737dc4d9a4e..d70de10c4b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -76,7 +76,7 @@ PyMetEireann==2021.8.0 PyMetno==0.11.0 # homeassistant.components.keymitt_ble -PyMicroBot==0.0.9 +PyMicroBot==0.0.10 # homeassistant.components.nina PyNINA==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 678b95063cf..e18168ee8cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -64,7 +64,7 @@ PyMetEireann==2021.8.0 PyMetno==0.11.0 # homeassistant.components.keymitt_ble -PyMicroBot==0.0.9 +PyMicroBot==0.0.10 # homeassistant.components.nina PyNINA==0.3.3 From 41a256a3ffd77294442241aac8efd12f9d9cee7c Mon Sep 17 00:00:00 2001 From: Myles Eftos Date: Mon, 5 Feb 2024 20:53:42 +1100 Subject: [PATCH 0279/1367] Show site state in Amberelectric config flow (#104702) --- .../components/amberelectric/config_flow.py | 71 +++++--- .../components/amberelectric/const.py | 1 - .../components/amberelectric/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../amberelectric/test_config_flow.py | 167 ++++++++++++++++-- .../amberelectric/test_coordinator.py | 25 ++- 7 files changed, 220 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/amberelectric/config_flow.py b/homeassistant/components/amberelectric/config_flow.py index 4011f442ee2..765e219b6d7 100644 --- a/homeassistant/components/amberelectric/config_flow.py +++ b/homeassistant/components/amberelectric/config_flow.py @@ -3,18 +3,46 @@ from __future__ import annotations import amberelectric from amberelectric.api import amber_api -from amberelectric.model.site import Site +from amberelectric.model.site import Site, SiteStatus import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_TOKEN from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, +) -from .const import CONF_SITE_ID, CONF_SITE_NAME, CONF_SITE_NMI, DOMAIN +from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN API_URL = "https://app.amber.com.au/developers" +def generate_site_selector_name(site: Site) -> str: + """Generate the name to show in the site drop down in the configuration flow.""" + if site.status == SiteStatus.CLOSED: + return site.nmi + " (Closed: " + site.closed_on.isoformat() + ")" # type: ignore[no-any-return] + if site.status == SiteStatus.PENDING: + return site.nmi + " (Pending)" # type: ignore[no-any-return] + return site.nmi # type: ignore[no-any-return] + + +def filter_sites(sites: list[Site]) -> list[Site]: + """Deduplicates the list of sites.""" + filtered: list[Site] = [] + filtered_nmi: set[str] = set() + + for site in sorted(sites, key=lambda site: site.status.value): + if site.status == SiteStatus.ACTIVE or site.nmi not in filtered_nmi: + filtered.append(site) + filtered_nmi.add(site.nmi) + + return filtered + + class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -31,7 +59,7 @@ class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): api: amber_api.AmberApi = amber_api.AmberApi.create(configuration) try: - sites: list[Site] = api.get_sites() + sites: list[Site] = filter_sites(api.get_sites()) if len(sites) == 0: self._errors[CONF_API_TOKEN] = "no_site" return None @@ -86,38 +114,31 @@ class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): assert self._sites is not None assert self._api_token is not None - api_token = self._api_token if user_input is not None: - site_nmi = user_input[CONF_SITE_NMI] - sites = [site for site in self._sites if site.nmi == site_nmi] - site = sites[0] - site_id = site.id + site_id = user_input[CONF_SITE_ID] name = user_input.get(CONF_SITE_NAME, site_id) return self.async_create_entry( title=name, - data={ - CONF_SITE_ID: site_id, - CONF_API_TOKEN: api_token, - CONF_SITE_NMI: site.nmi, - }, + data={CONF_SITE_ID: site_id, CONF_API_TOKEN: self._api_token}, ) - user_input = { - CONF_API_TOKEN: api_token, - CONF_SITE_NMI: "", - CONF_SITE_NAME: "", - } - return self.async_show_form( step_id="site", data_schema=vol.Schema( { - vol.Required( - CONF_SITE_NMI, default=user_input[CONF_SITE_NMI] - ): vol.In([site.nmi for site in self._sites]), - vol.Optional( - CONF_SITE_NAME, default=user_input[CONF_SITE_NAME] - ): str, + vol.Required(CONF_SITE_ID): SelectSelector( + SelectSelectorConfig( + options=[ + SelectOptionDict( + value=site.id, + label=generate_site_selector_name(site), + ) + for site in self._sites + ], + mode=SelectSelectorMode.DROPDOWN, + ) + ), + vol.Optional(CONF_SITE_NAME): str, } ), errors=self._errors, diff --git a/homeassistant/components/amberelectric/const.py b/homeassistant/components/amberelectric/const.py index 8416b7ca33c..6166b21c19f 100644 --- a/homeassistant/components/amberelectric/const.py +++ b/homeassistant/components/amberelectric/const.py @@ -6,7 +6,6 @@ from homeassistant.const import Platform DOMAIN = "amberelectric" CONF_SITE_NAME = "site_name" CONF_SITE_ID = "site_id" -CONF_SITE_NMI = "site_nmi" ATTRIBUTION = "Data provided by Amber Electric" diff --git a/homeassistant/components/amberelectric/manifest.json b/homeassistant/components/amberelectric/manifest.json index 29de18d96de..13a9f257adb 100644 --- a/homeassistant/components/amberelectric/manifest.json +++ b/homeassistant/components/amberelectric/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/amberelectric", "iot_class": "cloud_polling", "loggers": ["amberelectric"], - "requirements": ["amberelectric==1.0.4"] + "requirements": ["amberelectric==1.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index d70de10c4b5..4927a6d048a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -425,7 +425,7 @@ airtouch5py==0.2.8 alpha-vantage==2.3.1 # homeassistant.components.amberelectric -amberelectric==1.0.4 +amberelectric==1.1.0 # homeassistant.components.amcrest amcrest==1.9.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e18168ee8cb..d233f035c60 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -395,7 +395,7 @@ airtouch4pyapi==1.0.5 airtouch5py==0.2.8 # homeassistant.components.amberelectric -amberelectric==1.0.4 +amberelectric==1.1.0 # homeassistant.components.androidtv androidtv[async]==0.0.73 diff --git a/tests/components/amberelectric/test_config_flow.py b/tests/components/amberelectric/test_config_flow.py index 6325282aff8..2624bd96d31 100644 --- a/tests/components/amberelectric/test_config_flow.py +++ b/tests/components/amberelectric/test_config_flow.py @@ -1,17 +1,18 @@ """Tests for the Amber config flow.""" from collections.abc import Generator +from datetime import date from unittest.mock import Mock, patch from amberelectric import ApiException -from amberelectric.model.site import Site +from amberelectric.model.site import Site, SiteStatus import pytest from homeassistant import data_entry_flow +from homeassistant.components.amberelectric.config_flow import filter_sites from homeassistant.components.amberelectric.const import ( CONF_SITE_ID, CONF_SITE_NAME, - CONF_SITE_NMI, DOMAIN, ) from homeassistant.config_entries import SOURCE_USER @@ -26,29 +27,88 @@ pytestmark = pytest.mark.usefixtures("mock_setup_entry") @pytest.fixture(name="invalid_key_api") def mock_invalid_key_api() -> Generator: """Return an authentication error.""" - instance = Mock() - instance.get_sites.side_effect = ApiException(status=403) - with patch("amberelectric.api.AmberApi.create", return_value=instance): - yield instance + with patch("amberelectric.api.AmberApi.create") as mock: + mock.return_value.get_sites.side_effect = ApiException(status=403) + yield mock @pytest.fixture(name="api_error") def mock_api_error() -> Generator: """Return an authentication error.""" - instance = Mock() - instance.get_sites.side_effect = ApiException(status=500) - - with patch("amberelectric.api.AmberApi.create", return_value=instance): - yield instance + with patch("amberelectric.api.AmberApi.create") as mock: + mock.return_value.get_sites.side_effect = ApiException(status=500) + yield mock @pytest.fixture(name="single_site_api") def mock_single_site_api() -> Generator: + """Return a single site.""" + site = Site( + "01FG0AGP818PXK0DWHXJRRT2DH", + "11111111111", + [], + "Jemena", + SiteStatus.ACTIVE, + date(2002, 1, 1), + None, + ) + + with patch("amberelectric.api.AmberApi.create") as mock: + mock.return_value.get_sites.return_value = [site] + yield mock + + +@pytest.fixture(name="single_site_pending_api") +def mock_single_site_pending_api() -> Generator: + """Return a single site.""" + site = Site( + "01FG0AGP818PXK0DWHXJRRT2DH", + "11111111111", + [], + "Jemena", + SiteStatus.PENDING, + None, + None, + ) + + with patch("amberelectric.api.AmberApi.create") as mock: + mock.return_value.get_sites.return_value = [site] + yield mock + + +@pytest.fixture(name="single_site_rejoin_api") +def mock_single_site_rejoin_api() -> Generator: """Return a single site.""" instance = Mock() - site = Site("01FG0AGP818PXK0DWHXJRRT2DH", "11111111111", []) - instance.get_sites.return_value = [site] + site_1 = Site( + "01HGD9QB72HB3DWQNJ6SSCGXGV", + "11111111111", + [], + "Jemena", + SiteStatus.CLOSED, + date(2002, 1, 1), + date(2002, 6, 1), + ) + site_2 = Site( + "01FG0AGP818PXK0DWHXJRRT2DH", + "11111111111", + [], + "Jemena", + SiteStatus.ACTIVE, + date(2003, 1, 1), + None, + ) + site_3 = Site( + "01FG0AGP818PXK0DWHXJRRT2DH", + "11111111112", + [], + "Jemena", + SiteStatus.CLOSED, + date(2003, 1, 1), + date(2003, 6, 1), + ) + instance.get_sites.return_value = [site_1, site_2, site_3] with patch("amberelectric.api.AmberApi.create", return_value=instance): yield instance @@ -64,6 +124,39 @@ def mock_no_site_api() -> Generator: yield instance +async def test_single_pending_site( + hass: HomeAssistant, single_site_pending_api: Mock +) -> None: + """Test single site.""" + initial_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert initial_result.get("type") == data_entry_flow.FlowResultType.FORM + assert initial_result.get("step_id") == "user" + + # Test filling in API key + enter_api_key_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_API_TOKEN: API_KEY}, + ) + assert enter_api_key_result.get("type") == data_entry_flow.FlowResultType.FORM + assert enter_api_key_result.get("step_id") == "site" + + select_site_result = await hass.config_entries.flow.async_configure( + enter_api_key_result["flow_id"], + {CONF_SITE_ID: "01FG0AGP818PXK0DWHXJRRT2DH", CONF_SITE_NAME: "Home"}, + ) + + # Show available sites + assert select_site_result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert select_site_result.get("title") == "Home" + data = select_site_result.get("data") + assert data + assert data[CONF_API_TOKEN] == API_KEY + assert data[CONF_SITE_ID] == "01FG0AGP818PXK0DWHXJRRT2DH" + + async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: """Test single site.""" initial_result = await hass.config_entries.flow.async_init( @@ -83,7 +176,40 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: select_site_result = await hass.config_entries.flow.async_configure( enter_api_key_result["flow_id"], - {CONF_SITE_NMI: "11111111111", CONF_SITE_NAME: "Home"}, + {CONF_SITE_ID: "01FG0AGP818PXK0DWHXJRRT2DH", CONF_SITE_NAME: "Home"}, + ) + + # Show available sites + assert select_site_result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert select_site_result.get("title") == "Home" + data = select_site_result.get("data") + assert data + assert data[CONF_API_TOKEN] == API_KEY + assert data[CONF_SITE_ID] == "01FG0AGP818PXK0DWHXJRRT2DH" + + +async def test_single_site_rejoin( + hass: HomeAssistant, single_site_rejoin_api: Mock +) -> None: + """Test single site.""" + initial_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert initial_result.get("type") == data_entry_flow.FlowResultType.FORM + assert initial_result.get("step_id") == "user" + + # Test filling in API key + enter_api_key_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_API_TOKEN: API_KEY}, + ) + assert enter_api_key_result.get("type") == data_entry_flow.FlowResultType.FORM + assert enter_api_key_result.get("step_id") == "site" + + select_site_result = await hass.config_entries.flow.async_configure( + enter_api_key_result["flow_id"], + {CONF_SITE_ID: "01FG0AGP818PXK0DWHXJRRT2DH", CONF_SITE_NAME: "Home"}, ) # Show available sites @@ -93,7 +219,6 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: assert data assert data[CONF_API_TOKEN] == API_KEY assert data[CONF_SITE_ID] == "01FG0AGP818PXK0DWHXJRRT2DH" - assert data[CONF_SITE_NMI] == "11111111111" async def test_no_site(hass: HomeAssistant, no_site_api: Mock) -> None: @@ -148,3 +273,15 @@ async def test_unknown_error(hass: HomeAssistant, api_error: Mock) -> None: # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "unknown_error"} + + +async def test_site_deduplication(single_site_rejoin_api: Mock) -> None: + """Test site deduplication.""" + filtered = filter_sites(single_site_rejoin_api.get_sites()) + assert len(filtered) == 2 + assert ( + next(s for s in filtered if s.nmi == "11111111111").status == SiteStatus.ACTIVE + ) + assert ( + next(s for s in filtered if s.nmi == "11111111112").status == SiteStatus.CLOSED + ) diff --git a/tests/components/amberelectric/test_coordinator.py b/tests/components/amberelectric/test_coordinator.py index 64fa39192a6..7808d1adcde 100644 --- a/tests/components/amberelectric/test_coordinator.py +++ b/tests/components/amberelectric/test_coordinator.py @@ -2,13 +2,14 @@ from __future__ import annotations from collections.abc import Generator +from datetime import date from unittest.mock import Mock, patch from amberelectric import ApiException from amberelectric.model.channel import Channel, ChannelType from amberelectric.model.current_interval import CurrentInterval from amberelectric.model.interval import Descriptor, SpikeStatus -from amberelectric.model.site import Site +from amberelectric.model.site import Site, SiteStatus from dateutil import parser import pytest @@ -38,23 +39,35 @@ def mock_api_current_price() -> Generator: general_site = Site( GENERAL_ONLY_SITE_ID, "11111111111", - [Channel(identifier="E1", type=ChannelType.GENERAL)], + [Channel(identifier="E1", type=ChannelType.GENERAL, tariff="A100")], + "Jemena", + SiteStatus.ACTIVE, + date(2021, 1, 1), + None, ) general_and_controlled_load = Site( GENERAL_AND_CONTROLLED_SITE_ID, "11111111112", [ - Channel(identifier="E1", type=ChannelType.GENERAL), - Channel(identifier="E2", type=ChannelType.CONTROLLED_LOAD), + Channel(identifier="E1", type=ChannelType.GENERAL, tariff="A100"), + Channel(identifier="E2", type=ChannelType.CONTROLLED_LOAD, tariff="A180"), ], + "Jemena", + SiteStatus.ACTIVE, + date(2021, 1, 1), + None, ) general_and_feed_in = Site( GENERAL_AND_FEED_IN_SITE_ID, "11111111113", [ - Channel(identifier="E1", type=ChannelType.GENERAL), - Channel(identifier="E2", type=ChannelType.FEED_IN), + Channel(identifier="E1", type=ChannelType.GENERAL, tariff="A100"), + Channel(identifier="E2", type=ChannelType.FEED_IN, tariff="A100"), ], + "Jemena", + SiteStatus.ACTIVE, + date(2021, 1, 1), + None, ) instance.get_sites.return_value = [ general_site, From c82933175dd76b95e30fd847ab0f03d45fce883b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 5 Feb 2024 11:31:33 +0100 Subject: [PATCH 0280/1367] Use builtin TimeoutError [a-d] (#109678) --- .../components/accuweather/config_flow.py | 3 +-- homeassistant/components/acmeda/config_flow.py | 3 +-- homeassistant/components/ads/__init__.py | 2 +- .../components/aladdin_connect/__init__.py | 3 +-- .../components/aladdin_connect/config_flow.py | 7 +++---- homeassistant/components/alexa/auth.py | 2 +- homeassistant/components/alexa/state_report.py | 5 ++--- homeassistant/components/analytics/analytics.py | 2 +- .../components/androidtv_remote/__init__.py | 3 +-- homeassistant/components/apcupsd/config_flow.py | 3 +-- homeassistant/components/api/__init__.py | 2 +- homeassistant/components/arcam_fmj/__init__.py | 2 +- .../components/assist_pipeline/websocket_api.py | 4 ++-- homeassistant/components/august/__init__.py | 6 +++--- homeassistant/components/auth/indieauth.py | 3 +-- homeassistant/components/axis/device.py | 3 +-- homeassistant/components/baf/__init__.py | 3 +-- homeassistant/components/baf/config_flow.py | 3 +-- homeassistant/components/blink/__init__.py | 3 +-- .../components/blink/alarm_control_panel.py | 5 ++--- homeassistant/components/blink/camera.py | 7 +++---- homeassistant/components/blink/switch.py | 5 ++--- .../components/bluesound/media_player.py | 8 ++++---- .../bluetooth_le_tracker/device_tracker.py | 3 +-- homeassistant/components/bond/config_flow.py | 3 +-- homeassistant/components/buienradar/camera.py | 2 +- homeassistant/components/buienradar/util.py | 3 +-- homeassistant/components/camera/__init__.py | 6 +++--- homeassistant/components/cast/helpers.py | 3 +-- homeassistant/components/cert_expiry/helper.py | 2 +- homeassistant/components/citybikes/sensor.py | 2 +- homeassistant/components/cloud/account_link.py | 5 ++--- homeassistant/components/cloud/alexa_config.py | 2 +- homeassistant/components/cloud/http_api.py | 4 ++-- homeassistant/components/cloud/subscription.py | 4 ++-- .../components/color_extractor/__init__.py | 2 +- .../components/comed_hourly_pricing/sensor.py | 2 +- homeassistant/components/daikin/__init__.py | 2 +- homeassistant/components/daikin/config_flow.py | 2 +- homeassistant/components/deconz/config_flow.py | 6 +++--- homeassistant/components/deconz/gateway.py | 2 +- homeassistant/components/doorbird/camera.py | 2 +- homeassistant/components/dsmr/config_flow.py | 2 +- tests/components/analytics/test_analytics.py | 3 +-- tests/components/august/test_init.py | 3 +-- tests/components/baf/test_config_flow.py | 3 +-- tests/components/blink/test_init.py | 3 +-- tests/components/bluetooth/test_init.py | 2 +- .../bluetooth_le_tracker/test_device_tracker.py | 3 +-- tests/components/bond/test_config_flow.py | 3 +-- tests/components/bond/test_entity.py | 9 ++++----- tests/components/bond/test_init.py | 3 +-- tests/components/camera/test_init.py | 5 ++--- tests/components/cast/test_helpers.py | 3 +-- tests/components/cert_expiry/test_config_flow.py | 3 +-- tests/components/cloud/test_account_link.py | 2 +- tests/components/cloud/test_http_api.py | 15 +++++++-------- tests/components/cloud/test_subscription.py | 5 ++--- tests/components/daikin/test_config_flow.py | 3 +-- tests/components/daikin/test_init.py | 3 +-- tests/components/deconz/test_config_flow.py | 9 +++------ tests/components/deconz/test_gateway.py | 3 +-- tests/components/dsmr/test_config_flow.py | 5 ++--- 63 files changed, 97 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/accuweather/config_flow.py b/homeassistant/components/accuweather/config_flow.py index b1d113dad73..b3fc7872c85 100644 --- a/homeassistant/components/accuweather/config_flow.py +++ b/homeassistant/components/accuweather/config_flow.py @@ -1,7 +1,6 @@ """Adds config flow for AccuWeather.""" from __future__ import annotations -import asyncio from asyncio import timeout from typing import Any @@ -61,7 +60,7 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): longitude=user_input[CONF_LONGITUDE], ) await accuweather.async_get_location() - except (ApiError, ClientConnectorError, asyncio.TimeoutError, ClientError): + except (ApiError, ClientConnectorError, TimeoutError, ClientError): errors["base"] = "cannot_connect" except InvalidApiKeyError: errors[CONF_API_KEY] = "invalid_api_key" diff --git a/homeassistant/components/acmeda/config_flow.py b/homeassistant/components/acmeda/config_flow.py index b0dd287f428..56a11aff200 100644 --- a/homeassistant/components/acmeda/config_flow.py +++ b/homeassistant/components/acmeda/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Rollease Acmeda Automate Pulse Hub.""" from __future__ import annotations -import asyncio from asyncio import timeout from contextlib import suppress from typing import Any @@ -42,7 +41,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } hubs: list[aiopulse.Hub] = [] - with suppress(asyncio.TimeoutError): + with suppress(TimeoutError): async with timeout(5): async for hub in aiopulse.Hub.discover(): if hub.id not in already_configured: diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 1f80553031b..84d9e29a518 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -303,7 +303,7 @@ class AdsEntity(Entity): try: async with timeout(10): await self._event.wait() - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.debug("Variable %s: Timeout during first update", ads_var) @property diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 3df3c0dbe0a..d1c7bc5668b 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -1,5 +1,4 @@ """The aladdin_connect component.""" -import asyncio import logging from typing import Final @@ -29,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: await acc.login() - except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError) as ex: + except (ClientError, TimeoutError, Aladdin.ConnectionError) as ex: raise ConfigEntryNotReady("Can not connect to host") from ex except Aladdin.InvalidPasswordError as ex: raise ConfigEntryAuthFailed("Incorrect Password") from ex diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index e5170e9b0a2..d14b7b7c35e 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Aladdin Connect cover integration.""" from __future__ import annotations -import asyncio from collections.abc import Mapping from typing import Any @@ -42,7 +41,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: ) try: await acc.login() - except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError) as ex: + except (ClientError, TimeoutError, Aladdin.ConnectionError) as ex: raise ex except Aladdin.InvalidPasswordError as ex: @@ -81,7 +80,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except InvalidAuth: errors["base"] = "invalid_auth" - except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError): + except (ClientError, TimeoutError, Aladdin.ConnectionError): errors["base"] = "cannot_connect" else: @@ -117,7 +116,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except InvalidAuth: errors["base"] = "invalid_auth" - except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError): + except (ClientError, TimeoutError, Aladdin.ConnectionError): errors["base"] = "cannot_connect" else: diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 527e51b5390..10a7be4967e 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -122,7 +122,7 @@ class Auth: allow_redirects=True, ) - except (asyncio.TimeoutError, aiohttp.ClientError): + except (TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout calling LWA to get auth token") return None diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 20e66dfa084..3ad863747e5 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -1,7 +1,6 @@ """Alexa state report code.""" from __future__ import annotations -import asyncio from asyncio import timeout from http import HTTPStatus import json @@ -375,7 +374,7 @@ async def async_send_changereport_message( allow_redirects=True, ) - except (asyncio.TimeoutError, aiohttp.ClientError): + except (TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout sending report to Alexa for %s", alexa_entity.entity_id) return @@ -531,7 +530,7 @@ async def async_send_doorbell_event_message( allow_redirects=True, ) - except (asyncio.TimeoutError, aiohttp.ClientError): + except (TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout sending report to Alexa for %s", alexa_entity.entity_id) return diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 1c81eacd14a..bce4b69ecf1 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -329,7 +329,7 @@ class Analytics: response.status, self.endpoint, ) - except asyncio.TimeoutError: + except TimeoutError: LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENDPOINT_URL) except aiohttp.ClientError as err: LOGGER.error( diff --git a/homeassistant/components/androidtv_remote/__init__.py b/homeassistant/components/androidtv_remote/__init__.py index c78321589a9..9e99a93efa6 100644 --- a/homeassistant/components/androidtv_remote/__init__.py +++ b/homeassistant/components/androidtv_remote/__init__.py @@ -1,7 +1,6 @@ """The Android TV Remote integration.""" from __future__ import annotations -import asyncio from asyncio import timeout import logging @@ -50,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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, asyncio.TimeoutError) as exc: + except (CannotConnect, ConnectionClosed, 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 diff --git a/homeassistant/components/apcupsd/config_flow.py b/homeassistant/components/apcupsd/config_flow.py index 99c78fd5d33..25a1ccf7e02 100644 --- a/homeassistant/components/apcupsd/config_flow.py +++ b/homeassistant/components/apcupsd/config_flow.py @@ -1,7 +1,6 @@ """Config flow for APCUPSd integration.""" from __future__ import annotations -import asyncio from typing import Any import voluptuous as vol @@ -54,7 +53,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN): coordinator = APCUPSdCoordinator(self.hass, host, port) await coordinator.async_request_refresh() - if isinstance(coordinator.last_exception, (UpdateFailed, asyncio.TimeoutError)): + if isinstance(coordinator.last_exception, (UpdateFailed, TimeoutError)): errors = {"base": "cannot_connect"} return self.async_show_form( step_id="user", data_schema=_SCHEMA, errors=errors diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index d012dfc372f..266703bbab4 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -175,7 +175,7 @@ class APIEventStream(HomeAssistantView): msg = f"data: {payload}\n\n" _LOGGER.debug("STREAM %s WRITING %s", id(stop_obj), msg.strip()) await response.write(msg.encode("UTF-8")) - except asyncio.TimeoutError: + except TimeoutError: await to_write.put(STREAM_PING_PAYLOAD) except asyncio.CancelledError: diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index d9ab17dba86..a45dd89e180 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -83,7 +83,7 @@ async def _run_client(hass: HomeAssistant, client: Client, interval: float) -> N except ConnectionFailed: await asyncio.sleep(interval) - except asyncio.TimeoutError: + except TimeoutError: continue except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception, aborting arcam client") diff --git a/homeassistant/components/assist_pipeline/websocket_api.py b/homeassistant/components/assist_pipeline/websocket_api.py index bfba8563875..6d60426e730 100644 --- a/homeassistant/components/assist_pipeline/websocket_api.py +++ b/homeassistant/components/assist_pipeline/websocket_api.py @@ -241,7 +241,7 @@ async def websocket_run( # Task contains a timeout async with asyncio.timeout(timeout): await run_task - except asyncio.TimeoutError: + except TimeoutError: pipeline_input.run.process_event( PipelineEvent( PipelineEventType.ERROR, @@ -487,7 +487,7 @@ async def websocket_device_capture( ) try: - with contextlib.suppress(asyncio.TimeoutError): + with contextlib.suppress(TimeoutError): async with asyncio.timeout(timeout_seconds): while True: # Send audio chunks encoded as base64 diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 624121b8828..466160d2973 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -59,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await async_setup_august(hass, entry, august_gateway) except (RequireValidation, InvalidAuth) as err: raise ConfigEntryAuthFailed from err - except asyncio.TimeoutError as err: + except TimeoutError as err: raise ConfigEntryNotReady("Timed out connecting to august api") from err except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err: raise ConfigEntryNotReady from err @@ -233,7 +233,7 @@ class AugustData(AugustSubscriberMixin): return_exceptions=True, ): if isinstance(result, Exception) and not isinstance( - result, (asyncio.TimeoutError, ClientResponseError, CannotConnect) + result, (TimeoutError, ClientResponseError, CannotConnect) ): _LOGGER.warning( "Unexpected exception during initial sync: %s", @@ -292,7 +292,7 @@ class AugustData(AugustSubscriberMixin): for device_id in device_ids_list: try: await self._async_refresh_device_detail_by_id(device_id) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning( "Timed out calling august api during refresh of device: %s", device_id, diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index e2614af6a3e..cf7f38fa32a 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -1,7 +1,6 @@ """Helpers to resolve client ID/secret.""" from __future__ import annotations -import asyncio from html.parser import HTMLParser from ipaddress import ip_address import logging @@ -102,7 +101,7 @@ async def fetch_redirect_uris(hass: HomeAssistant, url: str) -> list[str]: if chunks == 10: break - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error("Timeout while looking up redirect_uri %s", url) except aiohttp.client_exceptions.ClientSSLError: _LOGGER.error("SSL error while looking up redirect_uri %s", url) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 67ef61af8ac..4a54843edfc 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -1,6 +1,5 @@ """Axis network device abstraction.""" -import asyncio from asyncio import timeout from types import MappingProxyType from typing import Any @@ -270,7 +269,7 @@ async def get_axis_device( ) raise AuthenticationRequired from err - except (asyncio.TimeoutError, axis.RequestError) as err: + except (TimeoutError, axis.RequestError) as err: LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST]) raise CannotConnect from err diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index fcc648f4001..e685ec6dc8c 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -1,7 +1,6 @@ """The Big Ass Fans integration.""" from __future__ import annotations -import asyncio from asyncio import timeout from aiobafi6 import Device, Service @@ -42,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( f"Unexpected device found at {ip_address}; expected {entry.unique_id}, found {device.dns_sd_uuid}" ) from ex - except asyncio.TimeoutError as ex: + except TimeoutError as ex: run_future.cancel() raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py index 9edb23abcf8..0aaf2189c28 100644 --- a/homeassistant/components/baf/config_flow.py +++ b/homeassistant/components/baf/config_flow.py @@ -1,7 +1,6 @@ """Config flow for baf.""" from __future__ import annotations -import asyncio from asyncio import timeout import logging from typing import Any @@ -28,7 +27,7 @@ async def async_try_connect(ip_address: str) -> Device: try: async with timeout(RUN_TIMEOUT): await device.async_wait_available() - except asyncio.TimeoutError as ex: + except TimeoutError as ex: raise CannotConnect from ex finally: run_future.cancel() diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 50c7fad516a..e86d07c8780 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -1,5 +1,4 @@ """Support for Blink Home Camera System.""" -import asyncio from copy import deepcopy import logging @@ -93,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await blink.start() - except (ClientError, asyncio.TimeoutError) as ex: + except (ClientError, TimeoutError) as ex: raise ConfigEntryNotReady("Can not connect to host") from ex if blink.auth.check_key_required(): diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index 80a6ceb50e0..f3e3d97fc5d 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -1,7 +1,6 @@ """Support for Blink Alarm Control Panel.""" from __future__ import annotations -import asyncio import logging from blinkpy.blinkpy import Blink, BlinkSyncModule @@ -91,7 +90,7 @@ class BlinkSyncModuleHA( try: await self.sync.async_arm(False) - except asyncio.TimeoutError as er: + except TimeoutError as er: raise HomeAssistantError("Blink failed to disarm camera") from er await self.coordinator.async_refresh() @@ -101,7 +100,7 @@ class BlinkSyncModuleHA( try: await self.sync.async_arm(True) - except asyncio.TimeoutError as er: + except TimeoutError as er: raise HomeAssistantError("Blink failed to arm camera away") from er await self.coordinator.async_refresh() diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index 838020c98c6..ff4fa6380a7 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -1,7 +1,6 @@ """Support for Blink system camera.""" from __future__ import annotations -import asyncio from collections.abc import Mapping import contextlib import logging @@ -96,7 +95,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera): try: await self._camera.async_arm(True) - except asyncio.TimeoutError as er: + except TimeoutError as er: raise HomeAssistantError("Blink failed to arm camera") from er self._camera.motion_enabled = True @@ -106,7 +105,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera): """Disable motion detection for the camera.""" try: await self._camera.async_arm(False) - except asyncio.TimeoutError as er: + except TimeoutError as er: raise HomeAssistantError("Blink failed to disarm camera") from er self._camera.motion_enabled = False @@ -124,7 +123,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera): async def trigger_camera(self) -> None: """Trigger camera to take a snapshot.""" - with contextlib.suppress(asyncio.TimeoutError): + with contextlib.suppress(TimeoutError): await self._camera.snap_picture() self.async_write_ha_state() diff --git a/homeassistant/components/blink/switch.py b/homeassistant/components/blink/switch.py index 197c8e08685..0a066850d5f 100644 --- a/homeassistant/components/blink/switch.py +++ b/homeassistant/components/blink/switch.py @@ -1,7 +1,6 @@ """Support for Blink Motion detection switches.""" from __future__ import annotations -import asyncio from typing import Any from homeassistant.components.switch import ( @@ -74,7 +73,7 @@ class BlinkSwitch(CoordinatorEntity[BlinkUpdateCoordinator], SwitchEntity): try: await self._camera.async_arm(True) - except asyncio.TimeoutError as er: + except TimeoutError as er: raise HomeAssistantError( "Blink failed to arm camera motion detection" ) from er @@ -86,7 +85,7 @@ class BlinkSwitch(CoordinatorEntity[BlinkUpdateCoordinator], SwitchEntity): try: await self._camera.async_arm(False) - except asyncio.TimeoutError as er: + except TimeoutError as er: raise HomeAssistantError( "Blink failed to dis-arm camera motion detection" ) from er diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index eba03963ebc..70c19b5fa6f 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -290,7 +290,7 @@ class BluesoundPlayer(MediaPlayerEntity): while True: await self.async_update_status() - except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException): + except (TimeoutError, ClientError, BluesoundPlayer._TimeoutException): _LOGGER.info("Node %s:%s is offline, retrying later", self.name, self.port) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) self.start_polling() @@ -317,7 +317,7 @@ class BluesoundPlayer(MediaPlayerEntity): self._retry_remove = None await self.force_update_sync_status(self._init_callback, True) - except (asyncio.TimeoutError, ClientError): + except (TimeoutError, ClientError): _LOGGER.info("Node %s:%s is offline, retrying later", self.host, self.port) self._retry_remove = async_track_time_interval( self._hass, self.async_init, NODE_RETRY_INITIATION @@ -370,7 +370,7 @@ class BluesoundPlayer(MediaPlayerEntity): _LOGGER.error("Error %s on %s", response.status, url) return None - except (asyncio.TimeoutError, aiohttp.ClientError): + except (TimeoutError, aiohttp.ClientError): if raise_timeout: _LOGGER.info("Timeout: %s:%s", self.host, self.port) raise @@ -437,7 +437,7 @@ class BluesoundPlayer(MediaPlayerEntity): "Error %s on %s. Trying one more time", response.status, url ) - except (asyncio.TimeoutError, ClientError): + except (TimeoutError, ClientError): self._is_online = False self._last_status_update = None self._status = None diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 3739734223e..f85a9506d72 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -1,7 +1,6 @@ """Tracking for bluetooth low energy devices.""" from __future__ import annotations -import asyncio from datetime import datetime, timedelta import logging from uuid import UUID @@ -155,7 +154,7 @@ async def async_setup_scanner( # noqa: C901 async with BleakClient(device) as client: bat_char = await client.read_gatt_char(BATTERY_CHARACTERISTIC_UUID) battery = ord(bat_char) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.debug( "Timeout when trying to get battery status for %s", service_info.name ) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 26b485127f2..33b5d2bf2c4 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Bond integration.""" from __future__ import annotations -import asyncio import contextlib from http import HTTPStatus import logging @@ -87,7 +86,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: if not (token := await async_get_token(self.hass, host)): return - except asyncio.TimeoutError: + except TimeoutError: return self._discovered[CONF_ACCESS_TOKEN] = token diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 1963041bcca..ba62cbfbb19 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -128,7 +128,7 @@ class BuienradarCam(Camera): _LOGGER.debug("HTTP 200 - Last-Modified: %s", last_modified) return True - except (asyncio.TimeoutError, aiohttp.ClientError) as err: + except (TimeoutError, aiohttp.ClientError) as err: _LOGGER.error("Failed to fetch image, %s", type(err)) return False diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 63e0004dc43..426f982bafc 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -1,5 +1,4 @@ """Shared utilities for different supported platforms.""" -import asyncio from asyncio import timeout from datetime import datetime, timedelta from http import HTTPStatus @@ -104,7 +103,7 @@ class BrData: result[MESSAGE] = "Got http statuscode: %d" % (resp.status) return result - except (asyncio.TimeoutError, aiohttp.ClientError) as err: + except (TimeoutError, aiohttp.ClientError) as err: result[MESSAGE] = str(err) return result finally: diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 5a78728697b..1abf1768fa3 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -181,7 +181,7 @@ async def _async_get_image( that we can scale, however the majority of cases are handled. """ - with suppress(asyncio.CancelledError, asyncio.TimeoutError): + with suppress(asyncio.CancelledError, TimeoutError): async with asyncio.timeout(timeout): image_bytes = ( await _async_get_stream_image( @@ -891,7 +891,7 @@ async def ws_camera_stream( except HomeAssistantError as ex: _LOGGER.error("Error requesting stream: %s", ex) connection.send_error(msg["id"], "start_stream_failed", str(ex)) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error("Timeout getting stream source") connection.send_error( msg["id"], "start_stream_failed", "Timeout getting stream source" @@ -936,7 +936,7 @@ async def ws_camera_web_rtc_offer( except (HomeAssistantError, ValueError) as ex: _LOGGER.error("Error handling WebRTC offer: %s", ex) connection.send_error(msg["id"], "web_rtc_offer_failed", str(ex)) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error("Timeout handling WebRTC offer") connection.send_error( msg["id"], "web_rtc_offer_failed", "Timeout handling WebRTC offer" diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index 8b8862ab318..a3158ee819e 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -1,7 +1,6 @@ """Helpers to deal with Cast devices.""" from __future__ import annotations -import asyncio import configparser from dataclasses import dataclass import logging @@ -257,7 +256,7 @@ async def _fetch_playlist(hass, url, supported_content_types): playlist_data = (await resp.content.read(64 * 1024)).decode(charset) except ValueError as err: raise PlaylistError(f"Could not decode playlist {url}") from err - except asyncio.TimeoutError as err: + except TimeoutError as err: raise PlaylistError(f"Timeout while fetching playlist {url}") from err except aiohttp.client_exceptions.ClientError as err: raise PlaylistError(f"Error while fetching playlist {url}") from err diff --git a/homeassistant/components/cert_expiry/helper.py b/homeassistant/components/cert_expiry/helper.py index cde9364214e..6d10d750705 100644 --- a/homeassistant/components/cert_expiry/helper.py +++ b/homeassistant/components/cert_expiry/helper.py @@ -55,7 +55,7 @@ async def get_cert_expiry_timestamp( cert = await async_get_cert(hass, hostname, port) except socket.gaierror as err: raise ResolveFailed(f"Cannot resolve hostname: {hostname}") from err - except asyncio.TimeoutError as err: + except TimeoutError as err: raise ConnectionTimeout( f"Connection timeout with server: {hostname}:{port}" ) from err diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index fcd780dba7d..fc49331c1b7 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -144,7 +144,7 @@ async def async_citybikes_request(hass, uri, schema): json_response = await req.json() return schema(json_response) - except (asyncio.TimeoutError, aiohttp.ClientError): + except (TimeoutError, aiohttp.ClientError): _LOGGER.error("Could not connect to CityBikes API endpoint") except ValueError: _LOGGER.error("Received non-JSON data from CityBikes API endpoint") diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 1423330cb44..f1e5d1a6903 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -1,7 +1,6 @@ """Account linking via the cloud.""" from __future__ import annotations -import asyncio from datetime import datetime import logging from typing import Any @@ -69,7 +68,7 @@ async def _get_services(hass: HomeAssistant) -> list[dict[str, Any]]: try: services = await account_link.async_fetch_available_services(hass.data[DOMAIN]) - except (aiohttp.ClientError, asyncio.TimeoutError): + except (aiohttp.ClientError, TimeoutError): return [] hass.data[DATA_SERVICES] = services @@ -114,7 +113,7 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement try: tokens = await helper.async_get_tokens() - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.info("Timeout fetching tokens for flow %s", flow_id) except account_link.AccountLinkException as err: _LOGGER.info( diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index e85c6dd277a..caed7b38c47 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -505,7 +505,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): return True - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning("Timeout trying to sync entities to Alexa") return False diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 849a1c99db9..be3271a88a3 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -55,7 +55,7 @@ _LOGGER = logging.getLogger(__name__) _CLOUD_ERRORS: dict[type[Exception], tuple[HTTPStatus, str]] = { - asyncio.TimeoutError: ( + TimeoutError: ( HTTPStatus.BAD_GATEWAY, "Unable to reach the Home Assistant cloud.", ), @@ -429,7 +429,7 @@ async def websocket_update_prefs( try: async with asyncio.timeout(10): await alexa_config.async_get_access_token() - except asyncio.TimeoutError: + except TimeoutError: connection.send_error( msg["id"], "alexa_timeout", "Timeout validating Alexa access token." ) diff --git a/homeassistant/components/cloud/subscription.py b/homeassistant/components/cloud/subscription.py index 9a62f2d115c..63b57d2fa3d 100644 --- a/homeassistant/components/cloud/subscription.py +++ b/homeassistant/components/cloud/subscription.py @@ -19,7 +19,7 @@ async def async_subscription_info(cloud: Cloud[CloudClient]) -> dict[str, Any] | try: async with asyncio.timeout(REQUEST_TIMEOUT): return await cloud_api.async_subscription_info(cloud) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error( ( "A timeout of %s was reached while trying to fetch subscription" @@ -40,7 +40,7 @@ async def async_migrate_paypal_agreement( try: async with asyncio.timeout(REQUEST_TIMEOUT): return await cloud_api.async_migrate_paypal_agreement(cloud) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error( "A timeout of %s was reached while trying to start agreement migration", REQUEST_TIMEOUT, diff --git a/homeassistant/components/color_extractor/__init__.py b/homeassistant/components/color_extractor/__init__.py index 2cc3e206958..e6095c9f925 100644 --- a/homeassistant/components/color_extractor/__init__.py +++ b/homeassistant/components/color_extractor/__init__.py @@ -139,7 +139,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with asyncio.timeout(10): response = await session.get(url) - except (asyncio.TimeoutError, aiohttp.ClientError) as err: + except (TimeoutError, aiohttp.ClientError) as err: _LOGGER.error("Failed to get ColorThief image due to HTTPError: %s", err) return None diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py index ef974b8f3ed..195bfa97b7d 100644 --- a/homeassistant/components/comed_hourly_pricing/sensor.py +++ b/homeassistant/components/comed_hourly_pricing/sensor.py @@ -123,7 +123,7 @@ class ComedHourlyPricingSensor(SensorEntity): else: self._attr_native_value = None - except (asyncio.TimeoutError, aiohttp.ClientError) as err: + except (TimeoutError, aiohttp.ClientError) as err: _LOGGER.error("Could not get data from ComEd API: %s", err) except (ValueError, KeyError): _LOGGER.warning("Could not update status for %s", self.name) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index e39fe97bc6c..b8e87d2b200 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -86,7 +86,7 @@ async def daikin_api_setup( device = await Appliance.factory( host, session, key=key, uuid=uuid, password=password ) - except asyncio.TimeoutError as err: + except TimeoutError as err: _LOGGER.debug("Connection to %s timed out", host) raise ConfigEntryNotReady from err except ClientConnectionError as err: diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index b79cc960fce..abd2d78c7fb 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -89,7 +89,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): uuid=uuid, password=password, ) - except (asyncio.TimeoutError, ClientError): + except (TimeoutError, ClientError): self.host = None return self.async_show_form( step_id="user", diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index c0361aa2bca..99fa6412364 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -103,7 +103,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): async with asyncio.timeout(10): self.bridges = await deconz_discovery(session) - except (asyncio.TimeoutError, ResponseError): + except (TimeoutError, ResponseError): self.bridges = [] if LOGGER.isEnabledFor(logging.DEBUG): @@ -164,7 +164,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): except LinkButtonNotPressed: errors["base"] = "linking_not_possible" - except (ResponseError, RequestError, asyncio.TimeoutError): + except (ResponseError, RequestError, TimeoutError): errors["base"] = "no_key" else: @@ -193,7 +193,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): } ) - except asyncio.TimeoutError: + except TimeoutError: return self.async_abort(reason="no_bridges") return self.async_create_entry( diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 156309c0903..a982d110f1f 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -360,6 +360,6 @@ async def get_deconz_session( LOGGER.warning("Invalid key for deCONZ at %s", config[CONF_HOST]) raise AuthenticationRequired from err - except (asyncio.TimeoutError, errors.RequestError, errors.ResponseError) as err: + except (TimeoutError, errors.RequestError, errors.ResponseError) as err: LOGGER.error("Error connecting to deCONZ gateway at %s", config[CONF_HOST]) raise CannotConnect from err diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index a4133f2da2c..3da47eb572a 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -108,7 +108,7 @@ class DoorBirdCamera(DoorBirdEntity, Camera): self._last_image = await response.read() self._last_update = now return self._last_image - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error("DoorBird %s: Camera image timed out", self.name) return self._last_image except aiohttp.ClientError as error: diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index 376b4d100fc..a38326c1346 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -123,7 +123,7 @@ class DSMRConnection: try: async with asyncio.timeout(30): await protocol.wait_closed() - except asyncio.TimeoutError: + except TimeoutError: # Timeout (no data received), close transport and return True (if telegram is empty, will result in CannotCommunicate error) transport.close() await protocol.wait_closed() diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index d22738a7e6b..fd1c7070f6d 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -1,5 +1,4 @@ """The tests for the analytics .""" -import asyncio from typing import Any from unittest.mock import ANY, AsyncMock, Mock, PropertyMock, patch @@ -756,7 +755,7 @@ async def test_timeout_while_sending( ) -> None: """Test timeout error while sending analytics.""" analytics = Analytics(hass) - aioclient_mock.post(ANALYTICS_ENDPOINT_URL_DEV, exc=asyncio.TimeoutError()) + aioclient_mock.post(ANALYTICS_ENDPOINT_URL_DEV, exc=TimeoutError()) await analytics.save_preferences({ATTR_BASE: True}) with patch( diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 55bc44c6f27..81d8992948f 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -1,5 +1,4 @@ """The tests for the august platform.""" -import asyncio from unittest.mock import Mock, patch from aiohttp import ClientResponseError @@ -68,7 +67,7 @@ async def test_august_is_offline(hass: HomeAssistant) -> None: with patch( "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/baf/test_config_flow.py b/tests/components/baf/test_config_flow.py index f770db05096..f15c624447c 100644 --- a/tests/components/baf/test_config_flow.py +++ b/tests/components/baf/test_config_flow.py @@ -1,5 +1,4 @@ """Test the baf config flow.""" -import asyncio from ipaddress import ip_address from unittest.mock import patch @@ -55,7 +54,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with _patch_device_config_flow(asyncio.TimeoutError): + with _patch_device_config_flow(TimeoutError): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_IP_ADDRESS: "127.0.0.1"}, diff --git a/tests/components/blink/test_init.py b/tests/components/blink/test_init.py index f3d9beaf21a..32c424aae60 100644 --- a/tests/components/blink/test_init.py +++ b/tests/components/blink/test_init.py @@ -1,5 +1,4 @@ """Test the Blink init.""" -import asyncio from unittest.mock import AsyncMock, MagicMock from aiohttp import ClientError @@ -23,7 +22,7 @@ PIN = "1234" @pytest.mark.parametrize( ("the_error", "available"), - [(ClientError, False), (asyncio.TimeoutError, False), (None, False)], + [(ClientError, False), (TimeoutError, False), (None, False)], ) async def test_setup_not_ready( hass: HomeAssistant, diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 35ee073bc87..e97ed0c27da 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -2294,7 +2294,7 @@ async def test_process_advertisements_timeout( def _callback(service_info: BluetoothServiceInfo) -> bool: return False - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): await async_process_advertisements( hass, _callback, {}, BluetoothScanningMode.ACTIVE, 0 ) diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index 15b5ef287ae..78ce96bde99 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -1,5 +1,4 @@ """Test Bluetooth LE device tracker.""" -import asyncio from datetime import timedelta from unittest.mock import patch @@ -47,7 +46,7 @@ class MockBleakClientTimesOut(MockBleakClient): async def read_gatt_char(self, *args, **kwargs): """Mock BleakClient.read_gatt_char.""" - raise asyncio.TimeoutError + raise TimeoutError class MockBleakClientFailing(MockBleakClient): diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 91d628e4841..7d639309ddc 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -1,7 +1,6 @@ """Test the Bond config flow.""" from __future__ import annotations -import asyncio from http import HTTPStatus from ipaddress import ip_address from typing import Any @@ -274,7 +273,7 @@ async def test_zeroconf_form_token_unavailable(hass: HomeAssistant) -> None: async def test_zeroconf_form_token_times_out(hass: HomeAssistant) -> None: """Test we get the discovery form and we handle the token request timeout.""" - with patch_bond_version(), patch_bond_token(side_effect=asyncio.TimeoutError): + with patch_bond_version(), patch_bond_token(side_effect=TimeoutError): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, diff --git a/tests/components/bond/test_entity.py b/tests/components/bond/test_entity.py index d61b4e06560..bcb61ddc92d 100644 --- a/tests/components/bond/test_entity.py +++ b/tests/components/bond/test_entity.py @@ -1,5 +1,4 @@ """Tests for the Bond entities.""" -import asyncio from datetime import timedelta from unittest.mock import patch @@ -85,7 +84,7 @@ async def test_bpup_goes_offline_and_recovers_same_entity(hass: HomeAssistant) - assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 33 bpup_subs.last_message_time = -BPUP_ALIVE_TIMEOUT - with patch_bond_device_state(side_effect=asyncio.TimeoutError): + with patch_bond_device_state(side_effect=TimeoutError): async_fire_time_changed(hass, utcnow() + timedelta(seconds=230)) await hass.async_block_till_done() @@ -147,7 +146,7 @@ async def test_bpup_goes_offline_and_recovers_different_entity( assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 33 bpup_subs.last_message_time = -BPUP_ALIVE_TIMEOUT - with patch_bond_device_state(side_effect=asyncio.TimeoutError): + with patch_bond_device_state(side_effect=TimeoutError): async_fire_time_changed(hass, utcnow() + timedelta(seconds=230)) await hass.async_block_till_done() @@ -178,7 +177,7 @@ async def test_polling_fails_and_recovers(hass: HomeAssistant) -> None: hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id" ) - with patch_bond_device_state(side_effect=asyncio.TimeoutError): + with patch_bond_device_state(side_effect=TimeoutError): async_fire_time_changed(hass, utcnow() + timedelta(seconds=230)) await hass.async_block_till_done() @@ -199,7 +198,7 @@ async def test_polling_stops_at_the_stop_event(hass: HomeAssistant) -> None: hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id" ) - with patch_bond_device_state(side_effect=asyncio.TimeoutError): + with patch_bond_device_state(side_effect=TimeoutError): async_fire_time_changed(hass, utcnow() + timedelta(seconds=230)) await hass.async_block_till_done() diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 6b462a02c26..6453fa39807 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -1,5 +1,4 @@ """Tests for the Bond module.""" -import asyncio from unittest.mock import MagicMock, Mock from aiohttp import ClientConnectionError, ClientResponseError @@ -45,7 +44,7 @@ async def test_async_setup_no_domain_config(hass: HomeAssistant) -> None: [ ClientConnectionError, ClientResponseError(MagicMock(), MagicMock(), status=404), - asyncio.TimeoutError, + TimeoutError, OSError, ], ) diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index f1e3a4fdef5..528c13bc08c 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -1,5 +1,4 @@ """The tests for the camera component.""" -import asyncio from http import HTTPStatus import io from types import ModuleType @@ -204,7 +203,7 @@ async def test_get_image_with_timeout(hass: HomeAssistant, image_mock_url) -> No """Try to get image with timeout.""" with patch( "homeassistant.components.demo.camera.DemoCamera.async_camera_image", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ), pytest.raises(HomeAssistantError): await camera.async_get_image(hass, "camera.demo_camera") @@ -670,7 +669,7 @@ async def test_websocket_web_rtc_offer_timeout( with patch( "homeassistant.components.camera.Camera.async_handle_web_rtc_offer", - side_effect=asyncio.TimeoutError(), + side_effect=TimeoutError(), ): await client.send_json( { diff --git a/tests/components/cast/test_helpers.py b/tests/components/cast/test_helpers.py index 79186019413..d19c4f212d0 100644 --- a/tests/components/cast/test_helpers.py +++ b/tests/components/cast/test_helpers.py @@ -1,5 +1,4 @@ """Tests for the Cast integration helpers.""" -import asyncio from aiohttp import client_exceptions import pytest @@ -141,7 +140,7 @@ async def test_parse_bad_playlist( @pytest.mark.parametrize( ("url", "exc"), ( - ("http://sverigesradio.se/164-hi-aac.pls", asyncio.TimeoutError), + ("http://sverigesradio.se/164-hi-aac.pls", TimeoutError), ("http://sverigesradio.se/164-hi-aac.pls", client_exceptions.ClientError), ), ) diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index 800a3ce54da..1e72e708d44 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -1,5 +1,4 @@ """Tests for the Cert Expiry config flow.""" -import asyncio import socket import ssl from unittest.mock import patch @@ -210,7 +209,7 @@ async def test_abort_on_socket_failed(hass: HomeAssistant) -> None: with patch( "homeassistant.components.cert_expiry.helper.async_get_cert", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index 471ecc119a9..14f99fe0fb1 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -160,7 +160,7 @@ async def test_get_services_error(hass: HomeAssistant) -> None: with patch.object(account_link, "CACHE_TIMEOUT", 0), patch( "hass_nabucasa.account_link.async_fetch_available_services", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): assert await account_link._get_services(hass) == [] assert account_link.DATA_SERVICES not in hass.data diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 4602c054392..78b06874d6d 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -1,5 +1,4 @@ """Tests for the HTTP API for the cloud component.""" -import asyncio from copy import deepcopy from http import HTTPStatus from typing import Any @@ -346,7 +345,7 @@ async def test_login_view_request_timeout( ) -> None: """Test request timeout while trying to log in.""" cloud_client = await hass_client() - cloud.login.side_effect = asyncio.TimeoutError + cloud.login.side_effect = TimeoutError req = await cloud_client.post( "/api/cloud/login", json={"email": "my_username", "password": "my_password"} @@ -409,7 +408,7 @@ async def test_logout_view_request_timeout( ) -> None: """Test timeout while logging out.""" cloud_client = await hass_client() - cloud.logout.side_effect = asyncio.TimeoutError + cloud.logout.side_effect = TimeoutError req = await cloud_client.post("/api/cloud/logout") @@ -524,7 +523,7 @@ async def test_register_view_request_timeout( ) -> None: """Test timeout while registering.""" cloud_client = await hass_client() - cloud.auth.async_register.side_effect = asyncio.TimeoutError + cloud.auth.async_register.side_effect = TimeoutError req = await cloud_client.post( "/api/cloud/register", json={"email": "hello@bla.com", "password": "falcon42"} @@ -590,7 +589,7 @@ async def test_forgot_password_view_request_timeout( ) -> None: """Test timeout while forgot password.""" cloud_client = await hass_client() - cloud.auth.async_forgot_password.side_effect = asyncio.TimeoutError + cloud.auth.async_forgot_password.side_effect = TimeoutError req = await cloud_client.post( "/api/cloud/forgot_password", json={"email": "hello@bla.com"} @@ -674,7 +673,7 @@ async def test_resend_confirm_view_request_timeout( ) -> None: """Test timeout while resend confirm.""" cloud_client = await hass_client() - cloud.auth.async_resend_email_confirm.side_effect = asyncio.TimeoutError + cloud.auth.async_resend_email_confirm.side_effect = TimeoutError req = await cloud_client.post( "/api/cloud/resend_confirm", json={"email": "hello@bla.com"} @@ -1400,7 +1399,7 @@ async def test_sync_alexa_entities_timeout( "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" ".async_sync_entities" ), - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): await client.send_json({"id": 5, "type": "cloud/alexa/sync"}) response = await client.receive_json() @@ -1484,7 +1483,7 @@ async def test_thingtalk_convert_timeout( with patch( "homeassistant.components.cloud.http_api.thingtalk.async_convert", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): await client.send_json( {"id": 5, "type": "cloud/thingtalk/convert", "query": "some-data"} diff --git a/tests/components/cloud/test_subscription.py b/tests/components/cloud/test_subscription.py index 9207c1fef2c..c7297e35744 100644 --- a/tests/components/cloud/test_subscription.py +++ b/tests/components/cloud/test_subscription.py @@ -1,5 +1,4 @@ """Test cloud subscription functions.""" -import asyncio from unittest.mock import AsyncMock, Mock from hass_nabucasa import Cloud @@ -33,7 +32,7 @@ async def test_fetching_subscription_with_timeout_error( """Test that we handle timeout error.""" aioclient_mock.get( "https://accounts.nabucasa.com/payments/subscription_info", - exc=asyncio.TimeoutError(), + exc=TimeoutError(), ) assert await async_subscription_info(mocked_cloud) is None @@ -51,7 +50,7 @@ async def test_migrate_paypal_agreement_with_timeout_error( """Test that we handle timeout error.""" aioclient_mock.post( "https://accounts.nabucasa.com/payments/migrate_paypal_agreement", - exc=asyncio.TimeoutError(), + exc=TimeoutError(), ) assert await async_migrate_paypal_agreement(mocked_cloud) is None diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 4d54d7483df..942137e8f6d 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -1,5 +1,4 @@ """Tests for the Daikin config flow.""" -import asyncio from ipaddress import ip_address from unittest.mock import PropertyMock, patch @@ -81,7 +80,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant, mock_daikin) -> None: @pytest.mark.parametrize( ("s_effect", "reason"), [ - (asyncio.TimeoutError, "cannot_connect"), + (TimeoutError, "cannot_connect"), (ClientError, "cannot_connect"), (web_exceptions.HTTPForbidden, "invalid_auth"), (DaikinException, "unknown"), diff --git a/tests/components/daikin/test_init.py b/tests/components/daikin/test_init.py index 857d9e399f4..7c4467c3031 100644 --- a/tests/components/daikin/test_init.py +++ b/tests/components/daikin/test_init.py @@ -1,5 +1,4 @@ """Define tests for the Daikin init.""" -import asyncio from datetime import timedelta from unittest.mock import AsyncMock, PropertyMock, patch @@ -224,7 +223,7 @@ async def test_timeout_error(hass: HomeAssistant, mock_daikin) -> None: ) config_entry.add_to_hass(hass) - mock_daikin.factory.side_effect = asyncio.TimeoutError + mock_daikin.factory.side_effect = TimeoutError await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 1211d4dfa46..7874b7899c8 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -1,5 +1,4 @@ """Tests for deCONZ config flow.""" -import asyncio import logging from unittest.mock import patch @@ -195,7 +194,7 @@ async def test_manual_configuration_after_discovery_timeout( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test failed discovery fallbacks to manual configuration.""" - aioclient_mock.get(pydeconz.utils.URL_DISCOVER, exc=asyncio.TimeoutError) + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, exc=TimeoutError) result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, context={"source": SOURCE_USER} @@ -347,9 +346,7 @@ async def test_manual_configuration_timeout_get_bridge( headers={"content-type": CONTENT_TYPE_JSON}, ) - aioclient_mock.get( - f"http://1.2.3.4:80/api/{API_KEY}/config", exc=asyncio.TimeoutError - ) + aioclient_mock.get(f"http://1.2.3.4:80/api/{API_KEY}/config", exc=TimeoutError) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -363,7 +360,7 @@ async def test_manual_configuration_timeout_get_bridge( ("raised_error", "error_string"), [ (pydeconz.errors.LinkButtonNotPressed, "linking_not_possible"), - (asyncio.TimeoutError, "no_key"), + (TimeoutError, "no_key"), (pydeconz.errors.ResponseError, "no_key"), (pydeconz.errors.RequestError, "no_key"), ], diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index cc5d2520f5d..84a57fe7595 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -1,5 +1,4 @@ """Test deCONZ gateway.""" -import asyncio from copy import deepcopy from unittest.mock import patch @@ -297,7 +296,7 @@ async def test_get_deconz_session(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("side_effect", "raised_exception"), [ - (asyncio.TimeoutError, CannotConnect), + (TimeoutError, CannotConnect), (pydeconz.RequestError, CannotConnect), (pydeconz.ResponseError, CannotConnect), (pydeconz.Unauthorized, AuthenticationRequired), diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 422bfa0c35c..2d44b67e870 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -1,5 +1,4 @@ """Test the DSMR config flow.""" -import asyncio from itertools import chain, repeat import os from typing import Any @@ -388,13 +387,13 @@ async def test_setup_serial_timeout( first_timeout_wait_closed = AsyncMock( return_value=True, - side_effect=chain([asyncio.TimeoutError], repeat(DEFAULT)), + side_effect=chain([TimeoutError], repeat(DEFAULT)), ) protocol.wait_closed = first_timeout_wait_closed first_timeout_wait_closed = AsyncMock( return_value=True, - side_effect=chain([asyncio.TimeoutError], repeat(DEFAULT)), + side_effect=chain([TimeoutError], repeat(DEFAULT)), ) rfxtrx_protocol.wait_closed = first_timeout_wait_closed From 7a89e58873fbe4c69c234f7741ac87d85c76fc11 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 5 Feb 2024 12:00:37 +0100 Subject: [PATCH 0281/1367] Use builtin TimeoutError [e-i] (#109679) --- homeassistant/components/eliqonline/sensor.py | 3 +-- homeassistant/components/elkm1/__init__.py | 4 ++-- homeassistant/components/elkm1/config_flow.py | 3 +-- homeassistant/components/emulated_hue/hue_api.py | 2 +- homeassistant/components/escea/config_flow.py | 2 +- homeassistant/components/evil_genius_labs/config_flow.py | 2 +- homeassistant/components/flick_electric/config_flow.py | 2 +- homeassistant/components/flock/notify.py | 2 +- homeassistant/components/flux_led/const.py | 3 +-- homeassistant/components/foobot/sensor.py | 5 ++--- homeassistant/components/forked_daapd/media_player.py | 4 ++-- homeassistant/components/freedns/__init__.py | 2 +- homeassistant/components/fully_kiosk/config_flow.py | 2 +- homeassistant/components/gardena_bluetooth/__init__.py | 3 +-- homeassistant/components/gios/config_flow.py | 2 +- homeassistant/components/google_assistant/http.py | 3 +-- homeassistant/components/google_assistant/smart_home.py | 2 +- homeassistant/components/google_cloud/tts.py | 2 +- homeassistant/components/google_domains/__init__.py | 2 +- homeassistant/components/govee_light_local/__init__.py | 2 +- .../components/govee_light_local/config_flow.py | 2 +- homeassistant/components/harmony/data.py | 3 +-- homeassistant/components/hassio/handler.py | 2 +- homeassistant/components/hassio/http.py | 3 +-- homeassistant/components/hlk_sw16/config_flow.py | 2 +- .../components/homeassistant_alerts/__init__.py | 3 +-- homeassistant/components/homekit_controller/__init__.py | 4 ++-- homeassistant/components/homekit_controller/const.py | 3 +-- homeassistant/components/honeywell/__init__.py | 3 +-- homeassistant/components/honeywell/climate.py | 5 ++--- homeassistant/components/honeywell/config_flow.py | 5 ++--- homeassistant/components/hue/bridge.py | 2 +- homeassistant/components/hue/config_flow.py | 2 +- homeassistant/components/huisbaasje/__init__.py | 2 +- .../components/hunterdouglas_powerview/const.py | 3 +-- .../components/hunterdouglas_powerview/cover.py | 2 +- homeassistant/components/ialarm/__init__.py | 2 +- homeassistant/components/iammeter/sensor.py | 5 ++--- homeassistant/components/iaqualink/__init__.py | 3 +-- homeassistant/components/image/__init__.py | 2 +- homeassistant/components/imap/__init__.py | 4 +--- homeassistant/components/imap/config_flow.py | 3 +-- homeassistant/components/imap/coordinator.py | 9 ++++----- homeassistant/components/incomfort/water_heater.py | 3 +-- homeassistant/components/ipma/__init__.py | 2 +- homeassistant/components/ipma/weather.py | 2 +- homeassistant/components/isy994/__init__.py | 2 +- homeassistant/components/izone/config_flow.py | 2 +- tests/components/esphome/test_config_flow.py | 3 +-- tests/components/esphome/test_dashboard.py | 5 ++--- tests/components/esphome/test_update.py | 5 ++--- tests/components/evil_genius_labs/test_config_flow.py | 3 +-- tests/components/flick_electric/test_config_flow.py | 3 +-- tests/components/foobot/test_sensor.py | 5 +---- tests/components/fully_kiosk/test_config_flow.py | 3 +-- tests/components/fully_kiosk/test_init.py | 3 +-- tests/components/generic/test_camera.py | 2 +- tests/components/gogogate2/test_init.py | 3 +-- tests/components/govee_light_local/test_light.py | 3 +-- tests/components/hassio/test_http.py | 3 +-- tests/components/hlk_sw16/test_config_flow.py | 2 +- tests/components/honeywell/test_config_flow.py | 3 +-- tests/components/hue/test_config_flow.py | 3 +-- tests/components/hue/test_light_v1.py | 5 ++--- tests/components/hue/test_sensor_v1.py | 3 +-- .../hunterdouglas_powerview/test_config_flow.py | 9 ++------- tests/components/iaqualink/test_init.py | 3 +-- tests/components/imap/test_config_flow.py | 7 +++---- tests/components/imap/test_init.py | 8 ++++---- 69 files changed, 88 insertions(+), 133 deletions(-) diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py index bea60b94a1c..2a929db4b0a 100644 --- a/homeassistant/components/eliqonline/sensor.py +++ b/homeassistant/components/eliqonline/sensor.py @@ -1,7 +1,6 @@ """Monitors home energy use for the ELIQ Online service.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging @@ -83,5 +82,5 @@ class EliqSensor(SensorEntity): _LOGGER.debug("Updated power from server %d W", self.native_value) except KeyError: _LOGGER.warning("Invalid response from ELIQ Online API") - except (OSError, asyncio.TimeoutError) as error: + except (OSError, TimeoutError) as error: _LOGGER.warning("Could not connect to the ELIQ Online API: %s", error) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index b633e1ae620..c51cb30776a 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -296,7 +296,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, SYNC_TIMEOUT): return False - except asyncio.TimeoutError as exc: + except TimeoutError as exc: raise ConfigEntryNotReady(f"Timed out connecting to {conf[CONF_HOST]}") from exc elk_temp_unit = elk.panel.temperature_units @@ -389,7 +389,7 @@ async def async_wait_for_elk_to_sync( try: async with asyncio.timeout(timeout): await event.wait() - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.debug("Timed out waiting for %s event", name) elk.disconnect() raise diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index ac7fc903330..e8d3f8cb0e4 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Elk-M1 Control integration.""" from __future__ import annotations -import asyncio import logging from typing import Any @@ -244,7 +243,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: info = await validate_input(user_input, self.unique_id) - except asyncio.TimeoutError: + except TimeoutError: return {"base": "cannot_connect"}, None except InvalidAuth: return {CONF_PASSWORD: "invalid_auth"}, None diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 94ac97b6b36..5da2fcab967 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -934,7 +934,7 @@ async def wait_for_state_change_or_timeout( try: async with asyncio.timeout(STATE_CHANGE_WAIT_TIMEOUT): await ev.wait() - except asyncio.TimeoutError: + except TimeoutError: pass finally: unsub() diff --git a/homeassistant/components/escea/config_flow.py b/homeassistant/components/escea/config_flow.py index 8766c30c04a..eb50e7d0fdc 100644 --- a/homeassistant/components/escea/config_flow.py +++ b/homeassistant/components/escea/config_flow.py @@ -31,7 +31,7 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: discovery_service = await async_start_discovery_service(hass) - with suppress(asyncio.TimeoutError): + with suppress(TimeoutError): async with asyncio.timeout(TIMEOUT_DISCOVERY): await controller_ready.wait() diff --git a/homeassistant/components/evil_genius_labs/config_flow.py b/homeassistant/components/evil_genius_labs/config_flow.py index beb16115bd7..ab2e116b2a6 100644 --- a/homeassistant/components/evil_genius_labs/config_flow.py +++ b/homeassistant/components/evil_genius_labs/config_flow.py @@ -63,7 +63,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: info = await validate_input(self.hass, user_input) - except asyncio.TimeoutError: + except TimeoutError: errors["base"] = "timeout" except CannotConnect: errors["base"] = "cannot_connect" diff --git a/homeassistant/components/flick_electric/config_flow.py b/homeassistant/components/flick_electric/config_flow.py index 557d0492320..842706172f1 100644 --- a/homeassistant/components/flick_electric/config_flow.py +++ b/homeassistant/components/flick_electric/config_flow.py @@ -46,7 +46,7 @@ class FlickConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: async with asyncio.timeout(60): token = await auth.async_get_access_token() - except asyncio.TimeoutError as err: + except TimeoutError as err: raise CannotConnect() from err except AuthException as err: raise InvalidAuth() from err diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index 3fdd54dd40d..c5926e3158e 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -58,5 +58,5 @@ class FlockNotificationService(BaseNotificationService): response.status, result, ) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error("Timeout accessing Flock at %s", self._url) diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index 8b42f5f2e0d..08e1d274ea7 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -1,6 +1,5 @@ """Constants of the FluxLed/MagicHome Integration.""" -import asyncio import socket from typing import Final @@ -38,7 +37,7 @@ DEFAULT_EFFECT_SPEED: Final = 50 FLUX_LED_DISCOVERY: Final = "flux_led_discovery" FLUX_LED_EXCEPTIONS: Final = ( - asyncio.TimeoutError, + TimeoutError, socket.error, RuntimeError, BrokenPipeError, diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index a865dd33053..0af1206dbd3 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -1,7 +1,6 @@ """Support for the Foobot indoor air quality monitor.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging from typing import Any @@ -118,7 +117,7 @@ async def async_setup_platform( ) except ( aiohttp.client_exceptions.ClientConnectorError, - asyncio.TimeoutError, + TimeoutError, FoobotClient.TooManyRequests, FoobotClient.InternalError, ) as err: @@ -175,7 +174,7 @@ class FoobotData: ) except ( aiohttp.client_exceptions.ClientConnectorError, - asyncio.TimeoutError, + TimeoutError, self._client.TooManyRequests, self._client.InternalError, ): diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 48c2be07c76..df12de944ae 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -668,7 +668,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): try: async with asyncio.timeout(CALLBACK_TIMEOUT): await self._paused_event.wait() # wait for paused - except asyncio.TimeoutError: + except TimeoutError: self._pause_requested = False self._paused_event.clear() @@ -764,7 +764,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): async with asyncio.timeout(TTS_TIMEOUT): await self._tts_playing_event.wait() # we have started TTS, now wait for completion - except asyncio.TimeoutError: + except TimeoutError: self._tts_requested = False _LOGGER.warning("TTS request timed out") await asyncio.sleep( diff --git a/homeassistant/components/freedns/__init__.py b/homeassistant/components/freedns/__init__.py index e65856e03f4..feb1fb9fed9 100644 --- a/homeassistant/components/freedns/__init__.py +++ b/homeassistant/components/freedns/__init__.py @@ -96,7 +96,7 @@ async def _update_freedns(hass, session, url, auth_token): except aiohttp.ClientError: _LOGGER.warning("Can't connect to FreeDNS API") - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning("Timeout from FreeDNS API at %s", url) return False diff --git a/homeassistant/components/fully_kiosk/config_flow.py b/homeassistant/components/fully_kiosk/config_flow.py index 4f9dadd6901..00eb1dd7101 100644 --- a/homeassistant/components/fully_kiosk/config_flow.py +++ b/homeassistant/components/fully_kiosk/config_flow.py @@ -58,7 +58,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except ( ClientConnectorError, FullyKioskError, - asyncio.TimeoutError, + TimeoutError, ) as error: LOGGER.debug(error.args, exc_info=True) errors["base"] = "cannot_connect" diff --git a/homeassistant/components/gardena_bluetooth/__init__.py b/homeassistant/components/gardena_bluetooth/__init__.py index df41b0a1c43..99c8fa69acf 100644 --- a/homeassistant/components/gardena_bluetooth/__init__.py +++ b/homeassistant/components/gardena_bluetooth/__init__.py @@ -1,7 +1,6 @@ """The Gardena Bluetooth integration.""" from __future__ import annotations -import asyncio import logging from bleak.backends.device import BLEDevice @@ -60,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) uuids = await client.get_all_characteristics_uuid() await client.update_timestamp(dt_util.now()) - except (asyncio.TimeoutError, CommunicationFailure, DeviceUnavailable) as exception: + except (TimeoutError, CommunicationFailure, DeviceUnavailable) as exception: await client.disconnect() raise ConfigEntryNotReady( f"Unable to connect to device {address} due to {exception}" diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py index ffc34bd2b78..1595b7ad131 100644 --- a/homeassistant/components/gios/config_flow.py +++ b/homeassistant/components/gios/config_flow.py @@ -45,7 +45,7 @@ class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): title=gios.station_name, data=user_input, ) - except (ApiError, ClientConnectorError, asyncio.TimeoutError): + except (ApiError, ClientConnectorError, TimeoutError): errors["base"] = "cannot_connect" except NoStationError: errors[CONF_STATION_ID] = "wrong_station_id" diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index c0e4f715c16..226c37fb717 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -1,7 +1,6 @@ """Support for Google Actions Smart Home Control.""" from __future__ import annotations -import asyncio from datetime import timedelta from http import HTTPStatus import logging @@ -216,7 +215,7 @@ class GoogleConfig(AbstractConfig): except ClientResponseError as error: _LOGGER.error("Request for %s failed: %d", url, error.status) return error.status - except (asyncio.TimeoutError, ClientError): + except (TimeoutError, ClientError): _LOGGER.error("Could not contact %s", url) return HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 7d8cc752342..19f097151d7 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -255,7 +255,7 @@ async def handle_devices_execute( for entity_id, result in zip(executions, execute_results): if result is not None: results[entity_id] = result - except asyncio.TimeoutError: + except TimeoutError: pass final_results = list(results.values()) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 720c7d9aa2b..8f30448ad61 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -292,7 +292,7 @@ class GoogleCloudTTSProvider(Provider): ) return _encoding, response.audio_content - except asyncio.TimeoutError as ex: + except TimeoutError as ex: _LOGGER.error("Timeout for Google Cloud TTS call: %s", ex) except Exception as ex: # pylint: disable=broad-except _LOGGER.exception("Error occurred during Google Cloud TTS call: %s", ex) diff --git a/homeassistant/components/google_domains/__init__.py b/homeassistant/components/google_domains/__init__.py index 52dcdb61e8f..1d420cb1497 100644 --- a/homeassistant/components/google_domains/__init__.py +++ b/homeassistant/components/google_domains/__init__.py @@ -80,7 +80,7 @@ async def _update_google_domains(hass, session, domain, user, password, timeout) except aiohttp.ClientError: _LOGGER.warning("Can't connect to Google Domains API") - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning("Timeout from Google Domains API for domain: %s", domain) return False diff --git a/homeassistant/components/govee_light_local/__init__.py b/homeassistant/components/govee_light_local/__init__.py index ab20f4cefcd..2d4594755c4 100644 --- a/homeassistant/components/govee_light_local/__init__.py +++ b/homeassistant/components/govee_light_local/__init__.py @@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with asyncio.timeout(delay=5): while not coordinator.devices: await asyncio.sleep(delay=1) - except asyncio.TimeoutError as ex: + except TimeoutError as ex: raise ConfigEntryNotReady from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator diff --git a/homeassistant/components/govee_light_local/config_flow.py b/homeassistant/components/govee_light_local/config_flow.py index 8ab14966828..8058668f0ca 100644 --- a/homeassistant/components/govee_light_local/config_flow.py +++ b/homeassistant/components/govee_light_local/config_flow.py @@ -44,7 +44,7 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: async with asyncio.timeout(delay=5): while not controller.devices: await asyncio.sleep(delay=1) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.debug("No devices found") devices_count = len(controller.devices) diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index 44c0fde19c1..f7eb96d6a8f 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -1,7 +1,6 @@ """Harmony data object which contains the Harmony Client.""" from __future__ import annotations -import asyncio from collections.abc import Iterable import logging @@ -121,7 +120,7 @@ class HarmonyData(HarmonySubscriberMixin): connected = False try: connected = await self._client.connect() - except (asyncio.TimeoutError, aioexc.TimeOut) as err: + except (TimeoutError, aioexc.TimeOut) as err: await self._client.close() raise ConfigEntryNotReady( f"{self._name}: Connection timed-out to {self._address}:8088" diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index c3532d553f4..f335c3dc488 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -593,7 +593,7 @@ class HassIO: return await request.json(encoding="utf-8") - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error("Timeout on %s request", command) except aiohttp.ClientError as err: diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 9d72d5842fd..8ba389f9054 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -1,7 +1,6 @@ """HTTP Support for Hass.io.""" from __future__ import annotations -import asyncio from http import HTTPStatus import logging import os @@ -193,7 +192,7 @@ class HassIOView(HomeAssistantView): except aiohttp.ClientError as err: _LOGGER.error("Client error on api %s request %s", path, err) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error("Client timeout error on API request %s", path) raise HTTPBadGateway() diff --git a/homeassistant/components/hlk_sw16/config_flow.py b/homeassistant/components/hlk_sw16/config_flow.py index 01f695ad1a6..6ea5f9d43db 100644 --- a/homeassistant/components/hlk_sw16/config_flow.py +++ b/homeassistant/components/hlk_sw16/config_flow.py @@ -43,7 +43,7 @@ async def validate_input(hass: HomeAssistant, user_input): """Validate the user input allows us to connect.""" try: client = await connect_client(hass, user_input) - except asyncio.TimeoutError as err: + except TimeoutError as err: raise CannotConnect from err try: diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 036eb07e067..f391b990761 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -1,7 +1,6 @@ """The Home Assistant alerts integration.""" from __future__ import annotations -import asyncio import dataclasses from datetime import timedelta import logging @@ -53,7 +52,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: f"https://alerts.home-assistant.io/alerts/{alert.alert_id}.json", timeout=aiohttp.ClientTimeout(total=30), ) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning("Error fetching %s: timeout", alert.filename) continue diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index ed9b8ca4622..1043164c801 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -43,13 +43,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await conn.async_setup() except ( - asyncio.TimeoutError, + TimeoutError, AccessoryNotFoundError, EncryptionError, AccessoryDisconnectedError, ) as ex: del hass.data[KNOWN_DEVICES][conn.unique_id] - with contextlib.suppress(asyncio.TimeoutError): + with contextlib.suppress(TimeoutError): await conn.pairing.close() raise ConfigEntryNotReady from ex diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index cc2c28cb5dc..939657eb8a5 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -1,5 +1,4 @@ """Constants for the homekit_controller component.""" -import asyncio from aiohomekit.exceptions import ( AccessoryDisconnectedError, @@ -108,7 +107,7 @@ CHARACTERISTIC_PLATFORMS = { } STARTUP_EXCEPTIONS = ( - asyncio.TimeoutError, + TimeoutError, AccessoryNotFoundError, EncryptionError, AccessoryDisconnectedError, diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index baabf4ca4d8..f58db72a07e 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -1,5 +1,4 @@ """Support for Honeywell (US) Total Connect Comfort climate systems.""" -import asyncio from dataclasses import dataclass import aiosomecomfort @@ -68,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b aiosomecomfort.device.ConnectionError, aiosomecomfort.device.ConnectionTimeout, aiosomecomfort.device.SomeComfortError, - asyncio.TimeoutError, + TimeoutError, ) as ex: raise ConfigEntryNotReady( "Failed to initialize the Honeywell client: Connection error" diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index efd06ba2905..9d2768334ff 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -1,7 +1,6 @@ """Support for Honeywell (US) Total Connect Comfort climate systems.""" from __future__ import annotations -import asyncio import datetime from typing import Any @@ -508,7 +507,7 @@ class HoneywellUSThermostat(ClimateEntity): AuthError, ClientConnectionError, AscConnectionError, - asyncio.TimeoutError, + TimeoutError, ): self._retry += 1 self._attr_available = self._retry <= RETRY @@ -524,7 +523,7 @@ class HoneywellUSThermostat(ClimateEntity): await _login() return - except (AscConnectionError, ClientConnectionError, asyncio.TimeoutError): + except (AscConnectionError, ClientConnectionError, TimeoutError): self._retry += 1 self._attr_available = self._retry <= RETRY return diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index 43d08ee2294..aeb72899e11 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -1,7 +1,6 @@ """Config flow to configure the honeywell integration.""" from __future__ import annotations -import asyncio from collections.abc import Mapping from typing import Any @@ -61,7 +60,7 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except ( aiosomecomfort.ConnectionError, aiosomecomfort.ConnectionTimeout, - asyncio.TimeoutError, + TimeoutError, ): errors["base"] = "cannot_connect" @@ -93,7 +92,7 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except ( aiosomecomfort.ConnectionError, aiosomecomfort.ConnectionTimeout, - asyncio.TimeoutError, + TimeoutError, ): errors["base"] = "cannot_connect" diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index c5ceebec3f8..abf91cf4577 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -83,7 +83,7 @@ class HueBridge: create_config_flow(self.hass, self.host) return False except ( - asyncio.TimeoutError, + TimeoutError, client_exceptions.ClientOSError, client_exceptions.ServerDisconnectedError, client_exceptions.ContentTypeError, diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 7262dea39ef..a1345cf3bba 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -111,7 +111,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): bridges = await discover_nupnp( websession=aiohttp_client.async_get_clientsession(self.hass) ) - except asyncio.TimeoutError: + except TimeoutError: return self.async_abort(reason="discover_timeout") if bridges: diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index b1c2d865e0c..9ea4b547596 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -84,7 +84,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_huisbaasje(energyflip: EnergyFlip) -> dict[str, dict[str, Any]]: """Update the data by performing a request to Huisbaasje.""" try: - # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # Note: TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with asyncio.timeout(FETCH_TIMEOUT): if not energyflip.is_authenticated(): diff --git a/homeassistant/components/hunterdouglas_powerview/const.py b/homeassistant/components/hunterdouglas_powerview/const.py index 7dd4c229c48..319ea0c5b73 100644 --- a/homeassistant/components/hunterdouglas_powerview/const.py +++ b/homeassistant/components/hunterdouglas_powerview/const.py @@ -1,6 +1,5 @@ """Support for Powerview scenes from a Powerview hub.""" -import asyncio from aiohttp.client_exceptions import ServerDisconnectedError from aiopvapi.helpers.aiorequest import PvApiConnectionError, PvApiResponseStatusError @@ -53,7 +52,7 @@ STATE_ATTRIBUTE_ROOM_NAME = "roomName" HUB_EXCEPTIONS = ( ServerDisconnectedError, - asyncio.TimeoutError, + TimeoutError, PvApiConnectionError, PvApiResponseStatusError, ) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 6d050bc1dbd..f9920c26f3a 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -82,7 +82,7 @@ async def async_setup_entry( # so we force a refresh when we add it if possible shade: BaseShade = PvShade(raw_shade, pv_entry.api) name_before_refresh = shade.name - with suppress(asyncio.TimeoutError): + with suppress(TimeoutError): async with asyncio.timeout(1): await shade.refresh() diff --git a/homeassistant/components/ialarm/__init__.py b/homeassistant/components/ialarm/__init__.py index 1b821025953..ff54c02a2d4 100644 --- a/homeassistant/components/ialarm/__init__.py +++ b/homeassistant/components/ialarm/__init__.py @@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: async with asyncio.timeout(10): mac = await hass.async_add_executor_job(ialarm.get_mac) - except (asyncio.TimeoutError, ConnectionError) as ex: + except (TimeoutError, ConnectionError) as ex: raise ConfigEntryNotReady from ex coordinator = IAlarmDataUpdateCoordinator(hass, ialarm, mac) diff --git a/homeassistant/components/iammeter/sensor.py b/homeassistant/components/iammeter/sensor.py index df3a873b6c1..3537737f122 100644 --- a/homeassistant/components/iammeter/sensor.py +++ b/homeassistant/components/iammeter/sensor.py @@ -1,7 +1,6 @@ """Support for iammeter via local API.""" from __future__ import annotations -import asyncio from asyncio import timeout from collections.abc import Callable from dataclasses import dataclass @@ -117,7 +116,7 @@ async def async_setup_platform( api = await hass.async_add_executor_job( IamMeter, config_host, config_port, config_name ) - except asyncio.TimeoutError as err: + except TimeoutError as err: _LOGGER.error("Device is not ready") raise PlatformNotReady from err @@ -125,7 +124,7 @@ async def async_setup_platform( try: async with timeout(PLATFORM_TIMEOUT): return await hass.async_add_executor_job(api.client.get_data) - except asyncio.TimeoutError as err: + except TimeoutError as err: raise UpdateFailed from err coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 062548666c4..49eaa2b24a5 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -1,7 +1,6 @@ """Component to embed Aqualink devices.""" from __future__ import annotations -import asyncio from collections.abc import Awaitable, Callable, Coroutine from datetime import datetime from functools import wraps @@ -79,7 +78,7 @@ async def async_setup_entry( # noqa: C901 _LOGGER.error("Failed to login: %s", login_exception) await aqualink.close() return False - except (asyncio.TimeoutError, httpx.HTTPError) as aio_exception: + except (TimeoutError, httpx.HTTPError) as aio_exception: await aqualink.close() raise ConfigEntryNotReady( f"Error while attempting login: {aio_exception}" diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index 4c5a9df8810..17ceac9c8db 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -75,7 +75,7 @@ def valid_image_content_type(content_type: str | None) -> str: async def _async_get_image(image_entity: ImageEntity, timeout: int) -> Image: """Fetch image from an image entity.""" - with suppress(asyncio.CancelledError, asyncio.TimeoutError, ImageContentTypeError): + with suppress(asyncio.CancelledError, TimeoutError, ImageContentTypeError): async with asyncio.timeout(timeout): if image_bytes := await image_entity.async_image(): content_type = valid_image_content_type(image_entity.content_type) diff --git a/homeassistant/components/imap/__init__.py b/homeassistant/components/imap/__init__.py index fea2583a27a..924408c30b9 100644 --- a/homeassistant/components/imap/__init__.py +++ b/homeassistant/components/imap/__init__.py @@ -1,8 +1,6 @@ """The imap integration.""" from __future__ import annotations -import asyncio - from aioimaplib import IMAP4_SSL, AioImapException from homeassistant.config_entries import ConfigEntry @@ -33,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryAuthFailed from err except InvalidFolder as err: raise ConfigEntryError("Selected mailbox folder is invalid.") from err - except (asyncio.TimeoutError, AioImapException) as err: + except (TimeoutError, AioImapException) as err: raise ConfigEntryNotReady from err coordinator_class: type[ diff --git a/homeassistant/components/imap/config_flow.py b/homeassistant/components/imap/config_flow.py index dea7a0e2e71..15b52ce6333 100644 --- a/homeassistant/components/imap/config_flow.py +++ b/homeassistant/components/imap/config_flow.py @@ -1,7 +1,6 @@ """Config flow for imap integration.""" from __future__ import annotations -import asyncio from collections.abc import Mapping import ssl from typing import Any @@ -108,7 +107,7 @@ async def validate_input( # See https://github.com/bamthomas/aioimaplib/issues/91 # This handler is added to be able to supply a better error message errors["base"] = "ssl_error" - except (asyncio.TimeoutError, AioImapException, ConnectionRefusedError): + except (TimeoutError, AioImapException, ConnectionRefusedError): errors["base"] = "cannot_connect" else: if result != "OK": diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index 2941b65be5c..3b2a3601eec 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -347,7 +347,7 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): await self.imap_client.stop_wait_server_push() await self.imap_client.close() await self.imap_client.logout() - except (AioImapException, asyncio.TimeoutError): + except (AioImapException, TimeoutError): if log_error: _LOGGER.debug("Error while cleaning up imap connection") finally: @@ -379,7 +379,7 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator): except ( AioImapException, UpdateFailed, - asyncio.TimeoutError, + TimeoutError, ) as ex: await self._cleanup() self.async_set_update_error(ex) @@ -451,7 +451,7 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): except ( UpdateFailed, AioImapException, - asyncio.TimeoutError, + TimeoutError, ) as ex: await self._cleanup() self.async_set_update_error(ex) @@ -467,8 +467,7 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): async with asyncio.timeout(10): await idle - # From python 3.11 asyncio.TimeoutError is an alias of TimeoutError - except (AioImapException, asyncio.TimeoutError): + except (AioImapException, TimeoutError): _LOGGER.debug( "Lost %s (will attempt to reconnect after %s s)", self.config_entry.data[CONF_SERVER], diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index f906270b2f5..367af73810b 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -1,7 +1,6 @@ """Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway.""" from __future__ import annotations -import asyncio import logging from typing import Any @@ -101,7 +100,7 @@ class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): try: await self._heater.update() - except (ClientResponseError, asyncio.TimeoutError) as err: + except (ClientResponseError, TimeoutError) as err: _LOGGER.warning("Update failed, message is: %s", err) else: diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 4cb8f921ba4..7668802c9e0 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: async with asyncio.timeout(30): location = await Location.get(api, float(latitude), float(longitude)) - except (IPMAException, asyncio.TimeoutError) as err: + except (IPMAException, TimeoutError) as err: raise ConfigEntryNotReady( f"Could not get location for ({latitude},{longitude})" ) from err diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index f9b93cbe954..866f44f0617 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -217,7 +217,7 @@ class IPMAWeather(WeatherEntity, IPMADevice): period: int, ) -> None: """Try to update weather forecast.""" - with contextlib.suppress(asyncio.TimeoutError): + with contextlib.suppress(TimeoutError): async with asyncio.timeout(10): await self._update_forecast(forecast_type, period, False) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index c611bf83050..0c5ea27a0b9 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -102,7 +102,7 @@ async def async_setup_entry( try: async with asyncio.timeout(60): await isy.initialize() - except asyncio.TimeoutError as err: + except TimeoutError as err: raise ConfigEntryNotReady( "Timed out initializing the ISY; device may be busy, trying again later:" f" {err}" diff --git a/homeassistant/components/izone/config_flow.py b/homeassistant/components/izone/config_flow.py index 8e6fe584456..d56fb93d4e6 100644 --- a/homeassistant/components/izone/config_flow.py +++ b/homeassistant/components/izone/config_flow.py @@ -25,7 +25,7 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: disco = await async_start_discovery_service(hass) - with suppress(asyncio.TimeoutError): + with suppress(TimeoutError): async with asyncio.timeout(TIMEOUT_DISCOVERY): await controller_ready.wait() diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 4161e69efd0..04f8dc9e5aa 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1,5 +1,4 @@ """Test config flow.""" -import asyncio from ipaddress import ip_address import json from unittest.mock import AsyncMock, MagicMock, patch @@ -500,7 +499,7 @@ async def test_user_discovers_name_and_dashboard_is_unavailable( with patch( "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): await dashboard.async_get_dashboard(hass).async_refresh() result = await hass.config_entries.flow.async_init( diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index 320b20832c8..11db8a73731 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -1,5 +1,4 @@ """Test ESPHome dashboard features.""" -import asyncio from unittest.mock import patch from aioesphomeapi import DeviceInfo, InvalidAuthAPIError @@ -69,7 +68,7 @@ async def test_setup_dashboard_fails( ) -> MockConfigEntry: """Test that nothing is stored on failed dashboard setup when there was no dashboard before.""" with patch.object( - dashboard.ESPHomeDashboardAPI, "get_devices", side_effect=asyncio.TimeoutError + dashboard.ESPHomeDashboardAPI, "get_devices", side_effect=TimeoutError ) as mock_get_devices: await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -99,7 +98,7 @@ async def test_setup_dashboard_fails_when_already_setup( await hass.async_block_till_done() with patch.object( - dashboard.ESPHomeDashboardAPI, "get_devices", side_effect=asyncio.TimeoutError + dashboard.ESPHomeDashboardAPI, "get_devices", side_effect=TimeoutError ) as mock_get_devices, patch( "homeassistant.components.esphome.async_setup_entry", return_value=True ) as mock_setup: diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index d267a13145f..842480d9433 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -1,5 +1,4 @@ """Test ESPHome update entities.""" -import asyncio from collections.abc import Awaitable, Callable import dataclasses from unittest.mock import Mock, patch @@ -280,7 +279,7 @@ async def test_update_entity_dashboard_not_available_startup( return_value=Mock(available=True, device_info=mock_device_info), ), patch( "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): await async_get_dashboard(hass).async_refresh() assert await hass.config_entries.async_forward_entry_setup( @@ -324,7 +323,7 @@ async def test_update_entity_dashboard_discovered_after_startup_but_update_faile """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, + side_effect=TimeoutError, ): await async_get_dashboard(hass).async_refresh() await hass.async_block_till_done() diff --git a/tests/components/evil_genius_labs/test_config_flow.py b/tests/components/evil_genius_labs/test_config_flow.py index c3d3c57d324..7826104b326 100644 --- a/tests/components/evil_genius_labs/test_config_flow.py +++ b/tests/components/evil_genius_labs/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Evil Genius Labs config flow.""" -import asyncio from unittest.mock import patch import aiohttp @@ -82,7 +81,7 @@ async def test_form_timeout(hass: HomeAssistant) -> None: with patch( "pyevilgenius.EvilGeniusDevice.get_all", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py index 9f212b2a1a9..bd77f1b6002 100644 --- a/tests/components/flick_electric/test_config_flow.py +++ b/tests/components/flick_electric/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Flick Electric config flow.""" -import asyncio from unittest.mock import patch from pyflick.authentication import AuthException @@ -86,7 +85,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" with patch( "homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result = await _flow_submit(hass) diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index 59fcde57a06..27e56816176 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -1,5 +1,4 @@ """The tests for the Foobot sensor platform.""" -import asyncio from http import HTTPStatus import re from unittest.mock import MagicMock @@ -65,9 +64,7 @@ async def test_setup_timeout_error( """Expected failures caused by a timeout in API response.""" fake_async_add_entities = MagicMock() - aioclient_mock.get( - re.compile("api.foobot.io/v2/owner/.*"), exc=asyncio.TimeoutError() - ) + aioclient_mock.get(re.compile("api.foobot.io/v2/owner/.*"), exc=TimeoutError()) with pytest.raises(PlatformNotReady): await foobot.async_setup_platform(hass, VALID_CONFIG, fake_async_add_entities) diff --git a/tests/components/fully_kiosk/test_config_flow.py b/tests/components/fully_kiosk/test_config_flow.py index 018a62b5dc7..b66940f9fc7 100644 --- a/tests/components/fully_kiosk/test_config_flow.py +++ b/tests/components/fully_kiosk/test_config_flow.py @@ -1,6 +1,5 @@ """Test the Fully Kiosk Browser config flow.""" -import asyncio from unittest.mock import AsyncMock, MagicMock, Mock from aiohttp.client_exceptions import ClientConnectorError @@ -67,7 +66,7 @@ async def test_user_flow( [ (FullyKioskError("error", "status"), "cannot_connect"), (ClientConnectorError(None, Mock()), "cannot_connect"), - (asyncio.TimeoutError, "cannot_connect"), + (TimeoutError, "cannot_connect"), (RuntimeError, "unknown"), ], ) diff --git a/tests/components/fully_kiosk/test_init.py b/tests/components/fully_kiosk/test_init.py index 2e77cdb2f1d..e74da6434cd 100644 --- a/tests/components/fully_kiosk/test_init.py +++ b/tests/components/fully_kiosk/test_init.py @@ -1,5 +1,4 @@ """Tests for the Fully Kiosk Browser integration.""" -import asyncio import json from unittest.mock import MagicMock, patch @@ -45,7 +44,7 @@ async def test_load_unload_config_entry( @pytest.mark.parametrize( "side_effect", - [FullyKioskError("error", "status"), asyncio.TimeoutError], + [FullyKioskError("error", "status"), TimeoutError], ) async def test_config_entry_not_ready( hass: HomeAssistant, diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 5a4bae22e9f..ba7f4d3d4a1 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -249,7 +249,7 @@ async def test_limit_refetch( hass.states.async_set("sensor.temp", "5") with pytest.raises(aiohttp.ServerTimeoutError), patch( - "asyncio.timeout", side_effect=asyncio.TimeoutError() + "asyncio.timeout", side_effect=TimeoutError() ): resp = await client.get("/api/camera_proxy/camera.config_test") diff --git a/tests/components/gogogate2/test_init.py b/tests/components/gogogate2/test_init.py index 1cfbf52284f..5c0755bb91b 100644 --- a/tests/components/gogogate2/test_init.py +++ b/tests/components/gogogate2/test_init.py @@ -1,5 +1,4 @@ """Tests for the GogoGate2 component.""" -import asyncio from unittest.mock import MagicMock, patch from ismartgate import GogoGate2Api @@ -94,6 +93,6 @@ async def test_api_failure_on_startup(hass: HomeAssistant) -> None: with patch( "homeassistant.components.gogogate2.common.ISmartGateApi.async_info", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ), pytest.raises(ConfigEntryNotReady): await async_setup_entry(hass, config_entry) diff --git a/tests/components/govee_light_local/test_light.py b/tests/components/govee_light_local/test_light.py index 1e211610d7a..66f471df267 100644 --- a/tests/components/govee_light_local/test_light.py +++ b/tests/components/govee_light_local/test_light.py @@ -1,6 +1,5 @@ """Test Govee light local.""" -import asyncio from unittest.mock import AsyncMock, MagicMock, patch from govee_local_api import GoveeDevice @@ -133,7 +132,7 @@ async def test_light_setup_retry( with patch( "homeassistant.components.govee_light_local.asyncio.timeout", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 2e14c21f20a..4e1e7436a58 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -1,5 +1,4 @@ """The tests for the hassio component.""" -import asyncio from http import HTTPStatus from unittest.mock import patch @@ -396,7 +395,7 @@ async def test_bad_gateway_when_cannot_find_supervisor( hassio_client, aioclient_mock: AiohttpClientMocker ) -> None: """Test we get a bad gateway error if we can't find supervisor.""" - aioclient_mock.get("http://127.0.0.1/app/entrypoint.js", exc=asyncio.TimeoutError) + aioclient_mock.get("http://127.0.0.1/app/entrypoint.js", exc=TimeoutError) resp = await hassio_client.get("/api/hassio/app/entrypoint.js") assert resp.status == HTTPStatus.BAD_GATEWAY diff --git a/tests/components/hlk_sw16/test_config_flow.py b/tests/components/hlk_sw16/test_config_flow.py index 6e90706eade..d390bcc6c79 100644 --- a/tests/components/hlk_sw16/test_config_flow.py +++ b/tests/components/hlk_sw16/test_config_flow.py @@ -189,7 +189,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: with patch( "homeassistant.components.hlk_sw16.config_flow.connect_client", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, return_value=None, ): result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index 25ffa0a6093..b76eb1bf1e4 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -1,5 +1,4 @@ """Tests for honeywell config flow.""" -import asyncio from unittest.mock import MagicMock, patch import aiosomecomfort @@ -213,7 +212,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, client: MagicMock) -> [ aiosomecomfort.device.ConnectionError, aiosomecomfort.device.ConnectionTimeout, - asyncio.TimeoutError, + TimeoutError, ], ) async def test_reauth_flow_connnection_error( diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 51e0a7dde7a..30d2a8c0b42 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -1,5 +1,4 @@ """Tests for Philips Hue config flow.""" -import asyncio from ipaddress import ip_address from unittest.mock import Mock, patch @@ -254,7 +253,7 @@ async def test_flow_timeout_discovery(hass: HomeAssistant) -> None: """Test config flow .""" with patch( "homeassistant.components.hue.config_flow.discover_nupnp", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/hue/test_light_v1.py b/tests/components/hue/test_light_v1.py index c03e04b633d..d1fd9cdc62f 100644 --- a/tests/components/hue/test_light_v1.py +++ b/tests/components/hue/test_light_v1.py @@ -1,5 +1,4 @@ """Philips Hue lights platform tests.""" -import asyncio from unittest.mock import Mock import aiohue @@ -558,8 +557,8 @@ async def test_other_light_update(hass: HomeAssistant, mock_bridge_v1) -> None: async def test_update_timeout(hass: HomeAssistant, mock_bridge_v1) -> None: """Test bridge marked as not available if timeout error during update.""" - mock_bridge_v1.api.lights.update = Mock(side_effect=asyncio.TimeoutError) - mock_bridge_v1.api.groups.update = Mock(side_effect=asyncio.TimeoutError) + mock_bridge_v1.api.lights.update = Mock(side_effect=TimeoutError) + mock_bridge_v1.api.groups.update = Mock(side_effect=TimeoutError) await setup_bridge(hass, mock_bridge_v1) assert len(mock_bridge_v1.mock_requests) == 0 assert len(hass.states.async_all()) == 0 diff --git a/tests/components/hue/test_sensor_v1.py b/tests/components/hue/test_sensor_v1.py index 1edaf18774f..df8c45119df 100644 --- a/tests/components/hue/test_sensor_v1.py +++ b/tests/components/hue/test_sensor_v1.py @@ -1,5 +1,4 @@ """Philips Hue sensors platform tests.""" -import asyncio from unittest.mock import Mock import aiohue @@ -433,7 +432,7 @@ async def test_sensor_removed(hass: HomeAssistant, mock_bridge_v1) -> None: async def test_update_timeout(hass: HomeAssistant, mock_bridge_v1) -> None: """Test bridge marked as not available if timeout error during update.""" - mock_bridge_v1.api.sensors.update = Mock(side_effect=asyncio.TimeoutError) + mock_bridge_v1.api.sensors.update = Mock(side_effect=TimeoutError) await setup_platform(hass, mock_bridge_v1, ["binary_sensor", "sensor"]) assert len(mock_bridge_v1.mock_requests) == 0 assert len(hass.states.async_all()) == 0 diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index 0511e7bf821..5a8017d3276 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Hunter Douglas Powerview config flow.""" -import asyncio from ipaddress import ip_address import json from unittest.mock import AsyncMock, MagicMock, patch @@ -191,9 +190,7 @@ async def test_form_homekit_and_dhcp_cannot_connect( ) ignored_config_entry.add_to_hass(hass) - mock_powerview_userdata = _get_mock_powerview_userdata( - get_resources=asyncio.TimeoutError - ) + mock_powerview_userdata = _get_mock_powerview_userdata(get_resources=TimeoutError) with patch( "homeassistant.components.hunterdouglas_powerview.UserData", return_value=mock_powerview_userdata, @@ -300,9 +297,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_powerview_userdata = _get_mock_powerview_userdata( - get_resources=asyncio.TimeoutError - ) + mock_powerview_userdata = _get_mock_powerview_userdata(get_resources=TimeoutError) with patch( "homeassistant.components.hunterdouglas_powerview.UserData", return_value=mock_powerview_userdata, diff --git a/tests/components/iaqualink/test_init.py b/tests/components/iaqualink/test_init.py index 646e9e4da86..a1e3ee6ac35 100644 --- a/tests/components/iaqualink/test_init.py +++ b/tests/components/iaqualink/test_init.py @@ -1,5 +1,4 @@ """Tests for iAqualink integration.""" -import asyncio import logging from unittest.mock import AsyncMock, patch @@ -56,7 +55,7 @@ async def test_setup_login_timeout(hass: HomeAssistant, config_entry) -> None: with patch( "homeassistant.components.iaqualink.AqualinkClient.login", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/imap/test_config_flow.py b/tests/components/imap/test_config_flow.py index 0561085823d..177aba04950 100644 --- a/tests/components/imap/test_config_flow.py +++ b/tests/components/imap/test_config_flow.py @@ -1,5 +1,4 @@ """Test the imap config flow.""" -import asyncio import ssl from unittest.mock import AsyncMock, patch @@ -117,7 +116,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("exc", "error"), [ - (asyncio.TimeoutError, "cannot_connect"), + (TimeoutError, "cannot_connect"), (AioImapException(""), "cannot_connect"), (ssl.SSLError, "ssl_error"), ], @@ -306,7 +305,7 @@ async def test_reauth_failed_conn_error(hass: HomeAssistant) -> None: with patch( "homeassistant.components.imap.config_flow.connect_to_server", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -520,7 +519,7 @@ async def test_config_flow_from_with_advanced_settings( with patch( "homeassistant.components.imap.config_flow.connect_to_server", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], config diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index 8a8ac88c8aa..9c194bf08a0 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -82,7 +82,7 @@ async def test_entry_startup_and_unload( [ InvalidAuth, InvalidFolder, - asyncio.TimeoutError, + TimeoutError, ], ) async def test_entry_startup_fails( @@ -417,7 +417,7 @@ async def test_late_folder_error( "imap_close", [ AsyncMock(side_effect=AioImapException("Something went wrong")), - AsyncMock(side_effect=asyncio.TimeoutError), + AsyncMock(side_effect=TimeoutError), ], ids=["AioImapException", "TimeoutError"], ) @@ -460,7 +460,7 @@ async def test_handle_cleanup_exception( "imap_wait_server_push_exception", [ AioImapException("Something went wrong"), - asyncio.TimeoutError, + TimeoutError, ], ids=["AioImapException", "TimeoutError"], ) @@ -468,7 +468,7 @@ async def test_lost_connection_with_imap_push( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_imap_protocol: MagicMock, - imap_wait_server_push_exception: AioImapException | asyncio.TimeoutError, + imap_wait_server_push_exception: AioImapException | TimeoutError, ) -> None: """Test error handling when the connection is lost.""" # Mock an error in waiting for a pushed update From a9147cf3dd67ca368f790f1ca5203531b6cc72dd Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 5 Feb 2024 12:08:18 +0100 Subject: [PATCH 0282/1367] Use builtin TimeoutError [k-n] (#109681) --- homeassistant/components/kaiterra/api_data.py | 2 +- .../components/kostal_plenticore/config_flow.py | 3 +-- homeassistant/components/kostal_plenticore/helper.py | 3 +-- .../components/landisgyr_heat_meter/config_flow.py | 2 +- homeassistant/components/laundrify/coordinator.py | 2 +- homeassistant/components/led_ble/__init__.py | 2 +- homeassistant/components/lifx/config_flow.py | 3 +-- homeassistant/components/lifx/coordinator.py | 2 +- homeassistant/components/lifx/light.py | 12 ++++++------ homeassistant/components/lifx/util.py | 6 ++---- homeassistant/components/lifx_cloud/scene.py | 4 ++-- homeassistant/components/logi_circle/__init__.py | 2 +- homeassistant/components/logi_circle/config_flow.py | 2 +- homeassistant/components/lookin/__init__.py | 2 +- homeassistant/components/loqed/__init__.py | 3 +-- homeassistant/components/lutron_caseta/__init__.py | 2 +- .../components/lutron_caseta/config_flow.py | 4 ++-- homeassistant/components/mailbox/__init__.py | 2 +- homeassistant/components/matter/__init__.py | 4 ++-- homeassistant/components/meater/__init__.py | 2 +- homeassistant/components/media_player/__init__.py | 2 +- homeassistant/components/melcloud/__init__.py | 2 +- homeassistant/components/melcloud/config_flow.py | 4 ++-- homeassistant/components/microsoft_face/__init__.py | 2 +- homeassistant/components/mjpeg/camera.py | 8 ++++---- homeassistant/components/mobile_app/notify.py | 2 +- .../components/moehlenhoff_alpha2/config_flow.py | 3 +-- homeassistant/components/mpd/media_player.py | 8 +++----- homeassistant/components/mqtt/client.py | 2 +- homeassistant/components/mqtt/util.py | 2 +- homeassistant/components/mutesync/config_flow.py | 2 +- homeassistant/components/mysensors/gateway.py | 4 ++-- homeassistant/components/nam/__init__.py | 2 +- homeassistant/components/nam/config_flow.py | 8 ++++---- homeassistant/components/netatmo/data_handler.py | 3 +-- homeassistant/components/nexia/__init__.py | 3 +-- homeassistant/components/nexia/config_flow.py | 3 +-- homeassistant/components/nextdns/__init__.py | 2 +- homeassistant/components/nextdns/config_flow.py | 2 +- homeassistant/components/nextdns/switch.py | 3 +-- homeassistant/components/nmap_tracker/__init__.py | 2 +- homeassistant/components/no_ip/__init__.py | 2 +- homeassistant/components/nuki/__init__.py | 2 +- tests/components/kmtronic/test_init.py | 3 +-- tests/components/kmtronic/test_switch.py | 3 +-- .../components/kostal_plenticore/test_config_flow.py | 3 +-- tests/components/logi_circle/test_config_flow.py | 2 +- tests/components/lutron_caseta/test_config_flow.py | 5 ++--- tests/components/media_player/test_init.py | 3 +-- tests/components/melcloud/test_config_flow.py | 5 ++--- tests/components/microsoft_face/test_init.py | 3 +-- .../moehlenhoff_alpha2/test_config_flow.py | 5 +---- tests/components/mqtt/test_init.py | 2 +- tests/components/mutesync/test_config_flow.py | 3 +-- tests/components/nam/test_config_flow.py | 5 ++--- tests/components/nexia/test_config_flow.py | 3 +-- tests/components/nextdns/test_config_flow.py | 3 +-- tests/components/nextdns/test_switch.py | 3 +-- 58 files changed, 80 insertions(+), 108 deletions(-) diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py index 09d470af1de..6ee73b8ace7 100644 --- a/homeassistant/components/kaiterra/api_data.py +++ b/homeassistant/components/kaiterra/api_data.py @@ -54,7 +54,7 @@ class KaiterraApiData: try: async with asyncio.timeout(10): data = await self._api.get_latest_sensor_readings(self._devices) - except (ClientResponseError, ClientConnectorError, asyncio.TimeoutError) as err: + except (ClientResponseError, ClientConnectorError, TimeoutError) as err: _LOGGER.debug("Couldn't fetch data from Kaiterra API: %s", err) self.data = {} async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) diff --git a/homeassistant/components/kostal_plenticore/config_flow.py b/homeassistant/components/kostal_plenticore/config_flow.py index ba8e762763d..8dd3a823570 100644 --- a/homeassistant/components/kostal_plenticore/config_flow.py +++ b/homeassistant/components/kostal_plenticore/config_flow.py @@ -1,5 +1,4 @@ """Config flow for Kostal Plenticore Solar Inverter integration.""" -import asyncio import logging from aiohttp.client_exceptions import ClientError @@ -57,7 +56,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except AuthenticationException as ex: errors[CONF_PASSWORD] = "invalid_auth" _LOGGER.error("Error response: %s", ex) - except (ClientError, asyncio.TimeoutError): + except (ClientError, TimeoutError): errors[CONF_HOST] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index c3228e1d449..a04415a4f31 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -1,7 +1,6 @@ """Code to handle the Plenticore API.""" from __future__ import annotations -import asyncio from collections import defaultdict from collections.abc import Callable, Mapping from datetime import datetime, timedelta @@ -66,7 +65,7 @@ class Plenticore: "Authentication exception connecting to %s: %s", self.host, err ) return False - except (ClientError, asyncio.TimeoutError) as err: + except (ClientError, TimeoutError) as err: _LOGGER.error("Error connecting to %s", self.host) raise ConfigEntryNotReady from err else: diff --git a/homeassistant/components/landisgyr_heat_meter/config_flow.py b/homeassistant/components/landisgyr_heat_meter/config_flow.py index 7d03ed2efaf..479e7107025 100644 --- a/homeassistant/components/landisgyr_heat_meter/config_flow.py +++ b/homeassistant/components/landisgyr_heat_meter/config_flow.py @@ -108,7 +108,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # validate and retrieve the model and device number for a unique id data = await self.hass.async_add_executor_job(heat_meter.read) - except (asyncio.TimeoutError, serial.SerialException) as err: + except (TimeoutError, serial.SerialException) as err: _LOGGER.warning("Failed read data from: %s. %s", port, err) raise CannotConnect(f"Error communicating with device: {err}") from err diff --git a/homeassistant/components/laundrify/coordinator.py b/homeassistant/components/laundrify/coordinator.py index 121d2cd913f..d67410c6aa3 100644 --- a/homeassistant/components/laundrify/coordinator.py +++ b/homeassistant/components/laundrify/coordinator.py @@ -34,7 +34,7 @@ class LaundrifyUpdateCoordinator(DataUpdateCoordinator[dict[str, LaundrifyDevice async def _async_update_data(self) -> dict[str, LaundrifyDevice]: """Fetch data from laundrify API.""" try: - # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # Note: TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with asyncio.timeout(REQUEST_TIMEOUT): return {m["_id"]: m for m in await self.laundrify_api.get_machines()} diff --git a/homeassistant/components/led_ble/__init__.py b/homeassistant/components/led_ble/__init__.py index 70b77ba6787..27a273ed7b0 100644 --- a/homeassistant/components/led_ble/__init__.py +++ b/homeassistant/components/led_ble/__init__.py @@ -79,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: async with asyncio.timeout(DEVICE_TIMEOUT): await startup_event.wait() - except asyncio.TimeoutError as ex: + except TimeoutError as ex: raise ConfigEntryNotReady( "Unable to communicate with the device; " f"Try moving the Bluetooth adapter closer to {led_ble.name}" diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py index 22ac66e3bc9..b6fd67c0356 100644 --- a/homeassistant/components/lifx/config_flow.py +++ b/homeassistant/components/lifx/config_flow.py @@ -1,7 +1,6 @@ """Config flow flow LIFX.""" from __future__ import annotations -import asyncio import socket from typing import Any @@ -242,7 +241,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): DEFAULT_ATTEMPTS, OVERALL_TIMEOUT, ) - except asyncio.TimeoutError: + except TimeoutError: return None finally: connection.async_stop() diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index e668a7ad79a..18a8a24cb94 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -315,7 +315,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]): """Get updated color information for all zones.""" try: await async_execute_lifx(self.device.get_extended_color_zones) - except asyncio.TimeoutError as ex: + except TimeoutError as ex: raise HomeAssistantError( f"Timeout getting color zones from {self.name}" ) from ex diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index e04e8afb3df..74ed209742c 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -281,7 +281,7 @@ class LIFXLight(LIFXEntity, LightEntity): """Send a power change to the bulb.""" try: await self.coordinator.async_set_power(pwr, duration) - except asyncio.TimeoutError as ex: + except TimeoutError as ex: raise HomeAssistantError(f"Timeout setting power for {self.name}") from ex async def set_color( @@ -294,7 +294,7 @@ class LIFXLight(LIFXEntity, LightEntity): merged_hsbk = merge_hsbk(self.bulb.color, hsbk) try: await self.coordinator.async_set_color(merged_hsbk, duration) - except asyncio.TimeoutError as ex: + except TimeoutError as ex: raise HomeAssistantError(f"Timeout setting color for {self.name}") from ex async def get_color( @@ -303,7 +303,7 @@ class LIFXLight(LIFXEntity, LightEntity): """Send a get color message to the bulb.""" try: await self.coordinator.async_get_color() - except asyncio.TimeoutError as ex: + except TimeoutError as ex: raise HomeAssistantError( f"Timeout setting getting color for {self.name}" ) from ex @@ -429,7 +429,7 @@ class LIFXMultiZone(LIFXColor): await self.coordinator.async_set_color_zones( zone, zone, zone_hsbk, duration, apply ) - except asyncio.TimeoutError as ex: + except TimeoutError as ex: raise HomeAssistantError( f"Timeout setting color zones for {self.name}" ) from ex @@ -444,7 +444,7 @@ class LIFXMultiZone(LIFXColor): """Send a get color zones message to the device.""" try: await self.coordinator.async_get_color_zones() - except asyncio.TimeoutError as ex: + except TimeoutError as ex: raise HomeAssistantError( f"Timeout getting color zones from {self.name}" ) from ex @@ -477,7 +477,7 @@ class LIFXExtendedMultiZone(LIFXMultiZone): await self.coordinator.async_set_extended_color_zones( color_zones, duration=duration ) - except asyncio.TimeoutError as ex: + except TimeoutError as ex: raise HomeAssistantError( f"Timeout setting color zones on {self.name}" ) from ex diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py index feaeba8da8f..5d41839f61d 100644 --- a/homeassistant/components/lifx/util.py +++ b/homeassistant/components/lifx/util.py @@ -202,7 +202,7 @@ async def async_multi_execute_lifx_with_retries( a response again. If we don't get a result after all attempts, we will raise an - asyncio.TimeoutError exception. + TimeoutError exception. """ loop = asyncio.get_running_loop() futures: list[asyncio.Future] = [loop.create_future() for _ in methods] @@ -236,8 +236,6 @@ async def async_multi_execute_lifx_with_retries( if failed: failed_methods = ", ".join(failed) - raise asyncio.TimeoutError( - f"{failed_methods} timed out after {attempts} attempts" - ) + raise TimeoutError(f"{failed_methods} timed out after {attempts} attempts") return results diff --git a/homeassistant/components/lifx_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py index bcf8ed1dc2c..61656741f82 100644 --- a/homeassistant/components/lifx_cloud/scene.py +++ b/homeassistant/components/lifx_cloud/scene.py @@ -50,7 +50,7 @@ async def async_setup_platform( async with asyncio.timeout(timeout): scenes_resp = await httpsession.get(url, headers=headers) - except (asyncio.TimeoutError, aiohttp.ClientError): + except (TimeoutError, aiohttp.ClientError): _LOGGER.exception("Error on %s", url) return @@ -92,5 +92,5 @@ class LifxCloudScene(Scene): async with asyncio.timeout(self._timeout): await httpsession.put(url, headers=self._headers) - except (asyncio.TimeoutError, aiohttp.ClientError): + except (TimeoutError, aiohttp.ClientError): _LOGGER.exception("Error on %s", url) diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index a14cd60c993..abdae4001f3 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -170,7 +170,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: notification_id=NOTIFICATION_ID, ) return False - except asyncio.TimeoutError: + except TimeoutError: # The TimeoutError exception object returns nothing when casted to a # string, so we'll handle it separately. err = f"{_TIMEOUT}s timeout exceeded when connecting to Logi Circle API" diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index 9785940aca2..be22a9a5d30 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -162,7 +162,7 @@ class LogiCircleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except AuthorizationFailed: (self.hass.data[DATA_FLOW_IMPL][DOMAIN][EXTERNAL_ERRORS]) = "invalid_auth" return self.async_abort(reason="external_error") - except asyncio.TimeoutError: + except TimeoutError: ( self.hass.data[DATA_FLOW_IMPL][DOMAIN][EXTERNAL_ERRORS] ) = "authorize_url_timeout" diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py index 37156e9ca08..358ccc5ae37 100644 --- a/homeassistant/components/lookin/__init__.py +++ b/homeassistant/components/lookin/__init__.py @@ -101,7 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: lookin_device = await lookin_protocol.get_info() devices = await lookin_protocol.get_devices() - except (asyncio.TimeoutError, aiohttp.ClientError, NoUsableService) as ex: + except (TimeoutError, aiohttp.ClientError, NoUsableService) as ex: raise ConfigEntryNotReady from ex if entry.unique_id != (found_uuid := lookin_device.id.upper()): diff --git a/homeassistant/components/loqed/__init__.py b/homeassistant/components/loqed/__init__.py index cc43baab1c8..e22987ba426 100644 --- a/homeassistant/components/loqed/__init__.py +++ b/homeassistant/components/loqed/__init__.py @@ -1,7 +1,6 @@ """The loqed integration.""" from __future__ import annotations -import asyncio import logging import re @@ -40,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), ) except ( - asyncio.TimeoutError, + TimeoutError, aiohttp.ClientError, ) as ex: raise ConfigEntryNotReady(f"Unable to connect to bridge at {host}") from ex diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 33cf6f21d6f..0dceada821e 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -171,7 +171,7 @@ async def async_setup_entry( return False timed_out = True - with contextlib.suppress(asyncio.TimeoutError): + with contextlib.suppress(TimeoutError): async with asyncio.timeout(BRIDGE_TIMEOUT): await bridge.connect() timed_out = False diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index 9b243a3ec98..21f7cbd9683 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -117,7 +117,7 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): assets = None try: assets = await async_pair(self.data[CONF_HOST]) - except (asyncio.TimeoutError, OSError): + except (TimeoutError, OSError): errors["base"] = "cannot_connect" if not errors: @@ -227,7 +227,7 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: async with asyncio.timeout(BRIDGE_TIMEOUT): await bridge.connect() - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error( "Timeout while trying to connect to bridge at %s", self.data[CONF_HOST], diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 623d0f06295..9cb52fb161c 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -262,7 +262,7 @@ class MailboxMediaView(MailboxView): """Retrieve media.""" mailbox = self.get_mailbox(platform) - with suppress(asyncio.CancelledError, asyncio.TimeoutError): + with suppress(asyncio.CancelledError, TimeoutError): async with asyncio.timeout(10): try: stream = await mailbox.async_get_media(msgid) diff --git a/homeassistant/components/matter/__init__.py b/homeassistant/components/matter/__init__.py index 3a82e466888..aa89856074f 100644 --- a/homeassistant/components/matter/__init__.py +++ b/homeassistant/components/matter/__init__.py @@ -64,7 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: async with asyncio.timeout(CONNECT_TIMEOUT): await matter_client.connect() - except (CannotConnect, asyncio.TimeoutError) as err: + except (CannotConnect, TimeoutError) as err: raise ConfigEntryNotReady("Failed to connect to matter server") from err except InvalidServerVersion as err: if use_addon: @@ -109,7 +109,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: async with asyncio.timeout(LISTEN_READY_TIMEOUT): await init_ready.wait() - except asyncio.TimeoutError as err: + except TimeoutError as err: listen_task.cancel() raise ConfigEntryNotReady("Matter client not ready") from err diff --git a/homeassistant/components/meater/__init__.py b/homeassistant/components/meater/__init__.py index 12fdb7f3a06..ee2307fbc84 100644 --- a/homeassistant/components/meater/__init__.py +++ b/homeassistant/components/meater/__init__.py @@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_data() -> dict[str, MeaterProbe]: """Fetch data from API endpoint.""" try: - # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # Note: TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with asyncio.timeout(10): devices: list[MeaterProbe] = await meater_api.get_all_devices() diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 673f0a44374..ffb1d6d4a32 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1345,7 +1345,7 @@ async def async_fetch_image( """Retrieve an image.""" content, content_type = (None, None) websession = async_get_clientsession(hass) - with suppress(asyncio.TimeoutError): + with suppress(TimeoutError): async with asyncio.timeout(10): response = await websession.get(url) if response.status == HTTPStatus.OK: diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 2187cb5b8b8..a9e000bb788 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if isinstance(ex, ClientResponseError) and ex.code == 401: raise ConfigEntryAuthFailed from ex raise ConfigEntryNotReady from ex - except (asyncio.TimeoutError, ClientConnectionError) as ex: + except (TimeoutError, ClientConnectionError) as ex: raise ConfigEntryNotReady from ex hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: mel_devices}) diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py index 9db44d5276c..f40aaa25cfd 100644 --- a/homeassistant/components/melcloud/config_flow.py +++ b/homeassistant/components/melcloud/config_flow.py @@ -60,7 +60,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if err.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): return self.async_abort(reason="invalid_auth") return self.async_abort(reason="cannot_connect") - except (asyncio.TimeoutError, ClientError): + except (TimeoutError, ClientError): return self.async_abort(reason="cannot_connect") return await self._create_entry(username, acquired_token) @@ -136,7 +136,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): else: errors["base"] = "cannot_connect" except ( - asyncio.TimeoutError, + TimeoutError, ClientError, ): errors["base"] = "cannot_connect" diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index af0567f99a1..e3f722ae2be 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -334,7 +334,7 @@ class MicrosoftFace: except aiohttp.ClientError: _LOGGER.warning("Can't connect to microsoft face api") - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning("Timeout from microsoft face api %s", response.url) raise HomeAssistantError("Network error on microsoft face api.") diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index a2b2de4eda8..d424df620cf 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -149,7 +149,7 @@ class MjpegCamera(Camera): image = await response.read() return image - except asyncio.TimeoutError: + except TimeoutError: LOGGER.error("Timeout getting camera image from %s", self.name) except aiohttp.ClientError as err: @@ -169,7 +169,7 @@ class MjpegCamera(Camera): try: if self._still_image_url: # Fallback to MJPEG stream if still image URL is not available - with suppress(asyncio.TimeoutError, httpx.HTTPError): + with suppress(TimeoutError, httpx.HTTPError): return ( await client.get( self._still_image_url, auth=auth, timeout=TIMEOUT @@ -183,7 +183,7 @@ class MjpegCamera(Camera): stream.aiter_bytes(BUFFER_SIZE) ) - except asyncio.TimeoutError: + except TimeoutError: LOGGER.error("Timeout getting camera image from %s", self.name) except httpx.HTTPError as err: @@ -201,7 +201,7 @@ class MjpegCamera(Camera): response = web.StreamResponse(headers=stream.headers) await response.prepare(request) # Stream until we are done or client disconnects - with suppress(asyncio.TimeoutError, httpx.HTTPError): + with suppress(TimeoutError, httpx.HTTPError): async for chunk in stream.aiter_bytes(BUFFER_SIZE): if not self.hass.is_running: break diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 164f21af15a..e6f7126b0b8 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -197,7 +197,7 @@ class MobileAppNotificationService(BaseNotificationService): else: _LOGGER.error(message) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error("Timeout sending notification to %s", push_url) except aiohttp.ClientError as err: _LOGGER.error("Error sending notification to %s: %r", push_url, err) diff --git a/homeassistant/components/moehlenhoff_alpha2/config_flow.py b/homeassistant/components/moehlenhoff_alpha2/config_flow.py index d2d14f27552..a4bdfd71cce 100644 --- a/homeassistant/components/moehlenhoff_alpha2/config_flow.py +++ b/homeassistant/components/moehlenhoff_alpha2/config_flow.py @@ -1,5 +1,4 @@ """Alpha2 config flow.""" -import asyncio import logging from typing import Any @@ -27,7 +26,7 @@ async def validate_input(data: dict[str, Any]) -> dict[str, str]: base = Alpha2Base(data[CONF_HOST]) try: await base.update_data() - except (aiohttp.client_exceptions.ClientConnectorError, asyncio.TimeoutError): + except (aiohttp.client_exceptions.ClientConnectorError, TimeoutError): return {"error": "cannot_connect"} except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 9b3adb38e0c..0721afa9d3a 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -128,18 +128,16 @@ class MpdDevice(MediaPlayerEntity): try: async with asyncio.timeout(self._client.timeout + 5): await self._client.connect(self.server, self.port) - except asyncio.TimeoutError as error: + except TimeoutError as error: # TimeoutError has no message (which hinders logging further # down the line), so provide one. - raise asyncio.TimeoutError( - "Connection attempt timed out" - ) from error + raise TimeoutError("Connection attempt timed out") from error if self.password is not None: await self._client.password(self.password) self._is_available = True yield except ( - asyncio.TimeoutError, + TimeoutError, gaierror, mpd.ConnectionError, OSError, diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 2f6c6dc648c..ace3cf9fd64 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -921,7 +921,7 @@ class MQTT: try: async with asyncio.timeout(TIMEOUT_ACK): await self._pending_operations[mid].wait() - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning( "No ACK from MQTT server in %s seconds (mid: %s)", TIMEOUT_ACK, mid ) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index f478ad712d7..fb47bbfc667 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -74,7 +74,7 @@ async def async_wait_for_mqtt_client(hass: HomeAssistant) -> bool: async with asyncio.timeout(AVAILABILITY_TIMEOUT): # Await the client setup or an error state was received return await state_reached_future - except asyncio.TimeoutError: + except TimeoutError: return False diff --git a/homeassistant/components/mutesync/config_flow.py b/homeassistant/components/mutesync/config_flow.py index e06c0b07c87..21bbcfe69bb 100644 --- a/homeassistant/components/mutesync/config_flow.py +++ b/homeassistant/components/mutesync/config_flow.py @@ -32,7 +32,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, if error.status == 403: raise InvalidAuth from error raise CannotConnect from error - except (aiohttp.ClientError, asyncio.TimeoutError) as error: + except (aiohttp.ClientError, TimeoutError) as error: raise CannotConnect from error return token diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 0818d68de2b..28cacbe7762 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -109,7 +109,7 @@ async def try_connect( async with asyncio.timeout(GATEWAY_READY_TIMEOUT): await gateway_ready.wait() return True - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.info("Try gateway connect failed with timeout") return False finally: @@ -301,7 +301,7 @@ async def _gw_start( try: async with asyncio.timeout(GATEWAY_READY_TIMEOUT): await gateway_ready.wait() - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning( "Gateway %s not connected after %s secs so continuing with setup", entry.data[CONF_DEVICE], diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 28f9c282a73..9df1b93a4d7 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: options = ConnectionOptions(host=host, username=username, password=password) try: nam = await NettigoAirMonitor.create(websession, options) - except (ApiError, ClientError, ClientConnectorError, asyncio.TimeoutError) as err: + except (ApiError, ClientError, ClientConnectorError, TimeoutError) as err: raise ConfigEntryNotReady from err try: diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index 7eee84a66a4..8f44c28df3a 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -92,7 +92,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: config = await async_get_config(self.hass, self.host) - except (ApiError, ClientConnectorError, asyncio.TimeoutError): + except (ApiError, ClientConnectorError, TimeoutError): errors["base"] = "cannot_connect" except CannotGetMacError: return self.async_abort(reason="device_unsupported") @@ -128,7 +128,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await async_check_credentials(self.hass, self.host, user_input) except AuthFailedError: errors["base"] = "invalid_auth" - except (ApiError, ClientConnectorError, asyncio.TimeoutError): + except (ApiError, ClientConnectorError, TimeoutError): errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") @@ -155,7 +155,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: self._config = await async_get_config(self.hass, self.host) - except (ApiError, ClientConnectorError, asyncio.TimeoutError): + except (ApiError, ClientConnectorError, TimeoutError): return self.async_abort(reason="cannot_connect") except CannotGetMacError: return self.async_abort(reason="device_unsupported") @@ -209,7 +209,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ApiError, AuthFailedError, ClientConnectorError, - asyncio.TimeoutError, + TimeoutError, ): return self.async_abort(reason="reauth_unsuccessful") diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index bfc77a09548..42d4ced6792 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -1,7 +1,6 @@ """The Netatmo data handler.""" from __future__ import annotations -import asyncio from collections import deque from dataclasses import dataclass from datetime import datetime, timedelta @@ -239,7 +238,7 @@ class NetatmoDataHandler: _LOGGER.debug(err) has_error = True - except (asyncio.TimeoutError, aiohttp.ClientConnectorError) as err: + except (TimeoutError, aiohttp.ClientConnectorError) as err: _LOGGER.debug(err) return True diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 0644de58ee7..f1954eb50b8 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -1,5 +1,4 @@ """Support for Nexia / Trane XL Thermostats.""" -import asyncio import logging import aiohttp @@ -45,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await nexia_home.login() - except asyncio.TimeoutError as ex: + except TimeoutError as ex: raise ConfigEntryNotReady( f"Timed out trying to connect to Nexia service: {ex}" ) from ex diff --git a/homeassistant/components/nexia/config_flow.py b/homeassistant/components/nexia/config_flow.py index de5640beef7..46dc1454a2a 100644 --- a/homeassistant/components/nexia/config_flow.py +++ b/homeassistant/components/nexia/config_flow.py @@ -1,5 +1,4 @@ """Config flow for Nexia integration.""" -import asyncio import logging import aiohttp @@ -57,7 +56,7 @@ async def validate_input(hass: core.HomeAssistant, data): ) try: await nexia_home.login() - except asyncio.TimeoutError as ex: + except TimeoutError as ex: _LOGGER.error("Unable to connect to Nexia service: %s", ex) raise CannotConnect from ex except aiohttp.ClientResponseError as http_ex: diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index ca59c7d0e3a..af972fb7509 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -163,7 +163,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: async with asyncio.timeout(10): nextdns = await NextDns.create(websession, api_key) - except (ApiError, ClientConnectorError, asyncio.TimeoutError) as err: + except (ApiError, ClientConnectorError, TimeoutError) as err: raise ConfigEntryNotReady from err tasks = [] diff --git a/homeassistant/components/nextdns/config_flow.py b/homeassistant/components/nextdns/config_flow.py index c502f788a86..b0a1d936752 100644 --- a/homeassistant/components/nextdns/config_flow.py +++ b/homeassistant/components/nextdns/config_flow.py @@ -43,7 +43,7 @@ class NextDnsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) except InvalidApiKeyError: errors["base"] = "invalid_api_key" - except (ApiError, ClientConnectorError, asyncio.TimeoutError): + except (ApiError, ClientConnectorError, TimeoutError): errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except errors["base"] = "unknown" diff --git a/homeassistant/components/nextdns/switch.py b/homeassistant/components/nextdns/switch.py index 177b4970a93..49c39efce06 100644 --- a/homeassistant/components/nextdns/switch.py +++ b/homeassistant/components/nextdns/switch.py @@ -1,7 +1,6 @@ """Support for the NextDNS service.""" from __future__ import annotations -import asyncio from collections.abc import Callable from dataclasses import dataclass from typing import Any, Generic @@ -647,7 +646,7 @@ class NextDnsSwitch(CoordinatorEntity[NextDnsSettingsUpdateCoordinator], SwitchE except ( ApiError, ClientConnectorError, - asyncio.TimeoutError, + TimeoutError, ClientError, ) as err: raise HomeAssistantError( diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index 3ebbce8361c..7fade3868dc 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -228,7 +228,7 @@ class NmapDeviceScanner: ) ) self._mac_vendor_lookup = AsyncMacLookup() - with contextlib.suppress((asyncio.TimeoutError, aiohttp.ClientError)): + with contextlib.suppress((TimeoutError, aiohttp.ClientError)): # We don't care if this fails since it only # improves the data when we don't have it from nmap await self._mac_vendor_lookup.load_vendors() diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py index e91b5cec92d..8ab277c3def 100644 --- a/homeassistant/components/no_ip/__init__.py +++ b/homeassistant/components/no_ip/__init__.py @@ -114,7 +114,7 @@ async def _update_no_ip( except aiohttp.ClientError: _LOGGER.warning("Can't connect to NO-IP API") - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning("Timeout from NO-IP API for domain: %s", domain) return False diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 41fc4c2e03e..51f5c02d2bf 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -304,7 +304,7 @@ class NukiCoordinator(DataUpdateCoordinator[None]): # pylint: disable=hass-enfo async def _async_update_data(self) -> None: """Fetch data from Nuki bridge.""" try: - # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # Note: TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with asyncio.timeout(10): events = await self.hass.async_add_executor_job( diff --git a/tests/components/kmtronic/test_init.py b/tests/components/kmtronic/test_init.py index c6cc38fe666..a8e9866edaf 100644 --- a/tests/components/kmtronic/test_init.py +++ b/tests/components/kmtronic/test_init.py @@ -1,5 +1,4 @@ """The tests for the KMtronic component.""" -import asyncio from homeassistant.components.kmtronic.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -46,7 +45,7 @@ async def test_config_entry_not_ready( aioclient_mock.get( "http://1.1.1.1/status.xml", - exc=asyncio.TimeoutError(), + exc=TimeoutError(), ) config_entry = MockConfigEntry( diff --git a/tests/components/kmtronic/test_switch.py b/tests/components/kmtronic/test_switch.py index cb72aba2704..fa60dddf1cd 100644 --- a/tests/components/kmtronic/test_switch.py +++ b/tests/components/kmtronic/test_switch.py @@ -1,5 +1,4 @@ """The tests for the KMtronic switch platform.""" -import asyncio from datetime import timedelta from http import HTTPStatus @@ -156,7 +155,7 @@ async def test_failed_update( aioclient_mock.clear_requests() aioclient_mock.get( "http://1.1.1.1/status.xml", - exc=asyncio.TimeoutError(), + exc=TimeoutError(), ) async_fire_time_changed(hass, future) diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index 8bfe227bfdf..d832dbcad47 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Kostal Plenticore Solar Inverter config flow.""" -import asyncio from collections.abc import Generator from unittest.mock import ANY, AsyncMock, MagicMock, patch @@ -212,7 +211,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: # mock of the context manager instance mock_api_ctx = MagicMock() mock_api_ctx.login = AsyncMock( - side_effect=asyncio.TimeoutError(), + side_effect=TimeoutError(), ) # mock of the return instance of ApiClient diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index de4a9bd4da4..830760040e0 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -146,7 +146,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("side_effect", "error"), [ - (asyncio.TimeoutError, "authorize_url_timeout"), + (TimeoutError, "authorize_url_timeout"), (AuthorizationFailed, "invalid_auth"), ], ) diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 631cb0ff1e7..759b23b8f4f 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Lutron Caseta config flow.""" -import asyncio from ipaddress import ip_address from pathlib import Path import ssl @@ -114,7 +113,7 @@ async def test_bridge_cannot_connect_unknown_error(hass: HomeAssistant) -> None: with patch.object(Smartbridge, "create_tls") as create_tls: mock_bridge = MockBridge() - mock_bridge.connect = AsyncMock(side_effect=asyncio.TimeoutError) + mock_bridge.connect = AsyncMock(side_effect=TimeoutError) create_tls.return_value = mock_bridge result = await hass.config_entries.flow.async_init( DOMAIN, @@ -270,7 +269,7 @@ async def test_form_user_pairing_fails(hass: HomeAssistant, tmp_path: Path) -> N with patch( "homeassistant.components.lutron_caseta.config_flow.async_pair", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ), patch( "homeassistant.components.lutron_caseta.async_setup", return_value=True ) as mock_setup, patch( diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index b4228d1ee69..d44ff28c772 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -1,5 +1,4 @@ """Test the base functions of the media player.""" -import asyncio from http import HTTPStatus from unittest.mock import patch @@ -103,7 +102,7 @@ async def test_get_image_http_log_credentials_redacted( state = hass.states.get("media_player.bedroom") assert "entity_picture_local" not in state.attributes - aioclient_mock.get(url, exc=asyncio.TimeoutError()) + aioclient_mock.get(url, exc=TimeoutError()) client = await hass_client_no_auth() diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py index 5e8614a555c..5ca44d4fe46 100644 --- a/tests/components/melcloud/test_config_flow.py +++ b/tests/components/melcloud/test_config_flow.py @@ -1,5 +1,4 @@ """Test the MELCloud config flow.""" -import asyncio from http import HTTPStatus from unittest.mock import patch @@ -75,7 +74,7 @@ async def test_form(hass: HomeAssistant, mock_login, mock_get_devices) -> None: @pytest.mark.parametrize( ("error", "reason"), - [(ClientError(), "cannot_connect"), (asyncio.TimeoutError(), "cannot_connect")], + [(ClientError(), "cannot_connect"), (TimeoutError(), "cannot_connect")], ) async def test_form_errors( hass: HomeAssistant, mock_login, mock_get_devices, error, reason @@ -195,7 +194,7 @@ async def test_token_reauthentication( @pytest.mark.parametrize( ("error", "reason"), [ - (asyncio.TimeoutError(), "cannot_connect"), + (TimeoutError(), "cannot_connect"), (AttributeError(name="get"), "invalid_auth"), ], ) diff --git a/tests/components/microsoft_face/test_init.py b/tests/components/microsoft_face/test_init.py index a33d9fcfdec..affdbb4e932 100644 --- a/tests/components/microsoft_face/test_init.py +++ b/tests/components/microsoft_face/test_init.py @@ -1,5 +1,4 @@ """The tests for the microsoft face platform.""" -import asyncio from unittest.mock import patch import pytest @@ -339,7 +338,7 @@ async def test_service_status_timeout( aioclient_mock.put( ENDPOINT_URL.format("persongroups/service_group"), status=400, - exc=asyncio.TimeoutError(), + exc=TimeoutError(), ) with assert_setup_component(3, mf.DOMAIN): diff --git a/tests/components/moehlenhoff_alpha2/test_config_flow.py b/tests/components/moehlenhoff_alpha2/test_config_flow.py index 4842d648828..7123400365e 100644 --- a/tests/components/moehlenhoff_alpha2/test_config_flow.py +++ b/tests/components/moehlenhoff_alpha2/test_config_flow.py @@ -1,5 +1,4 @@ """Test the moehlenhoff_alpha2 config flow.""" -import asyncio from unittest.mock import patch from homeassistant import config_entries @@ -74,9 +73,7 @@ async def test_form_cannot_connect_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "moehlenhoff_alpha2.Alpha2Base.update_data", side_effect=asyncio.TimeoutError - ): + with patch("moehlenhoff_alpha2.Alpha2Base.update_data", side_effect=TimeoutError): result2 = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input={"host": MOCK_BASE_HOST}, diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index bfbf4e8670c..b5b7a74840e 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -2480,7 +2480,7 @@ async def test_delayed_birth_message( await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): await asyncio.wait_for(birth.wait(), 0.2) assert not mqtt_client_mock.publish.called assert not birth.is_set() diff --git a/tests/components/mutesync/test_config_flow.py b/tests/components/mutesync/test_config_flow.py index b80104d83a5..bc2c739f15a 100644 --- a/tests/components/mutesync/test_config_flow.py +++ b/tests/components/mutesync/test_config_flow.py @@ -1,5 +1,4 @@ """Test the mütesync config flow.""" -import asyncio from unittest.mock import patch import aiohttp @@ -49,7 +48,7 @@ async def test_form(hass: HomeAssistant) -> None: (Exception, "unknown"), (aiohttp.ClientResponseError(None, None, status=403), "invalid_auth"), (aiohttp.ClientResponseError(None, None, status=500), "cannot_connect"), - (asyncio.TimeoutError, "cannot_connect"), + (TimeoutError, "cannot_connect"), ], ) async def test_form_error( diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index a8f1245d9d6..9319eddba81 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -1,5 +1,4 @@ """Define tests for the Nettigo Air Monitor config flow.""" -import asyncio from ipaddress import ip_address from unittest.mock import patch @@ -171,7 +170,7 @@ async def test_reauth_unsuccessful(hass: HomeAssistant) -> None: [ (ApiError("API Error"), "cannot_connect"), (AuthFailedError("Auth Error"), "invalid_auth"), - (asyncio.TimeoutError, "cannot_connect"), + (TimeoutError, "cannot_connect"), (ValueError, "unknown"), ], ) @@ -210,7 +209,7 @@ async def test_form_with_auth_errors(hass: HomeAssistant, error) -> None: "error", [ (ApiError("API Error"), "cannot_connect"), - (asyncio.TimeoutError, "cannot_connect"), + (TimeoutError, "cannot_connect"), (ValueError, "unknown"), ], ) diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index c58197e5fbb..c07b5c8540e 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -1,5 +1,4 @@ """Test the nexia config flow.""" -import asyncio from unittest.mock import MagicMock, patch import aiohttp @@ -81,7 +80,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: with patch( "homeassistant.components.nexia.config_flow.NexiaHome.login", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py index a27898629ad..da7fa131543 100644 --- a/tests/components/nextdns/test_config_flow.py +++ b/tests/components/nextdns/test_config_flow.py @@ -1,5 +1,4 @@ """Define tests for the NextDNS config flow.""" -import asyncio from unittest.mock import patch from nextdns import ApiError, InvalidApiKeyError @@ -53,7 +52,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: [ (ApiError("API Error"), "cannot_connect"), (InvalidApiKeyError, "invalid_api_key"), - (asyncio.TimeoutError, "cannot_connect"), + (TimeoutError, "cannot_connect"), (ValueError, "unknown"), ], ) diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py index 9a360a24b63..ef87b51e98e 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -1,5 +1,4 @@ """Test switch of NextDNS integration.""" -import asyncio from datetime import timedelta from unittest.mock import Mock, patch @@ -717,7 +716,7 @@ async def test_availability(hass: HomeAssistant) -> None: "exc", [ ApiError(Mock()), - asyncio.TimeoutError, + TimeoutError, ClientConnectorError(Mock(), Mock()), ClientError, ], From cd0ee98dba6f2572b18a2607e70cad0b6f20caac Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 5 Feb 2024 12:09:54 +0100 Subject: [PATCH 0283/1367] Use builtin TimeoutError [core + helpers] (#109684) --- homeassistant/bootstrap.py | 8 ++-- homeassistant/core.py | 10 ++-- homeassistant/helpers/aiohttp_client.py | 4 +- .../helpers/config_entry_oauth2_flow.py | 4 +- homeassistant/helpers/entity_platform.py | 4 +- homeassistant/helpers/intent.py | 2 +- homeassistant/helpers/script.py | 6 +-- homeassistant/helpers/template.py | 2 +- homeassistant/helpers/update_coordinator.py | 2 +- homeassistant/setup.py | 2 +- homeassistant/util/location.py | 3 +- homeassistant/util/timeout.py | 4 +- tests/helpers/test_aiohttp_client.py | 3 +- .../helpers/test_config_entry_oauth2_flow.py | 5 +- tests/helpers/test_event.py | 2 +- tests/helpers/test_script.py | 46 +++++++++---------- tests/helpers/test_update_coordinator.py | 3 +- tests/test_core.py | 2 +- tests/util/test_timeout.py | 32 ++++++------- 19 files changed, 70 insertions(+), 74 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index cc3d87319d0..985108fb9f8 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -217,7 +217,7 @@ async def async_setup_hass( ) # Ask integrations to shut down. It's messy but we can't # do a clean stop without knowing what is broken - with contextlib.suppress(asyncio.TimeoutError): + with contextlib.suppress(TimeoutError): async with hass.timeout.async_timeout(10): await hass.async_stop() @@ -738,7 +738,7 @@ async def _async_set_up_integrations( STAGE_1_TIMEOUT, cool_down=COOLDOWN_TIME ): await async_setup_multi_components(hass, stage_1_domains, config) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning("Setup timed out for stage 1 - moving forward") # Add after dependencies when setting up stage 2 domains @@ -751,7 +751,7 @@ async def _async_set_up_integrations( STAGE_2_TIMEOUT, cool_down=COOLDOWN_TIME ): await async_setup_multi_components(hass, stage_2_domains, config) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning("Setup timed out for stage 2 - moving forward") # Wrap up startup @@ -759,7 +759,7 @@ async def _async_set_up_integrations( try: async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME): await hass.async_block_till_done() - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning("Setup timed out for bootstrap - moving forward") watch_task.cancel() diff --git a/homeassistant/core.py b/homeassistant/core.py index f3ef4bc598e..004c0dc1ede 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -875,7 +875,7 @@ class HomeAssistant: tasks.append(task_or_none) if tasks: await asyncio.gather(*tasks, return_exceptions=True) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning( "Timed out waiting for shutdown jobs to complete, the shutdown will" " continue" @@ -906,7 +906,7 @@ class HomeAssistant: try: async with self.timeout.async_timeout(STOP_STAGE_SHUTDOWN_TIMEOUT): await self.async_block_till_done() - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning( "Timed out waiting for integrations to stop, the shutdown will" " continue" @@ -919,7 +919,7 @@ class HomeAssistant: try: async with self.timeout.async_timeout(FINAL_WRITE_STAGE_SHUTDOWN_TIMEOUT): await self.async_block_till_done() - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning( "Timed out waiting for final writes to complete, the shutdown will" " continue" @@ -951,7 +951,7 @@ class HomeAssistant: await task except asyncio.CancelledError: pass - except asyncio.TimeoutError: + except TimeoutError: # Task may be shielded from cancellation. _LOGGER.exception( "Task %s could not be canceled during final shutdown stage", task @@ -971,7 +971,7 @@ class HomeAssistant: try: async with self.timeout.async_timeout(CLOSE_STAGE_SHUTDOWN_TIMEOUT): await self.async_block_till_done() - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning( "Timed out waiting for close event to be processed, the shutdown will" " continue" diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 74527a5922f..cc0be0d5515 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -187,7 +187,7 @@ async def async_aiohttp_proxy_web( # The user cancelled the request return None - except asyncio.TimeoutError as err: + except TimeoutError as err: # Timeout trying to start the web request raise HTTPGatewayTimeout() from err @@ -219,7 +219,7 @@ async def async_aiohttp_proxy_stream( await response.prepare(request) # Suppressing something went wrong fetching data, closed connection - with suppress(asyncio.TimeoutError, aiohttp.ClientError): + with suppress(TimeoutError, aiohttp.ClientError): while hass.is_running: async with asyncio.timeout(timeout): data = await stream.read(buffer_size) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 5b4b803a8d4..f2f3f63b06e 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -294,7 +294,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): try: async with asyncio.timeout(OAUTH_AUTHORIZE_URL_TIMEOUT_SEC): url = await self.async_generate_authorize_url() - except asyncio.TimeoutError as err: + except TimeoutError as err: _LOGGER.error("Timeout generating authorize url: %s", err) return self.async_abort(reason="authorize_url_timeout") except NoURLAvailableError: @@ -320,7 +320,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): token = await self.flow_impl.async_resolve_external_data( self.external_data ) - except asyncio.TimeoutError as err: + except TimeoutError as err: _LOGGER.error("Timeout resolving OAuth token: %s", err) return self.async_abort(reason="oauth_timeout") except (ClientResponseError, ClientError) as err: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 7cf7ab62495..9904883c069 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -370,7 +370,7 @@ class EntityPlatform: EVENT_HOMEASSISTANT_STARTED, setup_again ) return False - except asyncio.TimeoutError: + except TimeoutError: logger.error( ( "Setup of platform %s is taking longer than %s seconds." @@ -513,7 +513,7 @@ class EntityPlatform: try: async with self.hass.timeout.async_timeout(timeout, self.domain): await asyncio.gather(*tasks) - except asyncio.TimeoutError: + except TimeoutError: self.logger.warning( "Timed out adding entities for domain %s with platform %s after %ds", self.domain, diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index fe399659a56..d932332b4c0 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -542,7 +542,7 @@ class ServiceIntentHandler(IntentHandler): """ try: await asyncio.wait({task}, timeout=self.service_timeout) - except asyncio.TimeoutError: + except TimeoutError: pass except asyncio.CancelledError: # Task calling us was cancelled, so cancel service call task, and wait for diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d1546528ef2..f2eee513790 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -595,7 +595,7 @@ class _ScriptRun: try: async with asyncio.timeout(delay): await self._stop.wait() - except asyncio.TimeoutError: + except TimeoutError: trace_set_result(delay=delay, done=True) async def _async_wait_template_step(self): @@ -643,7 +643,7 @@ class _ScriptRun: try: async with asyncio.timeout(timeout) as to_context: await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) - except asyncio.TimeoutError as ex: + except TimeoutError as ex: self._variables["wait"]["remaining"] = 0.0 if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): self._log(_TIMEOUT_MSG) @@ -1023,7 +1023,7 @@ class _ScriptRun: try: async with asyncio.timeout(timeout) as to_context: await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) - except asyncio.TimeoutError as ex: + except TimeoutError as ex: self._variables["wait"]["remaining"] = 0.0 if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): self._log(_TIMEOUT_MSG) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 1bb7220f784..63c24b0f9e9 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -665,7 +665,7 @@ class Template: await finish_event.wait() if self._exc_info: raise TemplateError(self._exc_info[1].with_traceback(self._exc_info[2])) - except asyncio.TimeoutError: + except TimeoutError: template_render_thread.raise_exc(TimeoutError) return True finally: diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index d8631398db7..2a926810ef1 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -312,7 +312,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): try: self.data = await self._async_update_data() - except (asyncio.TimeoutError, requests.exceptions.Timeout) as err: + except (TimeoutError, requests.exceptions.Timeout) as err: self.last_exception = err if self.last_update_success: if log_failures: diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 5408da20a70..99e0a3e2c4b 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -331,7 +331,7 @@ async def _async_setup_component( if task: async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, domain): result = await task - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error( ( "Setup of '%s' is taking longer than %s seconds." diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 9e9c434822e..d2179ecd112 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -4,7 +4,6 @@ detect_location_info and elevation are mocked by default during tests. """ from __future__ import annotations -import asyncio from functools import lru_cache import math from typing import Any, NamedTuple @@ -165,7 +164,7 @@ async def _get_whoami(session: aiohttp.ClientSession) -> dict[str, Any] | None: resp = await session.get( WHOAMI_URL_DEV if HA_VERSION.endswith("0.dev0") else WHOAMI_URL, timeout=30 ) - except (aiohttp.ClientError, asyncio.TimeoutError): + except (aiohttp.ClientError, TimeoutError): return None try: diff --git a/homeassistant/util/timeout.py b/homeassistant/util/timeout.py index e2e969d46d2..33cc0bd18e6 100644 --- a/homeassistant/util/timeout.py +++ b/homeassistant/util/timeout.py @@ -179,7 +179,7 @@ class _GlobalTaskContext: # Timeout on exit if exc_type is asyncio.CancelledError and self.state == _State.TIMEOUT: - raise asyncio.TimeoutError + raise TimeoutError self._state = _State.EXIT self._wait_zone.set() @@ -294,7 +294,7 @@ class _ZoneTaskContext: # Timeout on exit if exc_type is asyncio.CancelledError and self.state == _State.TIMEOUT: - raise asyncio.TimeoutError + raise TimeoutError self._state = _State.EXIT return None diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 46b389722e8..8488a5f15e3 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -1,5 +1,4 @@ """Test the aiohttp client helper.""" -import asyncio from unittest.mock import Mock, patch import aiohttp @@ -249,7 +248,7 @@ async def test_async_aiohttp_proxy_stream_timeout( aioclient_mock: AiohttpClientMocker, camera_client ) -> None: """Test that it fetches the given url.""" - aioclient_mock.get("http://example.com/mjpeg_stream", exc=asyncio.TimeoutError()) + aioclient_mock.get("http://example.com/mjpeg_stream", exc=TimeoutError()) resp = await camera_client.get("/api/camera_proxy_stream/camera.mjpeg_camera") assert resp.status == 504 diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 8c78b7dadc6..5deb88cab43 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -1,5 +1,4 @@ """Tests for the Somfy config flow.""" -import asyncio from http import HTTPStatus import logging import time @@ -143,7 +142,7 @@ async def test_abort_if_authorization_timeout( with patch( "homeassistant.helpers.config_entry_oauth2_flow.asyncio.timeout", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result = await flow.async_step_user() @@ -336,7 +335,7 @@ async def test_abort_on_oauth_timeout_error( with patch( "homeassistant.helpers.config_entry_oauth2_flow.asyncio.timeout", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 245354a09a0..0c2c530eb9f 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -4417,7 +4417,7 @@ async def test_async_call_later_cancel(hass: HomeAssistant) -> None: # fast forward time beyond scheduled async_fire_time_changed_exact(hass, dt_util.utcnow() + timedelta(seconds=delay)) - with contextlib.suppress(asyncio.TimeoutError): + with contextlib.suppress(TimeoutError): async with asyncio.timeout(delay + delay_tolerance): assert await future, "callback not canceled" diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index b0136fdebc9..b2508dc7163 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -654,7 +654,7 @@ async def test_delay_basic(hass: HomeAssistant) -> None: assert script_obj.is_running assert script_obj.last_action == delay_alias - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -695,7 +695,7 @@ async def test_multiple_runs_delay(hass: HomeAssistant) -> None: assert script_obj.is_running assert len(events) == 1 assert events[-1].data["value"] == 1 - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -725,7 +725,7 @@ async def test_delay_template_ok(hass: HomeAssistant) -> None: await asyncio.wait_for(delay_started_flag.wait(), 1) assert script_obj.is_running - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -792,7 +792,7 @@ async def test_delay_template_complex_ok(hass: HomeAssistant) -> None: hass.async_create_task(script_obj.async_run(context=Context())) await asyncio.wait_for(delay_started_flag.wait(), 1) assert script_obj.is_running - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -859,7 +859,7 @@ async def test_cancel_delay(hass: HomeAssistant) -> None: assert script_obj.is_running assert len(events) == 0 - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -908,7 +908,7 @@ async def test_wait_basic(hass: HomeAssistant, action_type) -> None: assert script_obj.is_running assert script_obj.last_action == wait_alias - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -991,7 +991,7 @@ async def test_wait_for_trigger_variables(hass: HomeAssistant) -> None: assert script_obj.last_action == wait_alias hass.states.async_set("switch.test", "off") await hass.async_block_till_done() - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -1028,7 +1028,7 @@ async def test_wait_basic_times_out(hass: HomeAssistant, action_type) -> None: async with asyncio.timeout(0.1): await hass.async_block_till_done() - except asyncio.TimeoutError: + except TimeoutError: timed_out = True await script_obj.async_stop() @@ -1101,7 +1101,7 @@ async def test_multiple_runs_wait(hass: HomeAssistant, action_type) -> None: hass.async_create_task(script_obj.async_run()) await asyncio.wait_for(wait_started_flag.wait(), 1) await asyncio.sleep(0) - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -1142,7 +1142,7 @@ async def test_cancel_wait(hass: HomeAssistant, action_type) -> None: assert script_obj.is_running assert len(events) == 0 - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -1252,7 +1252,7 @@ async def test_wait_timeout( assert script_obj.is_running assert len(events) == 0 - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -1320,7 +1320,7 @@ async def test_wait_continue_on_timeout( assert script_obj.is_running assert len(events) == 0 - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -1363,7 +1363,7 @@ async def test_wait_template_variables_in(hass: HomeAssistant) -> None: await asyncio.wait_for(wait_started_flag.wait(), 1) assert script_obj.is_running - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -1404,7 +1404,7 @@ async def test_wait_template_with_utcnow(hass: HomeAssistant) -> None: match_time = start_time.replace(hour=12) with freeze_time(match_time): async_fire_time_changed(hass, match_time) - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -1444,7 +1444,7 @@ async def test_wait_template_with_utcnow_no_match(hass: HomeAssistant) -> None: async with asyncio.timeout(0.1): await hass.async_block_till_done() - except asyncio.TimeoutError: + except TimeoutError: timed_out = True await script_obj.async_stop() @@ -1505,7 +1505,7 @@ async def test_wait_variables_out(hass: HomeAssistant, mode, action_type) -> Non assert script_obj.is_running assert len(events) == 0 - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -2450,7 +2450,7 @@ async def test_repeat_conditional( wait_started.clear() hass.states.async_set("sensor.test", "done") await asyncio.wait_for(hass.async_block_till_done(), 1) - except asyncio.TimeoutError: + except TimeoutError: await script_obj.async_stop() raise @@ -4069,7 +4069,7 @@ async def test_script_mode_single( assert "Already running" in caplog.text assert script_obj.is_running - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -4204,7 +4204,7 @@ async def test_script_mode_2( ) for message in messages ) - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -4299,7 +4299,7 @@ async def test_script_mode_queued(hass: HomeAssistant) -> None: assert script_obj.runs == 1 assert len(events) == 3 assert events[2].data["value"] == 1 - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -4351,7 +4351,7 @@ async def test_script_mode_queued_cancel(hass: HomeAssistant) -> None: assert not script_obj.is_running assert script_obj.runs == 0 - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise @@ -4412,7 +4412,7 @@ async def test_shutdown_at( assert script_obj.is_running assert script_obj.last_action == delay_alias - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: @@ -4448,7 +4448,7 @@ async def test_shutdown_after( assert script_obj.is_running assert script_obj.last_action == delay_alias - except (AssertionError, asyncio.TimeoutError): + except (AssertionError, TimeoutError): await script_obj.async_stop() raise else: diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 6497382ab9a..1e8ef93b872 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -1,5 +1,4 @@ """Tests for the update coordinator.""" -import asyncio from datetime import timedelta import logging from unittest.mock import AsyncMock, Mock, patch @@ -22,7 +21,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed _LOGGER = logging.getLogger(__name__) KNOWN_ERRORS: list[tuple[Exception, type[Exception], str]] = [ - (asyncio.TimeoutError(), asyncio.TimeoutError, "Timeout fetching test data"), + (TimeoutError(), TimeoutError, "Timeout fetching test data"), ( requests.exceptions.Timeout(), requests.exceptions.Timeout, diff --git a/tests/test_core.py b/tests/test_core.py index efc0b875b4f..eb1e6418476 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -410,7 +410,7 @@ async def test_stage_shutdown(hass: HomeAssistant) -> None: async def test_stage_shutdown_timeouts(hass: HomeAssistant) -> None: """Simulate a shutdown, test timeouts at each step.""" - with patch.object(hass.timeout, "async_timeout", side_effect=asyncio.TimeoutError): + with patch.object(hass.timeout, "async_timeout", side_effect=TimeoutError): await hass.async_stop() assert hass.state is CoreState.stopped diff --git a/tests/util/test_timeout.py b/tests/util/test_timeout.py index f301cd3c634..4f841c27f0d 100644 --- a/tests/util/test_timeout.py +++ b/tests/util/test_timeout.py @@ -13,7 +13,7 @@ async def test_simple_global_timeout() -> None: """Test a simple global timeout.""" timeout = TimeoutManager() - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): async with timeout.async_timeout(0.1): await asyncio.sleep(0.3) @@ -22,7 +22,7 @@ async def test_simple_global_timeout_with_executor_job(hass: HomeAssistant) -> N """Test a simple global timeout with executor job.""" timeout = TimeoutManager() - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): async with timeout.async_timeout(0.1): await hass.async_add_executor_job(lambda: time.sleep(0.2)) @@ -107,7 +107,7 @@ async def test_mix_global_timeout_freeze_and_zone_freeze_other_zone_inside_execu with timeout.freeze("not_recorder"): time.sleep(0.3) - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): async with timeout.async_timeout(0.1): async with timeout.async_timeout( 0.2, zone_name="recorder" @@ -125,7 +125,7 @@ async def test_mix_global_timeout_freeze_and_zone_freeze_inside_executor_job_sec with timeout.freeze("recorder"): time.sleep(0.3) - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): 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) @@ -146,7 +146,7 @@ async def test_simple_global_timeout_freeze_reset() -> None: """Test a simple global timeout freeze reset.""" timeout = TimeoutManager() - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): async with timeout.async_timeout(0.2): async with timeout.async_freeze(): await asyncio.sleep(0.1) @@ -157,7 +157,7 @@ async def test_simple_zone_timeout() -> None: """Test a simple zone timeout.""" timeout = TimeoutManager() - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): async with timeout.async_timeout(0.1, "test"): await asyncio.sleep(0.3) @@ -166,7 +166,7 @@ async def test_multiple_zone_timeout() -> None: """Test a simple zone timeout.""" timeout = TimeoutManager() - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): async with timeout.async_timeout(0.1, "test"): async with timeout.async_timeout(0.5, "test"): await asyncio.sleep(0.3) @@ -176,7 +176,7 @@ async def test_different_zone_timeout() -> None: """Test a simple zone timeout.""" timeout = TimeoutManager() - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): async with timeout.async_timeout(0.1, "test"): async with timeout.async_timeout(0.5, "other"): await asyncio.sleep(0.3) @@ -202,7 +202,7 @@ async def test_simple_zone_timeout_freeze_reset() -> None: """Test a simple zone timeout freeze reset.""" timeout = TimeoutManager() - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): async with timeout.async_timeout(0.2, "test"): async with timeout.async_freeze("test"): await asyncio.sleep(0.1) @@ -242,7 +242,7 @@ async def test_mix_zone_timeout() -> None: timeout = TimeoutManager() async with timeout.async_timeout(0.1): - with suppress(asyncio.TimeoutError): + with suppress(TimeoutError): async with timeout.async_timeout(0.2, "test"): await asyncio.sleep(0.4) @@ -251,9 +251,9 @@ async def test_mix_zone_timeout_trigger_global() -> None: """Test a mix zone timeout global with trigger it.""" timeout = TimeoutManager() - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): async with timeout.async_timeout(0.1): - with suppress(asyncio.TimeoutError): + with suppress(TimeoutError): async with timeout.async_timeout(0.1, "test"): await asyncio.sleep(0.3) @@ -265,7 +265,7 @@ async def test_mix_zone_timeout_trigger_global_cool_down() -> None: timeout = TimeoutManager() async with timeout.async_timeout(0.1, cool_down=0.3): - with suppress(asyncio.TimeoutError): + with suppress(TimeoutError): async with timeout.async_timeout(0.1, "test"): await asyncio.sleep(0.3) @@ -300,7 +300,7 @@ async def test_simple_zone_timeout_freeze_without_timeout_cleanup2( async with timeout.async_freeze("test"): await asyncio.sleep(0.2) - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): async with timeout.async_timeout(0.1): hass.async_create_task(background()) await asyncio.sleep(0.3) @@ -310,7 +310,7 @@ async def test_simple_zone_timeout_freeze_without_timeout_exeption() -> None: """Test a simple zone timeout freeze on a zone that does not have a timeout set.""" timeout = TimeoutManager() - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): async with timeout.async_timeout(0.1): with suppress(RuntimeError): async with timeout.async_freeze("test"): @@ -323,7 +323,7 @@ async def test_simple_zone_timeout_zone_with_timeout_exeption() -> None: """Test a simple zone timeout freeze on a zone that does not have a timeout set.""" timeout = TimeoutManager() - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): async with timeout.async_timeout(0.1): with suppress(RuntimeError): async with timeout.async_timeout(0.3, "test"): From 438d3b01b94b52511989b08ae04d7d03f8501124 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 5 Feb 2024 12:14:37 +0100 Subject: [PATCH 0284/1367] Use builtin TimeoutError [o-s] (#109682) --- homeassistant/components/oncue/const.py | 3 +-- homeassistant/components/onvif/camera.py | 2 +- homeassistant/components/onvif/event.py | 2 +- .../components/openalpr_cloud/image_processing.py | 2 +- .../components/openexchangerates/config_flow.py | 4 ++-- homeassistant/components/openhome/__init__.py | 3 +-- homeassistant/components/openhome/media_player.py | 9 ++++----- homeassistant/components/openhome/update.py | 3 +-- homeassistant/components/opentherm_gw/__init__.py | 2 +- homeassistant/components/opentherm_gw/config_flow.py | 2 +- homeassistant/components/otbr/__init__.py | 4 +--- homeassistant/components/otbr/config_flow.py | 3 +-- homeassistant/components/otbr/silabs_multiprotocol.py | 3 +-- homeassistant/components/picnic/coordinator.py | 2 +- homeassistant/components/ping/helpers.py | 2 +- homeassistant/components/point/config_flow.py | 2 +- homeassistant/components/powerwall/__init__.py | 4 ++-- homeassistant/components/powerwall/config_flow.py | 2 +- homeassistant/components/prowl/notify.py | 2 +- homeassistant/components/prusalink/config_flow.py | 2 +- homeassistant/components/ps4/media_player.py | 3 +-- homeassistant/components/push/camera.py | 2 +- homeassistant/components/qingping/config_flow.py | 3 +-- homeassistant/components/rabbitair/config_flow.py | 3 +-- homeassistant/components/rainbird/config_flow.py | 2 +- homeassistant/components/rainforest_raven/config_flow.py | 4 ++-- homeassistant/components/recorder/core.py | 2 +- homeassistant/components/rest/switch.py | 9 ++++----- homeassistant/components/rest_command/__init__.py | 3 +-- homeassistant/components/rflink/__init__.py | 2 +- homeassistant/components/rfxtrx/__init__.py | 2 +- homeassistant/components/rfxtrx/config_flow.py | 4 ++-- homeassistant/components/roomba/__init__.py | 4 ++-- homeassistant/components/samsungtv/media_player.py | 2 +- homeassistant/components/sense/const.py | 5 ++--- homeassistant/components/sensibo/const.py | 3 +-- homeassistant/components/sharkiq/__init__.py | 2 +- homeassistant/components/sharkiq/config_flow.py | 2 +- homeassistant/components/shell_command/__init__.py | 2 +- homeassistant/components/smart_meter_texas/__init__.py | 3 +-- .../components/smart_meter_texas/config_flow.py | 3 +-- homeassistant/components/smarttub/controller.py | 2 +- homeassistant/components/smhi/weather.py | 2 +- homeassistant/components/snooz/config_flow.py | 2 +- homeassistant/components/somfy_mylink/__init__.py | 3 +-- homeassistant/components/somfy_mylink/config_flow.py | 3 +-- homeassistant/components/songpal/media_player.py | 2 +- homeassistant/components/sonos/__init__.py | 4 ++-- homeassistant/components/sonos/speaker.py | 2 +- homeassistant/components/splunk/__init__.py | 3 +-- homeassistant/components/squeezebox/config_flow.py | 2 +- homeassistant/components/steamist/const.py | 3 +-- homeassistant/components/stream/core.py | 2 +- homeassistant/components/switchbot/coordinator.py | 2 +- homeassistant/components/switcher_kis/button.py | 3 +-- homeassistant/components/switcher_kis/climate.py | 3 +-- homeassistant/components/switcher_kis/cover.py | 3 +-- homeassistant/components/switcher_kis/switch.py | 3 +-- homeassistant/components/system_bridge/__init__.py | 6 +++--- homeassistant/components/system_bridge/config_flow.py | 2 +- homeassistant/components/system_bridge/coordinator.py | 2 +- homeassistant/components/system_health/__init__.py | 4 ++-- tests/components/oncue/test_config_flow.py | 3 +-- tests/components/oncue/test_init.py | 3 +-- tests/components/openalpr_cloud/test_image_processing.py | 3 +-- tests/components/opentherm_gw/test_config_flow.py | 3 +-- tests/components/otbr/test_config_flow.py | 2 +- tests/components/otbr/test_init.py | 2 +- tests/components/peco/test_init.py | 7 +++---- tests/components/point/test_config_flow.py | 3 +-- tests/components/powerwall/test_config_flow.py | 5 ++--- tests/components/prusalink/test_config_flow.py | 3 +-- tests/components/qingping/test_config_flow.py | 3 +-- tests/components/rabbitair/test_config_flow.py | 3 +-- tests/components/rainbird/test_config_flow.py | 3 +-- tests/components/rainforest_raven/test_config_flow.py | 5 ++--- tests/components/recorder/test_init.py | 2 +- tests/components/rest/test_binary_sensor.py | 3 +-- tests/components/rest/test_init.py | 5 ++--- tests/components/rest/test_sensor.py | 3 +-- tests/components/rest_command/test_init.py | 3 +-- tests/components/sensibo/test_config_flow.py | 5 ++--- tests/components/shell_command/test_init.py | 2 +- tests/components/smart_meter_texas/conftest.py | 3 +-- tests/components/smart_meter_texas/test_config_flow.py | 3 +-- tests/components/smarttub/test_init.py | 3 +-- tests/components/smhi/test_weather.py | 3 +-- tests/components/snooz/test_config_flow.py | 3 +-- tests/components/somfy_mylink/test_config_flow.py | 3 +-- tests/components/sonos/test_init.py | 2 +- tests/components/steamist/test_config_flow.py | 3 +-- tests/components/steamist/test_init.py | 3 +-- tests/components/system_bridge/test_config_flow.py | 3 +-- tests/components/system_health/test_init.py | 5 ++--- 94 files changed, 117 insertions(+), 169 deletions(-) diff --git a/homeassistant/components/oncue/const.py b/homeassistant/components/oncue/const.py index 7118944a4ec..599ef5ee22b 100644 --- a/homeassistant/components/oncue/const.py +++ b/homeassistant/components/oncue/const.py @@ -1,6 +1,5 @@ """Constants for the Oncue integration.""" -import asyncio import aiohttp from aiooncue import ServiceFailedException @@ -8,7 +7,7 @@ from aiooncue import ServiceFailedException DOMAIN = "oncue" CONNECTION_EXCEPTIONS = ( - asyncio.TimeoutError, + TimeoutError, aiohttp.ClientError, ServiceFailedException, ) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 013dd2e453f..c6ee74c2c50 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -197,7 +197,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): self._stream_uri_future = loop.create_future() try: uri_no_auth = await self.device.async_get_stream_uri(self.profile) - except (asyncio.TimeoutError, Exception) as err: + except (TimeoutError, Exception) as err: LOGGER.error("Failed to get stream uri: %s", err) if self._stream_uri_future: self._stream_uri_future.set_exception(err) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 603957a230e..c5539818a1c 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -32,7 +32,7 @@ from .parsers import PARSERS # entities for them. UNHANDLED_TOPICS: set[str] = {"tns1:MediaControl/VideoEncoderConfiguration"} -SUBSCRIPTION_ERRORS = (Fault, asyncio.TimeoutError, TransportError) +SUBSCRIPTION_ERRORS = (Fault, TimeoutError, TransportError) CREATE_ERRORS = (ONVIFError, Fault, RequestError, XMLParseError, ValidationError) SET_SYNCHRONIZATION_POINT_ERRORS = (*SUBSCRIPTION_ERRORS, TypeError) UNSUBSCRIBE_ERRORS = (XMLParseError, *SUBSCRIPTION_ERRORS) diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index 64b46a1da94..0dbebda6962 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -209,7 +209,7 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity): _LOGGER.error("Error %d -> %s", request.status, data.get("error")) return - except (asyncio.TimeoutError, aiohttp.ClientError): + except (TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout for OpenALPR API") return diff --git a/homeassistant/components/openexchangerates/config_flow.py b/homeassistant/components/openexchangerates/config_flow.py index b78227ed1e5..0425b44d9e6 100644 --- a/homeassistant/components/openexchangerates/config_flow.py +++ b/homeassistant/components/openexchangerates/config_flow.py @@ -81,7 +81,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" except OpenExchangeRatesClientError: errors["base"] = "cannot_connect" - except asyncio.TimeoutError: + except TimeoutError: errors["base"] = "timeout_connect" except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") @@ -126,6 +126,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.currencies = await client.get_currencies() except OpenExchangeRatesClientError as err: raise AbortFlow("cannot_connect") from err - except asyncio.TimeoutError as err: + except TimeoutError as err: raise AbortFlow("timeout_connect") from err return self.currencies diff --git a/homeassistant/components/openhome/__init__.py b/homeassistant/components/openhome/__init__.py index c7ee5a7d00c..fb03ab214f3 100644 --- a/homeassistant/components/openhome/__init__.py +++ b/homeassistant/components/openhome/__init__.py @@ -1,6 +1,5 @@ """The openhome component.""" -import asyncio import logging import aiohttp @@ -43,7 +42,7 @@ async def async_setup_entry( try: await device.init() - except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError) as exc: + except (TimeoutError, aiohttp.ClientError, UpnpError) as exc: raise ConfigEntryNotReady from exc _LOGGER.debug("Initialised device: %s", device.uuid()) diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 4935af1bc46..25052824ffe 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -1,7 +1,6 @@ """Support for Openhome Devices.""" from __future__ import annotations -import asyncio from collections.abc import Awaitable, Callable, Coroutine import functools import logging @@ -76,7 +75,7 @@ def catch_request_errors() -> ( [_FuncType[_OpenhomeDeviceT, _P, _R]], _ReturnFuncType[_OpenhomeDeviceT, _P, _R] ] ): - """Catch asyncio.TimeoutError, aiohttp.ClientError, UpnpError errors.""" + """Catch TimeoutError, aiohttp.ClientError, UpnpError errors.""" def call_wrapper( func: _FuncType[_OpenhomeDeviceT, _P, _R], @@ -87,10 +86,10 @@ def catch_request_errors() -> ( async def wrapper( self: _OpenhomeDeviceT, *args: _P.args, **kwargs: _P.kwargs ) -> _R | None: - """Catch asyncio.TimeoutError, aiohttp.ClientError, UpnpError errors.""" + """Catch TimeoutError, aiohttp.ClientError, UpnpError errors.""" try: return await func(self, *args, **kwargs) - except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): + except (TimeoutError, aiohttp.ClientError, UpnpError): _LOGGER.error("Error during call %s", func.__name__) return None @@ -186,7 +185,7 @@ class OpenhomeDevice(MediaPlayerEntity): self._attr_state = MediaPlayerState.PLAYING self._attr_available = True - except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): + except (TimeoutError, aiohttp.ClientError, UpnpError): self._attr_available = False @catch_request_errors() diff --git a/homeassistant/components/openhome/update.py b/homeassistant/components/openhome/update.py index 691776e4dfd..6d36bccec65 100644 --- a/homeassistant/components/openhome/update.py +++ b/homeassistant/components/openhome/update.py @@ -1,7 +1,6 @@ """Update entities for Linn devices.""" from __future__ import annotations -import asyncio import logging from typing import Any @@ -93,7 +92,7 @@ class OpenhomeUpdateEntity(UpdateEntity): try: if self.latest_version: await self._device.update_firmware() - except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError) as err: + except (TimeoutError, aiohttp.ClientError, UpnpError) as err: raise HomeAssistantError( f"Error updating {self._device.device.friendly_name}: {err}" ) from err diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index cd8b98880d5..12f4724e056 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -114,7 +114,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: async with asyncio.timeout(CONNECTION_TIMEOUT): await gateway.connect_and_subscribe() - except (asyncio.TimeoutError, ConnectionError, SerialException) as ex: + except (TimeoutError, ConnectionError, SerialException) as ex: await gateway.cleanup() raise ConfigEntryNotReady( f"Could not connect to gateway at {gateway.device_path}: {ex}" diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 07187f3a2ec..70bed0d1665 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -70,7 +70,7 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: async with asyncio.timeout(CONNECTION_TIMEOUT): await test_connection() - except asyncio.TimeoutError: + except TimeoutError: return self._show_form({"base": "timeout_connect"}) except (ConnectionError, SerialException): return self._show_form({"base": "cannot_connect"}) diff --git a/homeassistant/components/otbr/__init__.py b/homeassistant/components/otbr/__init__.py index 3c08a74ed61..fe4cc8c1145 100644 --- a/homeassistant/components/otbr/__init__.py +++ b/homeassistant/components/otbr/__init__.py @@ -1,8 +1,6 @@ """The Open Thread Border Router integration.""" from __future__ import annotations -import asyncio - import aiohttp import python_otbr_api @@ -42,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except ( HomeAssistantError, aiohttp.ClientError, - asyncio.TimeoutError, + TimeoutError, ) as err: raise ConfigEntryNotReady("Unable to connect") from err if border_agent_id is None: diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py index b96e276af8b..0248ffdd079 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -1,7 +1,6 @@ """Config flow for the Open Thread Border Router integration.""" from __future__ import annotations -import asyncio from contextlib import suppress import logging from typing import cast @@ -115,7 +114,7 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): except ( python_otbr_api.OTBRError, aiohttp.ClientError, - asyncio.TimeoutError, + TimeoutError, ): errors["base"] = "cannot_connect" else: diff --git a/homeassistant/components/otbr/silabs_multiprotocol.py b/homeassistant/components/otbr/silabs_multiprotocol.py index 9a462c4610b..bd7eb997558 100644 --- a/homeassistant/components/otbr/silabs_multiprotocol.py +++ b/homeassistant/components/otbr/silabs_multiprotocol.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio import logging import aiohttp @@ -64,7 +63,7 @@ async def async_get_channel(hass: HomeAssistant) -> int | None: except ( HomeAssistantError, aiohttp.ClientError, - asyncio.TimeoutError, + TimeoutError, ) as err: _LOGGER.warning("Failed to communicate with OTBR %s", err) return None diff --git a/homeassistant/components/picnic/coordinator.py b/homeassistant/components/picnic/coordinator.py index 00a9f534852..61af7e5cc91 100644 --- a/homeassistant/components/picnic/coordinator.py +++ b/homeassistant/components/picnic/coordinator.py @@ -42,7 +42,7 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict: """Fetch data from API endpoint.""" try: - # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # Note: TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with asyncio.timeout(10): data = await self.hass.async_add_executor_job(self.fetch_data) diff --git a/homeassistant/components/ping/helpers.py b/homeassistant/components/ping/helpers.py index ce3d5c3b461..e3ebaffec12 100644 --- a/homeassistant/components/ping/helpers.py +++ b/homeassistant/components/ping/helpers.py @@ -141,7 +141,7 @@ class PingDataSubProcess(PingData): assert match is not None rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev} - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.exception( "Timed out running command: `%s`, after: %ss", self._ping_cmd, diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index 201e397ba7d..718e4a831c9 100644 --- a/homeassistant/components/point/config_flow.py +++ b/homeassistant/components/point/config_flow.py @@ -95,7 +95,7 @@ class PointFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: async with asyncio.timeout(10): url = await self._get_authorization_url() - except asyncio.TimeoutError: + except TimeoutError: return self.async_abort(reason="authorize_url_timeout") except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected error generating auth url") diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index d975537ca61..80a8d19cefe 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -89,7 +89,7 @@ class PowerwallDataManager: if attempt == 1: await self._recreate_powerwall_login() data = await _fetch_powerwall_data(self.power_wall) - except (asyncio.TimeoutError, PowerwallUnreachableError) as err: + except (TimeoutError, PowerwallUnreachableError) as err: raise UpdateFailed("Unable to fetch data from powerwall") from err except MissingAttributeError as err: _LOGGER.error("The powerwall api has changed: %s", str(err)) @@ -136,7 +136,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Cancel closing power_wall on success stack.pop_all() - except (asyncio.TimeoutError, PowerwallUnreachableError) as err: + except (TimeoutError, PowerwallUnreachableError) as err: raise ConfigEntryNotReady from err except MissingAttributeError as err: # The error might include some important information about what exactly changed. diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index e86949e2227..2c0d5a3f096 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -166,7 +166,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders: dict[str, str] = {} try: info = await validate_input(self.hass, user_input) - except (PowerwallUnreachableError, asyncio.TimeoutError) as ex: + except (PowerwallUnreachableError, TimeoutError) as ex: errors[CONF_IP_ADDRESS] = "cannot_connect" description_placeholders = {"error": str(ex)} except WrongVersion as ex: diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index d0b35aaf4b9..c365ce151ec 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -73,5 +73,5 @@ class ProwlNotificationService(BaseNotificationService): response.status, result, ) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error("Timeout accessing Prowl at %s", url) diff --git a/homeassistant/components/prusalink/config_flow.py b/homeassistant/components/prusalink/config_flow.py index 378c5e7395a..e4e9b9d719c 100644 --- a/homeassistant/components/prusalink/config_flow.py +++ b/homeassistant/components/prusalink/config_flow.py @@ -50,7 +50,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, async with asyncio.timeout(5): version = await api.get_version() - except (asyncio.TimeoutError, ClientError) as err: + except (TimeoutError, ClientError) as err: _LOGGER.error("Could not connect to PrusaLink: %s", err) raise CannotConnect from err diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index f14ef6ce2aa..722b733c08b 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -1,5 +1,4 @@ """Support for PlayStation 4 consoles.""" -import asyncio from contextlib import suppress import logging from typing import Any, cast @@ -257,7 +256,7 @@ class PS4Device(MediaPlayerEntity): except PSDataIncomplete: title = None - except asyncio.TimeoutError: + except TimeoutError: title = None _LOGGER.error("PS Store Search Timed out") diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index a4fec1c3d4d..2cedcb8598a 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -75,7 +75,7 @@ async def handle_webhook(hass, webhook_id, request): try: async with asyncio.timeout(5): data = dict(await request.post()) - except (asyncio.TimeoutError, aiohttp.web.HTTPException) as error: + except (TimeoutError, aiohttp.web.HTTPException) as error: _LOGGER.error("Could not get information from POST <%s>", error) return diff --git a/homeassistant/components/qingping/config_flow.py b/homeassistant/components/qingping/config_flow.py index 5b7837a9694..a90085afb4f 100644 --- a/homeassistant/components/qingping/config_flow.py +++ b/homeassistant/components/qingping/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Qingping integration.""" from __future__ import annotations -import asyncio from typing import Any from qingping_ble import QingpingBluetoothDeviceData as DeviceData @@ -62,7 +61,7 @@ class QingpingConfigFlow(ConfigFlow, domain=DOMAIN): self._discovery_info = await self._async_wait_for_full_advertisement( discovery_info, device ) - except asyncio.TimeoutError: + except TimeoutError: return self.async_abort(reason="not_supported") self._discovery_info = discovery_info self._discovered_device = device diff --git a/homeassistant/components/rabbitair/config_flow.py b/homeassistant/components/rabbitair/config_flow.py index 70cd07f4d91..e265740179d 100644 --- a/homeassistant/components/rabbitair/config_flow.py +++ b/homeassistant/components/rabbitair/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Rabbit Air integration.""" from __future__ import annotations -import asyncio import logging from typing import Any @@ -36,7 +35,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, except ValueError as err: # Most likely caused by the invalid access token. raise InvalidAccessToken from err - except asyncio.TimeoutError as err: + except TimeoutError as err: # Either the host doesn't respond or the auth failed. raise TimeoutConnect from err except OSError as err: diff --git a/homeassistant/components/rainbird/config_flow.py b/homeassistant/components/rainbird/config_flow.py index f90e13d37f3..d9cf7b565a7 100644 --- a/homeassistant/components/rainbird/config_flow.py +++ b/homeassistant/components/rainbird/config_flow.py @@ -114,7 +114,7 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): controller.get_serial_number(), controller.get_wifi_params(), ) - except asyncio.TimeoutError as err: + except TimeoutError as err: raise ConfigFlowError( f"Timeout connecting to Rain Bird controller: {str(err)}", "timeout_connect", diff --git a/homeassistant/components/rainforest_raven/config_flow.py b/homeassistant/components/rainforest_raven/config_flow.py index cd8ce68c7e7..2f0234efb7a 100644 --- a/homeassistant/components/rainforest_raven/config_flow.py +++ b/homeassistant/components/rainforest_raven/config_flow.py @@ -106,7 +106,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(unique_id) try: await self._validate_device(dev_path) - except asyncio.TimeoutError: + except TimeoutError: return self.async_abort(reason="timeout_connect") except RAVEnConnectionError: return self.async_abort(reason="cannot_connect") @@ -147,7 +147,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(unique_id) try: await self._validate_device(dev_path) - except asyncio.TimeoutError: + except TimeoutError: errors[CONF_DEVICE] = "timeout_connect" except RAVEnConnectionError: errors[CONF_DEVICE] = "cannot_connect" diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 07591c468b8..ce539b7f0c8 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -1335,7 +1335,7 @@ class Recorder(threading.Thread): try: async with asyncio.timeout(DB_LOCK_TIMEOUT): await database_locked.wait() - except asyncio.TimeoutError as err: + except TimeoutError as err: task.database_unlock.set() raise TimeoutError( f"Could not lock database within {DB_LOCK_TIMEOUT} seconds." diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 7dbe295afee..e021b72ff3d 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -1,7 +1,6 @@ """Support for RESTful switches.""" from __future__ import annotations -import asyncio from http import HTTPStatus import logging from typing import Any @@ -117,7 +116,7 @@ async def async_setup_platform( "Missing resource or schema in configuration. " "Add http:// or https:// to your URL" ) - except (asyncio.TimeoutError, httpx.RequestError) as exc: + except (TimeoutError, httpx.RequestError) as exc: raise PlatformNotReady(f"No route to resource/endpoint: {resource}") from exc @@ -177,7 +176,7 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity): _LOGGER.error( "Can't turn on %s. Is resource/endpoint offline?", self._resource ) - except (asyncio.TimeoutError, httpx.RequestError): + except (TimeoutError, httpx.RequestError): _LOGGER.error("Error while switching on %s", self._resource) async def async_turn_off(self, **kwargs: Any) -> None: @@ -192,7 +191,7 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity): _LOGGER.error( "Can't turn off %s. Is resource/endpoint offline?", self._resource ) - except (asyncio.TimeoutError, httpx.RequestError): + except (TimeoutError, httpx.RequestError): _LOGGER.error("Error while switching off %s", self._resource) async def set_device_state(self, body: Any) -> httpx.Response: @@ -217,7 +216,7 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity): req = None try: req = await self.get_device_state(self.hass) - except (asyncio.TimeoutError, httpx.TimeoutException): + except (TimeoutError, httpx.TimeoutException): _LOGGER.exception("Timed out while fetching data") except httpx.RequestError as err: _LOGGER.exception("Error while fetching data: %s", err) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index c99df16170b..199186cf222 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -1,7 +1,6 @@ """Support for exposing regular REST commands as services.""" from __future__ import annotations -import asyncio from http import HTTPStatus from json.decoder import JSONDecodeError import logging @@ -188,7 +187,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) from err return {"content": _content, "status": response.status} - except asyncio.TimeoutError as err: + except TimeoutError as err: raise HomeAssistantError( f"Timeout when calling resource '{request_url}'", translation_domain=DOMAIN, diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 42b6d9a3ecf..5b90e656911 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -285,7 +285,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: except ( SerialException, OSError, - asyncio.TimeoutError, + TimeoutError, ): reconnect_interval = config[DOMAIN][CONF_RECONNECT_INTERVAL] _LOGGER.exception( diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index ffbc3d26421..8ddd5ffba4c 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -91,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await async_setup_internal(hass, entry) - except asyncio.TimeoutError: + except TimeoutError: # Library currently doesn't support reload _LOGGER.error( "Connection timeout: failed to receive response from RFXtrx device" diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 12b9290af99..fe6aaf07d40 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -372,7 +372,7 @@ class OptionsFlow(config_entries.OptionsFlow): entity_registry.async_remove(entry.entity_id) # Wait for entities to finish cleanup - with suppress(asyncio.TimeoutError): + with suppress(TimeoutError): async with asyncio.timeout(10): await wait_for_entities.wait() remove_track_state_changes() @@ -407,7 +407,7 @@ class OptionsFlow(config_entries.OptionsFlow): ) # Wait for entities to finish renaming - with suppress(asyncio.TimeoutError): + with suppress(TimeoutError): async with asyncio.timeout(10): await wait_for_entities.wait() remove_track_state_changes() diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 586e2a5f062..bd302e16a90 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -90,7 +90,7 @@ async def async_connect_or_timeout( except RoombaConnectionError as err: _LOGGER.debug("Error to connect to vacuum: %s", err) raise CannotConnect from err - except asyncio.TimeoutError as err: + except TimeoutError as err: # api looping if user or password incorrect and roomba exist await async_disconnect_or_timeout(hass, roomba) _LOGGER.debug("Timeout expired: %s", err) @@ -102,7 +102,7 @@ async def async_connect_or_timeout( async def async_disconnect_or_timeout(hass: HomeAssistant, roomba: Roomba) -> None: """Disconnect to vacuum.""" _LOGGER.debug("Disconnect vacuum") - with contextlib.suppress(asyncio.TimeoutError): + with contextlib.suppress(TimeoutError): async with asyncio.timeout(3): await hass.async_add_executor_job(roomba.disconnect) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 14589274da6..44fce7f953f 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -219,7 +219,7 @@ class SamsungTVDevice(SamsungTVEntity, MediaPlayerEntity): try: async with asyncio.timeout(APP_LIST_DELAY): await self._app_list_event.wait() - except asyncio.TimeoutError as err: + except TimeoutError as err: # No need to try again self._app_list_event.set() LOGGER.debug("Failed to load app list from %s: %r", self._host, err) diff --git a/homeassistant/components/sense/const.py b/homeassistant/components/sense/const.py index cfe1a12a24f..3ad35ff345d 100644 --- a/homeassistant/components/sense/const.py +++ b/homeassistant/components/sense/const.py @@ -1,6 +1,5 @@ """Constants for monitoring a Sense energy sensor.""" -import asyncio import socket from sense_energy import ( @@ -39,11 +38,11 @@ FROM_GRID_ID = "from_grid" SOLAR_POWERED_NAME = "Solar Powered Percentage" SOLAR_POWERED_ID = "solar_powered" -SENSE_TIMEOUT_EXCEPTIONS = (asyncio.TimeoutError, SenseAPITimeoutException) +SENSE_TIMEOUT_EXCEPTIONS = (TimeoutError, SenseAPITimeoutException) SENSE_WEBSOCKET_EXCEPTIONS = (socket.gaierror, SenseWebsocketException) SENSE_CONNECT_EXCEPTIONS = ( socket.gaierror, - asyncio.TimeoutError, + TimeoutError, SenseAPITimeoutException, SenseAPIException, ) diff --git a/homeassistant/components/sensibo/const.py b/homeassistant/components/sensibo/const.py index d6dbe957def..0b5f151c49f 100644 --- a/homeassistant/components/sensibo/const.py +++ b/homeassistant/components/sensibo/const.py @@ -1,6 +1,5 @@ """Constants for Sensibo.""" -import asyncio import logging from aiohttp.client_exceptions import ClientConnectionError @@ -27,7 +26,7 @@ TIMEOUT = 8 SENSIBO_ERRORS = ( ClientConnectionError, - asyncio.TimeoutError, + TimeoutError, AuthenticationError, SensiboError, ) diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py index f80e7acf9a6..53a8c4cba3d 100644 --- a/homeassistant/components/sharkiq/__init__.py +++ b/homeassistant/components/sharkiq/__init__.py @@ -40,7 +40,7 @@ async def async_connect_or_timeout(ayla_api: AylaApi) -> bool: except SharkIqAuthError: LOGGER.error("Authentication error connecting to Shark IQ api") return False - except asyncio.TimeoutError as exc: + except TimeoutError as exc: LOGGER.error("Timeout expired") raise CannotConnect from exc diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py index 1957d12048f..c0ca5e1b9e5 100644 --- a/homeassistant/components/sharkiq/config_flow.py +++ b/homeassistant/components/sharkiq/config_flow.py @@ -53,7 +53,7 @@ async def _validate_input( async with asyncio.timeout(10): LOGGER.debug("Initialize connection to Ayla networks API") await ayla_api.async_sign_in() - except (asyncio.TimeoutError, aiohttp.ClientError, TypeError) as error: + except (TimeoutError, aiohttp.ClientError, TypeError) as error: LOGGER.error(error) raise CannotConnect( "Unable to connect to SharkIQ services. Check your region settings." diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 67258d701e9..5aa8dadee19 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -90,7 +90,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: try: async with asyncio.timeout(COMMAND_TIMEOUT): stdout_data, stderr_data = await process.communicate() - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error( "Timed out running command: `%s`, after: %ss", cmd, COMMAND_TIMEOUT ) diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py index d1a3d5ae95f..47b74c53db6 100644 --- a/homeassistant/components/smart_meter_texas/__init__.py +++ b/homeassistant/components/smart_meter_texas/__init__.py @@ -1,5 +1,4 @@ """The Smart Meter Texas integration.""" -import asyncio import logging import ssl @@ -47,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SmartMeterTexasAuthError: _LOGGER.error("Username or password was not accepted") return False - except asyncio.TimeoutError as error: + except TimeoutError as error: raise ConfigEntryNotReady from error await smart_meter_texas_data.setup() diff --git a/homeassistant/components/smart_meter_texas/config_flow.py b/homeassistant/components/smart_meter_texas/config_flow.py index 53428131e17..dc0e4e93eff 100644 --- a/homeassistant/components/smart_meter_texas/config_flow.py +++ b/homeassistant/components/smart_meter_texas/config_flow.py @@ -1,5 +1,4 @@ """Config flow for Smart Meter Texas integration.""" -import asyncio import logging from aiohttp import ClientError @@ -36,7 +35,7 @@ async def validate_input(hass: core.HomeAssistant, data): try: await client.authenticate() - except (asyncio.TimeoutError, ClientError, SmartMeterTexasAPIError) as error: + except (TimeoutError, ClientError, SmartMeterTexasAPIError) as error: raise CannotConnect from error except SmartMeterTexasAuthError as error: raise InvalidAuth(error) from error diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index 72157e086e3..353e2093997 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -56,7 +56,7 @@ class SmartTubController: # credentials were changed or invalidated, we need new ones raise ConfigEntryAuthFailed from ex except ( - asyncio.TimeoutError, + TimeoutError, client_exceptions.ClientOSError, client_exceptions.ServerDisconnectedError, client_exceptions.ContentTypeError, diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 05683f19b11..5814db8168e 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -171,7 +171,7 @@ class SmhiWeather(WeatherEntity): self._forecast_daily = await self._smhi_api.async_get_forecast() self._forecast_hourly = await self._smhi_api.async_get_forecast_hour() self._fail_count = 0 - except (asyncio.TimeoutError, SmhiForecastException): + except (TimeoutError, SmhiForecastException): _LOGGER.error("Failed to connect to SMHI API, retry in 5 minutes") self._fail_count += 1 if self._fail_count < 3: diff --git a/homeassistant/components/snooz/config_flow.py b/homeassistant/components/snooz/config_flow.py index d2188eeec73..7174fbc358c 100644 --- a/homeassistant/components/snooz/config_flow.py +++ b/homeassistant/components/snooz/config_flow.py @@ -144,7 +144,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN): try: await self._pairing_task - except asyncio.TimeoutError: + except TimeoutError: return self.async_show_progress_done(next_step_id="pairing_timeout") finally: self._pairing_task = None diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index 8ac6c4672fd..7883c88f0b8 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -1,5 +1,4 @@ """Component for the Somfy MyLink device supporting the Synergy API.""" -import asyncio import logging from somfy_mylink_synergy import SomfyMyLinkSynergy @@ -30,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: mylink_status = await somfy_mylink.status_info() - except asyncio.TimeoutError as ex: + except TimeoutError as ex: raise ConfigEntryNotReady( "Unable to connect to the Somfy MyLink device, please check your settings" ) from ex diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py index de38ac271ce..e42191c1230 100644 --- a/homeassistant/components/somfy_mylink/config_flow.py +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Somfy MyLink integration.""" from __future__ import annotations -import asyncio from copy import deepcopy import logging @@ -40,7 +39,7 @@ async def validate_input(hass: core.HomeAssistant, data): try: status_info = await somfy_mylink.status_info() - except asyncio.TimeoutError as ex: + except TimeoutError as ex: raise CannotConnect from ex if not status_info or "error" in status_info: diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 7a8ced30eb7..582e62a67eb 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -72,7 +72,7 @@ async def async_setup_entry( 10 ): # set timeout to avoid blocking the setup process await device.get_supported_methods() - except (SongpalException, asyncio.TimeoutError) as ex: + except (SongpalException, TimeoutError) as ex: _LOGGER.warning("[%s(%s)] Unable to connect", name, endpoint) _LOGGER.debug("Unable to get methods from songpal: %s", ex) raise PlatformNotReady from ex diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index c79856c58b6..0df6a7422fe 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -393,7 +393,7 @@ class SonosDiscoveryManager: OSError, SoCoException, Timeout, - asyncio.TimeoutError, + TimeoutError, ) as ex: if not self.hosts_in_error.get(ip_addr): _LOGGER.warning( @@ -447,7 +447,7 @@ class SonosDiscoveryManager: OSError, SoCoException, Timeout, - asyncio.TimeoutError, + TimeoutError, ) as ex: _LOGGER.warning("Discovery message failed to %s : %s", ip_addr, ex) elif not known_speaker.available: diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index fea5b5de7de..1ffb45dd764 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -1126,7 +1126,7 @@ class SonosSpeaker: async with asyncio.timeout(5): while not _test_groups(groups): await hass.data[DATA_SONOS].topology_condition.wait() - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning("Timeout waiting for target groups %s", groups) any_speaker = next(iter(hass.data[DATA_SONOS].discovered.values())) diff --git a/homeassistant/components/splunk/__init__.py b/homeassistant/components/splunk/__init__.py index 1a2a868608e..32b63c42370 100644 --- a/homeassistant/components/splunk/__init__.py +++ b/homeassistant/components/splunk/__init__.py @@ -1,5 +1,4 @@ """Support to send data to a Splunk instance.""" -import asyncio from http import HTTPStatus import json import logging @@ -120,7 +119,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.warning(err) except ClientConnectionError as err: _LOGGER.warning(err) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning("Connection to %s:%s timed out", host, port) except ClientResponseError as err: _LOGGER.error(err.message) diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py index b155c7eddc0..d2786bf213b 100644 --- a/homeassistant/components/squeezebox/config_flow.py +++ b/homeassistant/components/squeezebox/config_flow.py @@ -140,7 +140,7 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async with asyncio.timeout(TIMEOUT): await self._discover() return await self.async_step_edit() - except asyncio.TimeoutError: + except TimeoutError: errors["base"] = "no_server_found" # display the form diff --git a/homeassistant/components/steamist/const.py b/homeassistant/components/steamist/const.py index cacd79b77ac..ae75193a3cc 100644 --- a/homeassistant/components/steamist/const.py +++ b/homeassistant/components/steamist/const.py @@ -1,12 +1,11 @@ """Constants for the Steamist integration.""" -import asyncio import aiohttp DOMAIN = "steamist" -CONNECTION_EXCEPTIONS = (asyncio.TimeoutError, aiohttp.ClientError) +CONNECTION_EXCEPTIONS = (TimeoutError, aiohttp.ClientError) STARTUP_SCAN_TIMEOUT = 5 DISCOVER_SCAN_TIMEOUT = 10 diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 5768f886adb..1d2957b35a3 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -333,7 +333,7 @@ class StreamOutput: try: async with asyncio.timeout(timeout): await self._part_event.wait() - except asyncio.TimeoutError: + except TimeoutError: return False return True diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 1965867887c..29679605e8b 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -115,7 +115,7 @@ class SwitchbotDataUpdateCoordinator(ActiveBluetoothDataUpdateCoordinator[None]) async def async_wait_ready(self) -> bool: """Wait for the device to be ready.""" - with contextlib.suppress(asyncio.TimeoutError): + with contextlib.suppress(TimeoutError): async with asyncio.timeout(DEVICE_STARTUP_TIMEOUT): await self._ready_event.wait() return True diff --git a/homeassistant/components/switcher_kis/button.py b/homeassistant/components/switcher_kis/button.py index 2085398232f..64571f15af0 100644 --- a/homeassistant/components/switcher_kis/button.py +++ b/homeassistant/components/switcher_kis/button.py @@ -1,7 +1,6 @@ """Switcher integration Button platform.""" from __future__ import annotations -import asyncio from collections.abc import Callable from dataclasses import dataclass @@ -147,7 +146,7 @@ class SwitcherThermostatButtonEntity( self.coordinator.data.device_key, ) as swapi: response = await self.entity_description.press_fn(swapi, self._remote) - except (asyncio.TimeoutError, OSError, RuntimeError) as err: + except (TimeoutError, OSError, RuntimeError) as err: error = repr(err) if error or not response or not response.successful: diff --git a/homeassistant/components/switcher_kis/climate.py b/homeassistant/components/switcher_kis/climate.py index 01c4814f985..180b71b1fe6 100644 --- a/homeassistant/components/switcher_kis/climate.py +++ b/homeassistant/components/switcher_kis/climate.py @@ -1,7 +1,6 @@ """Switcher integration Climate platform.""" from __future__ import annotations -import asyncio from typing import Any, cast from aioswitcher.api import SwitcherBaseResponse, SwitcherType2Api @@ -172,7 +171,7 @@ class SwitcherClimateEntity( self.coordinator.data.device_key, ) as swapi: response = await swapi.control_breeze_device(self._remote, **kwargs) - except (asyncio.TimeoutError, OSError, RuntimeError) as err: + except (TimeoutError, OSError, RuntimeError) as err: error = repr(err) if error or not response or not response.successful: diff --git a/homeassistant/components/switcher_kis/cover.py b/homeassistant/components/switcher_kis/cover.py index 1e34ddd2325..4d81480e136 100644 --- a/homeassistant/components/switcher_kis/cover.py +++ b/homeassistant/components/switcher_kis/cover.py @@ -1,7 +1,6 @@ """Switcher integration Cover platform.""" from __future__ import annotations -import asyncio import logging from typing import Any @@ -103,7 +102,7 @@ class SwitcherCoverEntity( self.coordinator.data.device_key, ) as swapi: response = await getattr(swapi, api)(*args) - except (asyncio.TimeoutError, OSError, RuntimeError) as err: + except (TimeoutError, OSError, RuntimeError) as err: error = repr(err) if error or not response or not response.successful: diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 88867393834..c24157f70fc 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -1,7 +1,6 @@ """Switcher integration Switch platform.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging from typing import Any @@ -118,7 +117,7 @@ class SwitcherBaseSwitchEntity( self.coordinator.data.device_key, ) as swapi: response = await getattr(swapi, api)(*args) - except (asyncio.TimeoutError, OSError, RuntimeError) as err: + except (TimeoutError, OSError, RuntimeError) as err: error = repr(err) if error or not response or not response.successful: diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 9eec64ec5f6..d2f5c795b7f 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -97,7 +97,7 @@ async def async_setup_entry( raise ConfigEntryNotReady( f"Could not connect to {entry.title} ({entry.data[CONF_HOST]})." ) from exception - except asyncio.TimeoutError as exception: + except TimeoutError as exception: raise ConfigEntryNotReady( f"Timed out waiting for {entry.title} ({entry.data[CONF_HOST]})." ) from exception @@ -117,7 +117,7 @@ async def async_setup_entry( raise ConfigEntryNotReady( f"Could not connect to {entry.title} ({entry.data[CONF_HOST]})." ) from exception - except asyncio.TimeoutError as exception: + except TimeoutError as exception: raise ConfigEntryNotReady( f"Timed out waiting for {entry.title} ({entry.data[CONF_HOST]})." ) from exception @@ -134,7 +134,7 @@ async def async_setup_entry( entry.data[CONF_HOST], ) await asyncio.sleep(1) - except asyncio.TimeoutError as exception: + except TimeoutError as exception: raise ConfigEntryNotReady( f"Timed out waiting for {entry.title} ({entry.data[CONF_HOST]})." ) from exception diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index a001f22c9e8..0b6a8b4622b 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -75,7 +75,7 @@ async def _validate_input( "Connection error when connecting to %s: %s", data[CONF_HOST], exception ) raise CannotConnect from exception - except asyncio.TimeoutError as exception: + except TimeoutError as exception: _LOGGER.warning("Timed out connecting to %s: %s", data[CONF_HOST], exception) raise CannotConnect from exception except ValueError as exception: diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 5a606721b00..532092ab133 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -215,7 +215,7 @@ class SystemBridgeDataUpdateCoordinator( ) self.last_update_success = False self.async_update_listeners() - except asyncio.TimeoutError as exception: + except TimeoutError as exception: self.logger.warning( "Timed out waiting for %s. Will retry: %s", self.title, diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index cd3cad8024e..4b33a2f7423 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -85,7 +85,7 @@ async def get_integration_info( assert registration.info_callback async with asyncio.timeout(INFO_CALLBACK_TIMEOUT): data = await registration.info_callback(hass) - except asyncio.TimeoutError: + except TimeoutError: data = {"error": {"type": "failed", "error": "timeout"}} except Exception: # pylint: disable=broad-except _LOGGER.exception("Error fetching info") @@ -236,7 +236,7 @@ async def async_check_can_reach_url( return "ok" except aiohttp.ClientError: data = {"type": "failed", "error": "unreachable"} - except asyncio.TimeoutError: + except TimeoutError: data = {"type": "failed", "error": "timeout"} if more_info is not None: data["more_info"] = more_info diff --git a/tests/components/oncue/test_config_flow.py b/tests/components/oncue/test_config_flow.py index 718a3b08adb..979cf3b2677 100644 --- a/tests/components/oncue/test_config_flow.py +++ b/tests/components/oncue/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Oncue config flow.""" -import asyncio from unittest.mock import patch from aiooncue import LoginFailedException @@ -72,7 +71,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: with patch( "homeassistant.components.oncue.config_flow.Oncue.async_login", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/oncue/test_init.py b/tests/components/oncue/test_init.py index ea733bb13b5..dd3cb20f373 100644 --- a/tests/components/oncue/test_init.py +++ b/tests/components/oncue/test_init.py @@ -1,7 +1,6 @@ """Tests for the oncue component.""" from __future__ import annotations -import asyncio from unittest.mock import patch from aiooncue import LoginFailedException @@ -62,7 +61,7 @@ async def test_config_entry_retry_later(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) with patch( "homeassistant.components.oncue.Oncue.async_login", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/openalpr_cloud/test_image_processing.py b/tests/components/openalpr_cloud/test_image_processing.py index 8dd8326ddd8..4a256d49112 100644 --- a/tests/components/openalpr_cloud/test_image_processing.py +++ b/tests/components/openalpr_cloud/test_image_processing.py @@ -1,5 +1,4 @@ """The tests for the openalpr cloud platform.""" -import asyncio from unittest.mock import PropertyMock, patch import pytest @@ -193,7 +192,7 @@ async def test_openalpr_process_image_api_timeout( aioclient_mock: AiohttpClientMocker, ) -> None: """Set up and scan a picture and test api error.""" - aioclient_mock.post(OPENALPR_API_URL, params=PARAMS, exc=asyncio.TimeoutError()) + aioclient_mock.post(OPENALPR_API_URL, params=PARAMS, exc=TimeoutError()) with patch( "homeassistant.components.camera.async_get_image", diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index ef1ac166f1e..e071785006a 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Opentherm Gateway config flow.""" -import asyncio from unittest.mock import patch from pyotgw.vars import OTGW, OTGW_ABOUT @@ -155,7 +154,7 @@ async def test_form_connection_timeout(hass: HomeAssistant) -> None: ) with patch( - "pyotgw.OpenThermGateway.connect", side_effect=(asyncio.TimeoutError) + "pyotgw.OpenThermGateway.connect", side_effect=(TimeoutError) ) as mock_connect, patch( "pyotgw.status.StatusManager._process_updates", return_value=None ): diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index 1a0216825b4..b827f4b7826 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -175,7 +175,7 @@ async def test_user_flow_404( @pytest.mark.parametrize( "error", [ - asyncio.TimeoutError, + TimeoutError, python_otbr_api.OTBRError, aiohttp.ClientError, ], diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py index 30569fe5428..89a8f433016 100644 --- a/tests/components/otbr/test_init.py +++ b/tests/components/otbr/test_init.py @@ -244,7 +244,7 @@ async def test_import_insecure_dataset(hass: HomeAssistant, dataset: bytes) -> N @pytest.mark.parametrize( "error", [ - asyncio.TimeoutError, + TimeoutError, python_otbr_api.OTBRError, aiohttp.ClientError, ], diff --git a/tests/components/peco/test_init.py b/tests/components/peco/test_init.py index 2919e508c97..c8a7c5ccbd5 100644 --- a/tests/components/peco/test_init.py +++ b/tests/components/peco/test_init.py @@ -1,5 +1,4 @@ """Test the PECO Outage Counter init file.""" -import asyncio from unittest.mock import patch from peco import ( @@ -72,7 +71,7 @@ async def test_update_timeout(hass: HomeAssistant, sensor) -> None: with patch( "peco.PecoOutageApi.get_outage_count", - side_effect=asyncio.TimeoutError(), + side_effect=TimeoutError(), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -97,7 +96,7 @@ async def test_total_update_timeout(hass: HomeAssistant, sensor) -> None: config_entry.add_to_hass(hass) with patch( "peco.PecoOutageApi.get_outage_totals", - side_effect=asyncio.TimeoutError(), + side_effect=TimeoutError(), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -256,7 +255,7 @@ async def test_meter_timeout(hass: HomeAssistant) -> None: with patch( "peco.PecoOutageApi.meter_check", - side_effect=asyncio.TimeoutError(), + side_effect=TimeoutError(), ), patch( "peco.PecoOutageApi.get_outage_count", return_value=OutageResults( diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index 9791da76a17..d58475f4994 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -1,5 +1,4 @@ """Tests for the Point config flow.""" -import asyncio from unittest.mock import AsyncMock, patch import pytest @@ -126,7 +125,7 @@ async def test_not_pick_implementation_if_only_one(hass: HomeAssistant) -> None: async def test_abort_if_timeout_generating_auth_url(hass: HomeAssistant) -> None: """Test we abort if generating authorize url fails.""" - flow = init_config_flow(hass, side_effect=asyncio.TimeoutError) + flow = init_config_flow(hass, side_effect=TimeoutError) result = await flow.async_step_user() assert result["type"] == data_entry_flow.FlowResultType.ABORT diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index f9dcc4e1c83..c10d8374bff 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -1,6 +1,5 @@ """Test the Powerwall config flow.""" -import asyncio from datetime import timedelta from unittest.mock import MagicMock, patch @@ -61,7 +60,7 @@ async def test_form_source_user(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -@pytest.mark.parametrize("exc", (PowerwallUnreachableError, asyncio.TimeoutError)) +@pytest.mark.parametrize("exc", (PowerwallUnreachableError, TimeoutError)) async def test_form_cannot_connect(hass: HomeAssistant, exc: Exception) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( @@ -586,7 +585,7 @@ async def test_discovered_wifi_does_not_update_ip_online_but_access_denied( # the discovery flow to probe to see if its online # which will result in an access denied error, which # means its still online and we should not update the ip - mock_powerwall.get_meters.side_effect = asyncio.TimeoutError + mock_powerwall.get_meters.side_effect = TimeoutError async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=60)) await hass.async_block_till_done() diff --git a/tests/components/prusalink/test_config_flow.py b/tests/components/prusalink/test_config_flow.py index 6a23e05adf9..3d6f6221a50 100644 --- a/tests/components/prusalink/test_config_flow.py +++ b/tests/components/prusalink/test_config_flow.py @@ -1,5 +1,4 @@ """Test the PrusaLink config flow.""" -import asyncio from unittest.mock import patch from homeassistant import config_entries @@ -137,7 +136,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: with patch( "homeassistant.components.prusalink.config_flow.PrusaLink.get_version", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/qingping/test_config_flow.py b/tests/components/qingping/test_config_flow.py index a27aaa23eeb..aed1b45286a 100644 --- a/tests/components/qingping/test_config_flow.py +++ b/tests/components/qingping/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Qingping config flow.""" -import asyncio from unittest.mock import patch from homeassistant import config_entries @@ -68,7 +67,7 @@ async def test_async_step_bluetooth_not_qingping(hass: HomeAssistant) -> None: """Test discovery via bluetooth not qingping.""" with patch( "homeassistant.components.qingping.config_flow.async_process_advertisements", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/rabbitair/test_config_flow.py b/tests/components/rabbitair/test_config_flow.py index 75b97d01065..b0c85fbf402 100644 --- a/tests/components/rabbitair/test_config_flow.py +++ b/tests/components/rabbitair/test_config_flow.py @@ -1,7 +1,6 @@ """Test the RabbitAir config flow.""" from __future__ import annotations -import asyncio from collections.abc import Generator from ipaddress import ip_address from unittest.mock import Mock, patch @@ -115,7 +114,7 @@ async def test_form(hass: HomeAssistant) -> None: [ (ValueError, "invalid_access_token"), (OSError, "invalid_host"), - (asyncio.TimeoutError, "timeout_connect"), + (TimeoutError, "timeout_connect"), (Exception, "cannot_connect"), ], ) diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py index 6c0e13fef39..7a4dc2a55d4 100644 --- a/tests/components/rainbird/test_config_flow.py +++ b/tests/components/rainbird/test_config_flow.py @@ -1,6 +1,5 @@ """Tests for the Rain Bird config flow.""" -import asyncio from collections.abc import Generator from http import HTTPStatus from typing import Any @@ -277,7 +276,7 @@ async def test_controller_timeout( with patch( "homeassistant.components.rainbird.config_flow.asyncio.timeout", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result = await complete_flow(hass) assert result.get("type") == FlowResultType.FORM diff --git a/tests/components/rainforest_raven/test_config_flow.py b/tests/components/rainforest_raven/test_config_flow.py index 7ec6c52349c..c7364c9435e 100644 --- a/tests/components/rainforest_raven/test_config_flow.py +++ b/tests/components/rainforest_raven/test_config_flow.py @@ -1,5 +1,4 @@ """Test Rainforest RAVEn config flow.""" -import asyncio from unittest.mock import patch from aioraven.device import RAVEnConnectionError @@ -48,8 +47,8 @@ def mock_device_comm_error(mock_device): @pytest.fixture def mock_device_timeout(mock_device): """Mock a device which times out when queried.""" - mock_device.get_meter_list.side_effect = asyncio.TimeoutError - mock_device.get_meter_info.side_effect = asyncio.TimeoutError + mock_device.get_meter_list.side_effect = TimeoutError + mock_device.get_meter_info.side_effect = TimeoutError return mock_device diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 78af9a64257..da0d1137a79 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1767,7 +1767,7 @@ async def test_database_lock_and_unlock( task = asyncio.create_task(async_wait_recording_done(hass)) # Recording can't be finished while lock is held - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): await asyncio.wait_for(asyncio.shield(task), timeout=0.25) db_events = await hass.async_add_executor_job(_get_db_events) assert len(db_events) == 0 diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 896f5544d93..08e385b50c8 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -1,6 +1,5 @@ """The tests for the REST binary sensor platform.""" -import asyncio from http import HTTPStatus import ssl from unittest.mock import MagicMock, patch @@ -107,7 +106,7 @@ async def test_setup_fail_on_ssl_erros( @respx.mock async def test_setup_timeout(hass: HomeAssistant) -> None: """Test setup when connection timeout occurs.""" - respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) + respx.get("http://localhost").mock(side_effect=TimeoutError()) assert await async_setup_component( hass, BINARY_SENSOR_DOMAIN, diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index e19c7dc3cc7..2c6c32783f1 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -1,6 +1,5 @@ """Tests for rest component.""" -import asyncio from datetime import timedelta from http import HTTPStatus import ssl @@ -33,7 +32,7 @@ async def test_setup_with_endpoint_timeout_with_recovery(hass: HomeAssistant) -> """Test setup with an endpoint that times out that recovers.""" await async_setup_component(hass, "homeassistant", {}) - respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) + respx.get("http://localhost").mock(side_effect=TimeoutError()) assert await async_setup_component( hass, DOMAIN, @@ -99,7 +98,7 @@ async def test_setup_with_endpoint_timeout_with_recovery(hass: HomeAssistant) -> assert hass.states.get("binary_sensor.binary_sensor2").state == "off" # Now the end point flakes out again - respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) + respx.get("http://localhost").mock(side_effect=TimeoutError()) # Refresh the coordinator async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 34e7233d33c..2e4b06ac2d2 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -1,5 +1,4 @@ """The tests for the REST sensor platform.""" -import asyncio from http import HTTPStatus import ssl from unittest.mock import AsyncMock, MagicMock, patch @@ -105,7 +104,7 @@ async def test_setup_fail_on_ssl_erros( @respx.mock async def test_setup_timeout(hass: HomeAssistant) -> None: """Test setup when connection timeout occurs.""" - respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) + respx.get("http://localhost").mock(side_effect=TimeoutError()) assert await async_setup_component( hass, SENSOR_DOMAIN, diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index b9e5070d457..ef7707cad76 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -1,5 +1,4 @@ """The tests for the rest command platform.""" -import asyncio import base64 from http import HTTPStatus from unittest.mock import patch @@ -64,7 +63,7 @@ async def test_rest_command_timeout( """Call a rest command with timeout.""" await setup_component() - aioclient_mock.get(TEST_URL, exc=asyncio.TimeoutError()) + aioclient_mock.get(TEST_URL, exc=TimeoutError()) with pytest.raises( HomeAssistantError, diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py index 9512349e952..feba0e2c39b 100644 --- a/tests/components/sensibo/test_config_flow.py +++ b/tests/components/sensibo/test_config_flow.py @@ -1,7 +1,6 @@ """Test the Sensibo config flow.""" from __future__ import annotations -import asyncio from typing import Any from unittest.mock import patch @@ -60,7 +59,7 @@ async def test_form(hass: HomeAssistant) -> None: ("error_message", "p_error"), [ (aiohttp.ClientConnectionError, "cannot_connect"), - (asyncio.TimeoutError, "cannot_connect"), + (TimeoutError, "cannot_connect"), (AuthenticationError, "invalid_auth"), (SensiboError, "cannot_connect"), ], @@ -219,7 +218,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ("sideeffect", "p_error"), [ (aiohttp.ClientConnectionError, "cannot_connect"), - (asyncio.TimeoutError, "cannot_connect"), + (TimeoutError, "cannot_connect"), (AuthenticationError, "invalid_auth"), (SensiboError, "cannot_connect"), ], diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index 1efcc9dc919..b0c0680c905 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -254,7 +254,7 @@ async def test_do_not_run_forever( "homeassistant.components.shell_command.asyncio.create_subprocess_shell", side_effect=mock_create_subprocess_shell, ): - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): await hass.services.async_call( shell_command.DOMAIN, "test_service", diff --git a/tests/components/smart_meter_texas/conftest.py b/tests/components/smart_meter_texas/conftest.py index b5193b59d08..782deafbcf3 100644 --- a/tests/components/smart_meter_texas/conftest.py +++ b/tests/components/smart_meter_texas/conftest.py @@ -1,5 +1,4 @@ """Test configuration and mocks for Smart Meter Texas.""" -import asyncio from http import HTTPStatus import json @@ -71,7 +70,7 @@ def mock_connection( json={"errormessage": "ERR-USR-INVALIDPASSWORDERROR"}, ) else: # auth_timeout - aioclient_mock.post(auth_endpoint, exc=asyncio.TimeoutError) + aioclient_mock.post(auth_endpoint, exc=TimeoutError) aioclient_mock.post( f"{BASE_ENDPOINT}{METER_ENDPOINT}", diff --git a/tests/components/smart_meter_texas/test_config_flow.py b/tests/components/smart_meter_texas/test_config_flow.py index bd65a30b19b..0fb56937f0a 100644 --- a/tests/components/smart_meter_texas/test_config_flow.py +++ b/tests/components/smart_meter_texas/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Smart Meter Texas config flow.""" -import asyncio from unittest.mock import patch from aiohttp import ClientError @@ -63,7 +62,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: @pytest.mark.parametrize( - "side_effect", [asyncio.TimeoutError, ClientError, SmartMeterTexasAPIError] + "side_effect", [TimeoutError, ClientError, SmartMeterTexasAPIError] ) async def test_form_cannot_connect(hass: HomeAssistant, side_effect) -> None: """Test we handle cannot connect error.""" diff --git a/tests/components/smarttub/test_init.py b/tests/components/smarttub/test_init.py index 929ad687e11..083e0dc8b46 100644 --- a/tests/components/smarttub/test_init.py +++ b/tests/components/smarttub/test_init.py @@ -1,5 +1,4 @@ """Test smarttub setup process.""" -import asyncio from unittest.mock import patch from smarttub import LoginFailed @@ -26,7 +25,7 @@ async def test_setup_entry_not_ready( setup_component, hass: HomeAssistant, config_entry, smarttub_api ) -> None: """Test setup when the entry is not ready.""" - smarttub_api.login.side_effect = asyncio.TimeoutError + smarttub_api.login.side_effect = TimeoutError config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index f12aa92df3c..2ad9153dd41 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -1,5 +1,4 @@ """Test for the smhi weather entity.""" -import asyncio from datetime import datetime, timedelta from unittest.mock import patch @@ -187,7 +186,7 @@ async def test_properties_unknown_symbol(hass: HomeAssistant) -> None: ) -@pytest.mark.parametrize("error", [SmhiForecastException(), asyncio.TimeoutError()]) +@pytest.mark.parametrize("error", [SmhiForecastException(), TimeoutError()]) async def test_refresh_weather_forecast_retry( hass: HomeAssistant, error: Exception ) -> None: diff --git a/tests/components/snooz/test_config_flow.py b/tests/components/snooz/test_config_flow.py index d8bac9ea18c..385a47cf578 100644 --- a/tests/components/snooz/test_config_flow.py +++ b/tests/components/snooz/test_config_flow.py @@ -1,7 +1,6 @@ """Test the Snooz config flow.""" from __future__ import annotations -import asyncio from asyncio import Event from unittest.mock import patch @@ -300,7 +299,7 @@ async def _test_pairs_timeout( ) -> str: with patch( "homeassistant.components.snooz.config_flow.async_process_advertisements", - side_effect=asyncio.TimeoutError(), + side_effect=TimeoutError(), ): result = await hass.config_entries.flow.async_configure( flow_id, user_input=user_input or {} diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py index 6b78c5c1a5b..8143ff89a72 100644 --- a/tests/components/somfy_mylink/test_config_flow.py +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Somfy MyLink config flow.""" -import asyncio from unittest.mock import patch import pytest @@ -123,7 +122,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: with patch( "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index f6b4db84630..83171e2029e 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -377,7 +377,7 @@ async def test_async_poll_manual_hosts_6( with caplog.at_level(logging.DEBUG): caplog.clear() # The discovery events should not fire, wait with a timeout. - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): async with asyncio.timeout(1.0): await speaker_1_activity.event.wait() await hass.async_block_till_done() diff --git a/tests/components/steamist/test_config_flow.py b/tests/components/steamist/test_config_flow.py index e405bca56f2..88c8c0ad697 100644 --- a/tests/components/steamist/test_config_flow.py +++ b/tests/components/steamist/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Steamist config flow.""" -import asyncio from unittest.mock import patch import pytest @@ -103,7 +102,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: with patch( "homeassistant.components.steamist.config_flow.Steamist.async_get_status", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/steamist/test_init.py b/tests/components/steamist/test_init.py index 0a98f746c4c..3c4b0084807 100644 --- a/tests/components/steamist/test_init.py +++ b/tests/components/steamist/test_init.py @@ -1,7 +1,6 @@ """Tests for the steamist component.""" from __future__ import annotations -import asyncio from unittest.mock import AsyncMock, MagicMock, patch from discovery30303 import AIODiscovery30303 @@ -60,7 +59,7 @@ async def test_config_entry_retry_later(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) with patch( "homeassistant.components.steamist.Steamist.async_get_status", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): await async_setup_component(hass, steamist.DOMAIN, {steamist.DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index ff517b8963d..53c8ecf88bd 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -1,5 +1,4 @@ """Test the System Bridge config flow.""" -import asyncio from ipaddress import ip_address from unittest.mock import patch @@ -231,7 +230,7 @@ async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( "systembridgeconnector.websocket_client.WebSocketClient.get_data", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ), patch( "systembridgeconnector.websocket_client.WebSocketClient.listen", ): diff --git a/tests/components/system_health/test_init.py b/tests/components/system_health/test_init.py index 77b6e3b3596..ceb1ec03fe3 100644 --- a/tests/components/system_health/test_init.py +++ b/tests/components/system_health/test_init.py @@ -1,5 +1,4 @@ """Tests for the system health component init.""" -import asyncio from unittest.mock import AsyncMock, Mock, patch from aiohttp.client_exceptions import ClientError @@ -92,7 +91,7 @@ async def test_info_endpoint_register_callback_timeout( """Test that the info endpoint timing out.""" async def mock_info(hass): - raise asyncio.TimeoutError + raise TimeoutError hass.components.system_health.async_register_info("lovelace", mock_info) assert await async_setup_component(hass, "system_health", {}) @@ -128,7 +127,7 @@ async def test_platform_loading( """Test registering via platform.""" aioclient_mock.get("http://example.com/status", text="") aioclient_mock.get("http://example.com/status_fail", exc=ClientError) - aioclient_mock.get("http://example.com/timeout", exc=asyncio.TimeoutError) + aioclient_mock.get("http://example.com/timeout", exc=TimeoutError) hass.config.components.add("fake_integration") mock_platform( hass, From 8b0c9d3d189fb1103c8dda20cfe02ffb54b8ad09 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 5 Feb 2024 12:20:36 +0100 Subject: [PATCH 0285/1367] Use builtin TimeoutError [t-z] (#109683) --- homeassistant/components/tellduslive/config_flow.py | 2 +- homeassistant/components/thethingsnetwork/sensor.py | 2 +- homeassistant/components/tibber/__init__.py | 3 +-- homeassistant/components/tibber/config_flow.py | 3 +-- homeassistant/components/tibber/notify.py | 3 +-- homeassistant/components/tibber/sensor.py | 5 ++--- homeassistant/components/tradfri/config_flow.py | 2 +- homeassistant/components/twinkly/__init__.py | 3 +-- homeassistant/components/twinkly/config_flow.py | 3 +-- homeassistant/components/twinkly/light.py | 3 +-- homeassistant/components/ukraine_alarm/config_flow.py | 3 +-- homeassistant/components/unifi/controller.py | 4 ++-- homeassistant/components/unifiprotect/__init__.py | 3 +-- homeassistant/components/upb/config_flow.py | 2 +- homeassistant/components/upnp/__init__.py | 2 +- homeassistant/components/viaggiatreno/sensor.py | 2 +- homeassistant/components/voicerss/tts.py | 2 +- homeassistant/components/voip/voip.py | 6 +++--- homeassistant/components/wake_word/__init__.py | 2 +- homeassistant/components/weatherflow/config_flow.py | 2 +- homeassistant/components/webostv/const.py | 2 +- homeassistant/components/webostv/media_player.py | 2 +- homeassistant/components/websocket_api/connection.py | 3 +-- homeassistant/components/websocket_api/http.py | 4 ++-- homeassistant/components/whirlpool/__init__.py | 3 +-- homeassistant/components/whirlpool/config_flow.py | 5 ++--- homeassistant/components/worxlandroid/sensor.py | 2 +- homeassistant/components/wyoming/data.py | 2 +- homeassistant/components/xiaomi_ble/config_flow.py | 5 ++--- homeassistant/components/yalexs_ble/__init__.py | 4 +--- homeassistant/components/yandextts/tts.py | 2 +- homeassistant/components/yardian/coordinator.py | 2 +- homeassistant/components/yeelight/__init__.py | 3 +-- homeassistant/components/yeelight/config_flow.py | 3 +-- homeassistant/components/yeelight/device.py | 3 +-- homeassistant/components/yeelight/light.py | 3 +-- homeassistant/components/yeelight/scanner.py | 2 +- homeassistant/components/yolink/__init__.py | 2 +- .../components/zha/core/cluster_handlers/__init__.py | 9 ++++----- .../components/zha/core/cluster_handlers/lightlink.py | 3 +-- homeassistant/components/zha/core/device.py | 8 ++++---- homeassistant/components/zha/core/group.py | 2 +- homeassistant/components/zwave_js/__init__.py | 2 +- homeassistant/components/zwave_js/config_flow.py | 2 +- tests/components/tellduslive/test_config_flow.py | 3 +-- tests/components/ukraine_alarm/test_config_flow.py | 3 +-- tests/components/unifi/test_controller.py | 5 ++--- tests/components/voicerss/test_tts.py | 3 +-- tests/components/voip/test_voip.py | 2 +- tests/components/weatherflow/test_config_flow.py | 2 +- tests/components/webostv/test_media_player.py | 7 +++---- tests/components/websocket_api/test_connection.py | 3 +-- tests/components/websocket_api/test_http.py | 2 +- tests/components/whirlpool/test_config_flow.py | 3 +-- tests/components/xiaomi_ble/test_config_flow.py | 5 ++--- tests/components/yandextts/test_tts.py | 3 +-- tests/components/yeelight/test_init.py | 3 +-- tests/components/yeelight/test_light.py | 5 ++--- tests/components/yolink/test_config_flow.py | 3 +-- tests/components/zha/test_cluster_handlers.py | 11 +++++------ tests/components/zha/test_cover.py | 4 ++-- tests/components/zha/test_gateway.py | 2 +- tests/components/zwave_js/test_config_flow.py | 6 +++--- 63 files changed, 88 insertions(+), 122 deletions(-) diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py index 060b90a7d70..33910f6ead1 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -94,7 +94,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): auth_url = await self.hass.async_add_executor_job(self._get_auth_url) if not auth_url: return self.async_abort(reason="unknown_authorize_url_generation") - except asyncio.TimeoutError: + except TimeoutError: return self.async_abort(reason="authorize_url_timeout") except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected error generating auth url") diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py index 06005d7e4ed..b9568a979fa 100644 --- a/homeassistant/components/thethingsnetwork/sensor.py +++ b/homeassistant/components/thethingsnetwork/sensor.py @@ -136,7 +136,7 @@ class TtnDataStorage: async with asyncio.timeout(DEFAULT_TIMEOUT): response = await session.get(self._url, headers=self._headers) - except (asyncio.TimeoutError, aiohttp.ClientError): + except (TimeoutError, aiohttp.ClientError): _LOGGER.error("Error while accessing: %s", self._url) return None diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 6bd68e17c4d..52db8421781 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -1,5 +1,4 @@ """Support for Tibber.""" -import asyncio import logging import aiohttp @@ -55,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await tibber_connection.update_info() except ( - asyncio.TimeoutError, + TimeoutError, aiohttp.ClientError, tibber.RetryableHttpException, ) as err: diff --git a/homeassistant/components/tibber/config_flow.py b/homeassistant/components/tibber/config_flow.py index 3fb426d6b11..8c926c5cc81 100644 --- a/homeassistant/components/tibber/config_flow.py +++ b/homeassistant/components/tibber/config_flow.py @@ -1,7 +1,6 @@ """Adds config flow for Tibber integration.""" from __future__ import annotations -import asyncio from typing import Any import aiohttp @@ -46,7 +45,7 @@ class TibberConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await tibber_connection.update_info() - except asyncio.TimeoutError: + except TimeoutError: errors[CONF_ACCESS_TOKEN] = ERR_TIMEOUT except tibber.InvalidLogin: errors[CONF_ACCESS_TOKEN] = ERR_TOKEN diff --git a/homeassistant/components/tibber/notify.py b/homeassistant/components/tibber/notify.py index 270528fc4e9..997afa62359 100644 --- a/homeassistant/components/tibber/notify.py +++ b/homeassistant/components/tibber/notify.py @@ -1,7 +1,6 @@ """Support for Tibber notifications.""" from __future__ import annotations -import asyncio from collections.abc import Callable import logging from typing import Any @@ -41,5 +40,5 @@ class TibberNotificationService(BaseNotificationService): title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) try: await self._notify(title=title, message=message) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.error("Timeout sending message with Tibber") diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 52e18c9c6a2..c6e1bdc1895 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -1,7 +1,6 @@ """Support for Tibber sensors.""" from __future__ import annotations -import asyncio import datetime from datetime import timedelta import logging @@ -255,7 +254,7 @@ async def async_setup_entry( for home in tibber_connection.get_homes(only_active=False): try: await home.update_info() - except asyncio.TimeoutError as err: + except TimeoutError as err: _LOGGER.error("Timeout connecting to Tibber home: %s ", err) raise PlatformNotReady() from err except aiohttp.ClientError as err: @@ -399,7 +398,7 @@ class TibberSensorElPrice(TibberSensor): _LOGGER.debug("Fetching data") try: await self._tibber_home.update_info_and_price_info() - except (asyncio.TimeoutError, aiohttp.ClientError): + except (TimeoutError, aiohttp.ClientError): return data = self._tibber_home.info["viewer"]["home"] self._attr_extra_state_attributes["app_nickname"] = data["appNickname"] diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index a383cc2bbee..9acdfb36a5d 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -144,7 +144,7 @@ async def authenticate( key = await api_factory.generate_psk(security_code) except RequestError as err: raise AuthError("invalid_security_code") from err - except asyncio.TimeoutError as err: + except TimeoutError as err: raise AuthError("timeout") from err finally: await api_factory.shutdown() diff --git a/homeassistant/components/twinkly/__init__.py b/homeassistant/components/twinkly/__init__.py index d57a56f489b..3b47a10d499 100644 --- a/homeassistant/components/twinkly/__init__.py +++ b/homeassistant/components/twinkly/__init__.py @@ -1,6 +1,5 @@ """The twinkly component.""" -import asyncio from aiohttp import ClientError from ttls.client import Twinkly @@ -31,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: device_info = await client.get_details() software_version = await client.get_firmware_version() - except (asyncio.TimeoutError, ClientError) as exception: + except (TimeoutError, ClientError) as exception: raise ConfigEntryNotReady from exception hass.data[DOMAIN][entry.entry_id] = { diff --git a/homeassistant/components/twinkly/config_flow.py b/homeassistant/components/twinkly/config_flow.py index e37e0fd6170..6d0785f648e 100644 --- a/homeassistant/components/twinkly/config_flow.py +++ b/homeassistant/components/twinkly/config_flow.py @@ -1,7 +1,6 @@ """Config flow to configure the Twinkly integration.""" from __future__ import annotations -import asyncio import logging from typing import Any @@ -40,7 +39,7 @@ class TwinklyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): device_info = await Twinkly( host, async_get_clientsession(self.hass) ).get_details() - except (asyncio.TimeoutError, ClientError): + except (TimeoutError, ClientError): errors[CONF_HOST] = "cannot_connect" else: await self.async_set_unique_id(device_info[DEV_ID]) diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index c4301936088..b3fa97ea247 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -1,7 +1,6 @@ """The Twinkly light component.""" from __future__ import annotations -import asyncio import logging from typing import Any @@ -282,7 +281,7 @@ class TwinklyLight(LightEntity): # We don't use the echo API to track the availability since # we already have to pull the device to get its state. self._attr_available = True - except (asyncio.TimeoutError, ClientError): + except (TimeoutError, ClientError): # We log this as "info" as it's pretty common that the Christmas # light are not reachable in July if self._attr_available: diff --git a/homeassistant/components/ukraine_alarm/config_flow.py b/homeassistant/components/ukraine_alarm/config_flow.py index 4f1e1c5cf23..db17b55b2e9 100644 --- a/homeassistant/components/ukraine_alarm/config_flow.py +++ b/homeassistant/components/ukraine_alarm/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Ukraine Alarm.""" from __future__ import annotations -import asyncio import logging import aiohttp @@ -50,7 +49,7 @@ class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except aiohttp.ClientError as ex: reason = "unknown" unknown_err_msg = str(ex) - except asyncio.TimeoutError: + except TimeoutError: reason = "timeout" if not reason and not regions: diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index eb127a5dfd9..5873fa92cf7 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -409,7 +409,7 @@ class UniFiController: async_dispatcher_send(self.hass, self.signal_reachable) except ( - asyncio.TimeoutError, + TimeoutError, aiounifi.BadGateway, aiounifi.ServiceUnavailable, aiounifi.AiounifiException, @@ -516,7 +516,7 @@ async def get_unifi_controller( raise AuthenticationRequired from err except ( - asyncio.TimeoutError, + TimeoutError, aiounifi.BadGateway, aiounifi.Forbidden, aiounifi.ServiceUnavailable, diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 174f60fd135..8639b0becdc 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -1,7 +1,6 @@ """UniFi Protect Platform.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging @@ -64,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: nvr_info = await protect.get_nvr() except NotAuthorized as err: raise ConfigEntryAuthFailed(err) from err - except (asyncio.TimeoutError, ClientError, ServerDisconnectedError) as err: + except (TimeoutError, ClientError, ServerDisconnectedError) as err: raise ConfigEntryNotReady from err if nvr_info.version < MIN_REQUIRED_PROTECT_V: diff --git a/homeassistant/components/upb/config_flow.py b/homeassistant/components/upb/config_flow.py index 318ba44f557..6d85febed9f 100644 --- a/homeassistant/components/upb/config_flow.py +++ b/homeassistant/components/upb/config_flow.py @@ -43,7 +43,7 @@ async def _validate_input(data): upb.connect(_connected_callback) - with suppress(asyncio.TimeoutError): + with suppress(TimeoutError): async with asyncio.timeout(VALIDATE_TIMEOUT): await connected_event.wait() diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 6af9d85bc87..2e546f8893f 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -72,7 +72,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: async with asyncio.timeout(10): await device_discovered_event.wait() - except asyncio.TimeoutError as err: + except TimeoutError as err: raise ConfigEntryNotReady(f"Device not discovered: {usn}") from err finally: cancel_discovered_callback() diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index 4043cc865c7..ce439b9e628 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -84,7 +84,7 @@ async def async_http_request(hass, uri): return {"error": req.status} json_response = await req.json() return json_response - except (asyncio.TimeoutError, aiohttp.ClientError) as exc: + except (TimeoutError, aiohttp.ClientError) as exc: _LOGGER.error("Cannot connect to ViaggiaTreno API endpoint: %s", exc) except ValueError: _LOGGER.error("Received non-JSON data from ViaggiaTreno API endpoint") diff --git a/homeassistant/components/voicerss/tts.py b/homeassistant/components/voicerss/tts.py index 5bdc8bee3ac..4ac2aae0a71 100644 --- a/homeassistant/components/voicerss/tts.py +++ b/homeassistant/components/voicerss/tts.py @@ -209,7 +209,7 @@ class VoiceRSSProvider(Provider): _LOGGER.error("Error receive %s from VoiceRSS", str(data, "utf-8")) return (None, None) - except (asyncio.TimeoutError, aiohttp.ClientError): + except (TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout for VoiceRSS API") return (None, None) diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index 11f70c631f1..a41f0965e8f 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -259,7 +259,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): if self.processing_tone_enabled: await self._play_processing_tone() - except asyncio.TimeoutError: + except TimeoutError: # Expected after caller hangs up _LOGGER.debug("Audio timeout") self._session_id = None @@ -304,7 +304,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): _LOGGER.debug("Pipeline finished") except PipelineNotFound: _LOGGER.warning("Pipeline not found") - except asyncio.TimeoutError: + except TimeoutError: # Expected after caller hangs up _LOGGER.debug("Pipeline timeout") self._session_id = None @@ -444,7 +444,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): async with asyncio.timeout(tts_seconds + self.tts_extra_timeout): # TTS audio is 16Khz 16-bit mono await self._async_send_audio(audio_bytes) - except asyncio.TimeoutError as err: + except TimeoutError as err: _LOGGER.warning("TTS timeout") raise err finally: diff --git a/homeassistant/components/wake_word/__init__.py b/homeassistant/components/wake_word/__init__.py index 8c8fb85b8b3..bf502023e2b 100644 --- a/homeassistant/components/wake_word/__init__.py +++ b/homeassistant/components/wake_word/__init__.py @@ -153,7 +153,7 @@ async def websocket_entity_info( try: async with asyncio.timeout(TIMEOUT_FETCH_WAKE_WORDS): wake_words = await entity.get_supported_wake_words() - except asyncio.TimeoutError: + except TimeoutError: connection.send_error( msg["id"], websocket_api.const.ERR_TIMEOUT, "Timeout fetching wake words" ) diff --git a/homeassistant/components/weatherflow/config_flow.py b/homeassistant/components/weatherflow/config_flow.py index 5ce737810b0..d4ee319e70b 100644 --- a/homeassistant/components/weatherflow/config_flow.py +++ b/homeassistant/components/weatherflow/config_flow.py @@ -36,7 +36,7 @@ async def _async_can_discover_devices() -> bool: try: client.on(EVENT_DEVICE_DISCOVERED, _async_found) await future_event - except asyncio.TimeoutError: + except TimeoutError: return False return True diff --git a/homeassistant/components/webostv/const.py b/homeassistant/components/webostv/const.py index 830c0a4134a..84675196d86 100644 --- a/homeassistant/components/webostv/const.py +++ b/homeassistant/components/webostv/const.py @@ -32,6 +32,6 @@ WEBOSTV_EXCEPTIONS = ( ConnectionClosedOK, ConnectionRefusedError, WebOsTvCommandError, - asyncio.TimeoutError, + TimeoutError, asyncio.CancelledError, ) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 554d5e0b1d6..aefb6e77444 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -474,7 +474,7 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): content = None websession = async_get_clientsession(self.hass) - with suppress(asyncio.TimeoutError): + with suppress(TimeoutError): async with asyncio.timeout(10): response = await websession.get(url, ssl=False) if response.status == HTTPStatus.OK: diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index e4540dfac35..280ff41c56e 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -1,7 +1,6 @@ """Connection session.""" from __future__ import annotations -import asyncio from collections.abc import Callable, Hashable from contextvars import ContextVar from typing import TYPE_CHECKING, Any @@ -266,7 +265,7 @@ class ActiveConnection: elif isinstance(err, vol.Invalid): code = const.ERR_INVALID_FORMAT err_message = vol.humanize.humanize_error(msg, err) - elif isinstance(err, asyncio.TimeoutError): + elif isinstance(err, TimeoutError): code = const.ERR_TIMEOUT err_message = "Timeout" elif isinstance(err, HomeAssistantError): diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 416573d493c..77e645a7314 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -282,7 +282,7 @@ class WebSocketHandler: try: async with asyncio.timeout(10): await wsock.prepare(request) - except asyncio.TimeoutError: + except TimeoutError: self._logger.warning("Timeout preparing request from %s", request.remote) return wsock @@ -310,7 +310,7 @@ class WebSocketHandler: # Auth Phase try: msg = await wsock.receive(10) - except asyncio.TimeoutError as err: + except TimeoutError as err: disconnect_warn = "Did not receive auth message within 10 seconds" raise Disconnect from err diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index 42ffe7dd77e..10b26801c10 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -1,5 +1,4 @@ """The Whirlpool Appliances integration.""" -import asyncio from dataclasses import dataclass import logging @@ -35,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: await auth.do_auth(store=False) - except (ClientError, asyncio.TimeoutError) as ex: + except (ClientError, TimeoutError) as ex: raise ConfigEntryNotReady("Cannot connect") from ex if not auth.is_access_token_valid(): diff --git a/homeassistant/components/whirlpool/config_flow.py b/homeassistant/components/whirlpool/config_flow.py index fbbb670b6da..dbd3f9b6fd4 100644 --- a/homeassistant/components/whirlpool/config_flow.py +++ b/homeassistant/components/whirlpool/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Whirlpool Appliances integration.""" from __future__ import annotations -import asyncio from collections.abc import Mapping import logging from typing import Any @@ -48,7 +47,7 @@ async def validate_input( auth = Auth(backend_selector, data[CONF_USERNAME], data[CONF_PASSWORD], session) try: await auth.do_auth() - except (asyncio.TimeoutError, ClientError) as exc: + except (TimeoutError, ClientError) as exc: raise CannotConnect from exc if not auth.is_access_token_valid(): @@ -92,7 +91,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await validate_input(self.hass, data) except InvalidAuth: errors["base"] = "invalid_auth" - except (CannotConnect, asyncio.TimeoutError): + except (CannotConnect, TimeoutError): errors["base"] = "cannot_connect" else: self.hass.config_entries.async_update_entry( diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index 111acc5fff6..16073a3d862 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -97,7 +97,7 @@ class WorxLandroidSensor(SensorEntity): async with asyncio.timeout(self.timeout): auth = aiohttp.helpers.BasicAuth("admin", self.pin) mower_response = await session.get(self.url, auth=auth) - except (asyncio.TimeoutError, aiohttp.ClientError): + except (TimeoutError, aiohttp.ClientError): if self.allow_unreachable is False: _LOGGER.error("Error connecting to mower at %s", self.url) diff --git a/homeassistant/components/wyoming/data.py b/homeassistant/components/wyoming/data.py index ea58181a707..adcb472d5e0 100644 --- a/homeassistant/components/wyoming/data.py +++ b/homeassistant/components/wyoming/data.py @@ -107,7 +107,7 @@ async def load_wyoming_info( if wyoming_info is not None: break # for - except (asyncio.TimeoutError, OSError, WyomingError): + except (TimeoutError, OSError, WyomingError): # Sleep and try again await asyncio.sleep(retry_wait) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index a0c03581eee..576d49296e9 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Xiaomi Bluetooth integration.""" from __future__ import annotations -import asyncio from collections.abc import Mapping import dataclasses from typing import Any @@ -96,7 +95,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self._discovery_info = await self._async_wait_for_full_advertisement( discovery_info, device ) - except asyncio.TimeoutError: + except TimeoutError: # This device might have a really long advertising interval # So create a config entry for it, and if we discover it has # encryption later, we can do a reauth @@ -220,7 +219,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self._discovery_info = await self._async_wait_for_full_advertisement( discovery.discovery_info, discovery.device ) - except asyncio.TimeoutError: + except TimeoutError: # This device might have a really long advertising interval # So create a config entry for it, and if we discover # it has encryption later, we can do a reauth diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index b5683777c24..9d2679d79d3 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -1,8 +1,6 @@ """The Yale Access Bluetooth integration.""" from __future__ import annotations -import asyncio - from yalexs_ble import ( AuthError, ConnectionInfo, @@ -89,7 +87,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await push_lock.wait_for_first_update(DEVICE_TIMEOUT) except AuthError as ex: raise ConfigEntryAuthFailed(str(ex)) from ex - except (YaleXSBLEError, asyncio.TimeoutError) as ex: + except (YaleXSBLEError, TimeoutError) as ex: raise ConfigEntryNotReady( f"{ex}; Try moving the Bluetooth adapter closer to {local_name}" ) from ex diff --git a/homeassistant/components/yandextts/tts.py b/homeassistant/components/yandextts/tts.py index 481678100de..ca4f8400022 100644 --- a/homeassistant/components/yandextts/tts.py +++ b/homeassistant/components/yandextts/tts.py @@ -139,7 +139,7 @@ class YandexSpeechKitProvider(Provider): return (None, None) data = await request.read() - except (asyncio.TimeoutError, aiohttp.ClientError): + except (TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout for yandex speech kit API") return (None, None) diff --git a/homeassistant/components/yardian/coordinator.py b/homeassistant/components/yardian/coordinator.py index e7102f9c74b..b0c8a882474 100644 --- a/homeassistant/components/yardian/coordinator.py +++ b/homeassistant/components/yardian/coordinator.py @@ -64,7 +64,7 @@ class YardianUpdateCoordinator(DataUpdateCoordinator[YardianDeviceState]): async with asyncio.timeout(10): return await self.controller.fetch_device_state() - except asyncio.TimeoutError as e: + except TimeoutError as e: raise UpdateFailed("Communication with Device was time out") from e except NotAuthorizedException as e: raise UpdateFailed("Invalid access token") from e diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index cc9faa33194..f77e4d08dc9 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -1,7 +1,6 @@ """Support for Xiaomi Yeelight WiFi color bulb.""" from __future__ import annotations -import asyncio import logging import voluptuous as vol @@ -214,7 +213,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: device = await _async_get_device(hass, entry.data[CONF_HOST], entry) await _async_initialize(hass, entry, device) - except (asyncio.TimeoutError, OSError, BulbException) as ex: + except (TimeoutError, OSError, BulbException) as ex: raise ConfigEntryNotReady from ex found_unique_id = device.unique_id diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 23a2a131913..3130d844767 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Yeelight integration.""" from __future__ import annotations -import asyncio import logging from urllib.parse import urlparse @@ -268,7 +267,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await bulb.async_listen(lambda _: True) await bulb.async_get_properties() await bulb.async_stop_listening() - except (asyncio.TimeoutError, yeelight.BulbException) as err: + except (TimeoutError, yeelight.BulbException) as err: _LOGGER.debug("Failed to get properties from %s: %s", host, err) raise CannotConnect from err _LOGGER.debug("Get properties: %s", bulb.last_properties) diff --git a/homeassistant/components/yeelight/device.py b/homeassistant/components/yeelight/device.py index 811a1904b04..bb5159c0b3b 100644 --- a/homeassistant/components/yeelight/device.py +++ b/homeassistant/components/yeelight/device.py @@ -1,7 +1,6 @@ """Support for Xiaomi Yeelight WiFi color bulb.""" from __future__ import annotations -import asyncio import logging from typing import Any @@ -176,7 +175,7 @@ class YeelightDevice: self._available = True if not self._initialized: self._initialized = True - except asyncio.TimeoutError as ex: + except TimeoutError as ex: _LOGGER.debug( "timed out while trying to update device %s, %s: %s", self._host, diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index a9834823f5e..abc17b8abd8 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1,7 +1,6 @@ """Light platform support for yeelight.""" from __future__ import annotations -import asyncio from collections.abc import Callable, Coroutine import logging import math @@ -255,7 +254,7 @@ def _async_cmd( try: _LOGGER.debug("Calling %s with %s %s", func, args, kwargs) return await func(self, *args, **kwargs) - except asyncio.TimeoutError as ex: + except TimeoutError as ex: # The wifi likely dropped, so we want to retry once since # python-yeelight will auto reconnect if attempts == 0: diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 43e976eeeac..8fa41bb92b1 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -155,7 +155,7 @@ class YeelightScanner: for listener in self._listeners: listener.async_search((host, SSDP_TARGET[1])) - with contextlib.suppress(asyncio.TimeoutError): + with contextlib.suppress(TimeoutError): async with asyncio.timeout(DISCOVERY_TIMEOUT): await host_event.wait() diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 473c85d563a..01395fd5f5f 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -118,7 +118,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) except YoLinkAuthFailError as yl_auth_err: raise ConfigEntryAuthFailed from yl_auth_err - except (YoLinkClientError, asyncio.TimeoutError) as err: + except (YoLinkClientError, TimeoutError) as err: raise ConfigEntryNotReady from err device_coordinators = {} diff --git a/homeassistant/components/zha/core/cluster_handlers/__init__.py b/homeassistant/components/zha/core/cluster_handlers/__init__.py index 6c65a993e95..94cd1f49ca8 100644 --- a/homeassistant/components/zha/core/cluster_handlers/__init__.py +++ b/homeassistant/components/zha/core/cluster_handlers/__init__.py @@ -1,7 +1,6 @@ """Cluster handlers module for Zigbee Home Automation.""" from __future__ import annotations -import asyncio from collections.abc import Awaitable, Callable, Coroutine, Iterator import contextlib from enum import Enum @@ -62,7 +61,7 @@ def wrap_zigpy_exceptions() -> Iterator[None]: """Wrap zigpy exceptions in `HomeAssistantError` exceptions.""" try: yield - except asyncio.TimeoutError as exc: + except TimeoutError as exc: raise HomeAssistantError( "Failed to send request: device did not respond" ) from exc @@ -214,7 +213,7 @@ class ClusterHandler(LogMixin): }, }, ) - except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: + except (zigpy.exceptions.ZigbeeException, TimeoutError) as ex: self.debug( "Failed to bind '%s' cluster: %s", self.cluster.ep_attribute, @@ -275,7 +274,7 @@ class ClusterHandler(LogMixin): try: res = await self.cluster.configure_reporting_multiple(reports, **kwargs) self._configure_reporting_status(reports, res[0], event_data) - except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: + except (zigpy.exceptions.ZigbeeException, TimeoutError) as ex: self.debug( "failed to set reporting on '%s' cluster for: %s", self.cluster.ep_attribute, @@ -518,7 +517,7 @@ class ClusterHandler(LogMixin): manufacturer=manufacturer, ) result.update(read) - except (asyncio.TimeoutError, zigpy.exceptions.ZigbeeException) as ex: + except (TimeoutError, zigpy.exceptions.ZigbeeException) as ex: self.debug( "failed to get attributes '%s' on '%s' cluster: %s", chunk, diff --git a/homeassistant/components/zha/core/cluster_handlers/lightlink.py b/homeassistant/components/zha/core/cluster_handlers/lightlink.py index e2ed36bdc83..85ec6905069 100644 --- a/homeassistant/components/zha/core/cluster_handlers/lightlink.py +++ b/homeassistant/components/zha/core/cluster_handlers/lightlink.py @@ -1,5 +1,4 @@ """Lightlink cluster handlers module for Zigbee Home Automation.""" -import asyncio import zigpy.exceptions from zigpy.zcl.clusters.lightlink import LightLink @@ -32,7 +31,7 @@ class LightLinkClusterHandler(ClusterHandler): try: rsp = await self.cluster.get_group_identifiers(0) - except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as exc: + except (zigpy.exceptions.ZigbeeException, TimeoutError) as exc: self.warning("Couldn't get list of groups: %s", str(exc)) return diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index dd5a39115ae..1fba6631bb9 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -870,7 +870,7 @@ class ZHADevice(LogMixin): # store it, so we cannot rely on it existing after being written. This is # only done to make the ZCL command valid. await self._zigpy_device.add_to_group(group_id, name=f"0x{group_id:04X}") - except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: + except (zigpy.exceptions.ZigbeeException, TimeoutError) as ex: self.debug( "Failed to add device '%s' to group: 0x%04x ex: %s", self._zigpy_device.ieee, @@ -882,7 +882,7 @@ class ZHADevice(LogMixin): """Remove this device from the provided zigbee group.""" try: await self._zigpy_device.remove_from_group(group_id) - except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: + except (zigpy.exceptions.ZigbeeException, TimeoutError) as ex: self.debug( "Failed to remove device '%s' from group: 0x%04x ex: %s", self._zigpy_device.ieee, @@ -898,7 +898,7 @@ class ZHADevice(LogMixin): await self._zigpy_device.endpoints[endpoint_id].add_to_group( group_id, name=f"0x{group_id:04X}" ) - except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: + except (zigpy.exceptions.ZigbeeException, TimeoutError) as ex: self.debug( "Failed to add endpoint: %s for device: '%s' to group: 0x%04x ex: %s", endpoint_id, @@ -913,7 +913,7 @@ class ZHADevice(LogMixin): """Remove the device endpoint from the provided zigbee group.""" try: await self._zigpy_device.endpoints[endpoint_id].remove_from_group(group_id) - except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: + except (zigpy.exceptions.ZigbeeException, TimeoutError) as ex: self.debug( ( "Failed to remove endpoint: %s for device '%s' from group: 0x%04x" diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index 519668052e0..fc7f1f8758f 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -112,7 +112,7 @@ class ZHAGroupMember(LogMixin): await self._zha_device.device.endpoints[ self._endpoint_id ].remove_from_group(self._zha_group.group_id) - except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: + except (zigpy.exceptions.ZigbeeException, TimeoutError) as ex: self.debug( ( "Failed to remove endpoint: %s for device '%s' from group: 0x%04x" diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 1321ef36f85..1e2a17fdf63 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -168,7 +168,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: translation_key="invalid_server_version", ) raise ConfigEntryNotReady(f"Invalid server version: {err}") from err - except (asyncio.TimeoutError, BaseZwaveJSServerError) as err: + except (TimeoutError, BaseZwaveJSServerError) as err: raise ConfigEntryNotReady(f"Failed to connect: {err}") from err async_delete_issue(hass, DOMAIN, "invalid_server_version") diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index e252a2ad693..c8baacfaf3f 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -118,7 +118,7 @@ async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> Versio version_info: VersionInfo = await get_server_version( ws_address, async_get_clientsession(hass) ) - except (asyncio.TimeoutError, aiohttp.ClientError) as err: + except (TimeoutError, aiohttp.ClientError) as err: # We don't want to spam the log if the add-on isn't started # or takes a long time to start. _LOGGER.debug("Failed to connect to Z-Wave JS server: %s", err) diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py index de284bb8c16..2d0a5fb2110 100644 --- a/tests/components/tellduslive/test_config_flow.py +++ b/tests/components/tellduslive/test_config_flow.py @@ -1,6 +1,5 @@ # flake8: noqa pylint: skip-file """Tests for the TelldusLive config flow.""" -import asyncio from unittest.mock import Mock, patch import pytest @@ -224,7 +223,7 @@ async def test_abort_if_timeout_generating_auth_url( hass: HomeAssistant, mock_tellduslive ) -> None: """Test abort if generating authorize url timeout.""" - flow = init_config_flow(hass, side_effect=asyncio.TimeoutError) + flow = init_config_flow(hass, side_effect=TimeoutError) result = await flow.async_step_user() assert result["type"] == data_entry_flow.FlowResultType.ABORT diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py index 66945e972de..3bb776dadb0 100644 --- a/tests/components/ukraine_alarm/test_config_flow.py +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Ukraine Alarm config flow.""" -import asyncio from collections.abc import Generator from unittest.mock import AsyncMock, patch @@ -271,7 +270,7 @@ async def test_unknown_client_error( async def test_timeout_error(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: """Test timeout error.""" - mock_get_regions.side_effect = asyncio.TimeoutError + mock_get_regions.side_effect = TimeoutError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 8953351f9fe..54c1d055b70 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -1,5 +1,4 @@ """Test UniFi Network.""" -import asyncio from copy import deepcopy from datetime import timedelta from http import HTTPStatus @@ -421,7 +420,7 @@ async def test_reconnect_mechanism( @pytest.mark.parametrize( "exception", [ - asyncio.TimeoutError, + TimeoutError, aiounifi.BadGateway, aiounifi.ServiceUnavailable, aiounifi.AiounifiException, @@ -459,7 +458,7 @@ async def test_get_unifi_controller_verify_ssl_false(hass: HomeAssistant) -> Non @pytest.mark.parametrize( ("side_effect", "raised_exception"), [ - (asyncio.TimeoutError, CannotConnect), + (TimeoutError, CannotConnect), (aiounifi.BadGateway, CannotConnect), (aiounifi.Forbidden, CannotConnect), (aiounifi.ServiceUnavailable, CannotConnect), diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index 24997c9d459..cb24d45246c 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -1,5 +1,4 @@ """The tests for the VoiceRSS speech platform.""" -import asyncio from http import HTTPStatus import pytest @@ -213,7 +212,7 @@ async def test_service_say_timeout( """Test service call say with http timeout.""" calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - aioclient_mock.post(URL, data=FORM_DATA, exc=asyncio.TimeoutError()) + aioclient_mock.post(URL, data=FORM_DATA, exc=TimeoutError()) config = {tts.DOMAIN: {"platform": "voicerss", "api_key": "1234567xx"}} diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py index dbb848f3b9d..703b99db962 100644 --- a/tests/components/voip/test_voip.py +++ b/tests/components/voip/test_voip.py @@ -319,7 +319,7 @@ async def test_tts_timeout( async def send_tts(*args, **kwargs): # Call original then end test successfully - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): await original_send_tts(*args, **kwargs) done.set() diff --git a/tests/components/weatherflow/test_config_flow.py b/tests/components/weatherflow/test_config_flow.py index 4188c737230..51aec02cab7 100644 --- a/tests/components/weatherflow/test_config_flow.py +++ b/tests/components/weatherflow/test_config_flow.py @@ -55,7 +55,7 @@ async def test_devices_with_mocks( @pytest.mark.parametrize( ("exception", "error_msg"), [ - (asyncio.TimeoutError, ERROR_MSG_NO_DEVICE_FOUND), + (TimeoutError, ERROR_MSG_NO_DEVICE_FOUND), (asyncio.exceptions.CancelledError, ERROR_MSG_CANNOT_CONNECT), (AddressInUseError, ERROR_MSG_ADDRESS_IN_USE), ], diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index c027b57acf8..cc060064b8b 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -1,5 +1,4 @@ """The tests for the LG webOS media player platform.""" -import asyncio from datetime import timedelta from http import HTTPStatus from unittest.mock import Mock @@ -469,7 +468,7 @@ async def test_client_disconnected(hass: HomeAssistant, client, monkeypatch) -> """Test error not raised when client is disconnected.""" await setup_webostv(hass) monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) - monkeypatch.setattr(client, "connect", Mock(side_effect=asyncio.TimeoutError)) + monkeypatch.setattr(client, "connect", Mock(side_effect=TimeoutError)) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) await hass.async_block_till_done() @@ -495,7 +494,7 @@ async def test_control_error_handling( # Device off, log a warning monkeypatch.setattr(client, "is_on", False) - monkeypatch.setattr(client, "play", Mock(side_effect=asyncio.TimeoutError)) + monkeypatch.setattr(client, "play", Mock(side_effect=TimeoutError)) await client.mock_state_update() await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PLAY, data, True) @@ -752,7 +751,7 @@ async def test_get_image_http_error( attrs = hass.states.get(ENTITY_ID).attributes assert "entity_picture_local" not in attrs - aioclient_mock.get(url, exc=asyncio.TimeoutError()) + aioclient_mock.get(url, exc=TimeoutError()) client = await hass_client_no_auth() resp = await client.get(attrs["entity_picture"]) diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index 80936d30752..7a6bd86586c 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -1,5 +1,4 @@ """Test WebSocket Connection class.""" -import asyncio import logging from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -32,7 +31,7 @@ from tests.common import MockUser "Error handling message: Invalid something. Got {'id': 5} (invalid_format) Mock User from 127.0.0.42 (Browser)", ), ( - asyncio.TimeoutError(), + TimeoutError(), websocket_api.ERR_TIMEOUT, "Timeout", "Error handling message: Timeout (timeout) Mock User from 127.0.0.42 (Browser)", diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index f6723f0a592..090f034b3d3 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -372,7 +372,7 @@ async def test_prepare_fail( """Test failing to prepare.""" with patch( "homeassistant.components.websocket_api.http.web.WebSocketResponse.prepare", - side_effect=(asyncio.TimeoutError, web.WebSocketResponse.prepare), + side_effect=(TimeoutError, web.WebSocketResponse.prepare), ), pytest.raises(ServerDisconnectedError): await hass_ws_client(hass) diff --git a/tests/components/whirlpool/test_config_flow.py b/tests/components/whirlpool/test_config_flow.py index d408ac1d15e..60e64c89929 100644 --- a/tests/components/whirlpool/test_config_flow.py +++ b/tests/components/whirlpool/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Whirlpool Sixth Sense config flow.""" -import asyncio from unittest.mock import patch import aiohttp @@ -110,7 +109,7 @@ async def test_form_auth_timeout(hass: HomeAssistant, region) -> None: ) with patch( "homeassistant.components.whirlpool.config_flow.Auth.do_auth", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 97aa878e1fb..e821f5ec779 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Xiaomi config flow.""" -import asyncio from unittest.mock import patch from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData @@ -50,7 +49,7 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload( """Test discovery via bluetooth with a valid device but missing payload.""" with patch( "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", - side_effect=asyncio.TimeoutError(), + side_effect=TimeoutError(), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -402,7 +401,7 @@ async def test_async_step_user_short_payload(hass: HomeAssistant) -> None: assert result["step_id"] == "user" with patch( "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", - side_effect=asyncio.TimeoutError(), + side_effect=TimeoutError(), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index a8052e45047..79e1f9108c5 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -1,5 +1,4 @@ """The tests for the Yandex SpeechKit speech platform.""" -import asyncio from http import HTTPStatus import pytest @@ -201,7 +200,7 @@ async def test_service_say_timeout( aioclient_mock.get( URL, status=HTTPStatus.OK, - exc=asyncio.TimeoutError(), + exc=TimeoutError(), params=url_param, ) diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index fb9ecc9bea4..a3c4c4d90b7 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -1,5 +1,4 @@ """Test Yeelight.""" -import asyncio from datetime import timedelta from unittest.mock import AsyncMock, patch @@ -571,7 +570,7 @@ async def test_oserror_on_first_update_results_in_unavailable( assert hass.states.get("light.test_name").state == STATE_UNAVAILABLE -@pytest.mark.parametrize("exception", [BulbException, asyncio.TimeoutError]) +@pytest.mark.parametrize("exception", [BulbException, TimeoutError]) async def test_non_oserror_exception_on_first_update( hass: HomeAssistant, exception: Exception ) -> None: diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index da907fdee33..e16692de990 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -1,5 +1,4 @@ """Test the Yeelight light.""" -import asyncio from datetime import timedelta import logging import socket @@ -504,7 +503,7 @@ async def test_services(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - ) assert hass.states.get(ENTITY_LIGHT).state == STATE_OFF - mocked_bulb.async_set_brightness = AsyncMock(side_effect=asyncio.TimeoutError) + mocked_bulb.async_set_brightness = AsyncMock(side_effect=TimeoutError) with pytest.raises(HomeAssistantError): await hass.services.async_call( "light", @@ -553,7 +552,7 @@ async def test_update_errors( # Timeout usually means the bulb is overloaded with commands # but will still respond eventually. - mocked_bulb.async_turn_off = AsyncMock(side_effect=asyncio.TimeoutError) + mocked_bulb.async_turn_off = AsyncMock(side_effect=TimeoutError) with pytest.raises(HomeAssistantError): await hass.services.async_call( "light", diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index 42f111dd837..2b6034fd597 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -1,5 +1,4 @@ """Test yolink config flow.""" -import asyncio from http import HTTPStatus from unittest.mock import patch @@ -127,7 +126,7 @@ async def test_abort_if_authorization_timeout( with patch( "homeassistant.components.yolink.config_entry_oauth2_flow." "LocalOAuth2Implementation.async_generate_authorize_url", - side_effect=asyncio.TimeoutError, + side_effect=TimeoutError, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/zha/test_cluster_handlers.py b/tests/components/zha/test_cluster_handlers.py index 7c17d79fe0e..252148481a7 100644 --- a/tests/components/zha/test_cluster_handlers.py +++ b/tests/components/zha/test_cluster_handlers.py @@ -1,5 +1,4 @@ """Test ZHA Core cluster handlers.""" -import asyncio from collections.abc import Callable import logging import math @@ -564,12 +563,12 @@ async def test_ep_cluster_handlers_configure(cluster_handler) -> None: ch_1 = cluster_handler(zha_const.CLUSTER_HANDLER_ON_OFF, 6) ch_2 = cluster_handler(zha_const.CLUSTER_HANDLER_LEVEL, 8) ch_3 = cluster_handler(zha_const.CLUSTER_HANDLER_COLOR, 768) - ch_3.async_configure = AsyncMock(side_effect=asyncio.TimeoutError) - ch_3.async_initialize = AsyncMock(side_effect=asyncio.TimeoutError) + ch_3.async_configure = AsyncMock(side_effect=TimeoutError) + ch_3.async_initialize = AsyncMock(side_effect=TimeoutError) ch_4 = cluster_handler(zha_const.CLUSTER_HANDLER_ON_OFF, 6) ch_5 = cluster_handler(zha_const.CLUSTER_HANDLER_LEVEL, 8) - ch_5.async_configure = AsyncMock(side_effect=asyncio.TimeoutError) - ch_5.async_initialize = AsyncMock(side_effect=asyncio.TimeoutError) + ch_5.async_configure = AsyncMock(side_effect=TimeoutError) + ch_5.async_initialize = AsyncMock(side_effect=TimeoutError) endpoint_mock = mock.MagicMock(spec_set=ZigpyEndpoint) type(endpoint_mock).in_clusters = mock.PropertyMock(return_value={}) @@ -959,7 +958,7 @@ async def test_quirk_id_cluster_handler(hass: HomeAssistant, caplog) -> None: zigpy.exceptions.ZigbeeException("Zigbee exception"), "Failed to send request: Zigbee exception", ), - (asyncio.TimeoutError(), "Failed to send request: device did not respond"), + (TimeoutError(), "Failed to send request: device did not respond"), ], ) async def test_retry_request( diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 55a4cbebfe7..964868118c4 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -835,7 +835,7 @@ async def test_shade( assert hass.states.get(entity_id).state == STATE_OPEN # test cover stop - with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): + with patch("zigpy.zcl.Cluster.request", side_effect=TimeoutError): with pytest.raises(HomeAssistantError): await hass.services.async_call( COVER_DOMAIN, @@ -924,7 +924,7 @@ async def test_keen_vent( assert hass.states.get(entity_id).state == STATE_CLOSED # open from UI command fails - p1 = patch.object(cluster_on_off, "request", side_effect=asyncio.TimeoutError) + p1 = patch.object(cluster_on_off, "request", side_effect=TimeoutError) p2 = patch.object(cluster_level, "request", return_value=[4, 0]) with p1, p2: diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index e117caf4325..ec467df9e52 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -198,7 +198,7 @@ async def test_gateway_group_methods( # the group entity should not have been cleaned up assert entity_id not in hass.states.async_entity_ids(Platform.LIGHT) - with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): + with patch("zigpy.zcl.Cluster.request", side_effect=TimeoutError): await zha_group.members[0].async_remove_from_group() assert len(zha_group.members) == 1 for member in zha_group.members: diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index a051f398d8c..3e4a578d858 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -374,7 +374,7 @@ async def test_supervisor_discovery( @pytest.mark.parametrize( ("discovery_info", "server_version_side_effect"), - [({"config": ADDON_DISCOVERY_INFO}, asyncio.TimeoutError())], + [({"config": ADDON_DISCOVERY_INFO}, TimeoutError())], ) async def test_supervisor_discovery_cannot_connect( hass: HomeAssistant, supervisor, get_addon_discovery_info @@ -1114,7 +1114,7 @@ async def test_addon_running( ( {"config": ADDON_DISCOVERY_INFO}, None, - asyncio.TimeoutError, + TimeoutError, None, "cannot_connect", ), @@ -1366,7 +1366,7 @@ async def test_addon_installed_start_failure( [ ( {"config": ADDON_DISCOVERY_INFO}, - asyncio.TimeoutError, + TimeoutError, ), ( None, From c096ac56db27c5fdf3d0f864526c48e55841413b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 Feb 2024 12:23:30 +0100 Subject: [PATCH 0286/1367] Remove raspberry_pi config entry if hassio is not present (#109687) --- .../components/raspberry_pi/__init__.py | 7 +++++- .../components/raspberry_pi/manifest.json | 3 ++- .../components/raspberry_pi/test_hardware.py | 4 +++ tests/components/raspberry_pi/test_init.py | 25 +++++++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/raspberry_pi/__init__.py b/homeassistant/components/raspberry_pi/__init__.py index 3750a1c7068..19697d9b69d 100644 --- a/homeassistant/components/raspberry_pi/__init__.py +++ b/homeassistant/components/raspberry_pi/__init__.py @@ -1,7 +1,7 @@ """The Raspberry Pi integration.""" from __future__ import annotations -from homeassistant.components.hassio import get_os_info +from homeassistant.components.hassio import get_os_info, is_hassio from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -9,6 +9,11 @@ from homeassistant.exceptions import ConfigEntryNotReady async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Raspberry Pi config entry.""" + if not is_hassio(hass): + # Not running under supervisor, Home Assistant may have been migrated + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return False + if (os_info := get_os_info(hass)) is None: # The hassio integration has not yet fetched data from the supervisor raise ConfigEntryNotReady diff --git a/homeassistant/components/raspberry_pi/manifest.json b/homeassistant/components/raspberry_pi/manifest.json index d30c637d2c3..5ed68154ce1 100644 --- a/homeassistant/components/raspberry_pi/manifest.json +++ b/homeassistant/components/raspberry_pi/manifest.json @@ -1,9 +1,10 @@ { "domain": "raspberry_pi", "name": "Raspberry Pi", + "after_dependencies": ["hassio"], "codeowners": ["@home-assistant/core"], "config_flow": false, - "dependencies": ["hardware", "hassio"], + "dependencies": ["hardware"], "documentation": "https://www.home-assistant.io/integrations/raspberry_pi", "integration_type": "hardware" } diff --git a/tests/components/raspberry_pi/test_hardware.py b/tests/components/raspberry_pi/test_hardware.py index 77236040486..d41fbf2f5e1 100644 --- a/tests/components/raspberry_pi/test_hardware.py +++ b/tests/components/raspberry_pi/test_hardware.py @@ -3,8 +3,10 @@ from unittest.mock import patch import pytest +from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN from homeassistant.components.raspberry_pi.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, MockModule, mock_integration from tests.typing import WebSocketGenerator @@ -15,6 +17,7 @@ async def test_hardware_info( ) -> None: """Test we can get the board info.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -66,6 +69,7 @@ async def test_hardware_info_fail( ) -> None: """Test async_info raises if os_info is not as expected.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( diff --git a/tests/components/raspberry_pi/test_init.py b/tests/components/raspberry_pi/test_init.py index b0e9ef89582..88518cc00b0 100644 --- a/tests/components/raspberry_pi/test_init.py +++ b/tests/components/raspberry_pi/test_init.py @@ -3,9 +3,11 @@ from unittest.mock import patch import pytest +from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN from homeassistant.components.raspberry_pi.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, MockModule, mock_integration @@ -23,6 +25,7 @@ def mock_rpi_power(): async def test_setup_entry(hass: HomeAssistant) -> None: """Test setup of a config entry.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -46,9 +49,30 @@ async def test_setup_entry(hass: HomeAssistant) -> None: assert len(hass.config_entries.async_entries("rpi_power")) == 1 +async def test_setup_entry_no_hassio(hass: HomeAssistant) -> None: + """Test setup of a config entry without hassio.""" + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Raspberry Pi", + ) + config_entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries()) == 1 + + with patch("homeassistant.components.raspberry_pi.get_os_info") as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(mock_get_os_info.mock_calls) == 0 + assert len(hass.config_entries.async_entries()) == 0 + + async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: """Test setup of a config entry with wrong board type.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -74,6 +98,7 @@ async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: """Test setup of a config entry when hassio has not fetched os_info.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( From 93e08109388d36dfa41b0d39f25e16ab770e36f4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 Feb 2024 12:25:26 +0100 Subject: [PATCH 0287/1367] Remove hardkernel config entry if hassio is not present (#109680) --- .../components/hardkernel/__init__.py | 7 +++++- .../components/hardkernel/manifest.json | 3 ++- tests/components/hardkernel/test_hardware.py | 4 +++ tests/components/hardkernel/test_init.py | 25 +++++++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hardkernel/__init__.py b/homeassistant/components/hardkernel/__init__.py index c81ad7860be..41e65ff8b5e 100644 --- a/homeassistant/components/hardkernel/__init__.py +++ b/homeassistant/components/hardkernel/__init__.py @@ -1,7 +1,7 @@ """The Hardkernel integration.""" from __future__ import annotations -from homeassistant.components.hassio import get_os_info +from homeassistant.components.hassio import get_os_info, is_hassio from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -9,6 +9,11 @@ from homeassistant.exceptions import ConfigEntryNotReady async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Hardkernel config entry.""" + if not is_hassio(hass): + # Not running under supervisor, Home Assistant may have been migrated + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return False + if (os_info := get_os_info(hass)) is None: # The hassio integration has not yet fetched data from the supervisor raise ConfigEntryNotReady diff --git a/homeassistant/components/hardkernel/manifest.json b/homeassistant/components/hardkernel/manifest.json index 1b29f0b0b22..2a528a5173e 100644 --- a/homeassistant/components/hardkernel/manifest.json +++ b/homeassistant/components/hardkernel/manifest.json @@ -1,9 +1,10 @@ { "domain": "hardkernel", "name": "Hardkernel", + "after_dependencies": ["hassio"], "codeowners": ["@home-assistant/core"], "config_flow": false, - "dependencies": ["hardware", "hassio"], + "dependencies": ["hardware"], "documentation": "https://www.home-assistant.io/integrations/hardkernel", "integration_type": "hardware" } diff --git a/tests/components/hardkernel/test_hardware.py b/tests/components/hardkernel/test_hardware.py index 7e063a9f07a..ee2299f383c 100644 --- a/tests/components/hardkernel/test_hardware.py +++ b/tests/components/hardkernel/test_hardware.py @@ -4,7 +4,9 @@ from unittest.mock import patch import pytest from homeassistant.components.hardkernel.const import DOMAIN +from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, MockModule, mock_integration from tests.typing import WebSocketGenerator @@ -15,6 +17,7 @@ async def test_hardware_info( ) -> None: """Test we can get the board info.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -66,6 +69,7 @@ async def test_hardware_info_fail( ) -> None: """Test async_info raises if os_info is not as expected.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( diff --git a/tests/components/hardkernel/test_init.py b/tests/components/hardkernel/test_init.py index 877a44a2ca2..98f4c08cc80 100644 --- a/tests/components/hardkernel/test_init.py +++ b/tests/components/hardkernel/test_init.py @@ -2,8 +2,10 @@ from unittest.mock import patch from homeassistant.components.hardkernel.const import DOMAIN +from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, MockModule, mock_integration @@ -11,6 +13,7 @@ from tests.common import MockConfigEntry, MockModule, mock_integration async def test_setup_entry(hass: HomeAssistant) -> None: """Test setup of a config entry.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -30,9 +33,30 @@ async def test_setup_entry(hass: HomeAssistant) -> None: assert len(mock_get_os_info.mock_calls) == 1 +async def test_setup_entry_no_hassio(hass: HomeAssistant) -> None: + """Test setup of a config entry without hassio.""" + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Hardkernel", + ) + config_entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries()) == 1 + + with patch("homeassistant.components.hardkernel.get_os_info") as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(mock_get_os_info.mock_calls) == 0 + assert len(hass.config_entries.async_entries()) == 0 + + async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: """Test setup of a config entry with wrong board type.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -58,6 +82,7 @@ async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: """Test setup of a config entry when hassio has not fetched os_info.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( From 40166ed51e0875027c043618291e5e29e4fa2a58 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Feb 2024 05:26:08 -0600 Subject: [PATCH 0288/1367] Avoid linear search of device registry when no areas are referenced in service calls (#109669) --- homeassistant/helpers/service.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 3fe0c0eb086..d397764c1be 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -487,9 +487,11 @@ def async_extract_referenced_entity_ids( # Find devices for targeted areas selected.referenced_devices.update(selector.device_ids) - for device_entry in dev_reg.devices.values(): - if device_entry.area_id in selector.area_ids: - selected.referenced_devices.add(device_entry.id) + + if selector.area_ids: + for device_entry in dev_reg.devices.values(): + if device_entry.area_id in selector.area_ids: + selected.referenced_devices.add(device_entry.id) if not selector.area_ids and not selected.referenced_devices: return selected From 048d9e75e608340f57ec02550b5326c1e699e70f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 Feb 2024 12:26:58 +0100 Subject: [PATCH 0289/1367] Remove homeassistant_green config entry if hassio is not present (#109685) --- .../homeassistant_green/__init__.py | 7 ++++- .../homeassistant_green/manifest.json | 3 ++- .../homeassistant_green/test_config_flow.py | 8 ++++++ .../homeassistant_green/test_hardware.py | 4 +++ .../homeassistant_green/test_init.py | 27 +++++++++++++++++++ 5 files changed, 47 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant_green/__init__.py b/homeassistant/components/homeassistant_green/__init__.py index fbcd2093778..ed86723ab94 100644 --- a/homeassistant/components/homeassistant_green/__init__.py +++ b/homeassistant/components/homeassistant_green/__init__.py @@ -1,7 +1,7 @@ """The Home Assistant Green integration.""" from __future__ import annotations -from homeassistant.components.hassio import get_os_info +from homeassistant.components.hassio import get_os_info, is_hassio from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -9,6 +9,11 @@ from homeassistant.exceptions import ConfigEntryNotReady async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Home Assistant Green config entry.""" + if not is_hassio(hass): + # Not running under supervisor, Home Assistant may have been migrated + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return False + if (os_info := get_os_info(hass)) is None: # The hassio integration has not yet fetched data from the supervisor raise ConfigEntryNotReady diff --git a/homeassistant/components/homeassistant_green/manifest.json b/homeassistant/components/homeassistant_green/manifest.json index 7c9dd0322ec..d543d562ee3 100644 --- a/homeassistant/components/homeassistant_green/manifest.json +++ b/homeassistant/components/homeassistant_green/manifest.json @@ -1,9 +1,10 @@ { "domain": "homeassistant_green", "name": "Home Assistant Green", + "after_dependencies": ["hassio"], "codeowners": ["@home-assistant/core"], "config_flow": false, - "dependencies": ["hardware", "hassio", "homeassistant_hardware"], + "dependencies": ["hardware", "homeassistant_hardware"], "documentation": "https://www.home-assistant.io/integrations/homeassistant_green", "integration_type": "hardware" } diff --git a/tests/components/homeassistant_green/test_config_flow.py b/tests/components/homeassistant_green/test_config_flow.py index 84af22509f9..cfac774f77e 100644 --- a/tests/components/homeassistant_green/test_config_flow.py +++ b/tests/components/homeassistant_green/test_config_flow.py @@ -3,9 +3,11 @@ from unittest.mock import patch import pytest +from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN from homeassistant.components.homeassistant_green.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, MockModule, mock_integration @@ -36,6 +38,7 @@ def mock_set_green_settings(): async def test_config_flow(hass: HomeAssistant) -> None: """Test the config flow.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) with patch( "homeassistant.components.homeassistant_green.async_setup_entry", @@ -60,6 +63,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: async def test_config_flow_single_entry(hass: HomeAssistant) -> None: """Test only a single entry is allowed.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -115,6 +119,7 @@ async def test_option_flow_led_settings( ) -> None: """Test updating LED settings.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -146,6 +151,7 @@ async def test_option_flow_led_settings_unchanged( ) -> None: """Test updating LED settings.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -171,6 +177,7 @@ async def test_option_flow_led_settings_unchanged( async def test_option_flow_led_settings_fail_1(hass: HomeAssistant) -> None: """Test updating LED settings.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -196,6 +203,7 @@ async def test_option_flow_led_settings_fail_2( ) -> None: """Test updating LED settings.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( diff --git a/tests/components/homeassistant_green/test_hardware.py b/tests/components/homeassistant_green/test_hardware.py index 0221bf3a577..c9f958b882c 100644 --- a/tests/components/homeassistant_green/test_hardware.py +++ b/tests/components/homeassistant_green/test_hardware.py @@ -3,8 +3,10 @@ from unittest.mock import patch import pytest +from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN from homeassistant.components.homeassistant_green.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, MockModule, mock_integration from tests.typing import WebSocketGenerator @@ -15,6 +17,7 @@ async def test_hardware_info( ) -> None: """Test we can get the board info.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -66,6 +69,7 @@ async def test_hardware_info_fail( ) -> None: """Test async_info raises if os_info is not as expected.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( diff --git a/tests/components/homeassistant_green/test_init.py b/tests/components/homeassistant_green/test_init.py index 0df7d918039..b183a332f50 100644 --- a/tests/components/homeassistant_green/test_init.py +++ b/tests/components/homeassistant_green/test_init.py @@ -1,9 +1,11 @@ """Test the Home Assistant Green integration.""" from unittest.mock import patch +from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN from homeassistant.components.homeassistant_green.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, MockModule, mock_integration @@ -11,6 +13,7 @@ from tests.common import MockConfigEntry, MockModule, mock_integration async def test_setup_entry(hass: HomeAssistant) -> None: """Test setup of a config entry.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -32,9 +35,32 @@ async def test_setup_entry(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(config_entry.entry_id) +async def test_setup_entry_no_hassio(hass: HomeAssistant) -> None: + """Test setup of a config entry without hassio.""" + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries()) == 1 + + with patch( + "homeassistant.components.homeassistant_green.get_os_info" + ) as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(mock_get_os_info.mock_calls) == 0 + assert len(hass.config_entries.async_entries()) == 0 + + async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: """Test setup of a config entry with wrong board type.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -60,6 +86,7 @@ async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: """Test setup of a config entry when hassio has not fetched os_info.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( From f1d3c417f9498d0d520a3fc03d161478d62ef90b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 Feb 2024 12:29:25 +0100 Subject: [PATCH 0290/1367] Remove homeassistant_yellow config entry if hassio is not present (#109686) --- .../homeassistant_yellow/__init__.py | 7 +++- .../homeassistant_yellow/manifest.json | 3 +- .../homeassistant_yellow/test_config_flow.py | 10 ++++++ .../homeassistant_yellow/test_hardware.py | 4 +++ .../homeassistant_yellow/test_init.py | 32 +++++++++++++++++++ 5 files changed, 54 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant_yellow/__init__.py b/homeassistant/components/homeassistant_yellow/__init__.py index b61e01061c3..092911d1532 100644 --- a/homeassistant/components/homeassistant_yellow/__init__.py +++ b/homeassistant/components/homeassistant_yellow/__init__.py @@ -1,7 +1,7 @@ """The Home Assistant Yellow integration.""" from __future__ import annotations -from homeassistant.components.hassio import get_os_info +from homeassistant.components.hassio import get_os_info, is_hassio from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( check_multi_pan_addon, get_zigbee_socket, @@ -16,6 +16,11 @@ from .const import RADIO_DEVICE, ZHA_HW_DISCOVERY_DATA async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Home Assistant Yellow config entry.""" + if not is_hassio(hass): + # Not running under supervisor, Home Assistant may have been migrated + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return False + if (os_info := get_os_info(hass)) is None: # The hassio integration has not yet fetched data from the supervisor raise ConfigEntryNotReady diff --git a/homeassistant/components/homeassistant_yellow/manifest.json b/homeassistant/components/homeassistant_yellow/manifest.json index dd74df9295f..a9715003172 100644 --- a/homeassistant/components/homeassistant_yellow/manifest.json +++ b/homeassistant/components/homeassistant_yellow/manifest.json @@ -1,9 +1,10 @@ { "domain": "homeassistant_yellow", "name": "Home Assistant Yellow", + "after_dependencies": ["hassio"], "codeowners": ["@home-assistant/core"], "config_flow": false, - "dependencies": ["hardware", "hassio", "homeassistant_hardware"], + "dependencies": ["hardware", "homeassistant_hardware"], "documentation": "https://www.home-assistant.io/integrations/homeassistant_yellow", "integration_type": "hardware" } diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index bd61400fa8e..1b80610953f 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -4,10 +4,12 @@ from unittest.mock import Mock, patch import pytest +from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN from homeassistant.components.homeassistant_yellow.const import DOMAIN from homeassistant.components.zha.core.const import DOMAIN as ZHA_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, MockModule, mock_integration @@ -52,6 +54,7 @@ def mock_reboot_host(): async def test_config_flow(hass: HomeAssistant) -> None: """Test the config flow.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) with patch( "homeassistant.components.homeassistant_yellow.async_setup_entry", @@ -76,6 +79,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: async def test_config_flow_single_entry(hass: HomeAssistant) -> None: """Test only a single entry is allowed.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -109,6 +113,7 @@ async def test_option_flow_install_multi_pan_addon( ) -> None: """Test installing the multi pan addon.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -179,6 +184,7 @@ async def test_option_flow_install_multi_pan_addon_zha( ) -> None: """Test installing the multi pan addon when a zha config entry exists.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -270,6 +276,7 @@ async def test_option_flow_led_settings( ) -> None: """Test updating LED settings.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -315,6 +322,7 @@ async def test_option_flow_led_settings_unchanged( ) -> None: """Test updating LED settings.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -346,6 +354,7 @@ async def test_option_flow_led_settings_unchanged( async def test_option_flow_led_settings_fail_1(hass: HomeAssistant) -> None: """Test updating LED settings.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -377,6 +386,7 @@ async def test_option_flow_led_settings_fail_2( ) -> None: """Test updating LED settings.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( diff --git a/tests/components/homeassistant_yellow/test_hardware.py b/tests/components/homeassistant_yellow/test_hardware.py index 5fb662471aa..b7843e75dcf 100644 --- a/tests/components/homeassistant_yellow/test_hardware.py +++ b/tests/components/homeassistant_yellow/test_hardware.py @@ -3,8 +3,10 @@ from unittest.mock import patch import pytest +from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN from homeassistant.components.homeassistant_yellow.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, MockModule, mock_integration from tests.typing import WebSocketGenerator @@ -15,6 +17,7 @@ async def test_hardware_info( ) -> None: """Test we can get the board info.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -66,6 +69,7 @@ async def test_hardware_info_fail( ) -> None: """Test async_info raises if os_info is not as expected.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index f8cdcd8a13b..f042a7bf54d 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -4,10 +4,12 @@ from unittest.mock import patch import pytest from homeassistant.components import zha +from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.homeassistant_yellow.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, MockModule, mock_integration @@ -20,6 +22,7 @@ async def test_setup_entry( ) -> None: """Test setup of a config entry, including setup of zha.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -62,6 +65,7 @@ async def test_setup_entry( async def test_setup_zha(hass: HomeAssistant, addon_store_info) -> None: """Test zha gets the right config.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -110,6 +114,7 @@ async def test_setup_zha_multipan( ) -> None: """Test zha gets the right config.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) addon_info.return_value["options"]["device"] = "/dev/ttyAMA1" @@ -160,6 +165,7 @@ async def test_setup_zha_multipan_other_device( ) -> None: """Test zha gets the right config.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) addon_info.return_value["options"]["device"] = "/dev/not_yellow_radio" @@ -205,9 +211,32 @@ async def test_setup_zha_multipan_other_device( assert config_entry.title == "Yellow" +async def test_setup_entry_no_hassio(hass: HomeAssistant) -> None: + """Test setup of a config entry without hassio.""" + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries()) == 1 + + with patch( + "homeassistant.components.homeassistant_yellow.get_os_info" + ) as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(mock_get_os_info.mock_calls) == 0 + assert len(hass.config_entries.async_entries()) == 0 + + async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: """Test setup of a config entry with wrong board type.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -233,6 +262,7 @@ async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: """Test setup of a config entry when hassio has not fetched os_info.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( @@ -258,6 +288,7 @@ async def test_setup_entry_addon_info_fails( ) -> None: """Test setup of a config entry when fetching addon info fails.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) addon_store_info.side_effect = HassioAPIError("Boom") # Setup the config entry @@ -285,6 +316,7 @@ async def test_setup_entry_addon_not_running( ) -> None: """Test the addon is started if it is not running.""" mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) # Setup the config entry config_entry = MockConfigEntry( From 8022d758ea5bd0ae7333169bd7cc096b8683925e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Feb 2024 12:30:09 +0100 Subject: [PATCH 0291/1367] Add data descriptions to analytics insights (#109694) --- .../components/analytics_insights/strings.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/analytics_insights/strings.json b/homeassistant/components/analytics_insights/strings.json index 96f13243868..58e47d1df08 100644 --- a/homeassistant/components/analytics_insights/strings.json +++ b/homeassistant/components/analytics_insights/strings.json @@ -5,6 +5,10 @@ "data": { "tracked_integrations": "Integrations", "tracked_custom_integrations": "Custom integrations" + }, + "data_description": { + "tracked_integrations": "Select the integrations you want to track", + "tracked_custom_integrations": "Select the custom integrations you want to track" } } }, @@ -17,7 +21,12 @@ "step": { "init": { "data": { - "tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]" + "tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]", + "tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]" + }, + "data_description": { + "tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]", + "tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]" } } }, From 682fff73f5c00429153de402b74a49c0cdf9623c Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 5 Feb 2024 12:40:40 +0100 Subject: [PATCH 0292/1367] Bump python matter server to 5.4.1 (#109692) --- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 4173e129895..d3d0568342e 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==5.4.0"] + "requirements": ["python-matter-server==5.4.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4927a6d048a..a8a19aa12b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2238,7 +2238,7 @@ python-kasa[speedups]==0.6.2.1 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==5.4.0 +python-matter-server==5.4.1 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d233f035c60..bd0935baf57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1711,7 +1711,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.6.2.1 # homeassistant.components.matter -python-matter-server==5.4.0 +python-matter-server==5.4.1 # homeassistant.components.xiaomi_miio python-miio==0.5.12 From 70ceddf165e6dd8c8ad54c266be6af876849baf2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Feb 2024 12:41:25 +0100 Subject: [PATCH 0293/1367] Set shorthand attribute in Epion (#109695) --- homeassistant/components/epion/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/epion/sensor.py b/homeassistant/components/epion/sensor.py index 826d565c2cd..c722e73ac6c 100644 --- a/homeassistant/components/epion/sensor.py +++ b/homeassistant/components/epion/sensor.py @@ -88,9 +88,9 @@ class EpionSensor(CoordinatorEntity[EpionCoordinator], SensorEntity): super().__init__(coordinator) self._epion_device_id = epion_device_id self.entity_description = description - self.unique_id = f"{epion_device_id}_{description.key}" + self._attr_unique_id = f"{epion_device_id}_{description.key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._epion_device_id)}, + identifiers={(DOMAIN, epion_device_id)}, manufacturer="Epion", name=self.device.get("deviceName"), sw_version=self.device.get("fwVersion"), From 30710815f01fa1d184f1625c5a74f5e20e9f42a5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 Feb 2024 13:07:57 +0100 Subject: [PATCH 0294/1367] Add test of remote enabling of remote UI (#109698) --- tests/components/cloud/test_client.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 0dfa682c07d..c8c0e40a5bb 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -472,3 +472,14 @@ async def test_logged_out( assert cloud.client._alexa_config is alexa_config_mock assert cloud.client._google_config is None google_config_mock.async_deinitialize.assert_called_once_with() + + +async def test_remote_enable(hass: HomeAssistant) -> None: + """Test enabling remote UI.""" + prefs = MagicMock(async_update=AsyncMock(return_value=None)) + client = CloudClient(hass, prefs, None, {}, {}) + client.cloud = MagicMock(is_logged_in=True, username="mock-username") + + result = await client.async_cloud_connect_update(True) + assert result is None + prefs.async_update.assert_called_once_with(remote_enabled=True) From bf8bd5ff21c665d2c76b6fdf5153f4157c489942 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 5 Feb 2024 13:56:10 +0100 Subject: [PATCH 0295/1367] Use dict.get instead of inline if (#109693) --- homeassistant/components/ihc/ihcdevice.py | 2 +- homeassistant/components/lg_soundbar/media_player.py | 2 +- homeassistant/components/modbus/modbus.py | 8 ++------ homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mqtt/mixins.py | 3 +-- homeassistant/components/panasonic_viera/config_flow.py | 8 ++------ homeassistant/components/starline/device_tracker.py | 2 +- homeassistant/components/telegram_bot/__init__.py | 8 ++------ homeassistant/components/tomorrowio/config_flow.py | 9 ++++----- 9 files changed, 15 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py index 0c077f8698e..30c84da40f8 100644 --- a/homeassistant/components/ihc/ihcdevice.py +++ b/homeassistant/components/ihc/ihcdevice.py @@ -39,7 +39,7 @@ class IHCDevice(Entity): self.ihc_name = product["name"] self.ihc_note = product["note"] self.ihc_position = product["position"] - self.suggested_area = product["group"] if "group" in product else None + self.suggested_area = product.get("group") if "id" in product: product_id = product["id"] self.device_id = f"{controller_id}_{product_id }" diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index 54d9be78df9..cfd0ebbd7a7 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -90,7 +90,7 @@ class LGDevice(MediaPlayerEntity): def handle_event(self, response): """Handle responses from the speakers.""" - data = response["data"] if "data" in response else {} + data = response.get("data") or {} if response["msg"] == "EQ_VIEW_INFO": if "i_bass" in data: self._bass = data["i_bass"] diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 71631352d52..c8e7fc3765e 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -172,9 +172,7 @@ async def async_modbus_setup( slave = int(float(service.data[ATTR_SLAVE])) address = int(float(service.data[ATTR_ADDRESS])) value = service.data[ATTR_VALUE] - hub = hub_collect[ - service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB - ] + hub = hub_collect[service.data.get(ATTR_HUB, DEFAULT_HUB)] if isinstance(value, list): await hub.async_pb_call( slave, @@ -196,9 +194,7 @@ async def async_modbus_setup( slave = int(float(service.data[ATTR_SLAVE])) address = service.data[ATTR_ADDRESS] state = service.data[ATTR_STATE] - hub = hub_collect[ - service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB - ] + hub = hub_collect[service.data.get(ATTR_HUB, DEFAULT_HUB)] if isinstance(state, list): await hub.async_pb_call(slave, address, state, CALL_TYPE_WRITE_COILS) else: diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 593d5bbd202..c8da1d7d8bc 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -544,7 +544,7 @@ async def websocket_subscribe( ) # Perform UTF-8 decoding directly in callback routine - qos: int = msg["qos"] if "qos" in msg else DEFAULT_QOS + qos: int = msg.get("qos", DEFAULT_QOS) connection.subscriptions[msg["id"]] = await async_subscribe( hass, msg["topic"], forward_messages, encoding=None, qos=qos ) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 4c7837a7a2b..aa42d257db4 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -646,8 +646,7 @@ class MqttAvailability(Entity): self._available_latest = False self._available = { - topic: (self._available[topic] if topic in self._available else False) - for topic in self._avail_topics + topic: (self._available.get(topic, False)) for topic in self._avail_topics } topics: dict[str, dict[str, Any]] = { f"availability_{topic}": { diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py index 38b6a351f29..80ab929231e 100644 --- a/homeassistant/components/panasonic_viera/config_flow.py +++ b/homeassistant/components/panasonic_viera/config_flow.py @@ -159,12 +159,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Load the data.""" self._data = config - self._data[CONF_PORT] = ( - self._data[CONF_PORT] if CONF_PORT in self._data else DEFAULT_PORT - ) - self._data[CONF_ON_ACTION] = ( - self._data[CONF_ON_ACTION] if CONF_ON_ACTION in self._data else None - ) + self._data[CONF_PORT] = self._data.get(CONF_PORT, DEFAULT_PORT) + self._data[CONF_ON_ACTION] = self._data.get(CONF_ON_ACTION) await self.async_set_unique_id(self._data[CONF_HOST]) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py index ca8118d6b43..1ddcbc9373b 100644 --- a/homeassistant/components/starline/device_tracker.py +++ b/homeassistant/components/starline/device_tracker.py @@ -44,7 +44,7 @@ class StarlineDeviceTracker(StarlineEntity, TrackerEntity, RestoreEntity): @property def location_accuracy(self): """Return the gps accuracy of the device.""" - return self._device.position["r"] if "r" in self._device.position else 0 + return self._device.position.get("r", 0) @property def latitude(self): diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 1d71e055e2e..2ba7752a85f 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -602,12 +602,8 @@ class TelegramNotificationService: if keys: params[ATTR_REPLYMARKUP] = ReplyKeyboardMarkup( [[key.strip() for key in row.split(",")] for row in keys], - resize_keyboard=data[ATTR_RESIZE_KEYBOARD] - if ATTR_RESIZE_KEYBOARD in data - else False, - one_time_keyboard=data[ATTR_ONE_TIME_KEYBOARD] - if ATTR_ONE_TIME_KEYBOARD in data - else False, + resize_keyboard=data.get(ATTR_RESIZE_KEYBOARD, False), + one_time_keyboard=data.get(ATTR_ONE_TIME_KEYBOARD, False), ) else: params[ATTR_REPLYMARKUP] = ReplyKeyboardRemove(True) diff --git a/homeassistant/components/tomorrowio/config_flow.py b/homeassistant/components/tomorrowio/config_flow.py index d6855f42c0a..aece537c867 100644 --- a/homeassistant/components/tomorrowio/config_flow.py +++ b/homeassistant/components/tomorrowio/config_flow.py @@ -56,13 +56,12 @@ def _get_config_schema( vol.Required(CONF_API_KEY, default=input_dict.get(CONF_API_KEY)): str, } - default_location = ( - input_dict[CONF_LOCATION] - if CONF_LOCATION in input_dict - else { + default_location = input_dict.get( + CONF_LOCATION, + { CONF_LATITUDE: hass.config.latitude, CONF_LONGITUDE: hass.config.longitude, - } + }, ) return vol.Schema( { From d49eff651b689014d46c88a325447db0dfd7b4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 5 Feb 2024 15:10:32 +0100 Subject: [PATCH 0296/1367] Fix log string in Traccar Server Coordinator (#109709) --- homeassistant/components/traccar_server/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/traccar_server/coordinator.py b/homeassistant/components/traccar_server/coordinator.py index 90c910e6062..df9b5adaf1a 100644 --- a/homeassistant/components/traccar_server/coordinator.py +++ b/homeassistant/components/traccar_server/coordinator.py @@ -78,7 +78,7 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat self.client.get_geofences(), ) except TraccarException as ex: - raise UpdateFailed("Error while updating device data: %s") from ex + raise UpdateFailed(f"Error while updating device data: {ex}") from ex if TYPE_CHECKING: assert isinstance(devices, list[DeviceModel]) # type: ignore[misc] From ebda0472e9f9c517c017ed811b28f321ddd9493d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 Feb 2024 15:11:39 +0100 Subject: [PATCH 0297/1367] Bump hass-nabucasa from 0.76.0 to 0.77.0 (#109699) --- homeassistant/components/cloud/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/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index d314aac2092..22383980c3c 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -8,5 +8,5 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["hass_nabucasa"], - "requirements": ["hass-nabucasa==0.76.0"] + "requirements": ["hass-nabucasa==0.77.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4ff23275e8f..bc4897ad501 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.1.0 habluetooth==2.4.0 -hass-nabucasa==0.76.0 +hass-nabucasa==0.77.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240202.0 diff --git a/requirements_all.txt b/requirements_all.txt index a8a19aa12b3..edabacc6d02 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1019,7 +1019,7 @@ habitipy==0.2.0 habluetooth==2.4.0 # homeassistant.components.cloud -hass-nabucasa==0.76.0 +hass-nabucasa==0.77.0 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd0935baf57..b9306206eb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -827,7 +827,7 @@ habitipy==0.2.0 habluetooth==2.4.0 # homeassistant.components.cloud -hass-nabucasa==0.76.0 +hass-nabucasa==0.77.0 # homeassistant.components.conversation hassil==1.6.1 From 3bcd367b65a215f7c6c2bb3894c9c2e39b9bf834 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Feb 2024 16:03:14 +0100 Subject: [PATCH 0298/1367] Set Analytics Insights as diagnostic (#109702) * Set Analytics Insights as diagnostic * Set Analytics Insights as diagnostic --- homeassistant/components/analytics_insights/sensor.py | 2 ++ .../analytics_insights/snapshots/test_sensor.ambr | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/analytics_insights/sensor.py b/homeassistant/components/analytics_insights/sensor.py index e0fe2c79413..90e9ff51b87 100644 --- a/homeassistant/components/analytics_insights/sensor.py +++ b/homeassistant/components/analytics_insights/sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -93,6 +94,7 @@ class HomeassistantAnalyticsSensor( """Home Assistant Analytics Sensor.""" _attr_has_entity_name = True + _attr_entity_category = EntityCategory.DIAGNOSTIC entity_description: AnalyticsSensorEntityDescription diff --git a/tests/components/analytics_insights/snapshots/test_sensor.ambr b/tests/components/analytics_insights/snapshots/test_sensor.ambr index 474263f68e9..dc4c3d6d795 100644 --- a/tests/components/analytics_insights/snapshots/test_sensor.ambr +++ b/tests/components/analytics_insights/snapshots/test_sensor.ambr @@ -12,7 +12,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.homeassistant_analytics_hacs_custom', 'has_entity_name': True, 'hidden_by': None, @@ -59,7 +59,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.homeassistant_analytics_myq', 'has_entity_name': True, 'hidden_by': None, @@ -106,7 +106,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.homeassistant_analytics_spotify', 'has_entity_name': True, 'hidden_by': None, @@ -153,7 +153,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.homeassistant_analytics_youtube', 'has_entity_name': True, 'hidden_by': None, From 0e9628bba06718742ea284d82250eaba1f1675d2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 5 Feb 2024 16:09:33 +0100 Subject: [PATCH 0299/1367] Update frontend to 20240205.0 (#109716) --- 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 039328b9cac..1af6a9da7b0 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==20240202.0"] + "requirements": ["home-assistant-frontend==20240205.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bc4897ad501..9b404e623e9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ habluetooth==2.4.0 hass-nabucasa==0.77.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240202.0 +home-assistant-frontend==20240205.0 home-assistant-intents==2024.2.2 httpx==0.26.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index edabacc6d02..fad05d0a6d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1059,7 +1059,7 @@ hole==0.8.0 holidays==0.41 # homeassistant.components.frontend -home-assistant-frontend==20240202.0 +home-assistant-frontend==20240205.0 # homeassistant.components.conversation home-assistant-intents==2024.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b9306206eb4..7d36ebdf88a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -855,7 +855,7 @@ hole==0.8.0 holidays==0.41 # homeassistant.components.frontend -home-assistant-frontend==20240202.0 +home-assistant-frontend==20240205.0 # homeassistant.components.conversation home-assistant-intents==2024.2.2 From aaff8a8c6233aa83064efb04d9a0cb23e77c9541 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Feb 2024 16:26:25 +0100 Subject: [PATCH 0300/1367] Add strings to Ruuvitag BLE (#109717) --- .../components/ruuvitag_ble/strings.json | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 homeassistant/components/ruuvitag_ble/strings.json diff --git a/homeassistant/components/ruuvitag_ble/strings.json b/homeassistant/components/ruuvitag_ble/strings.json new file mode 100644 index 00000000000..d1d544c2381 --- /dev/null +++ b/homeassistant/components/ruuvitag_ble/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:common::config_flow::data::device%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "not_supported": "Device not supported", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} From 8df305c881535e52a1511d1c1a35ac6fd28fa719 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Feb 2024 16:32:39 +0100 Subject: [PATCH 0301/1367] Remove obsolete check from Proximity (#109701) --- homeassistant/components/proximity/sensor.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/proximity/sensor.py b/homeassistant/components/proximity/sensor.py index bd788058869..c562467f8be 100644 --- a/homeassistant/components/proximity/sensor.py +++ b/homeassistant/components/proximity/sensor.py @@ -176,18 +176,19 @@ class ProximityTrackedEntitySensor( ) @property - def data(self) -> dict[str, str | int | None] | None: + def data(self) -> dict[str, str | int | None]: """Get data from coordinator.""" - return self.coordinator.data.entities.get(self.tracked_entity_id) + return self.coordinator.data.entities[self.tracked_entity_id] @property def available(self) -> bool: """Return if entity is available.""" - return super().available and self.data is not None + return ( + super().available + and self.tracked_entity_id in self.coordinator.data.entities + ) @property def native_value(self) -> str | float | None: """Return native sensor value.""" - if self.data is None: - return None return self.data.get(self.entity_description.key) From 6f28d79651524f9a46a70274550140c05a4a5e64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Feb 2024 10:07:21 -0600 Subject: [PATCH 0302/1367] Copy callbacks instead of slice for event dispatch (#109711) We established copy is faster in https://github.com/home-assistant/core/pull/108428#discussion_r1466932262 --- homeassistant/helpers/event.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index d3f4144a293..ea80591a989 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -291,7 +291,7 @@ def _async_dispatch_entity_id_event( """Dispatch to listeners.""" if not (callbacks_list := callbacks.get(event.data["entity_id"])): return - for job in callbacks_list[:]: + for job in callbacks_list.copy(): try: hass.async_run_hass_job(job, event) except Exception: # pylint: disable=broad-except @@ -428,7 +428,7 @@ def _async_dispatch_old_entity_id_or_entity_id_event( ) ): return - for job in callbacks_list[:]: + for job in callbacks_list.copy(): try: hass.async_run_hass_job(job, event) except Exception: # pylint: disable=broad-except @@ -499,7 +499,7 @@ def _async_dispatch_device_id_event( """Dispatch to listeners.""" if not (callbacks_list := callbacks.get(event.data["device_id"])): return - for job in callbacks_list[:]: + for job in callbacks_list.copy(): try: hass.async_run_hass_job(job, event) except Exception: # pylint: disable=broad-except From be6399410efd5911992fb8dab793b480ad16fbc5 Mon Sep 17 00:00:00 2001 From: Pascal Reeb Date: Mon, 5 Feb 2024 17:09:18 +0100 Subject: [PATCH 0303/1367] Use a single call to add entities in Nuki (#109696) * Nuki: use a single call to add entities * Clean up list addition --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/nuki/binary_sensor.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index e3b2d129017..f1da14bdd35 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -22,19 +22,20 @@ async def async_setup_entry( """Set up the Nuki binary sensors.""" entry_data: NukiEntryData = hass.data[NUKI_DOMAIN][entry.entry_id] - lock_entities = [] - opener_entities = [] + entities: list[NukiEntity] = [] for lock in entry_data.locks: if lock.is_door_sensor_activated: - lock_entities.extend([NukiDoorsensorEntity(entry_data.coordinator, lock)]) + entities.append(NukiDoorsensorEntity(entry_data.coordinator, lock)) - async_add_entities(lock_entities) + entities.extend( + [ + NukiRingactionEntity(entry_data.coordinator, opener) + for opener in entry_data.openers + ] + ) - for opener in entry_data.openers: - opener_entities.extend([NukiRingactionEntity(entry_data.coordinator, opener)]) - - async_add_entities(opener_entities) + async_add_entities(entities) class NukiDoorsensorEntity(NukiEntity[NukiDevice], BinarySensorEntity): From 5dfffb0818ea2842ba606921cf47ae745a1a7420 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 5 Feb 2024 18:01:06 +0100 Subject: [PATCH 0304/1367] Allow weight to be used as total_increasing state_class for sensors (#108505) * Allow weight to be used as total_increasing state_class for sensors * Add SensorStateClass.TOTAL --- homeassistant/components/sensor/const.py | 6 +++++- tests/components/sensor/test_recorder.py | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index aad882821d6..3dc8f878791 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -640,7 +640,11 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = { SensorStateClass.TOTAL, SensorStateClass.TOTAL_INCREASING, }, - SensorDeviceClass.WEIGHT: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.WEIGHT: { + SensorStateClass.MEASUREMENT, + SensorStateClass.TOTAL, + SensorStateClass.TOTAL_INCREASING, + }, SensorDeviceClass.WIND_SPEED: {SensorStateClass.MEASUREMENT}, } diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 2dcc873ca8b..b4b535473c1 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -988,6 +988,7 @@ async def test_compile_hourly_sum_statistics_amount( ("monetary", "SEK", "SEK", "SEK", None, 1), ("gas", "m³", "m³", "m³", "volume", 1), ("gas", "ft³", "ft³", "ft³", "volume", 1), + ("weight", "kg", "kg", "kg", "mass", 1), ], ) def test_compile_hourly_sum_statistics_amount_reset_every_state_change( @@ -1457,6 +1458,7 @@ def test_compile_hourly_sum_statistics_negative_state( ("monetary", "SEK", "SEK", "SEK", None, 1), ("gas", "m³", "m³", "m³", "volume", 1), ("gas", "ft³", "ft³", "ft³", "volume", 1), + ("weight", "kg", "kg", "kg", "mass", 1), ], ) def test_compile_hourly_sum_statistics_total_no_reset( @@ -1569,6 +1571,7 @@ def test_compile_hourly_sum_statistics_total_no_reset( ("energy", "Wh", "Wh", "Wh", "energy", 1), ("gas", "m³", "m³", "m³", "volume", 1), ("gas", "ft³", "ft³", "ft³", "volume", 1), + ("weight", "kg", "kg", "kg", "mass", 1), ], ) def test_compile_hourly_sum_statistics_total_increasing( @@ -1679,7 +1682,10 @@ def test_compile_hourly_sum_statistics_total_increasing( "unit_class", "factor", ), - [("energy", "kWh", "kWh", "kWh", "energy", 1)], + [ + ("energy", "kWh", "kWh", "kWh", "energy", 1), + ("weight", "kg", "kg", "kg", "mass", 1), + ], ) def test_compile_hourly_sum_statistics_total_increasing_small_dip( hass_recorder: Callable[..., HomeAssistant], From 46f8fb3ac19ca9810a3399520b59a119a6736614 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 5 Feb 2024 18:45:16 +0100 Subject: [PATCH 0305/1367] Use builtin TimeoutError [misc] (#109703) --- homeassistant/components/bond/__init__.py | 3 +-- homeassistant/components/bond/entity.py | 4 ++-- homeassistant/components/control4/config_flow.py | 3 +-- homeassistant/components/nightscout/__init__.py | 3 +-- homeassistant/components/nightscout/config_flow.py | 3 +-- homeassistant/components/nightscout/sensor.py | 3 +-- homeassistant/components/ourgroceries/__init__.py | 4 +--- homeassistant/components/ourgroceries/config_flow.py | 3 +-- tests/components/bond/common.py | 3 +-- tests/components/hassio/test_issues.py | 1 - tests/components/homewizard/test_init.py | 1 - tests/components/ourgroceries/test_config_flow.py | 3 +-- tests/components/ourgroceries/test_init.py | 8 ++------ tests/components/ourgroceries/test_todo.py | 3 +-- 14 files changed, 14 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index b6f402004f6..2e60512156f 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -1,5 +1,4 @@ """The Bond integration.""" -from asyncio import TimeoutError as AsyncIOTimeoutError from http import HTTPStatus import logging from typing import Any @@ -56,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Bond token no longer valid: %s", ex) return False raise ConfigEntryNotReady from ex - except (ClientError, AsyncIOTimeoutError, OSError) as error: + except (ClientError, TimeoutError, OSError) as error: raise ConfigEntryNotReady from error bpup_subs = BPUPSubscriptions() diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 2c54ad8f3dd..dd307547b81 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -2,7 +2,7 @@ from __future__ import annotations from abc import abstractmethod -from asyncio import Lock, TimeoutError as AsyncIOTimeoutError +from asyncio import Lock from datetime import datetime import logging @@ -139,7 +139,7 @@ class BondEntity(Entity): """Fetch via the API.""" try: state: dict = await self._hub.bond.device_state(self._device_id) - except (ClientError, AsyncIOTimeoutError, OSError) as error: + except (ClientError, TimeoutError, OSError) as error: if self.available: _LOGGER.warning( "Entity %s has become unavailable", self.entity_id, exc_info=error diff --git a/homeassistant/components/control4/config_flow.py b/homeassistant/components/control4/config_flow.py index 06dc62d114b..b93e586b7ca 100644 --- a/homeassistant/components/control4/config_flow.py +++ b/homeassistant/components/control4/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Control4 integration.""" from __future__ import annotations -from asyncio import TimeoutError as asyncioTimeoutError import logging from aiohttp.client_exceptions import ClientError @@ -82,7 +81,7 @@ class Control4Validator: ) await director.getAllItemInfo() return True - except (Unauthorized, ClientError, asyncioTimeoutError): + except (Unauthorized, ClientError, TimeoutError): _LOGGER.error("Failed to connect to the Control4 controller") return False diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index 88f12ffa4bc..798fcf1ec9d 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -1,5 +1,4 @@ """The Nightscout integration.""" -from asyncio import TimeoutError as AsyncIOTimeoutError from aiohttp import ClientError from py_nightscout import Api as NightscoutAPI @@ -26,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api = NightscoutAPI(server_url, session=session, api_secret=api_key) try: status = await api.get_server_status() - except (ClientError, AsyncIOTimeoutError, OSError) as error: + except (ClientError, TimeoutError, OSError) as error: raise ConfigEntryNotReady from error hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/nightscout/config_flow.py b/homeassistant/components/nightscout/config_flow.py index 98e075ba3c9..6249979c83d 100644 --- a/homeassistant/components/nightscout/config_flow.py +++ b/homeassistant/components/nightscout/config_flow.py @@ -1,5 +1,4 @@ """Config flow for Nightscout integration.""" -from asyncio import TimeoutError as AsyncIOTimeoutError import logging from typing import Any @@ -30,7 +29,7 @@ async def _validate_input(data: dict[str, Any]) -> dict[str, str]: await api.get_sgvs() except ClientResponseError as error: raise InputValidationError("invalid_auth") from error - except (ClientError, AsyncIOTimeoutError, OSError) as error: + except (ClientError, TimeoutError, OSError) as error: raise InputValidationError("cannot_connect") from error # Return info to be stored in the config entry. diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py index 851610ee374..bdc46e75cb8 100644 --- a/homeassistant/components/nightscout/sensor.py +++ b/homeassistant/components/nightscout/sensor.py @@ -1,7 +1,6 @@ """Support for Nightscout sensors.""" from __future__ import annotations -from asyncio import TimeoutError as AsyncIOTimeoutError from datetime import timedelta import logging from typing import Any @@ -51,7 +50,7 @@ class NightscoutSensor(SensorEntity): """Fetch the latest data from Nightscout REST API and update the state.""" try: values = await self.api.get_sgvs() - except (ClientError, AsyncIOTimeoutError, OSError) as error: + except (ClientError, TimeoutError, OSError) as error: _LOGGER.error("Error fetching data. Failed with %s", error) self._attr_available = False return diff --git a/homeassistant/components/ourgroceries/__init__.py b/homeassistant/components/ourgroceries/__init__.py index ebb928e72d0..472313aa315 100644 --- a/homeassistant/components/ourgroceries/__init__.py +++ b/homeassistant/components/ourgroceries/__init__.py @@ -1,8 +1,6 @@ """The OurGroceries integration.""" from __future__ import annotations -from asyncio import TimeoutError as AsyncIOTimeoutError - from aiohttp import ClientError from ourgroceries import OurGroceries from ourgroceries.exceptions import InvalidLoginException @@ -26,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: og = OurGroceries(data[CONF_USERNAME], data[CONF_PASSWORD]) try: await og.login() - except (AsyncIOTimeoutError, ClientError) as error: + except (TimeoutError, ClientError) as error: raise ConfigEntryNotReady from error except InvalidLoginException: return False diff --git a/homeassistant/components/ourgroceries/config_flow.py b/homeassistant/components/ourgroceries/config_flow.py index a982325fceb..65670dd7f92 100644 --- a/homeassistant/components/ourgroceries/config_flow.py +++ b/homeassistant/components/ourgroceries/config_flow.py @@ -1,7 +1,6 @@ """Config flow for OurGroceries integration.""" from __future__ import annotations -from asyncio import TimeoutError as AsyncIOTimeoutError import logging from typing import Any @@ -40,7 +39,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): og = OurGroceries(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) try: await og.login() - except (AsyncIOTimeoutError, ClientError): + except (TimeoutError, ClientError): errors["base"] = "cannot_connect" except InvalidLoginException: errors["base"] = "invalid_auth" diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index ff1f986583e..d97ef9a7a31 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -1,7 +1,6 @@ """Common methods used across tests for Bond.""" from __future__ import annotations -from asyncio import TimeoutError as AsyncIOTimeoutError from contextlib import nullcontext from datetime import timedelta from typing import Any @@ -248,7 +247,7 @@ async def help_test_entity_available( assert hass.states.get(entity_id).state != STATE_UNAVAILABLE - with patch_bond_device_state(side_effect=AsyncIOTimeoutError()): + with patch_bond_device_state(side_effect=TimeoutError()): async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNAVAILABLE diff --git a/tests/components/hassio/test_issues.py b/tests/components/hassio/test_issues.py index 4d694b79e46..21cd249bd53 100644 --- a/tests/components/hassio/test_issues.py +++ b/tests/components/hassio/test_issues.py @@ -1,7 +1,6 @@ """Test issues from supervisor issues.""" from __future__ import annotations -from asyncio import TimeoutError import os from typing import Any from unittest.mock import ANY, patch diff --git a/tests/components/homewizard/test_init.py b/tests/components/homewizard/test_init.py index a4893c77f42..e777b2d43c6 100644 --- a/tests/components/homewizard/test_init.py +++ b/tests/components/homewizard/test_init.py @@ -1,5 +1,4 @@ """Tests for the homewizard component.""" -from asyncio import TimeoutError from unittest.mock import MagicMock from homewizard_energy.errors import DisabledError, HomeWizardEnergyException diff --git a/tests/components/ourgroceries/test_config_flow.py b/tests/components/ourgroceries/test_config_flow.py index f9d274125c1..78504e1fb7a 100644 --- a/tests/components/ourgroceries/test_config_flow.py +++ b/tests/components/ourgroceries/test_config_flow.py @@ -5,7 +5,6 @@ import pytest from homeassistant import config_entries from homeassistant.components.ourgroceries.config_flow import ( - AsyncIOTimeoutError, ClientError, InvalidLoginException, ) @@ -49,7 +48,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: [ (InvalidLoginException, "invalid_auth"), (ClientError, "cannot_connect"), - (AsyncIOTimeoutError, "cannot_connect"), + (TimeoutError, "cannot_connect"), (Exception, "unknown"), ], ) diff --git a/tests/components/ourgroceries/test_init.py b/tests/components/ourgroceries/test_init.py index ef96c5e811c..43905c4fcf9 100644 --- a/tests/components/ourgroceries/test_init.py +++ b/tests/components/ourgroceries/test_init.py @@ -3,11 +3,7 @@ from unittest.mock import AsyncMock import pytest -from homeassistant.components.ourgroceries import ( - AsyncIOTimeoutError, - ClientError, - InvalidLoginException, -) +from homeassistant.components.ourgroceries import ClientError, InvalidLoginException from homeassistant.components.ourgroceries.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -41,7 +37,7 @@ def login_with_error(exception, ourgroceries: AsyncMock): [ (InvalidLoginException, ConfigEntryState.SETUP_ERROR), (ClientError, ConfigEntryState.SETUP_RETRY), - (AsyncIOTimeoutError, ConfigEntryState.SETUP_RETRY), + (TimeoutError, ConfigEntryState.SETUP_RETRY), ], ) async def test_init_failure( diff --git a/tests/components/ourgroceries/test_todo.py b/tests/components/ourgroceries/test_todo.py index 649e86f2b05..8ede2a40cc8 100644 --- a/tests/components/ourgroceries/test_todo.py +++ b/tests/components/ourgroceries/test_todo.py @@ -1,5 +1,4 @@ """Unit tests for the OurGroceries todo platform.""" -from asyncio import TimeoutError as AsyncIOTimeoutError from unittest.mock import AsyncMock from aiohttp import ClientError @@ -257,7 +256,7 @@ async def test_version_id_optimization( ("exception"), [ (ClientError), - (AsyncIOTimeoutError), + (TimeoutError), ], ) async def test_coordinator_error( From ed7307cdaf6cd6558278e52219f0d19843382a3f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 5 Feb 2024 18:46:11 +0100 Subject: [PATCH 0306/1367] Use builtin TimeoutError [socket.timeout] (#109704) --- homeassistant/components/blackbird/media_player.py | 3 +-- homeassistant/components/deluge/__init__.py | 7 +------ homeassistant/components/deluge/config_flow.py | 7 +------ homeassistant/components/deluge/coordinator.py | 3 +-- homeassistant/components/ebusd/__init__.py | 3 +-- homeassistant/components/lg_soundbar/config_flow.py | 3 +-- homeassistant/components/maxcube/__init__.py | 5 ++--- homeassistant/components/maxcube/climate.py | 3 +-- homeassistant/components/mikrotik/hub.py | 5 ++--- homeassistant/components/motion_blinds/coordinator.py | 5 ++--- homeassistant/components/motion_blinds/gateway.py | 2 +- homeassistant/components/pilight/__init__.py | 3 +-- homeassistant/components/pjlink/media_player.py | 4 +--- homeassistant/components/radiotherm/__init__.py | 3 +-- homeassistant/components/radiotherm/config_flow.py | 3 +-- homeassistant/components/radiotherm/coordinator.py | 3 +-- 16 files changed, 19 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index daa23553c96..61dca6550c0 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -import socket from pyblackbird import get_blackbird from serial import SerialException @@ -93,7 +92,7 @@ def setup_platform( try: blackbird = get_blackbird(host, False) connection = host - except socket.timeout: + except TimeoutError: _LOGGER.error("Error connecting to the Blackbird controller") return diff --git a/homeassistant/components/deluge/__init__.py b/homeassistant/components/deluge/__init__.py index 63412242dd0..40f4d772670 100644 --- a/homeassistant/components/deluge/__init__.py +++ b/homeassistant/components/deluge/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -import socket from ssl import SSLError from deluge_client.client import DelugeRPCClient @@ -40,11 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api.web_port = entry.data[CONF_WEB_PORT] try: await hass.async_add_executor_job(api.connect) - except ( - ConnectionRefusedError, - socket.timeout, - SSLError, - ) as ex: + except (ConnectionRefusedError, TimeoutError, SSLError) as ex: raise ConfigEntryNotReady("Connection to Deluge Daemon failed") from ex except Exception as ex: # pylint:disable=broad-except if type(ex).__name__ == "BadLoginError": diff --git a/homeassistant/components/deluge/config_flow.py b/homeassistant/components/deluge/config_flow.py index 5de61350039..db2598e1f67 100644 --- a/homeassistant/components/deluge/config_flow.py +++ b/homeassistant/components/deluge/config_flow.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Mapping -import socket from ssl import SSLError from typing import Any @@ -91,11 +90,7 @@ class DelugeFlowHandler(ConfigFlow, domain=DOMAIN): ) try: await self.hass.async_add_executor_job(api.connect) - except ( - ConnectionRefusedError, - socket.timeout, - SSLError, - ): + except (ConnectionRefusedError, TimeoutError, SSLError): return "cannot_connect" except Exception as ex: # pylint:disable=broad-except if type(ex).__name__ == "BadLoginError": diff --git a/homeassistant/components/deluge/coordinator.py b/homeassistant/components/deluge/coordinator.py index 9b0d5907b1a..7a3e840ff95 100644 --- a/homeassistant/components/deluge/coordinator.py +++ b/homeassistant/components/deluge/coordinator.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import timedelta -import socket from ssl import SSLError from typing import Any @@ -52,7 +51,7 @@ class DelugeDataUpdateCoordinator( ) except ( ConnectionRefusedError, - socket.timeout, + TimeoutError, SSLError, FailedToReconnectException, ) as ex: diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index ab83759bb2d..b1eb03989ea 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -1,6 +1,5 @@ """Support for Ebusd daemon for communication with eBUS heating systems.""" import logging -import socket import ebusdpy import voluptuous as vol @@ -80,7 +79,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.debug("Ebusd integration setup completed") return True - except (socket.timeout, OSError): + except (TimeoutError, OSError): return False diff --git a/homeassistant/components/lg_soundbar/config_flow.py b/homeassistant/components/lg_soundbar/config_flow.py index d2cb1749689..fde5c20ebd7 100644 --- a/homeassistant/components/lg_soundbar/config_flow.py +++ b/homeassistant/components/lg_soundbar/config_flow.py @@ -1,7 +1,6 @@ """Config flow to configure the LG Soundbar integration.""" import logging from queue import Empty, Full, Queue -import socket import temescal import voluptuous as vol @@ -60,7 +59,7 @@ def test_connect(host, port): details["uuid"] = uuid_q.get(timeout=QUEUE_TIMEOUT) except Empty: pass - except socket.timeout as err: + except TimeoutError as err: raise ConnectionError(f"Connection timeout with server: {host}:{port}") from err except OSError as err: raise ConnectionError(f"Cannot resolve hostname: {host}") from err diff --git a/homeassistant/components/maxcube/__init__.py b/homeassistant/components/maxcube/__init__.py index f8899ea082f..41aed4be15c 100644 --- a/homeassistant/components/maxcube/__init__.py +++ b/homeassistant/components/maxcube/__init__.py @@ -1,6 +1,5 @@ """Support for the MAX! Cube LAN Gateway.""" import logging -from socket import timeout from threading import Lock import time @@ -65,7 +64,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: try: cube = MaxCube(host, port, now=now) hass.data[DATA_KEY][host] = MaxCubeHandle(cube, scan_interval) - except timeout as ex: + except TimeoutError as ex: _LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex)) persistent_notification.create( hass, @@ -108,7 +107,7 @@ class MaxCubeHandle: try: self.cube.update() - except timeout: + except TimeoutError: _LOGGER.error("Max!Cube connection failed") return False diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index f3d302fc209..42abed48724 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -import socket from typing import Any from maxcube.device import ( @@ -152,7 +151,7 @@ class MaxCubeClimate(ClimateEntity): with self._cubehandle.mutex: try: self._cubehandle.cube.set_temperature_mode(self._device, temp, mode) - except (socket.timeout, OSError): + except (TimeoutError, OSError): _LOGGER.error("Setting HVAC mode failed") @property diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 44d60d5dcb4..044a45fb9b5 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import timedelta import logging -import socket import ssl from typing import Any @@ -227,7 +226,7 @@ class MikrotikData: except ( librouteros.exceptions.ConnectionClosed, OSError, - socket.timeout, + TimeoutError, ) as api_error: _LOGGER.error("Mikrotik %s connection error %s", self._host, api_error) # try to reconnect @@ -330,7 +329,7 @@ def get_api(entry: dict[str, Any]) -> librouteros.Api: except ( librouteros.exceptions.LibRouterosError, OSError, - socket.timeout, + TimeoutError, ) as api_error: _LOGGER.error("Mikrotik %s error: %s", entry[CONF_HOST], api_error) if "invalid user name or password" in str(api_error): diff --git a/homeassistant/components/motion_blinds/coordinator.py b/homeassistant/components/motion_blinds/coordinator.py index e8dc5494f25..57d67165320 100644 --- a/homeassistant/components/motion_blinds/coordinator.py +++ b/homeassistant/components/motion_blinds/coordinator.py @@ -2,7 +2,6 @@ import asyncio from datetime import timedelta import logging -from socket import timeout from typing import Any from motionblinds import DEVICE_TYPES_WIFI, ParseException @@ -50,7 +49,7 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): """Fetch data from gateway.""" try: self._gateway.Update() - except (timeout, ParseException): + except (TimeoutError, ParseException): # let the error be logged and handled by the motionblinds library return {ATTR_AVAILABLE: False} @@ -65,7 +64,7 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): blind.Update() else: blind.Update_trigger() - except (timeout, ParseException): + except (TimeoutError, ParseException): # let the error be logged and handled by the motionblinds library return {ATTR_AVAILABLE: False} diff --git a/homeassistant/components/motion_blinds/gateway.py b/homeassistant/components/motion_blinds/gateway.py index ac18840ddeb..c0ddc9b4287 100644 --- a/homeassistant/components/motion_blinds/gateway.py +++ b/homeassistant/components/motion_blinds/gateway.py @@ -50,7 +50,7 @@ class ConnectMotionGateway: try: # update device info and get the connected sub devices await self._hass.async_add_executor_job(self.update_gateway) - except socket.timeout: + except TimeoutError: _LOGGER.error( "Timeout trying to connect to Motion Gateway with host %s", host ) diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index 60568e722ef..51a15a52ec8 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -5,7 +5,6 @@ from collections.abc import Callable from datetime import timedelta import functools import logging -import socket import threading from typing import Any, ParamSpec @@ -75,7 +74,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: try: pilight_client = pilight.Client(host=host, port=port) - except (OSError, socket.timeout) as err: + except (OSError, TimeoutError) as err: _LOGGER.error("Unable to connect to %s on port %s: %s", host, port, err) return False diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index 4bbf1225a92..1a7ff877bb8 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -1,8 +1,6 @@ """Support for controlling projector via the PJLink protocol.""" from __future__ import annotations -import socket - from pypjlink import MUTE_AUDIO, Projector from pypjlink.projector import ProjectorError import voluptuous as vol @@ -116,7 +114,7 @@ class PjLinkDevice(MediaPlayerEntity): try: projector = Projector.from_address(self._host, self._port) projector.authenticate(self._password) - except (socket.timeout, OSError) as err: + except (TimeoutError, OSError) as err: self._attr_available = False raise ProjectorError(ERR_PROJECTOR_UNAVAILABLE) from err diff --git a/homeassistant/components/radiotherm/__init__.py b/homeassistant/components/radiotherm/__init__.py index 808ee56b092..86a9fe58013 100644 --- a/homeassistant/components/radiotherm/__init__.py +++ b/homeassistant/components/radiotherm/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Coroutine -from socket import timeout from typing import Any, TypeVar from urllib.error import URLError @@ -32,7 +31,7 @@ async def _async_call_or_raise_not_ready( except RadiothermTstatError as ex: msg = f"{host} was busy (invalid value returned): {ex}" raise ConfigEntryNotReady(msg) from ex - except timeout as ex: + except TimeoutError as ex: msg = f"{host} timed out waiting for a response: {ex}" raise ConfigEntryNotReady(msg) from ex except (OSError, URLError) as ex: diff --git a/homeassistant/components/radiotherm/config_flow.py b/homeassistant/components/radiotherm/config_flow.py index ca488ade461..c370cc86484 100644 --- a/homeassistant/components/radiotherm/config_flow.py +++ b/homeassistant/components/radiotherm/config_flow.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from socket import timeout from typing import Any from urllib.error import URLError @@ -30,7 +29,7 @@ async def validate_connection(hass: HomeAssistant, host: str) -> RadioThermInitD """Validate the connection.""" try: return await async_get_init_data(hass, host) - except (timeout, RadiothermTstatError, URLError, OSError) as ex: + except (TimeoutError, RadiothermTstatError, URLError, OSError) as ex: raise CannotConnect(f"Failed to connect to {host}: {ex}") from ex diff --git a/homeassistant/components/radiotherm/coordinator.py b/homeassistant/components/radiotherm/coordinator.py index ffc6bfcc8ba..5b0161d9f22 100644 --- a/homeassistant/components/radiotherm/coordinator.py +++ b/homeassistant/components/radiotherm/coordinator.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import timedelta import logging -from socket import timeout from urllib.error import URLError from radiotherm.validate import RadiothermTstatError @@ -39,7 +38,7 @@ class RadioThermUpdateCoordinator(DataUpdateCoordinator[RadioThermUpdate]): except RadiothermTstatError as ex: msg = f"{self._description} was busy (invalid value returned): {ex}" raise UpdateFailed(msg) from ex - except timeout as ex: + except TimeoutError as ex: msg = f"{self._description}) timed out waiting for a response: {ex}" raise UpdateFailed(msg) from ex except (OSError, URLError) as ex: From 53d46acc50e1e25c4411145f7ccc95e54d9d2915 Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Mon, 5 Feb 2024 18:51:01 +0100 Subject: [PATCH 0307/1367] Bump python-bring-api to 3.0.0 (#109720) --- homeassistant/components/bring/__init__.py | 11 ++++----- homeassistant/components/bring/config_flow.py | 13 ++++++----- homeassistant/components/bring/coordinator.py | 8 ++----- homeassistant/components/bring/manifest.json | 2 +- homeassistant/components/bring/todo.py | 23 +++++++------------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bring/conftest.py | 10 ++++---- tests/components/bring/test_config_flow.py | 14 ++++++----- tests/components/bring/test_init.py | 8 +++---- 10 files changed, 42 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/bring/__init__.py b/homeassistant/components/bring/__init__.py index e9501fc64b3..aec3cd53c94 100644 --- a/homeassistant/components/bring/__init__.py +++ b/homeassistant/components/bring/__init__.py @@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN from .coordinator import BringDataUpdateCoordinator @@ -29,14 +30,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: email = entry.data[CONF_EMAIL] password = entry.data[CONF_PASSWORD] - bring = Bring(email, password) - - def login_and_load_lists() -> None: - bring.login() - bring.loadLists() + session = async_get_clientsession(hass) + bring = Bring(email, password, sessionAsync=session) try: - await hass.async_add_executor_job(login_and_load_lists) + await bring.loginAsync() + await bring.loadListsAsync() except BringRequestException as e: raise ConfigEntryNotReady( f"Timeout while connecting for email '{email}'" diff --git a/homeassistant/components/bring/config_flow.py b/homeassistant/components/bring/config_flow.py index 21774117ff6..122e71feea6 100644 --- a/homeassistant/components/bring/config_flow.py +++ b/homeassistant/components/bring/config_flow.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ( TextSelector, TextSelectorConfig, @@ -48,14 +49,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: - bring = Bring(user_input[CONF_EMAIL], user_input[CONF_PASSWORD]) - - def login_and_load_lists() -> None: - bring.login() - bring.loadLists() + session = async_get_clientsession(self.hass) + bring = Bring( + user_input[CONF_EMAIL], user_input[CONF_PASSWORD], sessionAsync=session + ) try: - await self.hass.async_add_executor_job(login_and_load_lists) + await bring.loginAsync() + await bring.loadListsAsync() except BringRequestException: errors["base"] = "cannot_connect" except BringAuthException: diff --git a/homeassistant/components/bring/coordinator.py b/homeassistant/components/bring/coordinator.py index a7bd4a35f43..eb28f24e085 100644 --- a/homeassistant/components/bring/coordinator.py +++ b/homeassistant/components/bring/coordinator.py @@ -40,9 +40,7 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]): async def _async_update_data(self) -> dict[str, BringData]: try: - lists_response = await self.hass.async_add_executor_job( - self.bring.loadLists - ) + lists_response = await self.bring.loadListsAsync() except BringRequestException as e: raise UpdateFailed("Unable to connect and retrieve data from bring") from e except BringParseException as e: @@ -51,9 +49,7 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]): list_dict = {} for lst in lists_response["lists"]: try: - items = await self.hass.async_add_executor_job( - self.bring.getItems, lst["listUuid"] - ) + items = await self.bring.getItemsAsync(lst["listUuid"]) except BringRequestException as e: raise UpdateFailed( "Unable to connect and retrieve data from bring" diff --git a/homeassistant/components/bring/manifest.json b/homeassistant/components/bring/manifest.json index bc249ecea98..e7d23bfc3df 100644 --- a/homeassistant/components/bring/manifest.json +++ b/homeassistant/components/bring/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bring", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["python-bring-api==2.0.0"] + "requirements": ["python-bring-api==3.0.0"] } diff --git a/homeassistant/components/bring/todo.py b/homeassistant/components/bring/todo.py index bd87a2d18de..14279c894af 100644 --- a/homeassistant/components/bring/todo.py +++ b/homeassistant/components/bring/todo.py @@ -91,11 +91,8 @@ class BringTodoListEntity( async def async_create_todo_item(self, item: TodoItem) -> None: """Add an item to the To-do list.""" try: - await self.hass.async_add_executor_job( - self.coordinator.bring.saveItem, - self.bring_list["listUuid"], - item.summary, - item.description or "", + await self.coordinator.bring.saveItemAsync( + self.bring_list["listUuid"], item.summary, item.description or "" ) except BringRequestException as e: raise HomeAssistantError("Unable to save todo item for bring") from e @@ -126,16 +123,14 @@ class BringTodoListEntity( assert item.uid if item.status == TodoItemStatus.COMPLETED: - await self.hass.async_add_executor_job( - self.coordinator.bring.removeItem, + await self.coordinator.bring.removeItemAsync( bring_list["listUuid"], item.uid, ) elif item.summary == item.uid: try: - await self.hass.async_add_executor_job( - self.coordinator.bring.updateItem, + await self.coordinator.bring.updateItemAsync( bring_list["listUuid"], item.uid, item.description or "", @@ -144,13 +139,11 @@ class BringTodoListEntity( raise HomeAssistantError("Unable to update todo item for bring") from e else: try: - await self.hass.async_add_executor_job( - self.coordinator.bring.removeItem, + await self.coordinator.bring.removeItemAsync( bring_list["listUuid"], item.uid, ) - await self.hass.async_add_executor_job( - self.coordinator.bring.saveItem, + await self.coordinator.bring.saveItemAsync( bring_list["listUuid"], item.summary, item.description or "", @@ -164,8 +157,8 @@ class BringTodoListEntity( """Delete an item from the To-do list.""" for uid in uids: try: - await self.hass.async_add_executor_job( - self.coordinator.bring.removeItem, self.bring_list["listUuid"], uid + await self.coordinator.bring.removeItemAsync( + self.bring_list["listUuid"], uid ) except BringRequestException as e: raise HomeAssistantError("Unable to delete todo item for bring") from e diff --git a/requirements_all.txt b/requirements_all.txt index fad05d0a6d7..36e916f9fba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2181,7 +2181,7 @@ python-awair==0.2.4 python-blockchain-api==0.0.2 # homeassistant.components.bring -python-bring-api==2.0.0 +python-bring-api==3.0.0 # homeassistant.components.bsblan python-bsblan==0.5.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d36ebdf88a..401e47232dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1684,7 +1684,7 @@ python-MotionMount==0.3.1 python-awair==0.2.4 # homeassistant.components.bring -python-bring-api==2.0.0 +python-bring-api==3.0.0 # homeassistant.components.bsblan python-bsblan==0.5.18 diff --git a/tests/components/bring/conftest.py b/tests/components/bring/conftest.py index f8749d3dea9..81a76c9ee3e 100644 --- a/tests/components/bring/conftest.py +++ b/tests/components/bring/conftest.py @@ -1,6 +1,6 @@ """Common fixtures for the Bring! tests.""" from collections.abc import Generator -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, patch import pytest @@ -16,7 +16,7 @@ UUID = "00000000-00000000-00000000-00000000" @pytest.fixture -def mock_setup_entry() -> Generator[Mock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock, None, None]: """Override async_setup_entry.""" with patch( "homeassistant.components.bring.async_setup_entry", return_value=True @@ -25,7 +25,7 @@ def mock_setup_entry() -> Generator[Mock, None, None]: @pytest.fixture -def mock_bring_client() -> Generator[Mock, None, None]: +def mock_bring_client() -> Generator[AsyncMock, None, None]: """Mock a Bring client.""" with patch( "homeassistant.components.bring.Bring", @@ -36,8 +36,8 @@ def mock_bring_client() -> Generator[Mock, None, None]: ): client = mock_client.return_value client.uuid = UUID - client.login.return_value = True - client.loadLists.return_value = {"lists": []} + client.loginAsync.return_value = True + client.loadListsAsync.return_value = {"lists": []} yield client diff --git a/tests/components/bring/test_config_flow.py b/tests/components/bring/test_config_flow.py index 063d84a0e97..531554d584e 100644 --- a/tests/components/bring/test_config_flow.py +++ b/tests/components/bring/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Bring! config flow.""" -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock import pytest from python_bring_api.exceptions import ( @@ -25,7 +25,7 @@ MOCK_DATA_STEP = { async def test_form( - hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_bring_client: Mock + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_bring_client: AsyncMock ) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -59,10 +59,10 @@ async def test_form( ], ) async def test_flow_user_init_data_unknown_error_and_recover( - hass: HomeAssistant, mock_bring_client: Mock, raise_error, text_error + hass: HomeAssistant, mock_bring_client: AsyncMock, raise_error, text_error ) -> None: """Test unknown errors.""" - mock_bring_client.login.side_effect = raise_error + mock_bring_client.loginAsync.side_effect = raise_error result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} @@ -76,7 +76,7 @@ async def test_flow_user_init_data_unknown_error_and_recover( assert result["errors"]["base"] == text_error # Recover - mock_bring_client.login.side_effect = None + mock_bring_client.loginAsync.side_effect = None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) @@ -92,7 +92,9 @@ async def test_flow_user_init_data_unknown_error_and_recover( async def test_flow_user_init_data_already_configured( - hass: HomeAssistant, mock_bring_client: Mock, bring_config_entry: MockConfigEntry + hass: HomeAssistant, + mock_bring_client: AsyncMock, + bring_config_entry: MockConfigEntry, ) -> None: """Test we abort user data set when entry is already configured.""" diff --git a/tests/components/bring/test_init.py b/tests/components/bring/test_init.py index 3c605143ba0..59628fa59b7 100644 --- a/tests/components/bring/test_init.py +++ b/tests/components/bring/test_init.py @@ -1,5 +1,5 @@ """Unit tests for the bring integration.""" -from unittest.mock import Mock +from unittest.mock import AsyncMock import pytest @@ -27,7 +27,7 @@ async def setup_integration( async def test_load_unload( hass: HomeAssistant, - mock_bring_client: Mock, + mock_bring_client: AsyncMock, bring_config_entry: MockConfigEntry, ) -> None: """Test loading and unloading of the config entry.""" @@ -52,12 +52,12 @@ async def test_load_unload( ) async def test_init_failure( hass: HomeAssistant, - mock_bring_client: Mock, + mock_bring_client: AsyncMock, status: ConfigEntryState, exception: Exception, bring_config_entry: MockConfigEntry | None, ) -> None: """Test an initialization error on integration load.""" - mock_bring_client.login.side_effect = exception + mock_bring_client.loginAsync.side_effect = exception await setup_integration(hass, bring_config_entry) assert bring_config_entry.state == status From 32945061900aaf932fc34e3f8518be9c2276416d Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 5 Feb 2024 18:52:58 +0100 Subject: [PATCH 0308/1367] Queue climate calls for Comelit SimpleHome (#109707) --- homeassistant/components/comelit/climate.py | 5 +---- homeassistant/components/comelit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/comelit/climate.py b/homeassistant/components/comelit/climate.py index 877afd1414e..5a879bc2d24 100644 --- a/homeassistant/components/comelit/climate.py +++ b/homeassistant/components/comelit/climate.py @@ -1,12 +1,11 @@ """Support for climates.""" from __future__ import annotations -import asyncio from enum import StrEnum from typing import Any from aiocomelit import ComelitSerialBridgeObject -from aiocomelit.const import CLIMATE, SLEEP_BETWEEN_CALLS +from aiocomelit.const import CLIMATE from homeassistant.components.climate import ( ClimateEntity, @@ -191,7 +190,6 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity await self.coordinator.api.set_clima_status( self._device.index, ClimaAction.MANUAL ) - await asyncio.sleep(SLEEP_BETWEEN_CALLS) await self.coordinator.api.set_clima_status( self._device.index, ClimaAction.SET, target_temp ) @@ -203,7 +201,6 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity await self.coordinator.api.set_clima_status( self._device.index, ClimaAction.ON ) - await asyncio.sleep(SLEEP_BETWEEN_CALLS) await self.coordinator.api.set_clima_status( self._device.index, MODE_TO_ACTION[hvac_mode] ) diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json index f1b2cea9e73..bbbb4efe7d6 100644 --- a/homeassistant/components/comelit/manifest.json +++ b/homeassistant/components/comelit/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/comelit", "iot_class": "local_polling", "loggers": ["aiocomelit"], - "requirements": ["aiocomelit==0.8.2"] + "requirements": ["aiocomelit==0.8.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 36e916f9fba..51f9659cd4f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -215,7 +215,7 @@ aiobafi6==0.9.0 aiobotocore==2.9.1 # homeassistant.components.comelit -aiocomelit==0.8.2 +aiocomelit==0.8.3 # homeassistant.components.dhcp aiodiscover==1.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 401e47232dc..467a48d2f0d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -194,7 +194,7 @@ aiobafi6==0.9.0 aiobotocore==2.9.1 # homeassistant.components.comelit -aiocomelit==0.8.2 +aiocomelit==0.8.3 # homeassistant.components.dhcp aiodiscover==1.6.0 From 9d4245595591376bcef54b2c55cadb8a8c8dee70 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 Feb 2024 19:53:22 +0100 Subject: [PATCH 0309/1367] Move async_deinitialize to google_assistant AbstractConfig (#109736) --- homeassistant/components/cloud/google_config.py | 9 --------- homeassistant/components/google_assistant/helpers.py | 10 +++++++++- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 42f25f43ae1..10601bf4784 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -23,7 +23,6 @@ from homeassistant.components.homeassistant.exposed_entities import ( from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.core import ( - CALLBACK_TYPE, CoreState, Event, HomeAssistant, @@ -145,7 +144,6 @@ class CloudGoogleConfig(AbstractConfig): self._prefs = prefs self._cloud = cloud self._sync_entities_lock = asyncio.Lock() - self._on_deinitialize: list[CALLBACK_TYPE] = [] @property def enabled(self) -> bool: @@ -283,13 +281,6 @@ class CloudGoogleConfig(AbstractConfig): ) ) - @callback - def async_deinitialize(self) -> None: - """Remove listeners.""" - _LOGGER.debug("async_deinitialize") - while self._on_deinitialize: - self._on_deinitialize.pop()() - def should_expose(self, state: State) -> bool: """If a state object should be exposed.""" return self._should_expose_entity_id(state.entity_id) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index d75ebb49509..84cbdb6211e 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -105,6 +105,7 @@ class AbstractConfig(ABC): self._local_last_active: datetime | None = None self._local_sdk_version_warn = False self.is_supported_cache: dict[str, tuple[int | None, bool]] = {} + self._on_deinitialize: list[CALLBACK_TYPE] = [] async def async_initialize(self) -> None: """Perform async initialization of config.""" @@ -118,7 +119,14 @@ class AbstractConfig(ABC): """Sync entities to Google.""" await self.async_sync_entities_all() - start.async_at_start(self.hass, sync_google) + self._on_deinitialize.append(start.async_at_start(self.hass, sync_google)) + + @callback + def async_deinitialize(self) -> None: + """Remove listeners.""" + _LOGGER.debug("async_deinitialize") + while self._on_deinitialize: + self._on_deinitialize.pop()() @property def enabled(self): From 2318d2812777396b8a3d3a537b9977af13345ef8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Feb 2024 20:14:34 +0100 Subject: [PATCH 0310/1367] Handle startup error in Analytics insights (#109755) --- .../components/analytics_insights/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/analytics_insights/__init__.py b/homeassistant/components/analytics_insights/__init__.py index a59d2a1c97c..23965a9fcb5 100644 --- a/homeassistant/components/analytics_insights/__init__.py +++ b/homeassistant/components/analytics_insights/__init__.py @@ -3,11 +3,15 @@ from __future__ import annotations from dataclasses import dataclass -from python_homeassistant_analytics import HomeassistantAnalyticsClient +from python_homeassistant_analytics import ( + HomeassistantAnalyticsClient, + HomeassistantAnalyticsConnectionError, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_TRACKED_INTEGRATIONS, DOMAIN @@ -28,7 +32,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Homeassistant Analytics from a config entry.""" client = HomeassistantAnalyticsClient(session=async_get_clientsession(hass)) - integrations = await client.get_integrations() + try: + integrations = await client.get_integrations() + except HomeassistantAnalyticsConnectionError as ex: + raise ConfigEntryNotReady("Could not fetch integration list") from ex names = {} for integration in entry.options[CONF_TRACKED_INTEGRATIONS]: From 2899c296a8661a9e52072899eaebcd5fc46ff36c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Mon, 5 Feb 2024 20:15:51 +0100 Subject: [PATCH 0311/1367] Fix incorrectly assigning supported features for plugwise climates (#109749) --- homeassistant/components/plugwise/climate.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 8e4dccb9e05..3553df02e8d 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -63,11 +63,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): self.gateway_data = coordinator.data.devices[gateway_id] # Determine supported features self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE - if HVACMode.OFF in self.hvac_modes: - self._attr_supported_features |= ( - ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON - ) - if ( self.cdr_gateway["cooling_present"] and self.cdr_gateway["smile_name"] != "Adam" @@ -75,6 +70,10 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): self._attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) + if HVACMode.OFF in self.hvac_modes: + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) if presets := self.device.get("preset_modes"): self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE self._attr_preset_modes = presets From 3752e14362365ff2672ad89f990fefa205013f0d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 5 Feb 2024 14:17:13 -0500 Subject: [PATCH 0312/1367] Add missing new climate feature flags to Mill (#109748) --- homeassistant/components/mill/climate.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 2e7b22da833..a2e70b8f9c8 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -1,4 +1,5 @@ """Support for mill wifi-enabled home heaters.""" + from typing import Any import mill @@ -186,9 +187,14 @@ class LocalMillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntit _attr_max_temp = MAX_TEMP _attr_min_temp = MIN_TEMP _attr_name = None - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_target_temperature_step = PRECISION_TENTHS _attr_temperature_unit = UnitOfTemperature.CELSIUS + _enable_turn_on_off_backwards_compatibility = False def __init__(self, coordinator: MillDataUpdateCoordinator) -> None: """Initialize the thermostat.""" From 189f3dacfbf9b185229d61dde786822ceb45c152 Mon Sep 17 00:00:00 2001 From: Vilppu Vuorinen <8420095+vilppuvuorinen@users.noreply.github.com> Date: Mon, 5 Feb 2024 21:18:01 +0200 Subject: [PATCH 0313/1367] Reduce MELCloud poll frequency to avoid throttling (#109750) --- homeassistant/components/melcloud/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index a9e000bb788..df980aea1e4 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -22,7 +22,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER] @@ -138,8 +138,9 @@ async def mel_devices_setup( all_devices = await get_devices( token, session, - conf_update_interval=timedelta(minutes=5), - device_set_debounce=timedelta(seconds=1), + user_update_interval=timedelta(minutes=30), + conf_update_interval=timedelta(minutes=15), + device_set_debounce=timedelta(seconds=2), ) wrapped_devices: dict[str, list[MelCloudDevice]] = {} for device_type, devices in all_devices.items(): From 45f44e9216cb421e7156f14bd016e4422771e1e3 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 5 Feb 2024 20:18:59 +0100 Subject: [PATCH 0314/1367] Use tracked entity friendly name for proximity sensors (#109744) user tracked entity friendly name --- homeassistant/components/proximity/sensor.py | 18 +++++++++++++++--- tests/components/proximity/test_init.py | 13 ++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/proximity/sensor.py b/homeassistant/components/proximity/sensor.py index c562467f8be..8eb7aae9bb9 100644 --- a/homeassistant/components/proximity/sensor.py +++ b/homeassistant/components/proximity/sensor.py @@ -69,6 +69,7 @@ class TrackedEntityDescriptor(NamedTuple): entity_id: str identifier: str + name: str def _device_info(coordinator: ProximityDataUpdateCoordinator) -> DeviceInfo: @@ -95,13 +96,24 @@ async def async_setup_entry( entity_reg = er.async_get(hass) for tracked_entity_id in coordinator.tracked_entities: + tracked_entity_object_id = tracked_entity_id.split(".")[-1] if (entity_entry := entity_reg.async_get(tracked_entity_id)) is not None: tracked_entity_descriptors.append( - TrackedEntityDescriptor(tracked_entity_id, entity_entry.id) + TrackedEntityDescriptor( + tracked_entity_id, + entity_entry.id, + entity_entry.name + or entity_entry.original_name + or tracked_entity_object_id, + ) ) else: tracked_entity_descriptors.append( - TrackedEntityDescriptor(tracked_entity_id, tracked_entity_id) + TrackedEntityDescriptor( + tracked_entity_id, + tracked_entity_id, + tracked_entity_object_id, + ) ) entities += [ @@ -165,7 +177,7 @@ class ProximityTrackedEntitySensor( self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{tracked_entity_descriptor.identifier}_{description.key}" self._attr_device_info = _device_info(coordinator) self._attr_translation_placeholders = { - "tracked_entity": self.tracked_entity_id.split(".")[-1] + "tracked_entity": tracked_entity_descriptor.name } async def async_added_to_hass(self) -> None: diff --git a/tests/components/proximity/test_init.py b/tests/components/proximity/test_init.py index bce4c319ce0..8fa9e4a1ce1 100644 --- a/tests/components/proximity/test_init.py +++ b/tests/components/proximity/test_init.py @@ -11,7 +11,12 @@ from homeassistant.components.proximity.const import ( DOMAIN, ) from homeassistant.components.script import scripts_with_entity -from homeassistant.const import CONF_ZONE, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + CONF_ZONE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.helpers.issue_registry as ir @@ -1205,7 +1210,7 @@ async def test_sensor_unique_ids( ) -> None: """Test that when tracked entity is renamed.""" t1 = entity_registry.async_get_or_create( - "device_tracker", "device_tracker", "test1" + "device_tracker", "device_tracker", "test1", original_name="Test tracker 1" ) hass.states.async_set(t1.entity_id, "not_home") @@ -1227,10 +1232,12 @@ async def test_sensor_unique_ids( assert await hass.config_entries.async_setup(mock_config.entry_id) await hass.async_block_till_done() - sensor_t1 = f"sensor.home_{t1.entity_id.split('.')[-1]}_distance" + sensor_t1 = "sensor.home_test_tracker_1_distance" entity = entity_registry.async_get(sensor_t1) assert entity assert entity.unique_id == f"{mock_config.entry_id}_{t1.id}_dist_to_zone" + state = hass.states.get(sensor_t1) + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home Test tracker 1 Distance" entity = entity_registry.async_get("sensor.home_test2_distance") assert entity From 94ccd59123b4a837d67ffa6b2971a892d34392ce Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 5 Feb 2024 20:19:38 +0100 Subject: [PATCH 0315/1367] Fix generic camera error when template renders to an invalid URL (#109737) --- homeassistant/components/generic/camera.py | 7 +++++ tests/components/generic/test_camera.py | 30 +++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 902f5ebadde..cadc855ade6 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -8,6 +8,7 @@ import logging from typing import Any import httpx +import voluptuous as vol import yarl from homeassistant.components.camera import Camera, CameraEntityFeature @@ -140,6 +141,12 @@ class GenericCamera(Camera): _LOGGER.error("Error parsing template %s: %s", self._still_image_url, err) return self._last_image + try: + vol.Schema(vol.Url())(url) + except vol.Invalid as err: + _LOGGER.warning("Invalid URL '%s': %s, returning last image", url, err) + return self._last_image + if url == self._last_url and self._limit_refetch: return self._last_image diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index ba7f4d3d4a1..608b8666027 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -70,15 +70,20 @@ async def help_setup_mock_config_entry( @respx.mock async def test_fetching_url( - hass: HomeAssistant, hass_client: ClientSessionGenerator, fakeimgbytes_png + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + fakeimgbytes_png, + caplog: pytest.CaptureFixture, ) -> None: """Test that it fetches the given url.""" - respx.get("http://example.com").respond(stream=fakeimgbytes_png) + hass.states.async_set("sensor.temp", "http://example.com/0a") + respx.get("http://example.com/0a").respond(stream=fakeimgbytes_png) + respx.get("http://example.com/1a").respond(stream=fakeimgbytes_png) options = { "name": "config_test", "platform": "generic", - "still_image_url": "http://example.com", + "still_image_url": "{{ states.sensor.temp.state }}", "username": "user", "password": "pass", "authentication": "basic", @@ -101,6 +106,25 @@ async def test_fetching_url( resp = await client.get("/api/camera_proxy/camera.config_test") assert respx.calls.call_count == 2 + # If the template renders to an invalid URL we return the last image from cache + hass.states.async_set("sensor.temp", "invalid url") + + # sleep another .1 seconds to make cached image expire + await asyncio.sleep(0.1) + resp = await client.get("/api/camera_proxy/camera.config_test") + assert resp.status == HTTPStatus.OK + assert respx.calls.call_count == 2 + assert ( + "Invalid URL 'invalid url': expected a URL, returning last image" in caplog.text + ) + + # Restore a valid URL + hass.states.async_set("sensor.temp", "http://example.com/1a") + await asyncio.sleep(0.1) + resp = await client.get("/api/camera_proxy/camera.config_test") + assert resp.status == HTTPStatus.OK + assert respx.calls.call_count == 3 + @respx.mock async def test_image_caching( From 49a99559c7c505fb0353b2469ed2b8fd8e9e8cfe Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 Feb 2024 20:21:04 +0100 Subject: [PATCH 0316/1367] Remove lru cache size limit of TemperatureConverter (#109726) --- homeassistant/util/unit_conversion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index be356a8ad5f..fe1974f2bee 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -365,7 +365,7 @@ class TemperatureConverter(BaseUnitConverter): } @classmethod - @lru_cache(maxsize=8) + @lru_cache def converter_factory( cls, from_unit: str | None, to_unit: str | None ) -> Callable[[float], float]: @@ -379,7 +379,7 @@ class TemperatureConverter(BaseUnitConverter): return cls._converter_factory(from_unit, to_unit) @classmethod - @lru_cache(maxsize=8) + @lru_cache def converter_factory_allow_none( cls, from_unit: str | None, to_unit: str | None ) -> Callable[[float | None], float | None]: From 5b1e0b2602721971698c56ab8b6b166f0b594c95 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 5 Feb 2024 13:23:52 -0600 Subject: [PATCH 0317/1367] Set default for OSTYPE in run-in-env script (#109731) --- script/run-in-env.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/script/run-in-env.sh b/script/run-in-env.sh index 271e7a4a034..085e07bef84 100755 --- a/script/run-in-env.sh +++ b/script/run-in-env.sh @@ -1,6 +1,10 @@ #!/usr/bin/env sh set -eu +# Used in venv activate script. +# Would be an error if undefined. +OSTYPE="${OSTYPE-}" + # Activate pyenv and virtualenv if present, then run the specified command # pyenv, pyenv-virtualenv From d27ca83694fa10eddca0d892135e1b44fbf5918e Mon Sep 17 00:00:00 2001 From: Jirka Date: Mon, 5 Feb 2024 20:50:37 +0100 Subject: [PATCH 0318/1367] Fix string in xiaomi_ble (#109758) Update strings.json Fixed typo: From "rotate_right_pressed": "Rotate left (pressed)" to "rotate_right_pressed": "Rotate right (pressed)" --- homeassistant/components/xiaomi_ble/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index d2511869580..d764a436f4c 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -115,7 +115,7 @@ "rotate_left": "Rotate left", "rotate_right": "Rotate right", "rotate_left_pressed": "Rotate left (pressed)", - "rotate_right_pressed": "Rotate left (pressed)" + "rotate_right_pressed": "Rotate right (pressed)" } } } From e9a41e50237a0db54f6ab4e2968735d8043b06a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Feb 2024 13:53:33 -0600 Subject: [PATCH 0319/1367] Avoid linear search in entity registry to clear an area (#109735) --- homeassistant/helpers/entity_registry.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 5eb8a37176a..bab6ae245a7 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -1251,9 +1251,8 @@ class EntityRegistry: @callback def async_clear_area_id(self, area_id: str) -> None: """Clear area id from registry entries.""" - for entity_id, entry in self.entities.items(): - if area_id == entry.area_id: - self.async_update_entity(entity_id, area_id=None) + for entry in self.entities.get_entries_for_area_id(area_id): + self.async_update_entity(entry.entity_id, area_id=None) @callback From 908cedf981063d1ff228a70948e91bb6ec01ff11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Feb 2024 14:12:16 -0600 Subject: [PATCH 0320/1367] Avoid linear search of entity registry in async_clear_config_entry (#109724) --- homeassistant/helpers/entity_registry.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index bab6ae245a7..dc0c29dc0b7 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -1218,9 +1218,8 @@ class EntityRegistry: """Clear config entry from registry entries.""" now_time = time.time() for entity_id in [ - entity_id - for entity_id, entry in self.entities.items() - if config_entry_id == entry.config_entry_id + entry.entity_id + for entry in self.entities.get_entries_for_config_entry_id(config_entry_id) ]: self.async_remove(entity_id) for key, deleted_entity in list(self.deleted_entities.items()): From 4119d20f8704c911ea7e79cae5ed0c7f7e2b52ed Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 5 Feb 2024 15:59:02 -0500 Subject: [PATCH 0321/1367] Bump holidays to 0.42 (#109760) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 4d26e93e591..0608f8c404e 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.41", "babel==2.13.1"] + "requirements": ["holidays==0.42", "babel==2.13.1"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 27d440d4832..05026ae6e99 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.41"] + "requirements": ["holidays==0.42"] } diff --git a/requirements_all.txt b/requirements_all.txt index 51f9659cd4f..efc645deb64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1056,7 +1056,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.41 +holidays==0.42 # homeassistant.components.frontend home-assistant-frontend==20240205.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 467a48d2f0d..cd5a09d9e28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -852,7 +852,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.41 +holidays==0.42 # homeassistant.components.frontend home-assistant-frontend==20240205.0 From e399bebbcdfd5967cc0a8b5e19b4f44e3268a639 Mon Sep 17 00:00:00 2001 From: Vilppu Vuorinen <8420095+vilppuvuorinen@users.noreply.github.com> Date: Mon, 5 Feb 2024 23:30:20 +0200 Subject: [PATCH 0322/1367] Fix compatibility issues with older pymelcloud version (#109757) --- homeassistant/components/melcloud/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index df980aea1e4..baa601d4ab0 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -22,7 +22,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER] @@ -138,8 +138,7 @@ async def mel_devices_setup( all_devices = await get_devices( token, session, - user_update_interval=timedelta(minutes=30), - conf_update_interval=timedelta(minutes=15), + conf_update_interval=timedelta(minutes=30), device_set_debounce=timedelta(seconds=2), ) wrapped_devices: dict[str, list[MelCloudDevice]] = {} From f73431ac06c713477e113a5add52740117781dfc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Feb 2024 16:04:52 -0600 Subject: [PATCH 0323/1367] Switch utc_to_timestamp to .timestamp() where possible (#109729) * Switch utc_to_timestamp to .timestamp() .timestamp() is faster now in newer cpython ``` from homeassistant.util.dt import utc_to_timestamp, utcnow import timeit now = utcnow() print(timeit.timeit('utc_to_timestamp(now)',globals={"now":now,"utc_to_timestamp":utc_to_timestamp})) print(timeit.timeit('now.timestamp()',globals={"now":now})) ``` utc_to_timestamp = 0.18721245788037777 timestamp = 0.11421508435159922 * compat * revert * revert * revert * revert * revert --- homeassistant/components/history/websocket_api.py | 4 ++-- homeassistant/components/logbook/queries/__init__.py | 5 ++--- homeassistant/components/logbook/websocket_api.py | 4 ++-- homeassistant/components/recorder/purge.py | 10 ++++------ homeassistant/core.py | 4 +--- homeassistant/helpers/event.py | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/history/websocket_api.py b/homeassistant/components/history/websocket_api.py index 25422004797..f903a9904a9 100644 --- a/homeassistant/components/history/websocket_api.py +++ b/homeassistant/components/history/websocket_api.py @@ -179,8 +179,8 @@ def _generate_stream_message( """Generate a history stream message response.""" return { "states": states, - "start_time": dt_util.utc_to_timestamp(start_day), - "end_time": dt_util.utc_to_timestamp(end_day), + "start_time": start_day.timestamp(), + "end_time": end_day.timestamp(), } diff --git a/homeassistant/components/logbook/queries/__init__.py b/homeassistant/components/logbook/queries/__init__.py index 29d89a4c22f..af41374ec9b 100644 --- a/homeassistant/components/logbook/queries/__init__.py +++ b/homeassistant/components/logbook/queries/__init__.py @@ -9,7 +9,6 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components.recorder.filters import Filters from homeassistant.components.recorder.models import ulid_to_bytes_or_none from homeassistant.helpers.json import json_dumps -from homeassistant.util import dt as dt_util from .all import all_stmt from .devices import devices_stmt @@ -28,8 +27,8 @@ def statement_for_request( context_id: str | None = None, ) -> StatementLambdaElement: """Generate the logbook statement for a logbook request.""" - start_day = dt_util.utc_to_timestamp(start_day_dt) - end_day = dt_util.utc_to_timestamp(end_day_dt) + start_day = start_day_dt.timestamp() + end_day = end_day_dt.timestamp() # No entities: logbook sends everything for the timeframe # limited by the context_id and the yaml configured filter if not entity_ids and not device_ids: diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 82124247adf..0b1b34ca375 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -184,8 +184,8 @@ def _generate_stream_message( """Generate a logbook stream message response.""" return { "events": events, - "start_time": dt_util.utc_to_timestamp(start_day), - "end_time": dt_util.utc_to_timestamp(end_day), + "start_time": start_day.timestamp(), + "end_time": end_day.timestamp(), } diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 0b63bb8daa2..a9d8c0b2482 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -10,8 +10,6 @@ from typing import TYPE_CHECKING from sqlalchemy.orm.session import Session -import homeassistant.util.dt as dt_util - from .db_schema import Events, States, StatesMeta from .models import DatabaseEngine from .queries import ( @@ -251,7 +249,7 @@ def _select_state_attributes_ids_to_purge( state_ids = set() attributes_ids = set() for state_id, attributes_id in session.execute( - find_states_to_purge(dt_util.utc_to_timestamp(purge_before), max_bind_vars) + find_states_to_purge(purge_before.timestamp(), max_bind_vars) ).all(): state_ids.add(state_id) if attributes_id: @@ -271,7 +269,7 @@ def _select_event_data_ids_to_purge( event_ids = set() data_ids = set() for event_id, data_id in session.execute( - find_events_to_purge(dt_util.utc_to_timestamp(purge_before), max_bind_vars) + find_events_to_purge(purge_before.timestamp(), max_bind_vars) ).all(): event_ids.add(event_id) if data_id: @@ -464,7 +462,7 @@ def _select_legacy_detached_state_and_attributes_and_data_ids_to_purge( """ states = session.execute( find_legacy_detached_states_and_attributes_to_purge( - dt_util.utc_to_timestamp(purge_before), max_bind_vars + purge_before.timestamp(), max_bind_vars ) ).all() _LOGGER.debug("Selected %s state ids to remove", len(states)) @@ -489,7 +487,7 @@ def _select_legacy_event_state_and_attributes_and_data_ids_to_purge( """ events = session.execute( find_legacy_event_state_and_attributes_and_data_ids_to_purge( - dt_util.utc_to_timestamp(purge_before), max_bind_vars + purge_before.timestamp(), max_bind_vars ) ).all() _LOGGER.debug("Selected %s event ids to remove", len(events)) diff --git a/homeassistant/core.py b/homeassistant/core.py index 004c0dc1ede..047f46afdf5 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1077,9 +1077,7 @@ class Event: self.origin = origin self.time_fired = time_fired or dt_util.utcnow() if not context: - context = Context( - id=ulid_at_time(dt_util.utc_to_timestamp(self.time_fired)) - ) + context = Context(id=ulid_at_time(self.time_fired.timestamp())) self.context = context if not context.origin_event: context.origin_event = self diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index ea80591a989..b109ce16698 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1442,7 +1442,7 @@ def async_track_point_in_utc_time( """ # Ensure point_in_time is UTC utc_point_in_time = dt_util.as_utc(point_in_time) - expected_fire_timestamp = dt_util.utc_to_timestamp(utc_point_in_time) + expected_fire_timestamp = utc_point_in_time.timestamp() job = ( action if isinstance(action, HassJob) From 440212ddcee21ed82a81242f7aa8556fd2b217d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Feb 2024 16:23:53 -0600 Subject: [PATCH 0324/1367] Reduce dict lookups in entity registry indices (#109712) --- homeassistant/helpers/entity_registry.py | 39 +++++++++++------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index dc0c29dc0b7..f72aece4c70 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -449,9 +449,9 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): super().__init__() self._entry_ids: dict[str, RegistryEntry] = {} self._index: dict[tuple[str, str, str], str] = {} - self._config_entry_id_index: dict[str, list[str]] = {} - self._device_id_index: dict[str, list[str]] = {} - self._area_id_index: dict[str, list[str]] = {} + self._config_entry_id_index: dict[str, list[RegistryEntry]] = {} + self._device_id_index: dict[str, list[RegistryEntry]] = {} + self._area_id_index: dict[str, list[RegistryEntry]] = {} def values(self) -> ValuesView[RegistryEntry]: """Return the underlying values to avoid __iter__ overhead.""" @@ -466,23 +466,23 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): self._entry_ids[entry.id] = entry self._index[(entry.domain, entry.platform, entry.unique_id)] = entry.entity_id if (config_entry_id := entry.config_entry_id) is not None: - self._config_entry_id_index.setdefault(config_entry_id, []).append(key) + self._config_entry_id_index.setdefault(config_entry_id, []).append(entry) if (device_id := entry.device_id) is not None: - self._device_id_index.setdefault(device_id, []).append(key) + self._device_id_index.setdefault(device_id, []).append(entry) if (area_id := entry.area_id) is not None: - self._area_id_index.setdefault(area_id, []).append(key) + self._area_id_index.setdefault(area_id, []).append(entry) def _unindex_entry_value( - self, key: str, value: str, index: dict[str, list[str]] + self, entry: RegistryEntry, value: str, index: dict[str, list[RegistryEntry]] ) -> None: """Unindex an entry value. - key is the entry key + entry is the entry value is the value to unindex such as config_entry_id or device_id. index is the index to unindex from. """ entries = index[value] - entries.remove(key) + entries.remove(entry) if not entries: del index[value] @@ -492,11 +492,13 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): del self._entry_ids[entry.id] del self._index[(entry.domain, entry.platform, entry.unique_id)] if config_entry_id := entry.config_entry_id: - self._unindex_entry_value(key, config_entry_id, self._config_entry_id_index) + self._unindex_entry_value( + entry, config_entry_id, self._config_entry_id_index + ) if device_id := entry.device_id: - self._unindex_entry_value(key, device_id, self._device_id_index) + self._unindex_entry_value(entry, device_id, self._device_id_index) if area_id := entry.area_id: - self._unindex_entry_value(key, area_id, self._area_id_index) + self._unindex_entry_value(entry, area_id, self._area_id_index) def __delitem__(self, key: str) -> None: """Remove an item.""" @@ -515,26 +517,21 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): self, device_id: str, include_disabled_entities: bool = False ) -> list[RegistryEntry]: """Get entries for device.""" - data = self.data return [ entry - for key in self._device_id_index.get(device_id, ()) - if not (entry := data[key]).disabled_by or include_disabled_entities + for entry in self._device_id_index.get(device_id, ()) + if not entry.disabled_by or include_disabled_entities ] def get_entries_for_config_entry_id( self, config_entry_id: str ) -> list[RegistryEntry]: """Get entries for config entry.""" - data = self.data - return [ - data[key] for key in self._config_entry_id_index.get(config_entry_id, ()) - ] + return list(self._config_entry_id_index.get(config_entry_id, ())) def get_entries_for_area_id(self, area_id: str) -> list[RegistryEntry]: """Get entries for area.""" - data = self.data - return [data[key] for key in self._area_id_index.get(area_id, ())] + return list(self._area_id_index.get(area_id, ())) class EntityRegistry: From 6fce8a540356440c4d6ecef0558276967bf5224c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Feb 2024 16:25:12 -0600 Subject: [PATCH 0325/1367] Avoid linear search of the entity registry in ps4 (#109723) --- homeassistant/components/ps4/__init__.py | 50 ++++++++++---------- homeassistant/components/ps4/media_player.py | 12 +++-- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 1c87a275126..4e7f78c4b18 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -120,33 +120,35 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Prevent changing entity_id. Updates entity registry. registry = er.async_get(hass) - for entity_id, e_entry in registry.entities.items(): - if e_entry.config_entry_id == entry.entry_id: - unique_id = e_entry.unique_id + for e_entry in registry.entities.get_entries_for_config_entry_id( + entry.entry_id + ): + unique_id = e_entry.unique_id + entity_id = e_entry.entity_id - # Remove old entity entry. - registry.async_remove(entity_id) + # Remove old entity entry. + registry.async_remove(entity_id) - # Format old unique_id. - unique_id = format_unique_id(entry.data[CONF_TOKEN], unique_id) + # Format old unique_id. + unique_id = format_unique_id(entry.data[CONF_TOKEN], unique_id) - # Create new entry with old entity_id. - new_id = split_entity_id(entity_id)[1] - registry.async_get_or_create( - "media_player", - DOMAIN, - unique_id, - suggested_object_id=new_id, - config_entry=entry, - device_id=e_entry.device_id, - ) - entry.version = 3 - _LOGGER.info( - "PlayStation 4 identifier for entity: %s has changed", - entity_id, - ) - config_entries.async_update_entry(entry) - return True + # Create new entry with old entity_id. + new_id = split_entity_id(entity_id)[1] + registry.async_get_or_create( + "media_player", + DOMAIN, + unique_id, + suggested_object_id=new_id, + config_entry=entry, + device_id=e_entry.device_id, + ) + entry.version = 3 + _LOGGER.info( + "PlayStation 4 identifier for entity: %s has changed", + entity_id, + ) + config_entries.async_update_entry(entry) + return True msg = f"""{reason[version]} for the PlayStation 4 Integration. Please remove the PS4 Integration and re-configure diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 722b733c08b..42a1021afe4 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -344,11 +344,13 @@ class PS4Device(MediaPlayerEntity): _LOGGER.info("Assuming status from registry") e_registry = er.async_get(self.hass) d_registry = dr.async_get(self.hass) - for entity_id, entry in e_registry.entities.items(): - if entry.config_entry_id == self._entry_id: - self._attr_unique_id = entry.unique_id - self.entity_id = entity_id - break + + for entry in e_registry.entities.get_entries_for_config_entry_id( + self._entry_id + ): + self._attr_unique_id = entry.unique_id + self.entity_id = entry.entity_id + break for device in d_registry.devices.values(): if self._entry_id in device.config_entries: self._attr_device_info = DeviceInfo( From b7284b92acf3af18efd59344a1bff087084dd7c3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 Feb 2024 23:58:34 +0100 Subject: [PATCH 0326/1367] Clean up Alexa when logging out from cloud (#109738) * Clean up Alexa when logging out from cloud * Add test --- homeassistant/components/alexa/config.py | 8 +++++++ .../components/cloud/alexa_config.py | 22 ++++++++++++------- homeassistant/components/cloud/client.py | 4 ++++ tests/components/cloud/test_alexa_config.py | 8 +++++++ tests/components/cloud/test_client.py | 5 +++-- 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index a1ab1d77081..02aaed25742 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -29,12 +29,20 @@ class AbstractConfig(ABC): """Initialize abstract config.""" self.hass = hass self._enable_proactive_mode_lock = asyncio.Lock() + self._on_deinitialize: list[CALLBACK_TYPE] = [] async def async_initialize(self) -> None: """Perform async initialization of config.""" self._store = AlexaConfigStore(self.hass) await self._store.async_load() + @callback + def async_deinitialize(self) -> None: + """Remove listeners.""" + _LOGGER.debug("async_deinitialize") + while self._on_deinitialize: + self._on_deinitialize.pop()() + @property def supports_auth(self) -> bool: """Return if config supports auth.""" diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index caed7b38c47..415f2415095 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -246,21 +246,27 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): await self._prefs.async_update( alexa_settings_version=ALEXA_SETTINGS_VERSION ) - async_listen_entity_updates( - self.hass, CLOUD_ALEXA, self._async_exposed_entities_updated + self._on_deinitialize.append( + async_listen_entity_updates( + self.hass, CLOUD_ALEXA, self._async_exposed_entities_updated + ) ) async def on_hass_start(hass: HomeAssistant) -> None: if self.enabled and ALEXA_DOMAIN not in self.hass.config.components: await async_setup_component(self.hass, ALEXA_DOMAIN, {}) - start.async_at_start(self.hass, on_hass_start) - start.async_at_started(self.hass, on_hass_started) + self._on_deinitialize.append(start.async_at_start(self.hass, on_hass_start)) + self._on_deinitialize.append(start.async_at_started(self.hass, on_hass_started)) - self._prefs.async_listen_updates(self._async_prefs_updated) - self.hass.bus.async_listen( - er.EVENT_ENTITY_REGISTRY_UPDATED, - self._handle_entity_registry_updated, + self._on_deinitialize.append( + self._prefs.async_listen_updates(self._async_prefs_updated) + ) + self._on_deinitialize.append( + self.hass.bus.async_listen( + er.EVENT_ENTITY_REGISTRY_UPDATED, + self._handle_entity_registry_updated, + ) ) def _should_expose_legacy(self, entity_id: str) -> bool: diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 8cf79d20c5d..ea85821a0a9 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -213,6 +213,10 @@ class CloudClient(Interface): """Cleanup some stuff after logout.""" await self.prefs.async_set_username(None) + if self._alexa_config: + self._alexa_config.async_deinitialize() + self._alexa_config = None + if self._google_config: self._google_config.async_deinitialize() self._google_config = None diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 0ebc385b516..3ac2417247c 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -526,6 +526,9 @@ async def test_alexa_handle_logout( return_value=Mock(), ) as mock_enable: await aconf.async_enable_proactive_mode() + await hass.async_block_till_done() + + assert len(aconf._on_deinitialize) == 5 # This will trigger a prefs update when we logout. await cloud_prefs.get_cloud_user() @@ -536,8 +539,13 @@ async def test_alexa_handle_logout( "async_check_token", side_effect=AssertionError("Should not be called"), ): + # Fake logging out; CloudClient.logout_cleanups sets username to None + # and deinitializes the Google config. await cloud_prefs.async_set_username(None) + aconf.async_deinitialize() await hass.async_block_till_done() + # Check listeners are removed: + assert not aconf._on_deinitialize assert len(mock_enable.return_value.mock_calls) == 1 diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index c8c0e40a5bb..4284a11c94a 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -468,10 +468,11 @@ async def test_logged_out( await cloud.logout() await hass.async_block_till_done() - # Alexa is not cleaned up, Google is - assert cloud.client._alexa_config is alexa_config_mock + # Check we clean up Alexa and Google + assert cloud.client._alexa_config is None assert cloud.client._google_config is None google_config_mock.async_deinitialize.assert_called_once_with() + alexa_config_mock.async_deinitialize.assert_called_once_with() async def test_remote_enable(hass: HomeAssistant) -> None: From 13bc018e23ed23e921079f08e82a970110db9d40 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 5 Feb 2024 18:39:56 -0500 Subject: [PATCH 0327/1367] Buffer JsonDecodeError in Flo (#109767) --- homeassistant/components/flo/device.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index 0c805b932cb..27feb15a97e 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -7,6 +7,7 @@ from typing import Any from aioflo.api import API from aioflo.errors import RequestError +from orjson import JSONDecodeError from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -46,7 +47,7 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable= await self._update_device() await self._update_consumption_data() self._failure_count = 0 - except (RequestError, TimeoutError) as error: + except (RequestError, TimeoutError, JSONDecodeError) as error: self._failure_count += 1 if self._failure_count > 3: raise UpdateFailed(error) from error From 965f31a9e0ccba597c0000158ed447f6b00c7fac Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 6 Feb 2024 01:12:56 +0100 Subject: [PATCH 0328/1367] Fix ZHA creating unnecessary "summ received" entity after upgrade (#109268) * Do not create `current_summ_received` entity until initialized once * Update zha_devices_list.py to not expect summation received entities The attribute isn't initialized for these devices in the test (which our check now expects it to be), hence we need to remove them from this list. * Update sensor tests to have initial state for current_summ_received entity The attribute needs to be initialized for it to be created which we do by plugging the attribute read. The test expects the initial state to be "unknown", but hence we plugged the attribute (to create the entity), the state is whatever we plug the attribute read as. * Update sensor tests to expect not updating current_summ_received entity if it doesn't exist --- homeassistant/components/zha/sensor.py | 20 +++++++++ tests/components/zha/test_sensor.py | 29 ++++++++++--- tests/components/zha/zha_devices_list.py | 52 ------------------------ 3 files changed, 43 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 15985922ccd..929ac803b10 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -838,6 +838,26 @@ class SmartEnergySummationReceived(PolledSmartEnergySummation): _unique_id_suffix = "summation_received" _attr_translation_key: str = "summation_received" + @classmethod + def create_entity( + cls, + unique_id: str, + zha_device: ZHADevice, + cluster_handlers: list[ClusterHandler], + **kwargs: Any, + ) -> Self | None: + """Entity Factory. + + This attribute only started to be initialized in HA 2024.2.0, + so the entity would still be created on the first HA start after the upgrade for existing devices, + as the initialization to see if an attribute is unsupported happens later in the background. + To avoid creating a lot of unnecessary entities for existing devices, + wait until the attribute was properly initialized once for now. + """ + if cluster_handlers[0].cluster.get(cls._attribute_name) is None: + return None + return super().create_entity(unique_id, zha_device, cluster_handlers, **kwargs) + @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_PRESSURE) # pylint: disable-next=hass-invalid-inheritance # needs fixing diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 005e9b86e3a..4b71fd723ad 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -368,6 +368,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): "report_count", "read_plug", "unsupported_attrs", + "initial_sensor_state", ), ( ( @@ -377,6 +378,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): 1, None, None, + STATE_UNKNOWN, ), ( measurement.TemperatureMeasurement.cluster_id, @@ -385,6 +387,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): 1, None, None, + STATE_UNKNOWN, ), ( measurement.PressureMeasurement.cluster_id, @@ -393,6 +396,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): 1, None, None, + STATE_UNKNOWN, ), ( measurement.IlluminanceMeasurement.cluster_id, @@ -401,6 +405,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): 1, None, None, + STATE_UNKNOWN, ), ( smartenergy.Metering.cluster_id, @@ -415,6 +420,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): "status": 0x00, }, {"current_summ_delivered", "current_summ_received"}, + STATE_UNKNOWN, ), ( smartenergy.Metering.cluster_id, @@ -431,6 +437,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): "unit_of_measure": 0x00, }, {"instaneneous_demand", "current_summ_received"}, + STATE_UNKNOWN, ), ( smartenergy.Metering.cluster_id, @@ -445,8 +452,10 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): "status": 0x00, "summation_formatting": 0b1_0111_010, "unit_of_measure": 0x00, + "current_summ_received": 0, }, {"instaneneous_demand", "current_summ_delivered"}, + "0.0", ), ( homeautomation.ElectricalMeasurement.cluster_id, @@ -455,6 +464,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): 7, {"ac_power_divisor": 1000, "ac_power_multiplier": 1}, {"apparent_power", "rms_current", "rms_voltage"}, + STATE_UNKNOWN, ), ( homeautomation.ElectricalMeasurement.cluster_id, @@ -463,6 +473,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): 7, {"ac_power_divisor": 1000, "ac_power_multiplier": 1}, {"active_power", "rms_current", "rms_voltage"}, + STATE_UNKNOWN, ), ( homeautomation.ElectricalMeasurement.cluster_id, @@ -471,6 +482,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): 7, {"ac_power_divisor": 1000, "ac_power_multiplier": 1}, {"active_power", "apparent_power", "rms_current", "rms_voltage"}, + STATE_UNKNOWN, ), ( homeautomation.ElectricalMeasurement.cluster_id, @@ -479,6 +491,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): 7, {"ac_current_divisor": 1000, "ac_current_multiplier": 1}, {"active_power", "apparent_power", "rms_voltage"}, + STATE_UNKNOWN, ), ( homeautomation.ElectricalMeasurement.cluster_id, @@ -487,6 +500,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): 7, {"ac_voltage_divisor": 10, "ac_voltage_multiplier": 1}, {"active_power", "apparent_power", "rms_current"}, + STATE_UNKNOWN, ), ( general.PowerConfiguration.cluster_id, @@ -499,6 +513,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): "battery_quantity": 3, }, None, + STATE_UNKNOWN, ), ( general.PowerConfiguration.cluster_id, @@ -511,6 +526,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): "battery_quantity": 3, }, None, + STATE_UNKNOWN, ), ( general.DeviceTemperature.cluster_id, @@ -519,6 +535,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): 1, None, None, + STATE_UNKNOWN, ), ( hvac.Thermostat.cluster_id, @@ -527,6 +544,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): 10, None, None, + STATE_UNKNOWN, ), ( hvac.Thermostat.cluster_id, @@ -535,6 +553,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): 10, None, None, + STATE_UNKNOWN, ), ), ) @@ -548,6 +567,7 @@ async def test_sensor( report_count, read_plug, unsupported_attrs, + initial_sensor_state, ) -> None: """Test ZHA sensor platform.""" @@ -582,8 +602,8 @@ async def test_sensor( # allow traffic to flow through the gateway and devices await async_enable_traffic(hass, [zha_device]) - # test that the sensor now have a state of unknown - assert hass.states.get(entity_id).state == STATE_UNKNOWN + # test that the sensor now have their correct initial state (mostly unknown) + assert hass.states.get(entity_id).state == initial_sensor_state # test sensor associated logic await test_func(hass, cluster, entity_id) @@ -826,7 +846,6 @@ async def test_electrical_measurement_init( }, { "summation_delivered", - "summation_received", }, { "instantaneous_demand", @@ -834,12 +853,11 @@ async def test_electrical_measurement_init( ), ( smartenergy.Metering.cluster_id, - {"instantaneous_demand", "current_summ_delivered", "current_summ_received"}, + {"instantaneous_demand", "current_summ_delivered"}, {}, { "instantaneous_demand", "summation_delivered", - "summation_received", }, ), ( @@ -848,7 +866,6 @@ async def test_electrical_measurement_init( { "instantaneous_demand", "summation_delivered", - "summation_received", }, {}, ), diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 9a9535178d2..4c23244c5e0 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -243,11 +243,6 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_summation_delivered", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_summation_received", - }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CLUSTER_HANDLERS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", @@ -616,13 +611,6 @@ DEVICES = [ "sensor.climaxtechnology_psmp5_00_00_02_02tc_summation_delivered" ), }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: ( - "sensor.climaxtechnology_psmp5_00_00_02_02tc_summation_received" - ), - }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CLUSTER_HANDLERS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", @@ -1617,11 +1605,6 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_summation_delivered", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_summation_received", - }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CLUSTER_HANDLERS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", @@ -1682,11 +1665,6 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_summation_delivered", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_summation_received", - }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CLUSTER_HANDLERS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", @@ -1747,11 +1725,6 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_summation_delivered", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_summation_received", - }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CLUSTER_HANDLERS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", @@ -2370,11 +2343,6 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_summation_delivered", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_summation_received", - }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", @@ -4511,11 +4479,6 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_summation_delivered", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_summation_received", - }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CLUSTER_HANDLERS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", @@ -5362,11 +5325,6 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_summation_delivered", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_summation_received", - }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CLUSTER_HANDLERS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", @@ -5420,11 +5378,6 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_summation_delivered", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_summation_received", - }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CLUSTER_HANDLERS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", @@ -5478,11 +5431,6 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_summation_delivered", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_summation_received", - }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CLUSTER_HANDLERS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", From 668d036f71579961c6e83298cde727350a43c51b Mon Sep 17 00:00:00 2001 From: suaveolent <2163625+suaveolent@users.noreply.github.com> Date: Tue, 6 Feb 2024 01:20:14 +0100 Subject: [PATCH 0329/1367] Improve lupusec code quality (#109727) * renamed async_add_devices * fixed typo * patch class instead of __init__ * ensure non blocking get_alarm * exception handling * added test case for json decode error * avoid blockign calls --------- Co-authored-by: suaveolent --- homeassistant/components/lupusec/__init__.py | 10 +++------- .../components/lupusec/alarm_control_panel.py | 6 +++--- .../components/lupusec/binary_sensor.py | 9 ++++++--- homeassistant/components/lupusec/config_flow.py | 5 +++++ homeassistant/components/lupusec/strings.json | 2 +- homeassistant/components/lupusec/switch.py | 9 ++++++--- tests/components/lupusec/test_config_flow.py | 16 ++++++++-------- 7 files changed, 32 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/lupusec/__init__.py b/homeassistant/components/lupusec/__init__.py index bf7c30845a3..f937c7edd10 100644 --- a/homeassistant/components/lupusec/__init__.py +++ b/homeassistant/components/lupusec/__init__.py @@ -1,4 +1,5 @@ """Support for Lupusec Home Security system.""" +from json import JSONDecodeError import logging import lupupy @@ -111,16 +112,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: lupusec_system = await hass.async_add_executor_job( lupupy.Lupusec, username, password, host ) - except LupusecException: _LOGGER.error("Failed to connect to Lupusec device at %s", host) return False - except Exception as ex: # pylint: disable=broad-except - _LOGGER.error( - "Unknown error while trying to connect to Lupusec device at %s: %s", - host, - ex, - ) + except JSONDecodeError: + _LOGGER.error("Failed to connect to Lupusec device at %s", host) return False hass.data.setdefault(DOMAIN, {})[entry.entry_id] = lupusec_system diff --git a/homeassistant/components/lupusec/alarm_control_panel.py b/homeassistant/components/lupusec/alarm_control_panel.py index 2e4ca5cab63..cd4e433bd5d 100644 --- a/homeassistant/components/lupusec/alarm_control_panel.py +++ b/homeassistant/components/lupusec/alarm_control_panel.py @@ -29,14 +29,14 @@ SCAN_INTERVAL = timedelta(seconds=2) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_devices: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up an alarm control panel for a Lupusec device.""" data = hass.data[DOMAIN][config_entry.entry_id] - alarm_devices = [LupusecAlarm(data, data.get_alarm(), config_entry.entry_id)] + alarm = await hass.async_add_executor_job(data.get_alarm) - async_add_devices(alarm_devices) + async_add_entities([LupusecAlarm(data, alarm, config_entry.entry_id)]) class LupusecAlarm(LupusecDevice, AlarmControlPanelEntity): diff --git a/homeassistant/components/lupusec/binary_sensor.py b/homeassistant/components/lupusec/binary_sensor.py index ecff9a6266d..5cf63579984 100644 --- a/homeassistant/components/lupusec/binary_sensor.py +++ b/homeassistant/components/lupusec/binary_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from functools import partial import logging import lupupy.constants as CONST @@ -25,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_devices: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up a binary sensors for a Lupusec device.""" @@ -34,10 +35,12 @@ async def async_setup_entry( device_types = CONST.TYPE_OPENING + CONST.TYPE_SENSOR sensors = [] - for device in data.get_devices(generic_type=device_types): + partial_func = partial(data.get_devices, generic_type=device_types) + devices = await hass.async_add_executor_job(partial_func) + for device in devices: sensors.append(LupusecBinarySensor(device, config_entry.entry_id)) - async_add_devices(sensors) + async_add_entities(sensors) class LupusecBinarySensor(LupusecBaseSensor, BinarySensorEntity): diff --git a/homeassistant/components/lupusec/config_flow.py b/homeassistant/components/lupusec/config_flow.py index 64d53ce51f4..aad57897c91 100644 --- a/homeassistant/components/lupusec/config_flow.py +++ b/homeassistant/components/lupusec/config_flow.py @@ -1,5 +1,6 @@ """"Config flow for Lupusec integration.""" +from json import JSONDecodeError import logging from typing import Any @@ -50,6 +51,8 @@ class LupusecConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await test_host_connection(self.hass, host, username, password) except CannotConnect: errors["base"] = "cannot_connect" + except JSONDecodeError: + errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -80,6 +83,8 @@ class LupusecConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await test_host_connection(self.hass, host, username, password) except CannotConnect: return self.async_abort(reason="cannot_connect") + except JSONDecodeError: + return self.async_abort(reason="cannot_connect") except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") return self.async_abort(reason="unknown") diff --git a/homeassistant/components/lupusec/strings.json b/homeassistant/components/lupusec/strings.json index 53f84c8b872..6fa59aaeb3d 100644 --- a/homeassistant/components/lupusec/strings.json +++ b/homeassistant/components/lupusec/strings.json @@ -21,7 +21,7 @@ "issues": { "deprecated_yaml_import_issue_cannot_connect": { "title": "The Lupus Electronics LUPUSEC YAML configuration import failed", - "description": "Configuring Lupus Electronics LUPUSEC using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to Lupus Electronics LUPUSEC works and restart Home Assistant to try again or remove the Lupus Electronics LUPUSEC YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + "description": "Configuring Lupus Electronics LUPUSEC using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to Lupus Electronics LUPUSEC works and restart Home Assistant to try again or remove the Lupus Electronics LUPUSEC YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." }, "deprecated_yaml_import_issue_unknown": { "title": "The Lupus Electronics LUPUSEC YAML configuration import failed", diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py index a2b3796ef5b..e07c974f033 100644 --- a/homeassistant/components/lupusec/switch.py +++ b/homeassistant/components/lupusec/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from functools import partial from typing import Any import lupupy.constants as CONST @@ -20,7 +21,7 @@ SCAN_INTERVAL = timedelta(seconds=2) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_devices: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Lupusec switch devices.""" @@ -29,10 +30,12 @@ async def async_setup_entry( device_types = CONST.TYPE_SWITCH switches = [] - for device in data.get_devices(generic_type=device_types): + partial_func = partial(data.get_devices, generic_type=device_types) + devices = await hass.async_add_executor_job(partial_func) + for device in devices: switches.append(LupusecSwitch(device, config_entry.entry_id)) - async_add_devices(switches) + async_add_entities(switches) class LupusecSwitch(LupusecBaseSensor, SwitchEntity): diff --git a/tests/components/lupusec/test_config_flow.py b/tests/components/lupusec/test_config_flow.py index 5ef5f98ea00..6b07952ff54 100644 --- a/tests/components/lupusec/test_config_flow.py +++ b/tests/components/lupusec/test_config_flow.py @@ -1,5 +1,6 @@ """"Unit tests for the Lupusec config flow.""" +from json import JSONDecodeError from unittest.mock import patch from lupupy import LupusecException @@ -51,8 +52,7 @@ async def test_form_valid_input(hass: HomeAssistant) -> None: "homeassistant.components.lupusec.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "homeassistant.components.lupusec.config_flow.lupupy.Lupusec.__init__", - return_value=None, + "homeassistant.components.lupusec.config_flow.lupupy.Lupusec", ) as mock_initialize_lupusec: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -71,6 +71,7 @@ async def test_form_valid_input(hass: HomeAssistant) -> None: ("raise_error", "text_error"), [ (LupusecException("Test lupusec exception"), "cannot_connect"), + (JSONDecodeError("Test JSONDecodeError", "test", 1), "cannot_connect"), (Exception("Test unknown exception"), "unknown"), ], ) @@ -85,7 +86,7 @@ async def test_flow_user_init_data_error_and_recover( assert result["errors"] == {} with patch( - "homeassistant.components.lupusec.config_flow.lupupy.Lupusec.__init__", + "homeassistant.components.lupusec.config_flow.lupupy.Lupusec", side_effect=raise_error, ) as mock_initialize_lupusec: result2 = await hass.config_entries.flow.async_configure( @@ -104,8 +105,7 @@ async def test_flow_user_init_data_error_and_recover( "homeassistant.components.lupusec.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "homeassistant.components.lupusec.config_flow.lupupy.Lupusec.__init__", - return_value=None, + "homeassistant.components.lupusec.config_flow.lupupy.Lupusec", ) as mock_initialize_lupusec: result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -164,8 +164,7 @@ async def test_flow_source_import( "homeassistant.components.lupusec.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "homeassistant.components.lupusec.config_flow.lupupy.Lupusec.__init__", - return_value=None, + "homeassistant.components.lupusec.config_flow.lupupy.Lupusec", ) as mock_initialize_lupusec: result = await hass.config_entries.flow.async_init( DOMAIN, @@ -186,6 +185,7 @@ async def test_flow_source_import( ("raise_error", "text_error"), [ (LupusecException("Test lupusec exception"), "cannot_connect"), + (JSONDecodeError("Test JSONDecodeError", "test", 1), "cannot_connect"), (Exception("Test unknown exception"), "unknown"), ], ) @@ -195,7 +195,7 @@ async def test_flow_source_import_error_and_recover( """Test exceptions and recovery.""" with patch( - "homeassistant.components.lupusec.config_flow.lupupy.Lupusec.__init__", + "homeassistant.components.lupusec.config_flow.lupupy.Lupusec", side_effect=raise_error, ) as mock_initialize_lupusec: result = await hass.config_entries.flow.async_init( From 1706156fafbc98dd24dc4d6369610dde48fa2cd8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 5 Feb 2024 20:09:05 -0500 Subject: [PATCH 0330/1367] Add Process binary sensor in System Monitor (#108585) * Process binary sensor in System Monitor * Add repair flow * add issue * add platform * fix repair * Tests * Fix tests * add minor version * migrate * Mod repairs * Fix tests * config flow test * Last fixes * Review comments * Remove entities during repair * Remove snapshot --- .../components/systemmonitor/__init__.py | 29 ++- .../components/systemmonitor/binary_sensor.py | 147 +++++++++++++ .../components/systemmonitor/config_flow.py | 12 +- .../components/systemmonitor/repairs.py | 72 +++++++ .../components/systemmonitor/sensor.py | 16 ++ .../components/systemmonitor/strings.json | 18 ++ tests/components/systemmonitor/conftest.py | 2 +- .../snapshots/test_binary_sensor.ambr | 21 ++ .../systemmonitor/snapshots/test_repairs.ambr | 73 +++++++ .../systemmonitor/test_binary_sensor.py | 107 ++++++++++ .../systemmonitor/test_config_flow.py | 55 ++--- tests/components/systemmonitor/test_init.py | 53 ++++- .../components/systemmonitor/test_repairs.py | 197 ++++++++++++++++++ tests/components/systemmonitor/test_sensor.py | 116 ++++++++++- 14 files changed, 873 insertions(+), 45 deletions(-) create mode 100644 homeassistant/components/systemmonitor/binary_sensor.py create mode 100644 homeassistant/components/systemmonitor/repairs.py create mode 100644 tests/components/systemmonitor/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/systemmonitor/snapshots/test_repairs.ambr create mode 100644 tests/components/systemmonitor/test_binary_sensor.py create mode 100644 tests/components/systemmonitor/test_repairs.py diff --git a/homeassistant/components/systemmonitor/__init__.py b/homeassistant/components/systemmonitor/__init__.py index 69dbb1f7952..d99bc519eff 100644 --- a/homeassistant/components/systemmonitor/__init__.py +++ b/homeassistant/components/systemmonitor/__init__.py @@ -1,10 +1,16 @@ """The System Monitor integration.""" +import logging + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -PLATFORMS = [Platform.SENSOR] +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -23,3 +29,24 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old entry.""" + + if entry.version == 1: + new_options = {**entry.options} + if entry.minor_version == 1: + # Migration copies process sensors to binary sensors + # Repair will remove sensors when user submit the fix + if processes := entry.options.get(SENSOR_DOMAIN): + new_options[BINARY_SENSOR_DOMAIN] = processes + entry.version = 1 + entry.minor_version = 2 + hass.config_entries.async_update_entry(entry, options=new_options) + + _LOGGER.debug( + "Migration to version %s.%s successful", entry.version, entry.minor_version + ) + + return True diff --git a/homeassistant/components/systemmonitor/binary_sensor.py b/homeassistant/components/systemmonitor/binary_sensor.py new file mode 100644 index 00000000000..4dffc33e2b3 --- /dev/null +++ b/homeassistant/components/systemmonitor/binary_sensor.py @@ -0,0 +1,147 @@ +"""Binary sensors for System Monitor.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from functools import lru_cache +import logging +import sys +from typing import Generic, Literal + +import psutil + +from homeassistant.components.binary_sensor import ( + DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import slugify + +from .const import CONF_PROCESS, DOMAIN +from .coordinator import MonitorCoordinator, SystemMonitorProcessCoordinator, dataT + +_LOGGER = logging.getLogger(__name__) + +CONF_ARG = "arg" + + +SENSOR_TYPE_NAME = 0 +SENSOR_TYPE_UOM = 1 +SENSOR_TYPE_ICON = 2 +SENSOR_TYPE_DEVICE_CLASS = 3 +SENSOR_TYPE_MANDATORY_ARG = 4 + +SIGNAL_SYSTEMMONITOR_UPDATE = "systemmonitor_update" + + +@lru_cache +def get_cpu_icon() -> Literal["mdi:cpu-64-bit", "mdi:cpu-32-bit"]: + """Return cpu icon.""" + if sys.maxsize > 2**32: + return "mdi:cpu-64-bit" + return "mdi:cpu-32-bit" + + +def get_process(entity: SystemMonitorSensor[list[psutil.Process]]) -> bool: + """Return process.""" + state = False + for proc in entity.coordinator.data: + try: + _LOGGER.debug("process %s for argument %s", proc.name(), entity.argument) + if entity.argument == proc.name(): + state = True + break + except psutil.NoSuchProcess as err: + _LOGGER.warning( + "Failed to load process with ID: %s, old name: %s", + err.pid, + err.name, + ) + return state + + +@dataclass(frozen=True, kw_only=True) +class SysMonitorBinarySensorEntityDescription( + BinarySensorEntityDescription, Generic[dataT] +): + """Describes System Monitor binary sensor entities.""" + + value_fn: Callable[[SystemMonitorSensor[dataT]], bool] + + +SENSOR_TYPES: tuple[ + SysMonitorBinarySensorEntityDescription[list[psutil.Process]], ... +] = ( + SysMonitorBinarySensorEntityDescription[list[psutil.Process]]( + key="binary_process", + translation_key="process", + icon=get_cpu_icon(), + value_fn=get_process, + device_class=BinarySensorDeviceClass.RUNNING, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up System Montor binary sensors based on a config entry.""" + entities: list[SystemMonitorSensor] = [] + process_coordinator = SystemMonitorProcessCoordinator(hass, "Process coordinator") + await process_coordinator.async_request_refresh() + + for sensor_description in SENSOR_TYPES: + _entry = entry.options.get(BINARY_SENSOR_DOMAIN, {}) + for argument in _entry.get(CONF_PROCESS, []): + entities.append( + SystemMonitorSensor( + process_coordinator, + sensor_description, + entry.entry_id, + argument, + ) + ) + async_add_entities(entities) + + +class SystemMonitorSensor( + CoordinatorEntity[MonitorCoordinator[dataT]], BinarySensorEntity +): + """Implementation of a system monitor binary sensor.""" + + _attr_has_entity_name = True + _attr_entity_category = EntityCategory.DIAGNOSTIC + entity_description: SysMonitorBinarySensorEntityDescription[dataT] + + def __init__( + self, + coordinator: MonitorCoordinator[dataT], + sensor_description: SysMonitorBinarySensorEntityDescription[dataT], + entry_id: str, + argument: str, + ) -> None: + """Initialize the binary sensor.""" + super().__init__(coordinator) + self.entity_description = sensor_description + self._attr_translation_placeholders = {"process": argument} + self._attr_unique_id: str = slugify(f"{sensor_description.key}_{argument}") + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, entry_id)}, + manufacturer="System Monitor", + name="System Monitor", + ) + self.argument = argument + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.entity_description.value_fn(self) diff --git a/homeassistant/components/systemmonitor/config_flow.py b/homeassistant/components/systemmonitor/config_flow.py index 6d9787a39f5..9c7e739dbf9 100644 --- a/homeassistant/components/systemmonitor/config_flow.py +++ b/homeassistant/components/systemmonitor/config_flow.py @@ -6,8 +6,8 @@ from typing import Any import voluptuous as vol +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import entity_registry as er @@ -34,7 +34,7 @@ async def validate_sensor_setup( """Validate sensor input.""" # Standard behavior is to merge the result with the options. # In this case, we want to add a sub-item so we update the options directly. - sensors: dict[str, list] = handler.options.setdefault(SENSOR_DOMAIN, {}) + sensors: dict[str, list] = handler.options.setdefault(BINARY_SENSOR_DOMAIN, {}) processes = sensors.setdefault(CONF_PROCESS, []) previous_processes = processes.copy() processes.clear() @@ -44,7 +44,7 @@ async def validate_sensor_setup( for process in previous_processes: if process not in processes and ( entity_id := entity_registry.async_get_entity_id( - SENSOR_DOMAIN, DOMAIN, slugify(f"process_{process}") + BINARY_SENSOR_DOMAIN, DOMAIN, slugify(f"binary_process_{process}") ) ): entity_registry.async_remove(entity_id) @@ -58,7 +58,7 @@ async def validate_import_sensor_setup( """Validate sensor input.""" # Standard behavior is to merge the result with the options. # In this case, we want to add a sub-item so we update the options directly. - sensors: dict[str, list] = handler.options.setdefault(SENSOR_DOMAIN, {}) + sensors: dict[str, list] = handler.options.setdefault(BINARY_SENSOR_DOMAIN, {}) import_processes: list[str] = user_input["processes"] processes = sensors.setdefault(CONF_PROCESS, []) processes.extend(import_processes) @@ -104,7 +104,7 @@ async def get_sensor_setup_schema(handler: SchemaCommonFlowHandler) -> vol.Schem async def get_suggested_value(handler: SchemaCommonFlowHandler) -> dict[str, Any]: """Return suggested values for sensor setup.""" - sensors: dict[str, list] = handler.options.get(SENSOR_DOMAIN, {}) + sensors: dict[str, list] = handler.options.get(BINARY_SENSOR_DOMAIN, {}) processes: list[str] = sensors.get(CONF_PROCESS, []) return {CONF_PROCESS: processes} @@ -130,6 +130,8 @@ class SystemMonitorConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + VERSION = 1 + MINOR_VERSION = 2 def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" diff --git a/homeassistant/components/systemmonitor/repairs.py b/homeassistant/components/systemmonitor/repairs.py new file mode 100644 index 00000000000..10b5d18830d --- /dev/null +++ b/homeassistant/components/systemmonitor/repairs.py @@ -0,0 +1,72 @@ +"""Repairs platform for the System Monitor integration.""" + +from __future__ import annotations + +from typing import Any, cast + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + + +class ProcessFixFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + def __init__(self, entry: ConfigEntry, processes: list[str]) -> None: + """Create flow.""" + super().__init__() + self.entry = entry + self._processes = processes + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + return await self.async_step_migrate_process_sensor() + + async def async_step_migrate_process_sensor( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the options step of a fix flow.""" + if user_input is None: + return self.async_show_form( + step_id="migrate_process_sensor", + description_placeholders={"processes": ", ".join(self._processes)}, + ) + + # Migration has copied the sensors to binary sensors + # Pop the sensors to repair and remove entities + new_options: dict[str, Any] = self.entry.options.copy() + new_options.pop(SENSOR_DOMAIN) + + entity_reg = er.async_get(self.hass) + entries = er.async_entries_for_config_entry(entity_reg, self.entry.entry_id) + for entry in entries: + if entry.entity_id.startswith("sensor.") and entry.unique_id.startswith( + "process_" + ): + entity_reg.async_remove(entry.entity_id) + + self.hass.config_entries.async_update_entry(self.entry, options=new_options) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_create_entry(data={}) + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, Any] | None, +) -> RepairsFlow: + """Create flow.""" + entry = None + if data and (entry_id := data.get("entry_id")): + entry_id = cast(str, entry_id) + processes: list[str] = data["processes"] + entry = hass.config_entries.async_get_entry(entry_id) + assert entry + return ProcessFixFlow(entry, processes) + + return ConfirmRepairFlow() diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index e751ffebb12..0f8532804c7 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring the local system.""" + from __future__ import annotations from collections.abc import Callable @@ -39,6 +40,7 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType, 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, StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify @@ -638,6 +640,20 @@ async def async_setup_entry( # noqa: C901 True, ) ) + async_create_issue( + hass, + DOMAIN, + "process_sensor", + breaks_in_ha_version="2024.9.0", + is_fixable=True, + is_persistent=False, + severity=IssueSeverity.WARNING, + translation_key="process_sensor", + data={ + "entry_id": entry.entry_id, + "processes": _entry[CONF_PROCESS], + }, + ) continue if _type == "processor_use": diff --git a/homeassistant/components/systemmonitor/strings.json b/homeassistant/components/systemmonitor/strings.json index ff1fbc221ee..aae2463c9da 100644 --- a/homeassistant/components/systemmonitor/strings.json +++ b/homeassistant/components/systemmonitor/strings.json @@ -22,7 +22,25 @@ } } }, + "issues": { + "process_sensor": { + "title": "Process sensors are deprecated and will be removed", + "fix_flow": { + "step": { + "migrate_process_sensor": { + "title": "Process sensors have been setup as binary sensors", + "description": "Process sensors `{processes}` have been created as binary sensors and the sensors will be removed in 2024.9.0.\n\nPlease update all automations, scripts, dashboards or other things depending on these sensors to use the newly created binary sensors instead and press **Submit** to fix this issue." + } + } + } + } + }, "entity": { + "binary_sensor": { + "process": { + "name": "Process {process}" + } + }, "sensor": { "disk_free": { "name": "Disk free {mount_point}" diff --git a/tests/components/systemmonitor/conftest.py b/tests/components/systemmonitor/conftest.py index c03c3fff2ca..e41faf13c49 100644 --- a/tests/components/systemmonitor/conftest.py +++ b/tests/components/systemmonitor/conftest.py @@ -75,7 +75,7 @@ def mock_config_entry() -> MockConfigEntry: domain=DOMAIN, data={}, options={ - "sensor": {"process": ["python3", "pip"]}, + "binary_sensor": {"process": ["python3", "pip"]}, "resources": [ "disk_use_percent_/", "disk_use_percent_/home/notexist/", diff --git a/tests/components/systemmonitor/snapshots/test_binary_sensor.ambr b/tests/components/systemmonitor/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..0c04cfcfa06 --- /dev/null +++ b/tests/components/systemmonitor/snapshots/test_binary_sensor.ambr @@ -0,0 +1,21 @@ +# serializer version: 1 +# name: test_binary_sensor[System Monitor Process pip - attributes] + ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'System Monitor Process pip', + 'icon': 'mdi:cpu-64-bit', + }) +# --- +# name: test_binary_sensor[System Monitor Process pip - state] + 'on' +# --- +# name: test_binary_sensor[System Monitor Process python3 - attributes] + ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'System Monitor Process python3', + 'icon': 'mdi:cpu-64-bit', + }) +# --- +# name: test_binary_sensor[System Monitor Process python3 - state] + 'on' +# --- diff --git a/tests/components/systemmonitor/snapshots/test_repairs.ambr b/tests/components/systemmonitor/snapshots/test_repairs.ambr new file mode 100644 index 00000000000..dc659918b5f --- /dev/null +++ b/tests/components/systemmonitor/snapshots/test_repairs.ambr @@ -0,0 +1,73 @@ +# serializer version: 1 +# name: test_migrate_process_sensor[after_migration] + list([ + ConfigEntrySnapshot({ + 'data': dict({ + }), + 'disabled_by': None, + 'domain': 'systemmonitor', + 'entry_id': , + 'minor_version': 2, + 'options': dict({ + 'binary_sensor': dict({ + 'process': list([ + 'python3', + 'pip', + ]), + }), + 'resources': list([ + 'disk_use_percent_/', + 'disk_use_percent_/home/notexist/', + 'memory_free_', + 'network_out_eth0', + 'process_python3', + ]), + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'System Monitor', + 'unique_id': None, + 'version': 1, + }), + ]) +# --- +# name: test_migrate_process_sensor[before_migration] + list([ + ConfigEntrySnapshot({ + 'data': dict({ + }), + 'disabled_by': None, + 'domain': 'systemmonitor', + 'entry_id': , + 'minor_version': 2, + 'options': dict({ + 'binary_sensor': dict({ + 'process': list([ + 'python3', + 'pip', + ]), + }), + 'resources': list([ + 'disk_use_percent_/', + 'disk_use_percent_/home/notexist/', + 'memory_free_', + 'network_out_eth0', + 'process_python3', + ]), + 'sensor': dict({ + 'process': list([ + 'python3', + 'pip', + ]), + }), + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'System Monitor', + 'unique_id': None, + 'version': 1, + }), + ]) +# --- diff --git a/tests/components/systemmonitor/test_binary_sensor.py b/tests/components/systemmonitor/test_binary_sensor.py new file mode 100644 index 00000000000..82522db25f3 --- /dev/null +++ b/tests/components/systemmonitor/test_binary_sensor.py @@ -0,0 +1,107 @@ +"""Test System Monitor binary sensor.""" +from datetime import timedelta +from unittest.mock import Mock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.systemmonitor.binary_sensor import get_cpu_icon +from homeassistant.components.systemmonitor.const import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import MockProcess + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_binary_sensor( + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + mock_psutil: Mock, + mock_os: Mock, + mock_util: Mock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the binary sensor.""" + mock_config_entry = MockConfigEntry( + title="System Monitor", + domain=DOMAIN, + data={}, + options={ + "binary_sensor": {"process": ["python3", "pip"]}, + "resources": [ + "disk_use_percent_/", + "disk_use_percent_/home/notexist/", + "memory_free_", + "network_out_eth0", + "process_python3", + ], + }, + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + process_binary_sensor = hass.states.get( + "binary_sensor.system_monitor_process_python3" + ) + assert process_binary_sensor is not None + + for entity in er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ): + if entity.domain == BINARY_SENSOR_DOMAIN: + state = hass.states.get(entity.entity_id) + assert state.state == snapshot(name=f"{state.name} - state") + assert state.attributes == snapshot(name=f"{state.name} - attributes") + + +async def test_binary_sensor_icon( + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + mock_util: Mock, + mock_psutil: Mock, + mock_os: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the sensor icon for 32bit/64bit system.""" + + get_cpu_icon.cache_clear() + with patch("sys.maxsize", 2**32): + assert get_cpu_icon() == "mdi:cpu-32-bit" + get_cpu_icon.cache_clear() + with patch("sys.maxsize", 2**64): + assert get_cpu_icon() == "mdi:cpu-64-bit" + + +async def test_sensor_process_fails( + hass: HomeAssistant, + mock_added_config_entry: ConfigEntry, + mock_psutil: Mock, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test process not exist failure.""" + process_sensor = hass.states.get("binary_sensor.system_monitor_process_python3") + assert process_sensor is not None + assert process_sensor.state == STATE_ON + + _process = MockProcess("python3", True) + + mock_psutil.process_iter.return_value = [_process] + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + process_sensor = hass.states.get("binary_sensor.system_monitor_process_python3") + assert process_sensor is not None + assert process_sensor.state == STATE_OFF + + assert "Failed to load process with ID: 1, old name: python3" in caplog.text diff --git a/tests/components/systemmonitor/test_config_flow.py b/tests/components/systemmonitor/test_config_flow.py index 367d38b91aa..2536f847b43 100644 --- a/tests/components/systemmonitor/test_config_flow.py +++ b/tests/components/systemmonitor/test_config_flow.py @@ -6,11 +6,13 @@ from unittest.mock import AsyncMock from homeassistant import config_entries from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.components.systemmonitor.const import CONF_PROCESS, DOMAIN -from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import entity_registry as er, issue_registry as ir -from homeassistant.util import slugify +from homeassistant.helpers import ( + device_registry as dr, + entity_registry as er, + issue_registry as ir, +) from tests.common import MockConfigEntry @@ -59,7 +61,7 @@ async def test_import( assert result["type"] == FlowResultType.CREATE_ENTRY assert result["options"] == { - "sensor": {"process": ["systemd", "octave-cli"]}, + "binary_sensor": {"process": ["systemd", "octave-cli"]}, "resources": [ "disk_use_percent_/", "memory_free_", @@ -116,7 +118,7 @@ async def test_import_already_configured( domain=DOMAIN, source=config_entries.SOURCE_USER, options={ - "sensor": [{CONF_PROCESS: "systemd"}], + "binary_sensor": [{CONF_PROCESS: "systemd"}], "resources": [ "disk_use_percent_/", "memory_free_", @@ -158,16 +160,21 @@ async def test_import_already_configured( async def test_add_and_remove_processes( - hass: HomeAssistant, mock_setup_entry: AsyncMock + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test adding and removing process sensors.""" config_entry = MockConfigEntry( domain=DOMAIN, source=config_entries.SOURCE_USER, + data={}, options={}, entry_id="1", ) config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -184,7 +191,7 @@ async def test_add_and_remove_processes( assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { - "sensor": { + "binary_sensor": { CONF_PROCESS: ["systemd"], } } @@ -205,26 +212,19 @@ async def test_add_and_remove_processes( assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { - "sensor": { + "binary_sensor": { CONF_PROCESS: ["systemd", "octave-cli"], }, } - entity_reg = er.async_get(hass) - entity_reg.async_get_or_create( - domain=Platform.SENSOR, - platform=DOMAIN, - unique_id=slugify("process_systemd"), - config_entry=config_entry, + assert ( + entity_registry.async_get("binary_sensor.system_monitor_process_systemd") + is not None ) - entity_reg.async_get_or_create( - domain=Platform.SENSOR, - platform=DOMAIN, - unique_id=slugify("process_octave-cli"), - config_entry=config_entry, + assert ( + entity_registry.async_get("binary_sensor.system_monitor_process_octave_cli") + is not None ) - assert entity_reg.async_get("sensor.systemmonitor_process_systemd") is not None - assert entity_reg.async_get("sensor.systemmonitor_process_octave_cli") is not None # Remove one result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -242,7 +242,7 @@ async def test_add_and_remove_processes( assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { - "sensor": { + "binary_sensor": { CONF_PROCESS: ["systemd"], }, } @@ -263,8 +263,13 @@ async def test_add_and_remove_processes( assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { - "sensor": {CONF_PROCESS: []}, + "binary_sensor": {CONF_PROCESS: []}, } - assert entity_reg.async_get("sensor.systemmonitor_process_systemd") is None - assert entity_reg.async_get("sensor.systemmonitor_process_octave_cli") is None + assert ( + entity_registry.async_get("binary_sensor.systemmonitor_process_systemd") is None + ) + assert ( + entity_registry.async_get("binary_sensor.systemmonitor_process_octave_cli") + is None + ) diff --git a/tests/components/systemmonitor/test_init.py b/tests/components/systemmonitor/test_init.py index a352f9a1b95..12caa060006 100644 --- a/tests/components/systemmonitor/test_init.py +++ b/tests/components/systemmonitor/test_init.py @@ -1,12 +1,19 @@ """Test for System Monitor init.""" from __future__ import annotations -from homeassistant.components.systemmonitor.const import CONF_PROCESS +from unittest.mock import Mock + +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.components.systemmonitor.const import CONF_PROCESS, DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from tests.common import MockConfigEntry + async def test_load_unload_entry( hass: HomeAssistant, mock_added_config_entry: ConfigEntry @@ -23,7 +30,7 @@ async def test_adding_processor_to_options( hass: HomeAssistant, mock_added_config_entry: ConfigEntry ) -> None: """Test options listener.""" - process_sensor = hass.states.get("sensor.system_monitor_process_systemd") + process_sensor = hass.states.get("binary_sensor.system_monitor_process_systemd") assert process_sensor is None result = await hass.config_entries.options.async_init( @@ -43,7 +50,7 @@ async def test_adding_processor_to_options( assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { - "sensor": { + "binary_sensor": { CONF_PROCESS: ["python3", "pip", "systemd"], }, "resources": [ @@ -55,6 +62,42 @@ async def test_adding_processor_to_options( ], } - process_sensor = hass.states.get("sensor.system_monitor_process_systemd") + process_sensor = hass.states.get("binary_sensor.system_monitor_process_systemd") assert process_sensor is not None assert process_sensor.state == STATE_OFF + + +async def test_migrate_process_sensor_to_binary_sensors( + hass: HomeAssistant, + mock_psutil: Mock, + mock_os: Mock, + mock_util: Mock, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test process not exist failure.""" + mock_config_entry = MockConfigEntry( + title="System Monitor", + domain=DOMAIN, + data={}, + options={ + "sensor": {"process": ["python3", "pip"]}, + "resources": [ + "disk_use_percent_/", + "disk_use_percent_/home/notexist/", + "memory_free_", + "network_out_eth0", + "process_python3", + ], + }, + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + process_sensor = hass.states.get("sensor.system_monitor_process_python3") + assert process_sensor is not None + assert process_sensor.state == STATE_ON + process_sensor = hass.states.get("binary_sensor.system_monitor_process_python3") + assert process_sensor is not None + assert process_sensor.state == STATE_ON diff --git a/tests/components/systemmonitor/test_repairs.py b/tests/components/systemmonitor/test_repairs.py new file mode 100644 index 00000000000..18ca90278a2 --- /dev/null +++ b/tests/components/systemmonitor/test_repairs.py @@ -0,0 +1,197 @@ +"""Test repairs for System Monitor.""" + +from __future__ import annotations + +from http import HTTPStatus +from unittest.mock import Mock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.repairs.websocket_api import ( + RepairsFlowIndexView, + RepairsFlowResourceView, +) +from homeassistant.components.systemmonitor.const import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import entity_registry as er, issue_registry as ir +from homeassistant.setup import async_setup_component + +from tests.common import ANY, MockConfigEntry +from tests.typing import ClientSessionGenerator, WebSocketGenerator + + +async def test_migrate_process_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + entity_registry_enabled_by_default: None, + mock_psutil: Mock, + mock_os: Mock, + mock_util: Mock, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test migrating process sensor to binary sensor.""" + mock_config_entry = MockConfigEntry( + title="System Monitor", + domain=DOMAIN, + data={}, + options={ + "binary_sensor": {"process": ["python3", "pip"]}, + "sensor": {"process": ["python3", "pip"]}, + "resources": [ + "disk_use_percent_/", + "disk_use_percent_/home/notexist/", + "memory_free_", + "network_out_eth0", + "process_python3", + ], + }, + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert hass.config_entries.async_entries(DOMAIN) == snapshot( + name="before_migration" + ) + + assert await async_setup_component(hass, "repairs", {}) + await hass.async_block_till_done() + + entity = "sensor.system_monitor_process_python3" + state = hass.states.get(entity) + assert state + + assert entity_registry.async_get(entity) + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert len(msg["result"]["issues"]) > 0 + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "process_sensor": + issue = i + assert issue is not None + + url = RepairsFlowIndexView.url + resp = await client.post( + url, json={"handler": DOMAIN, "issue_id": "process_sensor"} + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["step_id"] == "migrate_process_sensor" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url, json={}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.system_monitor_process_python3") + assert state + + await ws_client.send_json({"id": 2, "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"] == "migrate_process_sensor": + issue = i + assert not issue + + entity = "sensor.system_monitor_process_python3" + state = hass.states.get(entity) + assert not state + + assert not entity_registry.async_get(entity) + + assert hass.config_entries.async_entries(DOMAIN) == snapshot(name="after_migration") + + +async def test_other_fixable_issues( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, + entity_registry_enabled_by_default: None, + mock_added_config_entry: ConfigEntry, +) -> None: + """Test fixing other issues.""" + assert await async_setup_component(hass, "repairs", {}) + await hass.async_block_till_done() + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + + issue = { + "breaks_in_ha_version": "2022.9.0dev0", + "domain": DOMAIN, + "issue_id": "issue_1", + "is_fixable": True, + "learn_more_url": "", + "severity": "error", + "translation_key": "issue_1", + } + ir.async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], + is_persistent=False, + learn_more_url=None, + severity=issue["severity"], + translation_key=issue["translation_key"], + ) + + await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + results = msg["result"]["issues"] + assert { + "breaks_in_ha_version": "2022.9.0dev0", + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "is_fixable": True, + "issue_domain": None, + "issue_id": "issue_1", + "learn_more_url": None, + "severity": "error", + "translation_key": "issue_1", + "translation_placeholders": None, + "ignored": False, + } in results + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "issue_1"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["step_id"] == "confirm" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() diff --git a/tests/components/systemmonitor/test_sensor.py b/tests/components/systemmonitor/test_sensor.py index 8beeddbefdc..e64f6dbefa1 100644 --- a/tests/components/systemmonitor/test_sensor.py +++ b/tests/components/systemmonitor/test_sensor.py @@ -8,6 +8,8 @@ from psutil._common import sdiskusage, shwtemp, snetio, snicaddr import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.systemmonitor.const import DOMAIN from homeassistant.components.systemmonitor.sensor import get_cpu_icon from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN @@ -23,11 +25,33 @@ from tests.common import MockConfigEntry, async_fire_time_changed async def test_sensor( hass: HomeAssistant, entity_registry_enabled_by_default: None, - mock_added_config_entry: ConfigEntry, + mock_psutil: Mock, + mock_os: Mock, + mock_util: Mock, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test the sensor.""" + mock_config_entry = MockConfigEntry( + title="System Monitor", + domain=DOMAIN, + data={}, + options={ + "binary_sensor": {"process": ["python3", "pip"]}, + "sensor": {"process": ["python3", "pip"]}, + "resources": [ + "disk_use_percent_/", + "disk_use_percent_/home/notexist/", + "memory_free_", + "network_out_eth0", + "process_python3", + ], + }, + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + memory_sensor = hass.states.get("sensor.system_monitor_memory_free") assert memory_sensor is not None assert memory_sensor.state == "40.0" @@ -44,11 +68,45 @@ async def test_sensor( assert process_sensor.state == STATE_ON for entity in er.async_entries_for_config_entry( - entity_registry, mock_added_config_entry.entry_id + entity_registry, mock_config_entry.entry_id ): - state = hass.states.get(entity.entity_id) - assert state.state == snapshot(name=f"{state.name} - state") - assert state.attributes == snapshot(name=f"{state.name} - attributes") + if entity.domain == SENSOR_DOMAIN: + state = hass.states.get(entity.entity_id) + assert state.state == snapshot(name=f"{state.name} - state") + assert state.attributes == snapshot(name=f"{state.name} - attributes") + + +async def test_process_sensor_not_loaded( + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + mock_psutil: Mock, + mock_os: Mock, + mock_util: Mock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the process sensor is not loaded once migrated.""" + mock_config_entry = MockConfigEntry( + title="System Monitor", + domain=DOMAIN, + data={}, + options={ + "binary_sensor": {"process": ["python3", "pip"]}, + "resources": [ + "disk_use_percent_/", + "disk_use_percent_/home/notexist/", + "memory_free_", + "network_out_eth0", + "process_python3", + ], + }, + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + process_sensor = hass.states.get("sensor.system_monitor_process_python3") + assert process_sensor is None async def test_sensor_not_loading_veth_networks( @@ -112,7 +170,7 @@ async def test_sensor_yaml( assert memory_sensor is not None assert memory_sensor.state == "40.0" - process_sensor = hass.states.get("sensor.system_monitor_process_python3") + process_sensor = hass.states.get("binary_sensor.system_monitor_process_python3") assert process_sensor is not None assert process_sensor.state == STATE_ON @@ -142,11 +200,32 @@ async def test_sensor_yaml_fails_missing_argument( async def test_sensor_updating( hass: HomeAssistant, - mock_added_config_entry: ConfigEntry, mock_psutil: Mock, + mock_os: Mock, + mock_util: Mock, freezer: FrozenDateTimeFactory, ) -> None: """Test the sensor.""" + mock_config_entry = MockConfigEntry( + title="System Monitor", + domain=DOMAIN, + data={}, + options={ + "binary_sensor": {"process": ["python3", "pip"]}, + "sensor": {"process": ["python3", "pip"]}, + "resources": [ + "disk_use_percent_/", + "disk_use_percent_/home/notexist/", + "memory_free_", + "network_out_eth0", + "process_python3", + ], + }, + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + memory_sensor = hass.states.get("sensor.system_monitor_memory_free") assert memory_sensor is not None assert memory_sensor.state == "40.0" @@ -189,12 +268,33 @@ async def test_sensor_updating( async def test_sensor_process_fails( hass: HomeAssistant, - mock_added_config_entry: ConfigEntry, mock_psutil: Mock, + mock_os: Mock, + mock_util: Mock, freezer: FrozenDateTimeFactory, caplog: pytest.LogCaptureFixture, ) -> None: """Test process not exist failure.""" + mock_config_entry = MockConfigEntry( + title="System Monitor", + domain=DOMAIN, + data={}, + options={ + "binary_sensor": {"process": ["python3", "pip"]}, + "sensor": {"process": ["python3", "pip"]}, + "resources": [ + "disk_use_percent_/", + "disk_use_percent_/home/notexist/", + "memory_free_", + "network_out_eth0", + "process_python3", + ], + }, + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + process_sensor = hass.states.get("sensor.system_monitor_process_python3") assert process_sensor is not None assert process_sensor.state == STATE_ON From 61ce328ce1866c7e307dde503cbea0868c27397b Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 6 Feb 2024 03:00:45 +0100 Subject: [PATCH 0331/1367] Fix docstring length in ZHA sensor class (#109774) Fix ZHA sensor docstring length --- homeassistant/components/zha/sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 929ac803b10..167edc935d0 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -849,9 +849,10 @@ class SmartEnergySummationReceived(PolledSmartEnergySummation): """Entity Factory. This attribute only started to be initialized in HA 2024.2.0, - so the entity would still be created on the first HA start after the upgrade for existing devices, - as the initialization to see if an attribute is unsupported happens later in the background. - To avoid creating a lot of unnecessary entities for existing devices, + so the entity would be created on the first HA start after the + upgrade for existing devices, as the initialization to see if + an attribute is unsupported happens later in the background. + To avoid creating unnecessary entities for existing devices, wait until the attribute was properly initialized once for now. """ if cluster_handlers[0].cluster.get(cls._attribute_name) is None: From cc4274bcc0e6636333b7755639e2edec0ad3834b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 6 Feb 2024 07:18:33 +0100 Subject: [PATCH 0332/1367] Fix Radarr health check singularity (#109762) * Fix Radarr health check singularity * Fix comment --- homeassistant/components/radarr/coordinator.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/radarr/coordinator.py b/homeassistant/components/radarr/coordinator.py index c14603fe9ca..7f395169644 100644 --- a/homeassistant/components/radarr/coordinator.py +++ b/homeassistant/components/radarr/coordinator.py @@ -96,7 +96,7 @@ class DiskSpaceDataUpdateCoordinator(RadarrDataUpdateCoordinator[list[RootFolder """Fetch the data.""" root_folders = await self.api_client.async_get_root_folders() if isinstance(root_folders, RootFolder): - root_folders = [root_folders] + return [root_folders] return root_folders @@ -105,7 +105,10 @@ class HealthDataUpdateCoordinator(RadarrDataUpdateCoordinator[list[Health]]): async def _fetch_data(self) -> list[Health]: """Fetch the health data.""" - return await self.api_client.async_get_failed_health_checks() + health = await self.api_client.async_get_failed_health_checks() + if isinstance(health, Health): + return [health] + return health class MoviesDataUpdateCoordinator(RadarrDataUpdateCoordinator[int]): From 29462fc99181f80794622d6a30b980200426827c Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 6 Feb 2024 09:34:02 +0100 Subject: [PATCH 0333/1367] Change state class of Tesla wall connector session energy entity (#109778) --- homeassistant/components/tesla_wall_connector/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tesla_wall_connector/sensor.py b/homeassistant/components/tesla_wall_connector/sensor.py index 67d3d4ba22e..da1e974f6a0 100644 --- a/homeassistant/components/tesla_wall_connector/sensor.py +++ b/homeassistant/components/tesla_wall_connector/sensor.py @@ -156,7 +156,7 @@ WALL_CONNECTOR_SENSORS = [ suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].session_energy_wh, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, ), WallConnectorSensorDescription( key="energy_kWh", From 00947b708fe09a8c9aac234bdee7c675c0f43241 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 6 Feb 2024 09:37:39 +0100 Subject: [PATCH 0334/1367] Remove aux heat support from mqtt climate (#109513) --- homeassistant/components/mqtt/climate.py | 110 +------------------- homeassistant/components/mqtt/strings.json | 4 - tests/components/mqtt/test_climate.py | 114 --------------------- 3 files changed, 5 insertions(+), 223 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 94311eeda61..6911a5ee3e8 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -44,7 +44,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback 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.template import Template from homeassistant.helpers.typing import ConfigType from homeassistant.util.unit_conversion import TemperatureConverter @@ -77,7 +76,6 @@ from .const import ( CONF_TEMP_STATE_TEMPLATE, CONF_TEMP_STATE_TOPIC, DEFAULT_OPTIMISTIC, - DOMAIN, PAYLOAD_NONE, ) from .debug_info import log_messages @@ -98,13 +96,11 @@ from .util import valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) -MQTT_CLIMATE_AUX_DOCS = "https://www.home-assistant.io/integrations/climate.mqtt/" - DEFAULT_NAME = "MQTT HVAC" # Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC # and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 -# Support will be removed in HA Core 2024.3 +# Support was removed in HA Core 2024.3 CONF_AUX_COMMAND_TOPIC = "aux_command_topic" CONF_AUX_STATE_TEMPLATE = "aux_state_template" CONF_AUX_STATE_TOPIC = "aux_state_topic" @@ -150,7 +146,6 @@ DEFAULT_INITIAL_TEMPERATURE = 21.0 MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset( { - climate.ATTR_AUX_HEAT, climate.ATTR_CURRENT_HUMIDITY, climate.ATTR_CURRENT_TEMPERATURE, climate.ATTR_FAN_MODE, @@ -174,7 +169,6 @@ MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset( ) VALUE_TEMPLATE_KEYS = ( - CONF_AUX_STATE_TEMPLATE, CONF_CURRENT_HUMIDITY_TEMPLATE, CONF_CURRENT_TEMP_TEMPLATE, CONF_FAN_MODE_STATE_TEMPLATE, @@ -204,8 +198,6 @@ COMMAND_TEMPLATE_KEYS = { TOPIC_KEYS = ( CONF_ACTION_TOPIC, - CONF_AUX_COMMAND_TOPIC, - CONF_AUX_STATE_TOPIC, CONF_CURRENT_HUMIDITY_TOPIC, CONF_CURRENT_TEMP_TOPIC, CONF_FAN_MODE_COMMAND_TOPIC, @@ -266,12 +258,6 @@ def valid_humidity_state_configuration(config: ConfigType) -> ConfigType: _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { - # Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC - # and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 - # Support will be removed in HA Core 2024.3 - vol.Optional(CONF_AUX_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_AUX_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_CURRENT_HUMIDITY_TEMPLATE): cv.template, vol.Optional(CONF_CURRENT_HUMIDITY_TOPIC): valid_subscribe_topic, vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template, @@ -369,10 +355,10 @@ PLATFORM_SCHEMA_MODERN = vol.All( cv.removed(CONF_POWER_STATE_TOPIC), # Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC # and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 - # Support will be removed in HA Core 2024.3 - cv.deprecated(CONF_AUX_COMMAND_TOPIC), - cv.deprecated(CONF_AUX_STATE_TEMPLATE), - cv.deprecated(CONF_AUX_STATE_TOPIC), + # Support was removed in HA Core 2024.3 + cv.removed(CONF_AUX_COMMAND_TOPIC), + cv.removed(CONF_AUX_STATE_TEMPLATE), + cv.removed(CONF_AUX_STATE_TOPIC), _PLATFORM_SCHEMA_BASE, valid_preset_mode_configuration, valid_humidity_range_configuration, @@ -603,7 +589,6 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): _attr_fan_mode: str | None = None _attr_hvac_mode: HVACMode | None = None - _attr_is_aux_heat: bool | None = None _attr_swing_mode: str | None = None _default_name = DEFAULT_NAME _entity_id_format = climate.ENTITY_ID_FORMAT @@ -662,11 +647,6 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): self._attr_swing_mode = SWING_OFF if self._topic[CONF_MODE_STATE_TOPIC] is None or self._optimistic: self._attr_hvac_mode = HVACMode.OFF - # Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC - # and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 - # Support will be removed in HA Core 2024.3 - if self._topic[CONF_AUX_STATE_TOPIC] is None or self._optimistic: - self._attr_is_aux_heat = False self._feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config if self._feature_preset_mode: presets = [] @@ -736,32 +716,8 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): if self._feature_preset_mode: support |= ClimateEntityFeature.PRESET_MODE - # Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC - # and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 - # Support will be removed in HA Core 2024.3 - if (self._topic[CONF_AUX_STATE_TOPIC] is not None) or ( - self._topic[CONF_AUX_COMMAND_TOPIC] is not None - ): - support |= ClimateEntityFeature.AUX_HEAT self._attr_supported_features = support - async def mqtt_async_added_to_hass(self) -> None: - """Handle deprecation issues.""" - if self._attr_supported_features & ClimateEntityFeature.AUX_HEAT: - async_create_issue( - self.hass, - DOMAIN, - "deprecated_climate_aux_property_{self.entity_id}", - breaks_in_ha_version="2024.3.0", - is_fixable=False, - translation_key="deprecated_climate_aux_property", - translation_placeholders={ - "entity_id": self.entity_id, - }, - learn_more_url=MQTT_CLIMATE_AUX_DOCS, - severity=IssueSeverity.WARNING, - ) - def _prepare_subscribe_topics(self) -> None: # noqa: C901 """(Re)Subscribe to topics.""" topics: dict[str, dict[str, Any]] = {} @@ -875,41 +831,6 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): topics, CONF_SWING_MODE_STATE_TOPIC, handle_swing_mode_received ) - @callback - def handle_onoff_mode_received( - msg: ReceiveMessage, template_name: str, attr: str - ) -> None: - """Handle receiving on/off mode via MQTT.""" - payload = self.render_template(msg, template_name) - payload_on: str = self._config[CONF_PAYLOAD_ON] - payload_off: str = self._config[CONF_PAYLOAD_OFF] - - if payload == "True": - payload = payload_on - elif payload == "False": - payload = payload_off - - if payload == payload_on: - setattr(self, attr, True) - elif payload == payload_off: - setattr(self, attr, False) - else: - _LOGGER.error("Invalid %s mode: %s", attr, payload) - - # Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC - # and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 - # Support will be removed in HA Core 2024.3 - @callback - @log_messages(self.hass, self.entity_id) - @write_state_on_attr_change(self, {"_attr_is_aux_heat"}) - def handle_aux_mode_received(msg: ReceiveMessage) -> None: - """Handle receiving aux mode via MQTT.""" - handle_onoff_mode_received( - msg, CONF_AUX_STATE_TEMPLATE, "_attr_is_aux_heat" - ) - - self.add_subscription(topics, CONF_AUX_STATE_TOPIC, handle_aux_mode_received) - @callback @log_messages(self.hass, self.entity_id) @write_state_on_attr_change(self, {"_attr_preset_mode"}) @@ -1002,27 +923,6 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): self._attr_preset_mode = preset_mode self.async_write_ha_state() - # Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC - # and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 - # Support will be removed in HA Core 2024.3 - async def _set_aux_heat(self, state: bool) -> None: - await self._publish( - CONF_AUX_COMMAND_TOPIC, - self._config[CONF_PAYLOAD_ON] if state else self._config[CONF_PAYLOAD_OFF], - ) - - if self._optimistic or self._topic[CONF_AUX_STATE_TOPIC] is None: - self._attr_is_aux_heat = state - self.async_write_ha_state() - - async def async_turn_aux_heat_on(self) -> None: - """Turn auxiliary heater on.""" - await self._set_aux_heat(True) - - async def async_turn_aux_heat_off(self) -> None: - """Turn auxiliary heater off.""" - await self._set_aux_heat(False) - async def async_turn_on(self) -> None: """Turn the entity on.""" if CONF_POWER_COMMAND_TOPIC in self._config: diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index ce892e97026..2c3a87a515b 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -4,10 +4,6 @@ "title": "MQTT vacuum entities with deprecated `schema` config settings found in your configuration.yaml", "description": "The `schema` setting for MQTT vacuum entities is deprecated and should be removed. Please adjust your configuration.yaml and restart Home Assistant to fix this issue." }, - "deprecated_climate_aux_property": { - "title": "MQTT entities with auxiliary heat support found", - "description": "Entity `{entity_id}` has auxiliary heat support enabled, which has been deprecated for MQTT climate devices. Please adjust your configuration and remove deprecated config options from your configuration and restart Home Assistant to fix this issue." - }, "invalid_platform_config": { "title": "Invalid config found for mqtt {domain} item", "description": "Home Assistant detected an invalid config for a manually configured item.\n\nPlatform domain: **{domain}**\nConfiguration file: **{config_file}**\nNear line: **{line}**\nConfiguration found:\n```yaml\n{config}\n```\nError: **{error}**.\n\nMake sure the configuration is valid and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue." diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 6fcb219f6b6..b572753d6e6 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -9,7 +9,6 @@ import voluptuous as vol from homeassistant.components import climate, mqtt from homeassistant.components.climate import ( - ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, @@ -1262,91 +1261,6 @@ async def test_set_preset_mode_pessimistic( assert state.attributes.get("preset_mode") == "home" -# Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC -# and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 -# Support will be removed in HA Core 2024.3 -@pytest.mark.parametrize( - "hass_config", - [ - help_custom_config( - climate.DOMAIN, - DEFAULT_CONFIG, - ({"aux_command_topic": "aux-topic", "aux_state_topic": "aux-state"},), - ) - ], -) -async def test_set_aux_pessimistic( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test setting of the aux heating in pessimistic mode.""" - await mqtt_mock_entry() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("aux_heat") == "off" - - await common.async_set_aux_heat(hass, True, ENTITY_CLIMATE) - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("aux_heat") == "off" - - async_fire_mqtt_message(hass, "aux-state", "ON") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("aux_heat") == "on" - - async_fire_mqtt_message(hass, "aux-state", "OFF") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("aux_heat") == "off" - - async_fire_mqtt_message(hass, "aux-state", "nonsense") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("aux_heat") == "off" - - -# Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC -# and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 -# Support will be removed in HA Core 2024.3 -# "aux_command_topic": "aux-topic" -@pytest.mark.parametrize( - "hass_config", - [ - help_custom_config( - climate.DOMAIN, DEFAULT_CONFIG, ({"aux_command_topic": "aux-topic"},) - ) - ], -) -async def test_set_aux( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test setting of the aux heating.""" - mqtt_mock = await mqtt_mock_entry() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("aux_heat") == "off" - await common.async_set_aux_heat(hass, True, ENTITY_CLIMATE) - mqtt_mock.async_publish.assert_called_once_with("aux-topic", "ON", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("aux_heat") == "on" - - await common.async_set_aux_heat(hass, False, ENTITY_CLIMATE) - mqtt_mock.async_publish.assert_called_once_with("aux-topic", "OFF", 0, False) - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("aux_heat") == "off" - - support = ( - ClimateEntityFeature.TARGET_TEMPERATURE - | ClimateEntityFeature.AUX_HEAT - | ClimateEntityFeature.SWING_MODE - | ClimateEntityFeature.FAN_MODE - | ClimateEntityFeature.PRESET_MODE - | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE - | ClimateEntityFeature.TARGET_HUMIDITY - | ClimateEntityFeature.TURN_OFF - | ClimateEntityFeature.TURN_ON - ) - - assert state.attributes.get("supported_features") == support - - @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_availability_when_connection_lost( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator @@ -1476,7 +1390,6 @@ async def test_get_target_temperature_low_high_with_templates( "temperature_high_command_topic": "temperature-high-topic", "fan_mode_command_topic": "fan-mode-topic", "swing_mode_command_topic": "swing-mode-topic", - "aux_command_topic": "aux-topic", "preset_mode_command_topic": "preset-mode-topic", "preset_modes": [ "eco", @@ -1493,8 +1406,6 @@ async def test_get_target_temperature_low_high_with_templates( "current_humidity_template": "{{ value_json }}", "current_temperature_template": "{{ value_json }}", "temperature_state_template": "{{ value_json }}", - # Rendering to a bool for aux heat - "aux_state_template": "{{ value == 'switchmeon' }}", # Rendering preset_mode "preset_mode_value_template": "{{ value_json.attribute }}", "action_topic": "action", @@ -1503,7 +1414,6 @@ async def test_get_target_temperature_low_high_with_templates( "swing_mode_state_topic": "swing-state", "temperature_state_topic": "temperature-state", "target_humidity_state_topic": "humidity-state", - "aux_state_topic": "aux-state", "current_temperature_topic": "current-temperature", "current_humidity_topic": "current-humidity", "preset_mode_state_topic": "current-preset-mode", @@ -1590,21 +1500,6 @@ async def test_get_with_templates( state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "eco" - # Aux mode - - # Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC - # and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 - # Support will be removed in HA Core 2024.3 - assert state.attributes.get("aux_heat") == "off" - async_fire_mqtt_message(hass, "aux-state", "switchmeon") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("aux_heat") == "on" - - # anything other than 'switchmeon' should turn Aux mode off - async_fire_mqtt_message(hass, "aux-state", "somerandomstring") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("aux_heat") == "off" - # Current temperature async_fire_mqtt_message(hass, "current-temperature", '"74656"') state = hass.states.get(ENTITY_CLIMATE) @@ -1648,7 +1543,6 @@ async def test_get_with_templates( "temperature_high_command_topic": "temperature-high-topic", "fan_mode_command_topic": "fan-mode-topic", "swing_mode_command_topic": "swing-mode-topic", - "aux_command_topic": "aux-topic", "preset_mode_command_topic": "preset-mode-topic", "preset_modes": [ "eco", @@ -2104,7 +1998,6 @@ async def test_unique_id( [ ("action_topic", "heating", ATTR_HVAC_ACTION, "heating"), ("action_topic", "cooling", ATTR_HVAC_ACTION, "cooling"), - ("aux_state_topic", "ON", ATTR_AUX_HEAT, "on"), ("current_temperature_topic", "22.1", ATTR_CURRENT_TEMPERATURE, 22.1), ("current_humidity_topic", "60.4", ATTR_CURRENT_HUMIDITY, 60.4), ("fan_mode_state_topic", "low", ATTR_FAN_MODE, "low"), @@ -2386,13 +2279,6 @@ async def test_precision_whole( "on", "swing_mode_command_template", ), - ( - climate.SERVICE_SET_AUX_HEAT, - "aux_command_topic", - {"aux_heat": "on"}, - "ON", - None, - ), ( climate.SERVICE_SET_TEMPERATURE, "temperature_command_topic", From 6ca002a6f4d8abaf97bce3801ef95a42a2ac88e7 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 6 Feb 2024 09:38:15 +0100 Subject: [PATCH 0335/1367] Improve tests of mqtt device triggers (#108318) Improve tests on mqtt device triggers --- tests/components/mqtt/test_device_trigger.py | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index ade28ac2c1d..bb8d973b9ed 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -312,7 +312,7 @@ async def test_if_fires_on_mqtt_message( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' ' "payload": "long_press",' - ' "topic": "foobar/triggers/button1",' + ' "topic": "foobar/triggers/button2",' ' "type": "button_long_press",' ' "subtype": "button_2" }' ) @@ -346,8 +346,8 @@ async def test_if_fires_on_mqtt_message( "domain": DOMAIN, "device_id": device_entry.id, "discovery_id": "bla2", - "type": "button_1", - "subtype": "button_long_press", + "type": "button_long_press", + "subtype": "button_2", }, "action": { "service": "test.automation", @@ -365,7 +365,7 @@ async def test_if_fires_on_mqtt_message( assert calls[0].data["some"] == "short_press" # Fake long press. - async_fire_mqtt_message(hass, "foobar/triggers/button1", "long_press") + async_fire_mqtt_message(hass, "foobar/triggers/button2", "long_press") await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "long_press" @@ -569,7 +569,7 @@ async def test_if_fires_on_mqtt_message_template( calls: list[ServiceCall], mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: - """Test triggers firing.""" + """Test triggers firing with a message template and a shared topic.""" await mqtt_mock_entry() data1 = ( '{ "automation_type":"trigger",' @@ -619,8 +619,8 @@ async def test_if_fires_on_mqtt_message_template( "domain": DOMAIN, "device_id": device_entry.id, "discovery_id": "bla2", - "type": "button_1", - "subtype": "button_long_press", + "type": "button_long_press", + "subtype": "button_2", }, "action": { "service": "test.automation", @@ -669,7 +669,7 @@ async def test_if_fires_on_mqtt_message_late_discover( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' ' "payload": "long_press",' - ' "topic": "foobar/triggers/button1",' + ' "topic": "foobar/triggers/button2",' ' "type": "button_long_press",' ' "subtype": "button_2" }' ) @@ -702,8 +702,8 @@ async def test_if_fires_on_mqtt_message_late_discover( "domain": DOMAIN, "device_id": device_entry.id, "discovery_id": "bla2", - "type": "button_1", - "subtype": "button_long_press", + "type": "button_long_press", + "subtype": "button_2", }, "action": { "service": "test.automation", @@ -725,7 +725,7 @@ async def test_if_fires_on_mqtt_message_late_discover( assert calls[0].data["some"] == "short_press" # Fake long press. - async_fire_mqtt_message(hass, "foobar/triggers/button1", "long_press") + async_fire_mqtt_message(hass, "foobar/triggers/button2", "long_press") await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "long_press" From f8862f64a1fb50e01a4b39028eb857a9b5b92e9a Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:10:43 +0000 Subject: [PATCH 0336/1367] Bump ring_doorbell to 0.8.7 (#109783) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index a2ccb2bf444..0390db640e5 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/ring", "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], - "requirements": ["ring-doorbell[listen]==0.8.6"] + "requirements": ["ring-doorbell[listen]==0.8.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index efc645deb64..19fd86aae3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2429,7 +2429,7 @@ rfk101py==0.0.1 rflink==0.0.65 # homeassistant.components.ring -ring-doorbell[listen]==0.8.6 +ring-doorbell[listen]==0.8.7 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd5a09d9e28..004ac75a605 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1860,7 +1860,7 @@ reolink-aio==0.8.7 rflink==0.0.65 # homeassistant.components.ring -ring-doorbell[listen]==0.8.6 +ring-doorbell[listen]==0.8.7 # homeassistant.components.roku rokuecp==0.19.0 From d4c235622f7167340a7f95b593c2ff9fab835c50 Mon Sep 17 00:00:00 2001 From: tronikos Date: Tue, 6 Feb 2024 01:50:58 -0800 Subject: [PATCH 0337/1367] Break long strings in Google generative ai conversation (#109771) * Update test_init.py * Update __init__.py --- .../google_generative_ai_conversation/__init__.py | 4 +++- .../google_generative_ai_conversation/test_init.py | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/__init__.py b/homeassistant/components/google_generative_ai_conversation/__init__.py index a522eeab5cd..73450e9f5b9 100644 --- a/homeassistant/components/google_generative_ai_conversation/__init__.py +++ b/homeassistant/components/google_generative_ai_conversation/__init__.py @@ -63,7 +63,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: for image_filename in image_filenames: if not hass.config.is_allowed_path(image_filename): raise HomeAssistantError( - f"Cannot read `{image_filename}`, no access to path; `allowlist_external_dirs` may need to be adjusted in `configuration.yaml`" + f"Cannot read `{image_filename}`, no access to path; " + "`allowlist_external_dirs` may need to be adjusted in " + "`configuration.yaml`" ) if not Path(image_filename).exists(): raise HomeAssistantError(f"`{image_filename}` does not exist") diff --git a/tests/components/google_generative_ai_conversation/test_init.py b/tests/components/google_generative_ai_conversation/test_init.py index 380d5e82638..eee00fadfac 100644 --- a/tests/components/google_generative_ai_conversation/test_init.py +++ b/tests/components/google_generative_ai_conversation/test_init.py @@ -163,7 +163,7 @@ async def test_generate_content_service_without_images( """Test generate content service.""" stubbed_generated_content = ( "I'm thrilled to welcome you all to the release " - + "party for the latest version of Home Assistant!" + "party for the latest version of Home Assistant!" ) with patch("google.generativeai.GenerativeModel") as mock_model: @@ -257,7 +257,11 @@ async def test_generate_content_service_with_image_not_allowed_path( hass.config, "is_allowed_path", return_value=False ), pytest.raises( HomeAssistantError, - match="Cannot read `doorbell_snapshot.jpg`, no access to path; `allowlist_external_dirs` may need to be adjusted in `configuration.yaml`", + match=( + "Cannot read `doorbell_snapshot.jpg`, no access to path; " + "`allowlist_external_dirs` may need to be adjusted in " + "`configuration.yaml`" + ), ): await hass.services.async_call( "google_generative_ai_conversation", From b8c4821e486003457d5926e5ce79757c96841484 Mon Sep 17 00:00:00 2001 From: Matrix Date: Tue, 6 Feb 2024 17:52:41 +0800 Subject: [PATCH 0338/1367] Bump yolink-api to 0.3.7 (#109776) --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index 6fd62ce571c..aae5be3f9d3 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.3.6"] + "requirements": ["yolink-api==0.3.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 19fd86aae3d..792efebb350 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2889,7 +2889,7 @@ yeelight==0.7.14 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.3.6 +yolink-api==0.3.7 # homeassistant.components.youless youless-api==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 004ac75a605..01ff2927c17 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2212,7 +2212,7 @@ yalexs==1.10.0 yeelight==0.7.14 # homeassistant.components.yolink -yolink-api==0.3.6 +yolink-api==0.3.7 # homeassistant.components.youless youless-api==1.0.1 From 6701806ed26589786cccb0e1a7f8e1f45ac7a664 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:55:03 +0000 Subject: [PATCH 0339/1367] Use has_capability instead of hasattr for ring history (#109791) --- homeassistant/components/ring/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ring/coordinator.py b/homeassistant/components/ring/coordinator.py index 5b6412caffa..636d8eb069f 100644 --- a/homeassistant/components/ring/coordinator.py +++ b/homeassistant/components/ring/coordinator.py @@ -77,7 +77,7 @@ class RingDataCoordinator(DataUpdateCoordinator[dict[int, RingDeviceData]]): try: history_task = None async with TaskGroup() as tg: - if hasattr(device, "history"): + if device.has_capability("history"): history_task = tg.create_task( _call_api( self.hass, From 5de76c0be03eefef187b28afac719e2c02c7efde Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 6 Feb 2024 12:17:39 +0100 Subject: [PATCH 0340/1367] Include exception when reraising inside except (#109706) --- homeassistant/components/comelit/coordinator.py | 4 ++-- homeassistant/components/lupusec/config_flow.py | 4 ++-- homeassistant/components/roborock/__init__.py | 6 +++--- homeassistant/components/suez_water/__init__.py | 4 ++-- homeassistant/components/suez_water/config_flow.py | 4 ++-- homeassistant/components/zwave_js/api.py | 4 ++-- homeassistant/config_entries.py | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/comelit/coordinator.py b/homeassistant/components/comelit/coordinator.py index 4ff75ba5307..fe23cb1f5d3 100644 --- a/homeassistant/components/comelit/coordinator.py +++ b/homeassistant/components/comelit/coordinator.py @@ -83,8 +83,8 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[dict[str, Any]]): return await self._async_update_system_data() except (exceptions.CannotConnect, exceptions.CannotRetrieveData) as err: raise UpdateFailed(repr(err)) from err - except exceptions.CannotAuthenticate: - raise ConfigEntryAuthFailed + except exceptions.CannotAuthenticate as err: + raise ConfigEntryAuthFailed from err @abstractmethod async def _async_update_system_data(self) -> dict[str, Any]: diff --git a/homeassistant/components/lupusec/config_flow.py b/homeassistant/components/lupusec/config_flow.py index aad57897c91..1fae687cbdb 100644 --- a/homeassistant/components/lupusec/config_flow.py +++ b/homeassistant/components/lupusec/config_flow.py @@ -106,9 +106,9 @@ async def test_host_connection( try: await hass.async_add_executor_job(lupupy.Lupusec, username, password, host) - except lupupy.LupusecException: + except lupupy.LupusecException as ex: _LOGGER.error("Failed to connect to Lupusec device at %s", host) - raise CannotConnect + raise CannotConnect from ex class CannotConnect(HomeAssistantError): diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index 0b4dfa29e78..e0dfb2b271f 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -124,7 +124,7 @@ async def setup_device( coordinator.api.is_available = True try: await coordinator.async_config_entry_first_refresh() - except ConfigEntryNotReady: + except ConfigEntryNotReady as ex: if isinstance(coordinator.api, RoborockMqttClient): _LOGGER.warning( "Not setting up %s because the we failed to get data for the first time using the online client. " @@ -136,7 +136,7 @@ async def setup_device( # but in case if it isn't, the error can be included in debug logs for the user to grab. if coordinator.last_exception: _LOGGER.debug(coordinator.last_exception) - raise coordinator.last_exception + raise coordinator.last_exception from ex elif coordinator.last_exception: # If this is reached, we have verified that we can communicate with the Vacuum locally, # so if there is an error here - it is not a communication issue but some other problem @@ -147,7 +147,7 @@ async def setup_device( device.name, extra_error, ) - raise coordinator.last_exception + raise coordinator.last_exception from ex return coordinator diff --git a/homeassistant/components/suez_water/__init__.py b/homeassistant/components/suez_water/__init__.py index 66c3981705c..02d78dfee41 100644 --- a/homeassistant/components/suez_water/__init__.py +++ b/homeassistant/components/suez_water/__init__.py @@ -28,8 +28,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not client.check_credentials(): raise ConfigEntryError return client - except PySuezError: - raise ConfigEntryNotReady + except PySuezError as ex: + raise ConfigEntryNotReady from ex hass.data.setdefault(DOMAIN, {})[ entry.entry_id diff --git a/homeassistant/components/suez_water/config_flow.py b/homeassistant/components/suez_water/config_flow.py index ba288c90e34..d01b8035a0c 100644 --- a/homeassistant/components/suez_water/config_flow.py +++ b/homeassistant/components/suez_water/config_flow.py @@ -40,8 +40,8 @@ def validate_input(data: dict[str, Any]) -> None: ) if not client.check_credentials(): raise InvalidAuth - except PySuezError: - raise CannotConnect + except PySuezError as ex: + raise CannotConnect from ex class SuezWaterConfigFlow(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 8d14c8ed5b6..5aa27ada977 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -2202,8 +2202,8 @@ class FirmwareUploadView(HomeAssistantView): node = async_get_node_from_device_id(hass, device_id, self._dev_reg) except ValueError as err: if "not loaded" in err.args[0]: - raise web_exceptions.HTTPBadRequest - raise web_exceptions.HTTPNotFound + raise web_exceptions.HTTPBadRequest from err + raise web_exceptions.HTTPNotFound from err # If this was not true, we wouldn't have been able to get the node from the # device ID above diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b0a8f952b1b..4debdc8e495 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2249,7 +2249,7 @@ async def _load_integration( domain, err, ) - raise data_entry_flow.UnknownHandler + raise data_entry_flow.UnknownHandler from err async def _async_get_flow_handler( From 25f065a98076b9a70786c294fa057d097a6b67f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Tue, 6 Feb 2024 12:40:03 +0100 Subject: [PATCH 0341/1367] Add myuplink sensor descriptions for current and frequency sensors (#109784) Add device_descriptions for current and frequency sensors --- homeassistant/components/myuplink/sensor.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/myuplink/sensor.py b/homeassistant/components/myuplink/sensor.py index 31cb6715e0c..916cf723866 100644 --- a/homeassistant/components/myuplink/sensor.py +++ b/homeassistant/components/myuplink/sensor.py @@ -9,7 +9,11 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import UnitOfTemperature +from homeassistant.const import ( + UnitOfElectricCurrent, + UnitOfFrequency, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -25,6 +29,18 @@ DEVICE_POINT_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), + "A": SensorEntityDescription( + key="ampere", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + ), + "Hz": SensorEntityDescription( + key="hertz", + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfFrequency.HERTZ, + ), } From 1519df6e55a6ae23572036be1c5aa4e677e08a17 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Feb 2024 13:09:14 +0100 Subject: [PATCH 0342/1367] Improve typing of cloud HTTP API (#109780) --- homeassistant/components/cloud/http_api.py | 34 +++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index be3271a88a3..7c7b1328408 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -235,7 +235,7 @@ class CloudLogoutView(HomeAssistantView): async def post(self, request: web.Request) -> web.Response: """Handle logout request.""" hass = request.app["hass"] - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] async with asyncio.timeout(REQUEST_TIMEOUT): await cloud.logout() @@ -262,7 +262,7 @@ class CloudRegisterView(HomeAssistantView): async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle registration request.""" hass = request.app["hass"] - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] client_metadata = None @@ -299,7 +299,7 @@ class CloudResendConfirmView(HomeAssistantView): async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle resending confirm email code request.""" hass = request.app["hass"] - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] async with asyncio.timeout(REQUEST_TIMEOUT): await cloud.auth.async_resend_email_confirm(data["email"]) @@ -319,7 +319,7 @@ class CloudForgotPasswordView(HomeAssistantView): async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle forgot password request.""" hass = request.app["hass"] - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] async with asyncio.timeout(REQUEST_TIMEOUT): await cloud.auth.async_forgot_password(data["email"]) @@ -338,7 +338,7 @@ async def websocket_cloud_status( Async friendly. """ - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] connection.send_message( websocket_api.result_message(msg["id"], await _account_data(hass, cloud)) ) @@ -362,7 +362,7 @@ def _require_cloud_login( msg: dict[str, Any], ) -> None: """Require to be logged into the cloud.""" - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] if not cloud.is_logged_in: connection.send_message( websocket_api.error_message( @@ -385,7 +385,7 @@ async def websocket_subscription( msg: dict[str, Any], ) -> None: """Handle request for account info.""" - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] if (data := await async_subscription_info(cloud)) is None: connection.send_error( msg["id"], "request_failed", "Failed to request subscription" @@ -417,7 +417,7 @@ async def websocket_update_prefs( msg: dict[str, Any], ) -> None: """Handle request for account info.""" - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] changes = dict(msg) changes.pop("id") @@ -468,7 +468,7 @@ async def websocket_hook_create( msg: dict[str, Any], ) -> None: """Handle request for account info.""" - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] hook = await cloud.cloudhooks.async_create(msg["webhook_id"], False) connection.send_message(websocket_api.result_message(msg["id"], hook)) @@ -488,7 +488,7 @@ async def websocket_hook_delete( msg: dict[str, Any], ) -> None: """Handle request for account info.""" - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] await cloud.cloudhooks.async_delete(msg["webhook_id"]) connection.send_message(websocket_api.result_message(msg["id"])) @@ -557,7 +557,7 @@ async def websocket_remote_connect( msg: dict[str, Any], ) -> None: """Handle request for connect remote.""" - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] await cloud.client.prefs.async_update(remote_enabled=True) connection.send_result(msg["id"], await _account_data(hass, cloud)) @@ -573,7 +573,7 @@ async def websocket_remote_disconnect( msg: dict[str, Any], ) -> None: """Handle request for disconnect remote.""" - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] await cloud.client.prefs.async_update(remote_enabled=False) connection.send_result(msg["id"], await _account_data(hass, cloud)) @@ -594,7 +594,7 @@ async def google_assistant_get( msg: dict[str, Any], ) -> None: """Get data for a single google assistant entity.""" - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] gconf = await cloud.client.get_google_config() entity_id: str = msg["entity_id"] state = hass.states.get(entity_id) @@ -642,7 +642,7 @@ async def google_assistant_list( msg: dict[str, Any], ) -> None: """List all google assistant entities.""" - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] gconf = await cloud.client.get_google_config() entities = google_helpers.async_get_entities(hass, gconf) @@ -736,7 +736,7 @@ async def alexa_list( msg: dict[str, Any], ) -> None: """List all alexa entities.""" - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] alexa_config = await cloud.client.get_alexa_config() entities = alexa_entities.async_get_entities(hass, alexa_config) @@ -764,7 +764,7 @@ async def alexa_sync( msg: dict[str, Any], ) -> None: """Sync with Alexa.""" - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] alexa_config = await cloud.client.get_alexa_config() async with asyncio.timeout(10): @@ -794,7 +794,7 @@ async def thingtalk_convert( msg: dict[str, Any], ) -> None: """Convert a query.""" - cloud = hass.data[DOMAIN] + cloud: Cloud[CloudClient] = hass.data[DOMAIN] async with asyncio.timeout(10): try: From 198cf28a2aa3d0f06d4cd720fe01e8a353237dc3 Mon Sep 17 00:00:00 2001 From: Vilppu Vuorinen <8420095+vilppuvuorinen@users.noreply.github.com> Date: Tue, 6 Feb 2024 14:27:50 +0200 Subject: [PATCH 0343/1367] Update MELCloud codeowners (#109793) Co-authored-by: Franck Nijhof --- CODEOWNERS | 2 -- homeassistant/components/melcloud/manifest.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index af196548bb3..144883db68f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -786,8 +786,6 @@ build.json @home-assistant/supervisor /homeassistant/components/media_source/ @hunterjm /tests/components/media_source/ @hunterjm /homeassistant/components/mediaroom/ @dgomes -/homeassistant/components/melcloud/ @vilppuvuorinen -/tests/components/melcloud/ @vilppuvuorinen /homeassistant/components/melissa/ @kennedyshead /tests/components/melissa/ @kennedyshead /homeassistant/components/melnor/ @vanstinator diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index 8be40b22d9c..fde248467bf 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "melcloud", "name": "MELCloud", - "codeowners": ["@vilppuvuorinen"], + "codeowners": [], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/melcloud", "iot_class": "cloud_polling", From 78f21ecc589a4caae0d7f4fc69f967bd1932b0a3 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 6 Feb 2024 14:30:53 +0100 Subject: [PATCH 0344/1367] Update xknx to 2.12.0 and xknxproject to 3.5.0 (#109787) --- homeassistant/components/knx/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 4159a7a56a5..397af9ac181 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -11,8 +11,8 @@ "loggers": ["xknx", "xknxproject"], "quality_scale": "platinum", "requirements": [ - "xknx==2.11.2", - "xknxproject==3.4.0", + "xknx==2.12.0", + "xknxproject==3.5.0", "knx-frontend==2024.1.20.105944" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 792efebb350..5236ea920a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2856,10 +2856,10 @@ xbox-webapi==2.0.11 xiaomi-ble==0.25.2 # homeassistant.components.knx -xknx==2.11.2 +xknx==2.12.0 # homeassistant.components.knx -xknxproject==3.4.0 +xknxproject==3.5.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 01ff2927c17..3cd242e9530 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2185,10 +2185,10 @@ xbox-webapi==2.0.11 xiaomi-ble==0.25.2 # homeassistant.components.knx -xknx==2.11.2 +xknx==2.12.0 # homeassistant.components.knx -xknxproject==3.4.0 +xknxproject==3.5.0 # homeassistant.components.bluesound # homeassistant.components.fritz From 1e5ab3ad473a5a0d75cd07156954ec2923007e6b Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 6 Feb 2024 15:16:15 +0100 Subject: [PATCH 0345/1367] Bump aioelectricitymaps to 0.3.1 (#109797) --- homeassistant/components/co2signal/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json index a4cbed00684..4f22ee68910 100644 --- a/homeassistant/components/co2signal/manifest.json +++ b/homeassistant/components/co2signal/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aioelectricitymaps"], - "requirements": ["aioelectricitymaps==0.3.0"] + "requirements": ["aioelectricitymaps==0.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5236ea920a2..bfa7eda9702 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -233,7 +233,7 @@ aioeagle==1.1.0 aioecowitt==2023.5.0 # homeassistant.components.co2signal -aioelectricitymaps==0.3.0 +aioelectricitymaps==0.3.1 # homeassistant.components.emonitor aioemonitor==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3cd242e9530..3d055d8dd4c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioeagle==1.1.0 aioecowitt==2023.5.0 # homeassistant.components.co2signal -aioelectricitymaps==0.3.0 +aioelectricitymaps==0.3.1 # homeassistant.components.emonitor aioemonitor==1.0.5 From cf188eabdf1a67ed1c08a3d82f041d3a0bd90ed3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Feb 2024 08:40:03 -0600 Subject: [PATCH 0346/1367] Add slots to bluetooth matcher objects (#109768) --- homeassistant/components/bluetooth/match.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 453ab996abc..2fd650d9580 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -90,6 +90,8 @@ def seen_all_fields( class IntegrationMatcher: """Integration matcher for the bluetooth integration.""" + __slots__ = ("_integration_matchers", "_matched", "_matched_connectable", "_index") + def __init__(self, integration_matchers: list[BluetoothMatcher]) -> None: """Initialize the matcher.""" self._integration_matchers = integration_matchers @@ -159,6 +161,16 @@ class BluetoothMatcherIndexBase(Generic[_T]): any bucket and we can quickly reject the service info as not matching. """ + __slots__ = ( + "local_name", + "service_uuid", + "service_data_uuid", + "manufacturer_id", + "service_uuid_set", + "service_data_uuid_set", + "manufacturer_id_set", + ) + def __init__(self) -> None: """Initialize the matcher index.""" self.local_name: dict[str, list[_T]] = {} @@ -285,6 +297,8 @@ class BluetoothCallbackMatcherIndex( Supports matching on addresses. """ + __slots__ = ("address", "connectable") + def __init__(self) -> None: """Initialize the matcher index.""" super().__init__() From 0cb913370f3c006fb93481b03a5a83ad1e2fa43e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Feb 2024 15:40:12 +0100 Subject: [PATCH 0347/1367] Rename google_assistant.AbstractConfig.get_local_agent_user_id (#109798) * Rename google_assistant.AbstractConfig get_local_agent_user_id to get_local_user_id * Fix --- homeassistant/components/cloud/google_config.py | 8 ++++++-- .../components/google_assistant/helpers.py | 13 +++++++++---- tests/components/google_assistant/test_http.py | 4 ++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 10601bf4784..98dee500421 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -173,8 +173,12 @@ class CloudGoogleConfig(AbstractConfig): """Return the webhook ID to be used for actions for a given agent user id via the local SDK.""" return self._prefs.google_local_webhook_id - def get_local_agent_user_id(self, webhook_id: Any) -> str: - """Return the user ID to be used for actions received via the local SDK.""" + def get_local_user_id(self, webhook_id: Any) -> str: + """Map webhook ID to a Home Assistant user ID. + + Any action inititated by Google Assistant via the local SDK will be attributed + to the returned user ID. + """ return self._user @property diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 84cbdb6211e..7d431f8c94c 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -167,11 +167,16 @@ class AbstractConfig(ABC): and self._local_last_active > utcnow() - timedelta(seconds=70) ) - def get_local_agent_user_id(self, webhook_id): - """Return the user ID to be used for actions received via the local SDK. + def get_local_user_id(self, webhook_id): + """Map webhook ID to a Home Assistant user ID. - Return None is no agent user id is found. + Any action inititated by Google Assistant via the local SDK will be attributed + to the returned user ID. + + Return None if no user id is found for the webhook_id. """ + # Note: The manually setup Google Assistant currently returns the Google agent + # user ID instead of a valid Home Assistant user ID found_agent_user_id = None for agent_user_id, agent_user_data in self._store.agent_user_ids.items(): if agent_user_data[STORE_GOOGLE_LOCAL_WEBHOOK_ID] == webhook_id: @@ -423,7 +428,7 @@ class AbstractConfig(ABC): pprint.pformat(async_redact_request_msg(payload)), ) - if (agent_user_id := self.get_local_agent_user_id(webhook_id)) is None: + if (agent_user_id := self.get_local_user_id(webhook_id)) is None: # No agent user linked to this webhook, means that the user has somehow unregistered # removing webhook and stopping processing of this request. _LOGGER.error( diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index c6589555c3e..b3ec486e818 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -258,8 +258,8 @@ async def test_google_config_local_fulfillment( await hass.async_block_till_done() assert config.get_local_webhook_id(agent_user_id) == local_webhook_id - assert config.get_local_agent_user_id(local_webhook_id) == agent_user_id - assert config.get_local_agent_user_id("INCORRECT") is None + assert config.get_local_user_id(local_webhook_id) == agent_user_id + assert config.get_local_user_id("INCORRECT") is None async def test_secure_device_pin_config(hass: HomeAssistant) -> None: From a533fa222ef5a10f9e6f49ae4e1fa025e3b94b2c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:40:29 -0500 Subject: [PATCH 0348/1367] Bump ZHA dependencies (#109770) * Bump ZHA dependencies * Bump universal-silabs-flasher to 0.0.18 * Flip `Server_to_Client` enum in ZHA unit test * Bump zigpy to 0.62.2 --- homeassistant/components/zha/manifest.json | 10 +++++----- requirements_all.txt | 10 +++++----- requirements_test_all.txt | 10 +++++----- tests/components/zha/test_update.py | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 9e09e20819f..263b069bbe8 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,16 +21,16 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.37.6", + "bellows==0.38.0", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.110", - "zigpy-deconz==0.22.4", - "zigpy==0.61.0", + "zha-quirks==0.0.111", + "zigpy-deconz==0.23.0", + "zigpy==0.62.2", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", - "universal-silabs-flasher==0.0.15", + "universal-silabs-flasher==0.0.18", "pyserial-asyncio-fast==0.11" ], "usb": [ diff --git a/requirements_all.txt b/requirements_all.txt index bfa7eda9702..54c8c1a592d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -535,7 +535,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.37.6 +bellows==0.38.0 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -2757,7 +2757,7 @@ unifi_ap==0.0.1 unifiled==0.11 # homeassistant.components.zha -universal-silabs-flasher==0.0.15 +universal-silabs-flasher==0.0.18 # homeassistant.components.upb upb-lib==0.5.4 @@ -2913,7 +2913,7 @@ zeroconf==0.131.0 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.110 +zha-quirks==0.0.111 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.9 @@ -2922,7 +2922,7 @@ zhong-hong-hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.22.4 +zigpy-deconz==0.23.0 # homeassistant.components.zha zigpy-xbee==0.20.1 @@ -2934,7 +2934,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.61.0 +zigpy==0.62.2 # homeassistant.components.zoneminder zm-py==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d055d8dd4c..8ea94904496 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -457,7 +457,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.37.6 +bellows==0.38.0 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -2098,7 +2098,7 @@ ultraheat-api==0.5.7 unifi-discovery==1.1.7 # homeassistant.components.zha -universal-silabs-flasher==0.0.15 +universal-silabs-flasher==0.0.18 # homeassistant.components.upb upb-lib==0.5.4 @@ -2233,10 +2233,10 @@ zeroconf==0.131.0 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.110 +zha-quirks==0.0.111 # homeassistant.components.zha -zigpy-deconz==0.22.4 +zigpy-deconz==0.23.0 # homeassistant.components.zha zigpy-xbee==0.20.1 @@ -2248,7 +2248,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.61.0 +zigpy==0.62.2 # homeassistant.components.zwave_js zwave-js-server-python==0.55.3 diff --git a/tests/components/zha/test_update.py b/tests/components/zha/test_update.py index 981b8ba5e1b..894b5af9aba 100644 --- a/tests/components/zha/test_update.py +++ b/tests/components/zha/test_update.py @@ -205,7 +205,7 @@ def make_packet(zigpy_device, cluster, cmd_name: str, **kwargs): command_id=cluster.commands_by_name[cmd_name].id, schema=cluster.commands_by_name[cmd_name].schema, disable_default_response=False, - direction=foundation.Direction.Server_to_Client, + direction=foundation.Direction.Client_to_Server, args=(), kwargs=kwargs, ) From 545b888034966f45127477bed5a9cf2fa08a9730 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 6 Feb 2024 15:41:34 +0100 Subject: [PATCH 0349/1367] Update ruff to 0.2.1 (#109796) * Update ruff to 0.2.1 * Rename config sections * Update remapped error codes * Add ignores --- .pre-commit-config.yaml | 2 +- homeassistant/components/ring/coordinator.py | 2 +- homeassistant/components/sensor/recorder.py | 3 ++- pylint/ruff.toml | 4 ++-- pyproject.toml | 21 ++++++++++---------- requirements_test_pre_commit.txt | 2 +- script/ruff.toml | 4 ++-- tests/ruff.toml | 6 ++++-- 8 files changed, 23 insertions(+), 21 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc3be5c2391..4b96b5ee2aa 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.1.15 + rev: v0.2.1 hooks: - id: ruff args: diff --git a/homeassistant/components/ring/coordinator.py b/homeassistant/components/ring/coordinator.py index 636d8eb069f..943b1c628bf 100644 --- a/homeassistant/components/ring/coordinator.py +++ b/homeassistant/components/ring/coordinator.py @@ -96,7 +96,7 @@ class RingDataCoordinator(DataUpdateCoordinator[dict[int, RingDeviceData]]): if history_task: data[device.id].history = history_task.result() except ExceptionGroup as eg: - raise eg.exceptions[0] + raise eg.exceptions[0] # noqa: B904 return data diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index ebf138d39e6..a53ae906718 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -148,7 +148,8 @@ def _equivalent_units(units: set[str | None]) -> bool: if len(units) == 1: return True units = { - EQUIVALENT_UNITS[unit] if unit in EQUIVALENT_UNITS else unit for unit in units + EQUIVALENT_UNITS[unit] if unit in EQUIVALENT_UNITS else unit # noqa: SIM401 + for unit in units } return len(units) == 1 diff --git a/pylint/ruff.toml b/pylint/ruff.toml index 271881141a9..ebf53daa903 100644 --- a/pylint/ruff.toml +++ b/pylint/ruff.toml @@ -1,7 +1,7 @@ # This extend our general Ruff rules specifically for tests extend = "../pyproject.toml" -[isort] +[lint.isort] known-third-party = [ "pylint", -] \ No newline at end of file +] diff --git a/pyproject.toml b/pyproject.toml index 910670c6caa..be53e50da3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -221,7 +221,7 @@ disable = [ "duplicate-key", # F601 "duplicate-string-formatting-argument", # F "duplicate-value", # F - "eval-used", # PGH001 + "eval-used", # S307 "exec-used", # S102 # "expression-not-assigned", # B018, ruff catches new occurrences, needs more work "f-string-without-interpolation", # F541 @@ -241,7 +241,7 @@ disable = [ "named-expr-without-context", # PLW0131 "nested-min-max", # PLW3301 # "pointless-statement", # B018, ruff catches new occurrences, needs more work - "raise-missing-from", # TRY200 + "raise-missing-from", # B904 # "redefined-builtin", # A001, ruff is way more stricter, needs work "try-except-raise", # TRY302 "unused-argument", # ARG001, we don't use it @@ -569,13 +569,14 @@ filterwarnings = [ "ignore:pkg_resources is deprecated as an API:DeprecationWarning:webrtcvad", ] -[tool.ruff] +[tool.ruff.lint] select = [ "B002", # Python does not support the unary prefix increment "B007", # Loop control variable {name} not used within loop body "B014", # Exception handler with duplicate exception "B023", # Function definition does not bind loop variable {name} "B026", # Star-arg unpacking after a keyword argument is strongly discouraged + "B904", # Use raise from to specify exception cause "C", # complexity "COM818", # Trailing comma on bare tuple prohibited "D", # docstrings @@ -589,7 +590,6 @@ select = [ "N804", # First argument of a class method should be named cls "N805", # First argument of a method should be named self "N815", # Variable {name} in class scope should not be mixedCase - "PGH001", # No builtin eval() allowed "PGH004", # Use specific rule codes when using noqa "PLC0414", # Useless import alias. Import alias does not rename original package. "PLC", # pylint @@ -628,7 +628,6 @@ select = [ "T20", # flake8-print "TID251", # Banned imports "TRY004", # Prefer TypeError exception for invalid type - "TRY200", # Use raise from to specify exception cause "TRY302", # Remove exception handler; error is immediately re-raised "UP", # pyupgrade "W", # pycodestyle @@ -681,7 +680,7 @@ ignore = [ "PLE0605", ] -[tool.ruff.flake8-import-conventions.extend-aliases] +[tool.ruff.lint.flake8-import-conventions.extend-aliases] voluptuous = "vol" "homeassistant.helpers.area_registry" = "ar" "homeassistant.helpers.config_validation" = "cv" @@ -690,14 +689,14 @@ voluptuous = "vol" "homeassistant.helpers.issue_registry" = "ir" "homeassistant.util.dt" = "dt_util" -[tool.ruff.flake8-pytest-style] +[tool.ruff.lint.flake8-pytest-style] fixture-parentheses = false -[tool.ruff.flake8-tidy-imports.banned-api] +[tool.ruff.lint.flake8-tidy-imports.banned-api] "async_timeout".msg = "use asyncio.timeout instead" "pytz".msg = "use zoneinfo instead" -[tool.ruff.isort] +[tool.ruff.lint.isort] force-sort-within-sections = true known-first-party = [ "homeassistant", @@ -705,12 +704,12 @@ known-first-party = [ combine-as-imports = true split-on-trailing-comma = false -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] # Allow for main entry & scripts to write to stdout "homeassistant/__main__.py" = ["T201"] "homeassistant/scripts/*" = ["T201"] "script/*" = ["T20"] -[tool.ruff.mccabe] +[tool.ruff.lint.mccabe] max-complexity = 25 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index e12fefa0768..32d3f9e0c8b 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit codespell==2.2.2 -ruff==0.1.15 +ruff==0.2.1 yamllint==1.32.0 diff --git a/script/ruff.toml b/script/ruff.toml index 9d77bf60af9..c32b39022cc 100644 --- a/script/ruff.toml +++ b/script/ruff.toml @@ -1,7 +1,7 @@ # This extend our general Ruff rules specifically for tests extend = "../pyproject.toml" -[isort] +[lint.isort] forced-separate = [ "tests", -] \ No newline at end of file +] diff --git a/tests/ruff.toml b/tests/ruff.toml index 05971ce9a08..76e4feacdd2 100644 --- a/tests/ruff.toml +++ b/tests/ruff.toml @@ -1,6 +1,7 @@ # This extend our general Ruff rules specifically for tests extend = "../pyproject.toml" +[lint] extend-select = [ "PT001", # Use @pytest.fixture without parentheses "PT002", # Configuration for fixture specified via positional args, use kwargs @@ -17,10 +18,11 @@ extend-ignore = [ "PLE", # pylint "PLR", # pylint "PLW", # pylint + "B904", # Use raise from to specify exception cause "N815", # Variable {name} in class scope should not be mixedCase ] -[isort] +[lint.isort] known-first-party = [ "homeassistant", "tests", @@ -34,4 +36,4 @@ known-third-party = [ ] forced-separate = [ "tests", -] \ No newline at end of file +] From 6295f91a1fe95e22fe57f78b11503fedd1d02264 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 6 Feb 2024 10:55:51 -0500 Subject: [PATCH 0350/1367] Pin `chacha20poly1305-reuseable>=0.12.1` (#109807) * Pin `chacha20poly1305-reuseable` Prevents a runtime `assert isinstance(cipher, AESGCM)` error * Update `gen_requirements_all.py` as well --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9b404e623e9..a09c00b7192 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -188,3 +188,6 @@ dacite>=1.7.0 # Musle wheels for pandas 2.2.0 cannot be build for any architecture. pandas==2.1.4 + +# chacha20poly1305-reuseable==0.12.0 is incompatible with cryptography==42.0.x +chacha20poly1305-reuseable>=0.12.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 64d897b7ee7..3e61a266ae1 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -181,6 +181,9 @@ dacite>=1.7.0 # Musle wheels for pandas 2.2.0 cannot be build for any architecture. pandas==2.1.4 + +# chacha20poly1305-reuseable==0.12.0 is incompatible with cryptography==42.0.x +chacha20poly1305-reuseable>=0.12.1 """ GENERATED_MESSAGE = ( From 6519b24319c571580294ade48738123d35dc7b12 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 6 Feb 2024 17:12:15 +0100 Subject: [PATCH 0351/1367] Make bluetooth use naming from the entity description (#97401) * Make bluetooth use the translation from the entity description * Remove links to other platforms * Remove links to other platforms * Remove links to other platforms * Add test * Use is * Fix test * Update homeassistant/components/bluetooth/passive_update_processor.py Co-authored-by: J. Nick Koston --------- Co-authored-by: J. Nick Koston --- .../bluetooth/passive_update_processor.py | 3 +- .../test_passive_update_processor.py | 90 +++++++++++++++++-- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 43991672e81..a92a5317ba4 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -649,7 +649,8 @@ class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProce self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name if device_id is None: self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_BLUETOOTH, address)} - self._attr_name = processor.entity_names.get(entity_key) + if (name := processor.entity_names.get(entity_key)) is not None: + self._attr_name = name @property def available(self) -> bool: diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 5c7c4e39083..f773088fc1a 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -41,6 +41,7 @@ from homeassistant.config_entries import current_entry from homeassistant.const import UnitOfTemperature from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.typing import UNDEFINED from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -1263,14 +1264,8 @@ async def test_passive_bluetooth_entity_with_entity_platform( await hass.async_block_till_done() inject_bluetooth_service_info(hass, NO_DEVICES_BLUETOOTH_SERVICE_INFO_2) await hass.async_block_till_done() - assert ( - hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_temperature") - is not None - ) - assert ( - hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure") - is not None - ) + assert hass.states.get("test_domain.temperature") is not None + assert hass.states.get("test_domain.pressure") is not None cancel_coordinator() @@ -1765,3 +1760,82 @@ async def test_integration_multiple_entity_platforms_with_reload_and_restart( unregister_binary_sensor_processor() unregister_sensor_processor() await hass.async_stop() + + +NAMING_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("temperature", None): 14.5, + }, + entity_names={ + PassiveBluetoothEntityKey("temperature", None): None, + }, + entity_descriptions={ + PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( + key="temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + }, +) + + +async def test_naming( + hass: HomeAssistant, + mock_bleak_scanner_start: MagicMock, + mock_bluetooth_adapters: None, +) -> None: + """Test basic usage of the PassiveBluetoothProcessorCoordinator.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + @callback + def _mock_update_method( + service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, + ) + assert coordinator.available is False # no data yet + + sensor_processor = PassiveBluetoothDataProcessor( + lambda service_info: NAMING_PASSIVE_BLUETOOTH_DATA_UPDATE + ) + + coordinator.async_register_processor(sensor_processor) + cancel_coordinator = coordinator.async_start() + + sensor_processor.async_add_listener(MagicMock()) + + mock_add_sensor_entities = MagicMock() + + sensor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_sensor_entities, + ) + + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_sensor_entities.mock_calls) == 1 + + sensor_entities = [ + *mock_add_sensor_entities.mock_calls[0][1][0], + ] + + sensor_entity: PassiveBluetoothProcessorEntity = sensor_entities[0] + sensor_entity.hass = hass + assert sensor_entity.available is True + assert sensor_entity.name is UNDEFINED + assert sensor_entity.device_class is SensorDeviceClass.TEMPERATURE + assert sensor_entity.translation_key is None + + cancel_coordinator() From 7d24ae88e6b1cd68fae8b376ba7d1472637fad8d Mon Sep 17 00:00:00 2001 From: wittypluck Date: Tue, 6 Feb 2024 18:33:10 +0100 Subject: [PATCH 0352/1367] Mark Unifi bandwidth sensors as unavailable when client disconnects (#109812) * Set sensor as unavailable instead of resetting value to 0 on disconnect * Update unit test on unavailable bandwidth sensor --- homeassistant/components/unifi/sensor.py | 6 +++--- tests/components/unifi/test_sensor.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 28db9abb94f..a0cd3a7f1e7 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -430,10 +430,10 @@ class UnifiSensorEntity(UnifiEntity[HandlerT, ApiItemT], SensorEntity): def _make_disconnected(self, *_: core_Event) -> None: """No heart beat by device. - Reset sensor value to 0 when client device is disconnected + Set sensor as unavailable when client device is disconnected """ - if self._attr_native_value != 0: - self._attr_native_value = 0 + if self._attr_available: + self._attr_available = False self.async_write_ha_state() @callback diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 1a3c81ec4c4..9ebdd207b54 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -416,8 +416,8 @@ async def test_bandwidth_sensors( async_fire_time_changed(hass, new_time) await hass.async_block_till_done() - assert hass.states.get("sensor.wireless_client_rx").state == "0" - assert hass.states.get("sensor.wireless_client_tx").state == "0" + assert hass.states.get("sensor.wireless_client_rx").state == STATE_UNAVAILABLE + assert hass.states.get("sensor.wireless_client_tx").state == STATE_UNAVAILABLE # Disable option From fabcf2948e8177cb0550529251f53c51a4a12f25 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Feb 2024 19:02:32 +0100 Subject: [PATCH 0353/1367] Bump hass-nabucasa from 0.77.0 to 0.78.0 (#109813) --- homeassistant/components/cloud/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/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 22383980c3c..e816516fd7a 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -8,5 +8,5 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["hass_nabucasa"], - "requirements": ["hass-nabucasa==0.77.0"] + "requirements": ["hass-nabucasa==0.78.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a09c00b7192..bd279eebb2c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.1.0 habluetooth==2.4.0 -hass-nabucasa==0.77.0 +hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240205.0 diff --git a/requirements_all.txt b/requirements_all.txt index 54c8c1a592d..d2cf448d821 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1019,7 +1019,7 @@ habitipy==0.2.0 habluetooth==2.4.0 # homeassistant.components.cloud -hass-nabucasa==0.77.0 +hass-nabucasa==0.78.0 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ea94904496..b5b33fca09e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -827,7 +827,7 @@ habitipy==0.2.0 habluetooth==2.4.0 # homeassistant.components.cloud -hass-nabucasa==0.77.0 +hass-nabucasa==0.78.0 # homeassistant.components.conversation hassil==1.6.1 From 674e4ceb2c399df828ad0236c256b287250f6454 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Feb 2024 19:14:12 +0100 Subject: [PATCH 0354/1367] Make additional methods of google_assistant.AbstractConfig abstract (#109811) --- .../components/google_assistant/helpers.py | 26 +++++---------- .../components/google_assistant/http.py | 33 ++++++++++++++++--- tests/components/google_assistant/__init__.py | 6 ++-- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 7d431f8c94c..65782c9ec24 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -129,19 +129,19 @@ class AbstractConfig(ABC): self._on_deinitialize.pop()() @property + @abstractmethod def enabled(self): """Return if Google is enabled.""" - return False @property + @abstractmethod def entity_config(self): """Return entity config.""" - return {} @property + @abstractmethod def secure_devices_pin(self): """Return entity config.""" - return None @property def is_reporting_state(self): @@ -154,9 +154,9 @@ class AbstractConfig(ABC): return self._local_sdk_active @property + @abstractmethod def should_report_state(self): """Return if states should be proactively reported.""" - return False @property def is_local_connected(self) -> bool: @@ -167,6 +167,7 @@ class AbstractConfig(ABC): and self._local_last_active > utcnow() - timedelta(seconds=70) ) + @abstractmethod def get_local_user_id(self, webhook_id): """Map webhook ID to a Home Assistant user ID. @@ -175,21 +176,10 @@ class AbstractConfig(ABC): Return None if no user id is found for the webhook_id. """ - # Note: The manually setup Google Assistant currently returns the Google agent - # user ID instead of a valid Home Assistant user ID - found_agent_user_id = None - for agent_user_id, agent_user_data in self._store.agent_user_ids.items(): - if agent_user_data[STORE_GOOGLE_LOCAL_WEBHOOK_ID] == webhook_id: - found_agent_user_id = agent_user_id - break - - return found_agent_user_id + @abstractmethod def get_local_webhook_id(self, agent_user_id): """Return the webhook ID to be used for actions for a given agent user id via the local SDK.""" - if data := self._store.agent_user_ids.get(agent_user_id): - return data[STORE_GOOGLE_LOCAL_WEBHOOK_ID] - return None @abstractmethod def get_agent_user_id(self, context): @@ -199,15 +189,15 @@ class AbstractConfig(ABC): def should_expose(self, state) -> bool: """Return if entity should be exposed.""" + @abstractmethod def should_2fa(self, state): """If an entity should have 2FA checked.""" - return True + @abstractmethod async def async_report_state( self, message: dict[str, Any], agent_user_id: str, event_id: str | None = None ) -> HTTPStatus | None: """Send a state report to Google.""" - raise NotImplementedError async def async_report_state_all(self, message): """Send a state report to Google for all previously synced users.""" diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 226c37fb717..9207f917458 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -36,6 +36,7 @@ from .const import ( REPORT_STATE_BASE_URL, REQUEST_SYNC_BASE_URL, SOURCE_CLOUD, + STORE_GOOGLE_LOCAL_WEBHOOK_ID, ) from .helpers import AbstractConfig from .smart_home import async_handle_message @@ -110,6 +111,34 @@ class GoogleConfig(AbstractConfig): """Return if states should be proactively reported.""" return self._config.get(CONF_REPORT_STATE) + def get_local_user_id(self, webhook_id): + """Map webhook ID to a Home Assistant user ID. + + Any action inititated by Google Assistant via the local SDK will be attributed + to the returned user ID. + + Return None if no user id is found for the webhook_id. + """ + # Note: The manually setup Google Assistant currently returns the Google agent + # user ID instead of a valid Home Assistant user ID + found_agent_user_id = None + for agent_user_id, agent_user_data in self._store.agent_user_ids.items(): + if agent_user_data[STORE_GOOGLE_LOCAL_WEBHOOK_ID] == webhook_id: + found_agent_user_id = agent_user_id + break + + return found_agent_user_id + + def get_local_webhook_id(self, agent_user_id): + """Return the webhook ID to be used for actions for a given agent user id via the local SDK.""" + if data := self._store.agent_user_ids.get(agent_user_id): + return data[STORE_GOOGLE_LOCAL_WEBHOOK_ID] + return None + + def get_agent_user_id(self, context): + """Get agent user ID making request.""" + return context.user_id + def should_expose(self, state) -> bool: """Return if entity should be exposed.""" expose_by_default = self._config.get(CONF_EXPOSE_BY_DEFAULT) @@ -149,10 +178,6 @@ class GoogleConfig(AbstractConfig): return is_default_exposed or explicit_expose - def get_agent_user_id(self, context): - """Get agent user ID making request.""" - return context.user_id - def should_2fa(self, state): """If an entity should have 2FA checked.""" return True diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 931f4d25522..b7d329575c9 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -1,7 +1,7 @@ """Tests for the Google Assistant integration.""" from unittest.mock import MagicMock -from homeassistant.components.google_assistant import helpers +from homeassistant.components.google_assistant import helpers, http def mock_google_config_store(agent_user_ids=None): @@ -14,7 +14,7 @@ def mock_google_config_store(agent_user_ids=None): return store -class MockConfig(helpers.AbstractConfig): +class MockConfig(http.GoogleConfig): """Fake config that always exposes everything.""" def __init__( @@ -30,7 +30,7 @@ class MockConfig(helpers.AbstractConfig): should_report_state=False, ): """Initialize config.""" - super().__init__(hass) + helpers.AbstractConfig.__init__(self, hass) self._enabled = enabled self._entity_config = entity_config or {} self._secure_devices_pin = secure_devices_pin From 09c609459d4a1fb3246042de59a955017ba0db05 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Feb 2024 12:41:57 -0600 Subject: [PATCH 0355/1367] Fix entity services targeting entities outside the platform when using areas/devices (#109810) --- homeassistant/helpers/entity_platform.py | 26 +++++++- tests/helpers/test_entity_platform.py | 82 ++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 9904883c069..9b05e4939ba 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -57,6 +57,7 @@ SLOW_ADD_MIN_TIMEOUT = 500 PLATFORM_NOT_READY_RETRIES = 10 DATA_ENTITY_PLATFORM = "entity_platform" DATA_DOMAIN_ENTITIES = "domain_entities" +DATA_DOMAIN_PLATFORM_ENTITIES = "domain_platform_entities" PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds _LOGGER = getLogger(__name__) @@ -124,6 +125,8 @@ class EntityPlatform: self.scan_interval = scan_interval self.entity_namespace = entity_namespace self.config_entry: config_entries.ConfigEntry | None = None + # Storage for entities for this specific platform only + # which are indexed by entity_id self.entities: dict[str, Entity] = {} self.component_translations: dict[str, Any] = {} self.platform_translations: dict[str, Any] = {} @@ -145,9 +148,24 @@ class EntityPlatform: # which powers entity_component.add_entities self.parallel_updates_created = platform is None - self.domain_entities: dict[str, Entity] = hass.data.setdefault( + # Storage for entities indexed by domain + # with the child dict indexed by entity_id + # + # This is usually media_player, light, switch, etc. + domain_entities: dict[str, dict[str, Entity]] = hass.data.setdefault( DATA_DOMAIN_ENTITIES, {} - ).setdefault(domain, {}) + ) + self.domain_entities = domain_entities.setdefault(domain, {}) + + # Storage for entities indexed by domain and platform + # with the child dict indexed by entity_id + # + # This is usually media_player.yamaha, light.hue, switch.tplink, etc. + domain_platform_entities: dict[ + tuple[str, str], dict[str, Entity] + ] = hass.data.setdefault(DATA_DOMAIN_PLATFORM_ENTITIES, {}) + key = (domain, platform_name) + self.domain_platform_entities = domain_platform_entities.setdefault(key, {}) def __repr__(self) -> str: """Represent an EntityPlatform.""" @@ -743,6 +761,7 @@ class EntityPlatform: entity_id = entity.entity_id self.entities[entity_id] = entity self.domain_entities[entity_id] = entity + self.domain_platform_entities[entity_id] = entity if not restored: # Reserve the state in the state machine @@ -756,6 +775,7 @@ class EntityPlatform: """Remove entity from entities dict.""" self.entities.pop(entity_id) self.domain_entities.pop(entity_id) + self.domain_platform_entities.pop(entity_id) entity.async_on_remove(remove_entity_cb) @@ -852,7 +872,7 @@ class EntityPlatform: partial( service.entity_service_call, self.hass, - self.domain_entities, + self.domain_platform_entities, service_func, required_features=required_features, ), diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 01558c426c7..f16b5c16b5a 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -19,6 +19,7 @@ from homeassistant.core import ( ) from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import ( + area_registry as ar, device_registry as dr, entity_platform, entity_registry as er, @@ -1628,6 +1629,87 @@ async def test_register_entity_service_response_data_multiple_matches_raises( ) +async def test_register_entity_service_limited_to_matching_platforms( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + area_registry: ar.AreaRegistry, +) -> None: + """Test an entity services only targets entities for the platform and domain.""" + + mock_area = area_registry.async_get_or_create("mock_area") + + entity1_entry = entity_registry.async_get_or_create( + "base_platform", "mock_platform", "1234", suggested_object_id="entity1" + ) + entity_registry.async_update_entity(entity1_entry.entity_id, area_id=mock_area.id) + entity2_entry = entity_registry.async_get_or_create( + "base_platform", "mock_platform", "5678", suggested_object_id="entity2" + ) + entity_registry.async_update_entity(entity2_entry.entity_id, area_id=mock_area.id) + entity3_entry = entity_registry.async_get_or_create( + "base_platform", "other_mock_platform", "7891", suggested_object_id="entity3" + ) + entity_registry.async_update_entity(entity3_entry.entity_id, area_id=mock_area.id) + entity4_entry = entity_registry.async_get_or_create( + "base_platform", "other_mock_platform", "1433", suggested_object_id="entity4" + ) + entity_registry.async_update_entity(entity4_entry.entity_id, area_id=mock_area.id) + + async def generate_response( + target: MockEntity, call: ServiceCall + ) -> ServiceResponse: + assert call.return_response + return {"response-key": f"response-value-{target.entity_id}"} + + entity_platform = MockEntityPlatform( + hass, domain="base_platform", platform_name="mock_platform", platform=None + ) + entity1 = MockEntity( + entity_id=entity1_entry.entity_id, unique_id=entity1_entry.unique_id + ) + entity2 = MockEntity( + entity_id=entity2_entry.entity_id, unique_id=entity2_entry.unique_id + ) + await entity_platform.async_add_entities([entity1, entity2]) + + other_entity_platform = MockEntityPlatform( + hass, domain="base_platform", platform_name="other_mock_platform", platform=None + ) + entity3 = MockEntity( + entity_id=entity3_entry.entity_id, unique_id=entity3_entry.unique_id + ) + entity4 = MockEntity( + entity_id=entity4_entry.entity_id, unique_id=entity4_entry.unique_id + ) + await other_entity_platform.async_add_entities([entity3, entity4]) + + entity_platform.async_register_entity_service( + "hello", + {"some": str}, + generate_response, + supports_response=SupportsResponse.ONLY, + ) + + response_data = await hass.services.async_call( + "mock_platform", + "hello", + service_data={"some": "data"}, + target={"area_id": [mock_area.id]}, + blocking=True, + return_response=True, + ) + # We should not target entity3 and entity4 even though they are in the area + # because they are only part of the domain and not the platform + assert response_data == { + "base_platform.entity1": { + "response-key": "response-value-base_platform.entity1" + }, + "base_platform.entity2": { + "response-key": "response-value-base_platform.entity2" + }, + } + + async def test_invalid_entity_id(hass: HomeAssistant) -> None: """Test specifying an invalid entity id.""" platform = MockEntityPlatform(hass) From b5f049b84cf1400ff2da9ece5db476f1c4e50833 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Feb 2024 20:14:45 +0100 Subject: [PATCH 0356/1367] Bump python-otbr-api to 2.6.0 (#109823) --- 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 cf6aba33e80..ca0faa160f0 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.5.0"] + "requirements": ["python-otbr-api==2.6.0"] } diff --git a/homeassistant/components/thread/manifest.json b/homeassistant/components/thread/manifest.json index eeac24a626f..65d4c9d044c 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.5.0", "pyroute2==0.7.5"], + "requirements": ["python-otbr-api==2.6.0", "pyroute2==0.7.5"], "zeroconf": ["_meshcop._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index d2cf448d821..8b48e3f64a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2257,7 +2257,7 @@ python-opensky==1.0.0 # homeassistant.components.otbr # homeassistant.components.thread -python-otbr-api==2.5.0 +python-otbr-api==2.6.0 # homeassistant.components.picnic python-picnic-api==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5b33fca09e..e269baa9bc4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1727,7 +1727,7 @@ python-opensky==1.0.0 # homeassistant.components.otbr # homeassistant.components.thread -python-otbr-api==2.5.0 +python-otbr-api==2.6.0 # homeassistant.components.picnic python-picnic-api==1.1.0 From 88086dfa0cc8f045b46b2d7660ceb428075451da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Feb 2024 14:01:10 -0600 Subject: [PATCH 0357/1367] Bump aioesphomeapi to 21.0.2 (#109824) --- 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 e3437e5aa73..35b8e91f12b 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "requirements": [ - "aioesphomeapi==21.0.1", + "aioesphomeapi==21.0.2", "esphome-dashboard-api==1.2.3", "bleak-esphome==0.4.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index 8b48e3f64a4..1a82dcbe70e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -239,7 +239,7 @@ aioelectricitymaps==0.3.1 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==21.0.1 +aioesphomeapi==21.0.2 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e269baa9bc4..4295340f2f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -218,7 +218,7 @@ aioelectricitymaps==0.3.1 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==21.0.1 +aioesphomeapi==21.0.2 # homeassistant.components.flo aioflo==2021.11.0 From 7c233c2bd097a5cda6f4b89eb6a68c665c9e0b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 6 Feb 2024 21:57:00 +0100 Subject: [PATCH 0358/1367] Bump awesomeversion from 23.11.0 to 24.2.0 (#109830) --- 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 bd279eebb2c..63f424b5116 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -9,7 +9,7 @@ astral==2.2 async-upnp-client==0.38.1 atomicwrites-homeassistant==1.4.1 attrs==23.2.0 -awesomeversion==23.11.0 +awesomeversion==24.2.0 bcrypt==4.0.1 bleak-retry-connector==3.4.0 bleak==0.21.1 diff --git a/pyproject.toml b/pyproject.toml index be53e50da3c..a404669de91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "astral==2.2", "attrs==23.2.0", "atomicwrites-homeassistant==1.4.1", - "awesomeversion==23.11.0", + "awesomeversion==24.2.0", "bcrypt==4.0.1", "certifi>=2021.5.30", "ciso8601==2.3.0", diff --git a/requirements.txt b/requirements.txt index 44c11281517..63ea582eba8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ aiohttp-zlib-ng==0.3.1 astral==2.2 attrs==23.2.0 atomicwrites-homeassistant==1.4.1 -awesomeversion==23.11.0 +awesomeversion==24.2.0 bcrypt==4.0.1 certifi>=2021.5.30 ciso8601==2.3.0 From 3ec0ea3ef9584224c249aae80221eb1b35726e56 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 6 Feb 2024 22:06:59 +0100 Subject: [PATCH 0359/1367] Update nibe to 2.8.0 with LOG.SET fixes (#109825) Update nibe to 2.8.0 --- homeassistant/components/nibe_heatpump/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nibe_heatpump/manifest.json b/homeassistant/components/nibe_heatpump/manifest.json index c5c94145e4b..970f53837ea 100644 --- a/homeassistant/components/nibe_heatpump/manifest.json +++ b/homeassistant/components/nibe_heatpump/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nibe_heatpump", "iot_class": "local_polling", - "requirements": ["nibe==2.7.0"] + "requirements": ["nibe==2.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1a82dcbe70e..8f4e6d3432f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1367,7 +1367,7 @@ nextcord==2.0.0a8 nextdns==2.1.0 # homeassistant.components.nibe_heatpump -nibe==2.7.0 +nibe==2.8.0 # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4295340f2f2..26f4a4ef524 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1094,7 +1094,7 @@ nextcord==2.0.0a8 nextdns==2.1.0 # homeassistant.components.nibe_heatpump -nibe==2.7.0 +nibe==2.8.0 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 From db16b739a6089ffeabaf512f29e4b155374b8943 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Feb 2024 22:34:53 +0100 Subject: [PATCH 0360/1367] Don't block Supervisor entry setup with refreshing updates (#109809) --- homeassistant/components/hassio/__init__.py | 8 ++- homeassistant/components/hassio/handler.py | 2 +- tests/components/hassio/test_init.py | 60 +++++++++++---------- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 87860644754..1472843e14d 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -1001,12 +1001,18 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=has raise_on_entry_error: bool = False, ) -> None: """Refresh data.""" - if not scheduled: + if not scheduled and not raise_on_auth_failed: # Force refreshing updates for non-scheduled updates + # If `raise_on_auth_failed` is set, it means this is + # the first refresh and we do not want to delay + # startup or cause a timeout so we only refresh the + # updates if this is not a scheduled refresh and + # we are not doing the first refresh. try: await self.hassio.refresh_updates() except HassioAPIError as err: _LOGGER.warning("Error on Supervisor API: %s", err) + await super()._async_refresh( log_failures, raise_on_auth_failed, scheduled, raise_on_entry_error ) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index f335c3dc488..37b4b0ded9c 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -459,7 +459,7 @@ class HassIO: This method returns a coroutine. """ - return self.send_command("/refresh_updates", timeout=None) + return self.send_command("/refresh_updates", timeout=300) @api_data def retrieve_discovery_messages(self) -> Coroutine: diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 4bf3e29154e..fe8eeb0b0f6 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -245,7 +245,7 @@ async def test_setup_api_ping( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 20 + assert aioclient_mock.call_count == 19 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -290,7 +290,7 @@ async def test_setup_api_push_api_data( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 20 + assert aioclient_mock.call_count == 19 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert aioclient_mock.mock_calls[1][2]["watchdog"] @@ -309,7 +309,7 @@ async def test_setup_api_push_api_data_server_host( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 20 + assert aioclient_mock.call_count == 19 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert not aioclient_mock.mock_calls[1][2]["watchdog"] @@ -326,7 +326,7 @@ async def test_setup_api_push_api_data_default( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 20 + assert aioclient_mock.call_count == 19 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"] @@ -406,7 +406,7 @@ async def test_setup_api_existing_hassio_user( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 20 + assert aioclient_mock.call_count == 19 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token @@ -423,7 +423,7 @@ async def test_setup_core_push_timezone( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 20 + assert aioclient_mock.call_count == 19 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -443,7 +443,7 @@ async def test_setup_hassio_no_additional_data( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 20 + assert aioclient_mock.call_count == 19 assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456" @@ -525,14 +525,14 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 24 + assert aioclient_mock.call_count == 23 assert aioclient_mock.mock_calls[-1][2] == "test" await hass.services.async_call("hassio", "host_shutdown", {}) await hass.services.async_call("hassio", "host_reboot", {}) await hass.async_block_till_done() - assert aioclient_mock.call_count == 26 + assert aioclient_mock.call_count == 25 await hass.services.async_call("hassio", "backup_full", {}) await hass.services.async_call( @@ -547,7 +547,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 28 + assert aioclient_mock.call_count == 27 assert aioclient_mock.mock_calls[-1][2] == { "name": "2021-11-13 03:48:00", "homeassistant": True, @@ -572,7 +572,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 30 + assert aioclient_mock.call_count == 29 assert aioclient_mock.mock_calls[-1][2] == { "addons": ["test"], "folders": ["ssl"], @@ -591,7 +591,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 31 + assert aioclient_mock.call_count == 30 assert aioclient_mock.mock_calls[-1][2] == { "name": "backup_name", "location": "backup_share", @@ -607,7 +607,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 32 + assert aioclient_mock.call_count == 31 assert aioclient_mock.mock_calls[-1][2] == { "name": "2021-11-13 03:48:00", "location": None, @@ -625,7 +625,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 34 + assert aioclient_mock.call_count == 33 assert aioclient_mock.mock_calls[-1][2] == { "name": "2021-11-13 11:48:00", "location": None, @@ -702,12 +702,12 @@ async def test_service_calls_core( await hass.services.async_call("homeassistant", "stop") await hass.async_block_till_done() - assert aioclient_mock.call_count == 6 + assert aioclient_mock.call_count == 5 await hass.services.async_call("homeassistant", "check_config") await hass.async_block_till_done() - assert aioclient_mock.call_count == 6 + assert aioclient_mock.call_count == 5 with patch( "homeassistant.config.async_check_ha_config_file", return_value=None @@ -716,7 +716,7 @@ async def test_service_calls_core( await hass.async_block_till_done() assert mock_check_config.called - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 6 async def test_entry_load_and_unload(hass: HomeAssistant) -> None: @@ -897,14 +897,17 @@ async def test_coordinator_updates( config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - # Initial refresh without stats - assert refresh_updates_mock.call_count == 1 + + # Initial refresh, no update refresh call + assert refresh_updates_mock.call_count == 0 with patch( "homeassistant.components.hassio.HassIO.refresh_updates", ) as refresh_updates_mock: async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20)) await hass.async_block_till_done() + + # Scheduled refresh, no update refresh call assert refresh_updates_mock.call_count == 0 with patch( @@ -921,13 +924,14 @@ async def test_coordinator_updates( }, blocking=True, ) - assert refresh_updates_mock.call_count == 0 - # There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer - async_fire_time_changed( - hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY) - ) - await hass.async_block_till_done() + # There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer + assert refresh_updates_mock.call_count == 0 + async_fire_time_changed( + hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY) + ) + await hass.async_block_till_done() + assert refresh_updates_mock.call_count == 1 with patch( "homeassistant.components.hassio.HassIO.refresh_updates", @@ -968,14 +972,14 @@ async def test_coordinator_updates_stats_entities_enabled( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() # Initial refresh without stats - assert refresh_updates_mock.call_count == 1 + assert refresh_updates_mock.call_count == 0 # Refresh with stats once we know which ones are needed async_fire_time_changed( hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY) ) await hass.async_block_till_done() - assert refresh_updates_mock.call_count == 2 + assert refresh_updates_mock.call_count == 1 with patch( "homeassistant.components.hassio.HassIO.refresh_updates", @@ -1059,7 +1063,7 @@ async def test_setup_hardware_integration( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 20 + assert aioclient_mock.call_count == 19 assert len(mock_setup_entry.mock_calls) == 1 From a6f0b6a005d2d8f8c4ed4fd0969413d8a48e3c59 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 6 Feb 2024 22:36:12 +0100 Subject: [PATCH 0361/1367] Ignore `trackable` without `details` in Tractive integration (#109814) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/tractive/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 8dd0ed8e91b..38080fffe6e 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -129,6 +129,13 @@ async def _generate_trackables( if not trackable["device_id"]: return None + if "details" not in trackable: + _LOGGER.info( + "Tracker %s has no details and will be skipped. This happens for shared trackers", + trackable["device_id"], + ) + return None + tracker = client.tracker(trackable["device_id"]) tracker_details, hw_info, pos_report = await asyncio.gather( From 252baa93aa2b722eb4abab29390ae2fbd87e6137 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 6 Feb 2024 22:37:20 +0100 Subject: [PATCH 0362/1367] Bump aioecowitt to 2024.2.0 (#109817) --- homeassistant/components/ecowitt/entity.py | 4 ++-- homeassistant/components/ecowitt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ecowitt/entity.py b/homeassistant/components/ecowitt/entity.py index a5d769e6749..cf62cfb2d94 100644 --- a/homeassistant/components/ecowitt/entity.py +++ b/homeassistant/components/ecowitt/entity.py @@ -38,8 +38,8 @@ class EcowittEntity(Entity): """Update the state on callback.""" self.async_write_ha_state() - self.ecowitt.update_cb.append(_update_state) # type: ignore[arg-type] # upstream bug - self.async_on_remove(lambda: self.ecowitt.update_cb.remove(_update_state)) # type: ignore[arg-type] # upstream bug + self.ecowitt.update_cb.append(_update_state) + self.async_on_remove(lambda: self.ecowitt.update_cb.remove(_update_state)) @property def available(self) -> bool: diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json index 9f0f668ee81..d3dfe0331ef 100644 --- a/homeassistant/components/ecowitt/manifest.json +++ b/homeassistant/components/ecowitt/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["webhook"], "documentation": "https://www.home-assistant.io/integrations/ecowitt", "iot_class": "local_push", - "requirements": ["aioecowitt==2023.5.0"] + "requirements": ["aioecowitt==2024.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8f4e6d3432f..fd50a9324fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -230,7 +230,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2023.5.0 +aioecowitt==2024.2.0 # homeassistant.components.co2signal aioelectricitymaps==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26f4a4ef524..980cf84eba0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -209,7 +209,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2023.5.0 +aioecowitt==2024.2.0 # homeassistant.components.co2signal aioelectricitymaps==0.3.1 From fd5efd1f7929aad58e5469f80dbfab432b34b6a3 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 6 Feb 2024 22:59:55 +0100 Subject: [PATCH 0363/1367] Add transition support to Matter light platform (#109803) * Add support for transitions to Matter light platform * fix the feature check * add tests --- homeassistant/components/matter/light.py | 43 +++++--- tests/components/matter/test_light.py | 123 ++++++++++++++++++++++- 2 files changed, 146 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 7e6f42f44b4..aa93cef9916 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -10,10 +10,12 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_TRANSITION, ATTR_XY_COLOR, ColorMode, LightEntity, LightEntityDescription, + LightEntityFeature, filter_supported_color_modes, ) from homeassistant.config_entries import ConfigEntry @@ -38,6 +40,7 @@ COLOR_MODE_MAP = { clusters.ColorControl.Enums.ColorMode.kCurrentXAndCurrentY: ColorMode.XY, clusters.ColorControl.Enums.ColorMode.kColorTemperature: ColorMode.COLOR_TEMP, } +DEFAULT_TRANSITION = 0.2 async def async_setup_entry( @@ -58,7 +61,9 @@ class MatterLight(MatterEntity, LightEntity): _supports_color = False _supports_color_temperature = False - async def _set_xy_color(self, xy_color: tuple[float, float]) -> None: + async def _set_xy_color( + self, xy_color: tuple[float, float], transition: float = 0.0 + ) -> None: """Set xy color.""" matter_xy = convert_to_matter_xy(xy_color) @@ -67,8 +72,8 @@ class MatterLight(MatterEntity, LightEntity): clusters.ColorControl.Commands.MoveToColor( colorX=int(matter_xy[0]), colorY=int(matter_xy[1]), - # It's required in TLV. We don't implement transition time yet. - transitionTime=0, + # transition in matter is measured in tenths of a second + transitionTime=int(transition * 10), # allow setting the color while the light is off, # by setting the optionsMask to 1 (=ExecuteIfOff) optionsMask=1, @@ -76,7 +81,9 @@ class MatterLight(MatterEntity, LightEntity): ) ) - async def _set_hs_color(self, hs_color: tuple[float, float]) -> None: + async def _set_hs_color( + self, hs_color: tuple[float, float], transition: float = 0.0 + ) -> None: """Set hs color.""" matter_hs = convert_to_matter_hs(hs_color) @@ -85,8 +92,8 @@ class MatterLight(MatterEntity, LightEntity): clusters.ColorControl.Commands.MoveToHueAndSaturation( hue=int(matter_hs[0]), saturation=int(matter_hs[1]), - # It's required in TLV. We don't implement transition time yet. - transitionTime=0, + # transition in matter is measured in tenths of a second + transitionTime=int(transition * 10), # allow setting the color while the light is off, # by setting the optionsMask to 1 (=ExecuteIfOff) optionsMask=1, @@ -94,14 +101,14 @@ class MatterLight(MatterEntity, LightEntity): ) ) - async def _set_color_temp(self, color_temp: int) -> None: + async def _set_color_temp(self, color_temp: int, transition: float = 0.0) -> None: """Set color temperature.""" await self.send_device_command( clusters.ColorControl.Commands.MoveToColorTemperature( colorTemperatureMireds=color_temp, - # It's required in TLV. We don't implement transition time yet. - transitionTime=0, + # transition in matter is measured in tenths of a second + transitionTime=int(transition * 10), # allow setting the color while the light is off, # by setting the optionsMask to 1 (=ExecuteIfOff) optionsMask=1, @@ -109,7 +116,7 @@ class MatterLight(MatterEntity, LightEntity): ) ) - async def _set_brightness(self, brightness: int) -> None: + async def _set_brightness(self, brightness: int, transition: float = 0.0) -> None: """Set brightness.""" level_control = self._endpoint.get_cluster(clusters.LevelControl) @@ -127,8 +134,8 @@ class MatterLight(MatterEntity, LightEntity): await self.send_device_command( clusters.LevelControl.Commands.MoveToLevelWithOnOff( level=level, - # It's required in TLV. We don't implement transition time yet. - transitionTime=0, + # transition in matter is measured in tenths of a second + transitionTime=int(transition * 10), ) ) @@ -251,20 +258,21 @@ class MatterLight(MatterEntity, LightEntity): xy_color = kwargs.get(ATTR_XY_COLOR) color_temp = kwargs.get(ATTR_COLOR_TEMP) brightness = kwargs.get(ATTR_BRIGHTNESS) + transition = kwargs.get(ATTR_TRANSITION, DEFAULT_TRANSITION) if self.supported_color_modes is not None: if hs_color is not None and ColorMode.HS in self.supported_color_modes: - await self._set_hs_color(hs_color) + await self._set_hs_color(hs_color, transition) elif xy_color is not None and ColorMode.XY in self.supported_color_modes: - await self._set_xy_color(xy_color) + await self._set_xy_color(xy_color, transition) elif ( color_temp is not None and ColorMode.COLOR_TEMP in self.supported_color_modes ): - await self._set_color_temp(color_temp) + await self._set_color_temp(color_temp, transition) if brightness is not None and self._supports_brightness: - await self._set_brightness(brightness) + await self._set_brightness(brightness, transition) return await self.send_device_command( @@ -324,6 +332,9 @@ class MatterLight(MatterEntity, LightEntity): supported_color_modes = filter_supported_color_modes(supported_color_modes) self._attr_supported_color_modes = supported_color_modes + # flag support for transition as soon as we support setting brightness and/or color + if supported_color_modes != {ColorMode.ONOFF}: + self._attr_supported_features |= LightEntityFeature.TRANSITION LOGGER.debug( "Supported color modes: %s for %s", diff --git a/tests/components/matter/test_light.py b/tests/components/matter/test_light.py index fb988d26a1c..0376a902f32 100644 --- a/tests/components/matter/test_light.py +++ b/tests/components/matter/test_light.py @@ -142,7 +142,26 @@ async def test_dimmable_light( endpoint_id=1, command=clusters.LevelControl.Commands.MoveToLevelWithOnOff( level=128, - transitionTime=0, + transitionTime=2, + ), + ) + matter_client.send_device_command.reset_mock() + + # Change brightness with custom transition + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, "brightness": 128, "transition": 3}, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=light_node.node_id, + endpoint_id=1, + command=clusters.LevelControl.Commands.MoveToLevelWithOnOff( + level=128, + transitionTime=30, ), ) matter_client.send_device_command.reset_mock() @@ -201,7 +220,37 @@ async def test_color_temperature_light( endpoint_id=1, command=clusters.ColorControl.Commands.MoveToColorTemperature( colorTemperatureMireds=300, - transitionTime=0, + transitionTime=2, + optionsMask=1, + optionsOverride=1, + ), + ), + call( + node_id=light_node.node_id, + endpoint_id=1, + command=clusters.OnOff.Commands.On(), + ), + ] + ) + matter_client.send_device_command.reset_mock() + + # Change color temperature with custom transition + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, "color_temp": 300, "transition": 4.0}, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 2 + matter_client.send_device_command.assert_has_calls( + [ + call( + node_id=light_node.node_id, + endpoint_id=1, + command=clusters.ColorControl.Commands.MoveToColorTemperature( + colorTemperatureMireds=300, + transitionTime=40, optionsMask=1, optionsOverride=1, ), @@ -282,7 +331,38 @@ async def test_extended_color_light( command=clusters.ColorControl.Commands.MoveToColor( colorX=0.5 * 65536, colorY=0.5 * 65536, - transitionTime=0, + transitionTime=2, + optionsMask=1, + optionsOverride=1, + ), + ), + call( + node_id=light_node.node_id, + endpoint_id=1, + command=clusters.OnOff.Commands.On(), + ), + ] + ) + matter_client.send_device_command.reset_mock() + + # Turn the light on with XY color and custom transition + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, "xy_color": (0.5, 0.5), "transition": 4.0}, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 2 + matter_client.send_device_command.assert_has_calls( + [ + call( + node_id=light_node.node_id, + endpoint_id=1, + command=clusters.ColorControl.Commands.MoveToColor( + colorX=0.5 * 65536, + colorY=0.5 * 65536, + transitionTime=40, optionsMask=1, optionsOverride=1, ), @@ -316,7 +396,42 @@ async def test_extended_color_light( command=clusters.ColorControl.Commands.MoveToHueAndSaturation( hue=167, saturation=254, - transitionTime=0, + transitionTime=2, + optionsMask=1, + optionsOverride=1, + ), + ), + call( + node_id=light_node.node_id, + endpoint_id=1, + command=clusters.OnOff.Commands.On(), + ), + ] + ) + matter_client.send_device_command.reset_mock() + + # Turn the light on with HS color and custom transition + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": entity_id, + "hs_color": (236.69291338582678, 100.0), + "transition": 4.0, + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 2 + matter_client.send_device_command.assert_has_calls( + [ + call( + node_id=1, + endpoint_id=1, + command=clusters.ColorControl.Commands.MoveToHueAndSaturation( + hue=167, + saturation=254, + transitionTime=40, optionsMask=1, optionsOverride=1, ), From 59e9010b653fd7616c6c75801a3edb88ae6df687 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 6 Feb 2024 23:03:35 +0100 Subject: [PATCH 0364/1367] Show domain in oauth2 error log (#109708) * Show token url in oauth2 error log * Fix tests * Use domain --- homeassistant/helpers/config_entry_oauth2_flow.py | 5 ++++- tests/helpers/test_config_entry_oauth2_flow.py | 10 +++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index f2f3f63b06e..fc740aa8a4b 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -209,7 +209,10 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): error_code = error_response.get("error", "unknown") error_description = error_response.get("error_description", "unknown error") _LOGGER.error( - "Token request failed (%s): %s", error_code, error_description + "Token request for %s failed (%s): %s", + self.domain, + error_code, + error_description, ) resp.raise_for_status() return cast(dict, await resp.json()) diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 5deb88cab43..0f4812082ee 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -395,19 +395,19 @@ async def test_abort_discovered_multiple( HTTPStatus.UNAUTHORIZED, {}, "oauth_unauthorized", - "Token request failed (unknown): unknown", + "Token request for oauth2_test failed (unknown): unknown", ), ( HTTPStatus.NOT_FOUND, {}, "oauth_failed", - "Token request failed (unknown): unknown", + "Token request for oauth2_test failed (unknown): unknown", ), ( HTTPStatus.INTERNAL_SERVER_ERROR, {}, "oauth_failed", - "Token request failed (unknown): unknown", + "Token request for oauth2_test failed (unknown): unknown", ), ( HTTPStatus.BAD_REQUEST, @@ -417,7 +417,7 @@ async def test_abort_discovered_multiple( "error_uri": "See the full API docs at https://authorization-server.com/docs/access_token", }, "oauth_failed", - "Token request failed (invalid_request): Request was missing the", + "Token request for oauth2_test failed (invalid_request): Request was missing the", ), ], ) @@ -540,7 +540,7 @@ async def test_abort_if_oauth_token_closing_error( with caplog.at_level(logging.DEBUG): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert "Token request failed (unknown): unknown" in caplog.text + assert "Token request for oauth2_test failed (unknown): unknown" in caplog.text assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "oauth_unauthorized" From 9250dd0355eee4721b17c416acd5331d1ac84c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Tue, 6 Feb 2024 23:14:31 +0100 Subject: [PATCH 0365/1367] Add update platform to myuplink (#109786) * Add update platform to myuplink * Address comments from review --- homeassistant/components/myuplink/__init__.py | 2 +- homeassistant/components/myuplink/update.py | 73 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/myuplink/update.py diff --git a/homeassistant/components/myuplink/__init__.py b/homeassistant/components/myuplink/__init__.py index 15ae1eb75c2..271135f9db9 100644 --- a/homeassistant/components/myuplink/__init__.py +++ b/homeassistant/components/myuplink/__init__.py @@ -16,7 +16,7 @@ from .api import AsyncConfigEntryAuth from .const import DOMAIN from .coordinator import MyUplinkDataCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.UPDATE] async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/myuplink/update.py b/homeassistant/components/myuplink/update.py new file mode 100644 index 00000000000..372beaba456 --- /dev/null +++ b/homeassistant/components/myuplink/update.py @@ -0,0 +1,73 @@ +"""Update entity for myUplink.""" +from typing import cast + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import MyUplinkDataCoordinator +from .const import DOMAIN +from .entity import MyUplinkEntity + +UPDATE_DESCRIPTION = UpdateEntityDescription( + key="update", + device_class=UpdateDeviceClass.FIRMWARE, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up update entity.""" + entities: list[UpdateEntity] = [] + coordinator: MyUplinkDataCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + # Setup update entities + for device_id in coordinator.data.devices: + entities.append( + MyUplinkDeviceUpdate( + coordinator=coordinator, + device_id=device_id, + entity_description=UPDATE_DESCRIPTION, + unique_id_suffix="upd", + ) + ) + + async_add_entities(entities) + + +class MyUplinkDeviceUpdate(MyUplinkEntity, UpdateEntity): + """Representation of a myUplink device update entity.""" + + def __init__( + self, + coordinator: MyUplinkDataCoordinator, + device_id: str, + entity_description: UpdateEntityDescription, + unique_id_suffix: str, + ) -> None: + """Initialize the update entity.""" + super().__init__( + coordinator=coordinator, + device_id=device_id, + unique_id_suffix=unique_id_suffix, + ) + + self.entity_description = entity_description + + @property + def installed_version(self) -> str | None: + """Return installed_version.""" + return cast(str, self.coordinator.data.devices[self.device_id].firmwareCurrent) + + @property + def latest_version(self) -> str | None: + """Return latest_version.""" + return cast(str, self.coordinator.data.devices[self.device_id].firmwareDesired) From 26e6bc8a6a6c6269369abdd241c92c301f28e520 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 7 Feb 2024 00:22:54 -0500 Subject: [PATCH 0366/1367] Bump ZHA dependency zigpy to 0.62.3 (#109848) --- 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 263b069bbe8..e9ab98fa6bf 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -26,7 +26,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.111", "zigpy-deconz==0.23.0", - "zigpy==0.62.2", + "zigpy==0.62.3", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", diff --git a/requirements_all.txt b/requirements_all.txt index fd50a9324fb..29d370c5a19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2934,7 +2934,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.62.2 +zigpy==0.62.3 # homeassistant.components.zoneminder zm-py==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 980cf84eba0..ccf98c5bc3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2248,7 +2248,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.62.2 +zigpy==0.62.3 # homeassistant.components.zwave_js zwave-js-server-python==0.55.3 From 8e51affe50cb8f5781508e170ee9a8f5b865e021 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 7 Feb 2024 06:23:57 +0100 Subject: [PATCH 0367/1367] Bump motionblinds to 0.6.20 (#109837) --- homeassistant/components/motion_blinds/cover.py | 2 ++ homeassistant/components/motion_blinds/manifest.json | 2 +- homeassistant/components/motion_blinds/sensor.py | 5 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 833d2640202..c987e1bb10a 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -51,6 +51,7 @@ POSITION_DEVICE_MAP = { BlindType.CurtainLeft: CoverDeviceClass.CURTAIN, BlindType.CurtainRight: CoverDeviceClass.CURTAIN, BlindType.SkylightBlind: CoverDeviceClass.SHADE, + BlindType.InsectScreen: CoverDeviceClass.SHADE, } TILT_DEVICE_MAP = { @@ -69,6 +70,7 @@ TILT_ONLY_DEVICE_MAP = { TDBU_DEVICE_MAP = { BlindType.TopDownBottomUp: CoverDeviceClass.SHADE, + BlindType.TriangleBlind: CoverDeviceClass.BLIND, } diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index f9115cd8146..6f7b7dfae38 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -21,5 +21,5 @@ "documentation": "https://www.home-assistant.io/integrations/motion_blinds", "iot_class": "local_push", "loggers": ["motionblinds"], - "requirements": ["motionblinds==0.6.19"] + "requirements": ["motionblinds==0.6.20"] } diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index e71abe09069..dddcb0e00fd 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -1,5 +1,6 @@ """Support for Motion Blinds sensors.""" -from motionblinds import DEVICE_TYPES_WIFI, BlindType +from motionblinds import DEVICE_TYPES_WIFI +from motionblinds.motion_blinds import DEVICE_TYPE_TDBU from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry @@ -29,7 +30,7 @@ async def async_setup_entry( for blind in motion_gateway.device_list.values(): entities.append(MotionSignalStrengthSensor(coordinator, blind)) - if blind.type == BlindType.TopDownBottomUp: + if blind.device_type == DEVICE_TYPE_TDBU: entities.append(MotionTDBUBatterySensor(coordinator, blind, "Bottom")) entities.append(MotionTDBUBatterySensor(coordinator, blind, "Top")) elif blind.battery_voltage is not None and blind.battery_voltage > 0: diff --git a/requirements_all.txt b/requirements_all.txt index 29d370c5a19..2820fbb478f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1310,7 +1310,7 @@ moehlenhoff-alpha2==1.3.0 mopeka-iot-ble==0.7.0 # homeassistant.components.motion_blinds -motionblinds==0.6.19 +motionblinds==0.6.20 # homeassistant.components.motioneye motioneye-client==0.3.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ccf98c5bc3e..320f314508b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1046,7 +1046,7 @@ moehlenhoff-alpha2==1.3.0 mopeka-iot-ble==0.7.0 # homeassistant.components.motion_blinds -motionblinds==0.6.19 +motionblinds==0.6.20 # homeassistant.components.motioneye motioneye-client==0.3.14 From 905e25b3a11940856c663841eadc5191577a5433 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 7 Feb 2024 06:26:33 +0100 Subject: [PATCH 0368/1367] Make integration fields in Analytics Insights optional (#109789) --- .../analytics_insights/config_flow.py | 53 ++++-- .../analytics_insights/strings.json | 6 + .../analytics_insights/test_config_flow.py | 166 +++++++++++++++++- 3 files changed, 212 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/analytics_insights/config_flow.py b/homeassistant/components/analytics_insights/config_flow.py index b409a9c0fb9..d2ebdd943a2 100644 --- a/homeassistant/components/analytics_insights/config_flow.py +++ b/homeassistant/components/analytics_insights/config_flow.py @@ -53,10 +53,25 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle the initial step.""" self._async_abort_entries_match() - if user_input: - return self.async_create_entry( - title="Home Assistant Analytics Insights", data={}, options=user_input - ) + errors: dict[str, str] = {} + if user_input is not None: + if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get( + CONF_TRACKED_CUSTOM_INTEGRATIONS + ): + errors["base"] = "no_integrations_selected" + else: + return self.async_create_entry( + title="Home Assistant Analytics Insights", + data={}, + options={ + CONF_TRACKED_INTEGRATIONS: user_input.get( + CONF_TRACKED_INTEGRATIONS, [] + ), + CONF_TRACKED_CUSTOM_INTEGRATIONS: user_input.get( + CONF_TRACKED_CUSTOM_INTEGRATIONS, [] + ), + }, + ) client = HomeassistantAnalyticsClient( session=async_get_clientsession(self.hass) @@ -78,16 +93,17 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN): ] return self.async_show_form( step_id="user", + errors=errors, data_schema=vol.Schema( { - vol.Required(CONF_TRACKED_INTEGRATIONS): SelectSelector( + vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector( SelectSelectorConfig( options=options, multiple=True, sort=True, ) ), - vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector( + vol.Optional(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector( SelectSelectorConfig( options=list(custom_integrations), multiple=True, @@ -106,8 +122,24 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the options.""" - if user_input: - return self.async_create_entry(title="", data=user_input) + errors: dict[str, str] = {} + if user_input is not None: + if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get( + CONF_TRACKED_CUSTOM_INTEGRATIONS + ): + errors["base"] = "no_integrations_selected" + else: + return self.async_create_entry( + title="", + data={ + CONF_TRACKED_INTEGRATIONS: user_input.get( + CONF_TRACKED_INTEGRATIONS, [] + ), + CONF_TRACKED_CUSTOM_INTEGRATIONS: user_input.get( + CONF_TRACKED_CUSTOM_INTEGRATIONS, [] + ), + }, + ) client = HomeassistantAnalyticsClient( session=async_get_clientsession(self.hass) @@ -129,17 +161,18 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry): ] return self.async_show_form( step_id="init", + errors=errors, data_schema=self.add_suggested_values_to_schema( vol.Schema( { - vol.Required(CONF_TRACKED_INTEGRATIONS): SelectSelector( + vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector( SelectSelectorConfig( options=options, multiple=True, sort=True, ) ), - vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector( + vol.Optional(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector( SelectSelectorConfig( options=list(custom_integrations), multiple=True, diff --git a/homeassistant/components/analytics_insights/strings.json b/homeassistant/components/analytics_insights/strings.json index 58e47d1df08..6de1ab9dbe4 100644 --- a/homeassistant/components/analytics_insights/strings.json +++ b/homeassistant/components/analytics_insights/strings.json @@ -15,6 +15,9 @@ "abort": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, + "error": { + "no_integration_selected": "You must select at least one integration to track" } }, "options": { @@ -32,6 +35,9 @@ }, "abort": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "error": { + "no_integration_selected": "[%key:component::analytics_insights::config::error::no_integration_selected%]" } }, "entity": { diff --git a/tests/components/analytics_insights/test_config_flow.py b/tests/components/analytics_insights/test_config_flow.py index 8cefa29ee7b..6ddbe285df7 100644 --- a/tests/components/analytics_insights/test_config_flow.py +++ b/tests/components/analytics_insights/test_config_flow.py @@ -1,6 +1,8 @@ """Test the Homeassistant Analytics config flow.""" +from typing import Any from unittest.mock import AsyncMock +import pytest from python_homeassistant_analytics import HomeassistantAnalyticsConnectionError from homeassistant import config_entries @@ -16,8 +18,45 @@ from tests.common import MockConfigEntry from tests.components.analytics_insights import setup_integration +@pytest.mark.parametrize( + ("user_input", "expected_options"), + [ + ( + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + ), + ( + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + }, + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: [], + }, + ), + ( + { + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + { + CONF_TRACKED_INTEGRATIONS: [], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + ), + ], +) async def test_form( - hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_analytics_client: AsyncMock + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_analytics_client: AsyncMock, + user_input: dict[str, Any], + expected_options: dict[str, Any], ) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -25,6 +64,50 @@ async def test_form( ) assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Home Assistant Analytics Insights" + assert result["data"] == {} + assert result["options"] == expected_options + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "user_input", + [ + { + CONF_TRACKED_INTEGRATIONS: [], + CONF_TRACKED_CUSTOM_INTEGRATIONS: [], + }, + {}, + ], +) +async def test_submitting_empty_form( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_analytics_client: AsyncMock, + user_input: dict[str, Any], +) -> None: + """Test we can't submit an empty form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "no_integrations_selected"} + result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -81,10 +164,45 @@ async def test_form_already_configured( assert result["reason"] == "already_configured" +@pytest.mark.parametrize( + ("user_input", "expected_options"), + [ + ( + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + ), + ( + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + }, + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: [], + }, + ), + ( + { + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + { + CONF_TRACKED_INTEGRATIONS: [], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + ), + ], +) async def test_options_flow( hass: HomeAssistant, mock_analytics_client: AsyncMock, mock_config_entry: MockConfigEntry, + user_input: dict[str, Any], + expected_options: dict[str, Any], ) -> None: """Test options flow.""" await setup_integration(hass, mock_config_entry) @@ -95,7 +213,50 @@ async def test_options_flow( mock_analytics_client.get_integrations.reset_mock() result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={ + user_input, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == expected_options + await hass.async_block_till_done() + mock_analytics_client.get_integrations.assert_called_once() + + +@pytest.mark.parametrize( + "user_input", + [ + { + CONF_TRACKED_INTEGRATIONS: [], + CONF_TRACKED_CUSTOM_INTEGRATIONS: [], + }, + {}, + ], +) +async def test_submitting_empty_options_flow( + hass: HomeAssistant, + mock_analytics_client: AsyncMock, + mock_config_entry: MockConfigEntry, + user_input: dict[str, Any], +) -> None: + """Test options flow.""" + await setup_integration(hass, mock_config_entry) + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "no_integrations_selected"} + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + { CONF_TRACKED_INTEGRATIONS: ["youtube", "hue"], CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], }, @@ -108,7 +269,6 @@ async def test_options_flow( CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], } await hass.async_block_till_done() - mock_analytics_client.get_integrations.assert_called_once() async def test_options_flow_cannot_connect( From d9f97bc7ec9ddc257b33472ecaa4373a10666013 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 7 Feb 2024 06:29:26 +0100 Subject: [PATCH 0369/1367] Add tapo virtual integration (#109765) --- homeassistant/brands/tplink.json | 2 +- homeassistant/components/tplink_tapo/__init__.py | 1 + homeassistant/components/tplink_tapo/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 6 ++++++ 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tplink_tapo/__init__.py create mode 100644 homeassistant/components/tplink_tapo/manifest.json diff --git a/homeassistant/brands/tplink.json b/homeassistant/brands/tplink.json index bc8d38b3e71..06ab621ed32 100644 --- a/homeassistant/brands/tplink.json +++ b/homeassistant/brands/tplink.json @@ -1,6 +1,6 @@ { "domain": "tplink", "name": "TP-Link", - "integrations": ["tplink", "tplink_omada", "tplink_lte"], + "integrations": ["tplink", "tplink_omada", "tplink_lte", "tplink_tapo"], "iot_standards": ["matter"] } diff --git a/homeassistant/components/tplink_tapo/__init__.py b/homeassistant/components/tplink_tapo/__init__.py new file mode 100644 index 00000000000..d76870ccea4 --- /dev/null +++ b/homeassistant/components/tplink_tapo/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: TP-Link Tapo.""" diff --git a/homeassistant/components/tplink_tapo/manifest.json b/homeassistant/components/tplink_tapo/manifest.json new file mode 100644 index 00000000000..a0d86b2dc62 --- /dev/null +++ b/homeassistant/components/tplink_tapo/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "tplink_tapo", + "name": "Tapo", + "integration_type": "virtual", + "supported_by": "tplink" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index d4ff080c9fc..ccf21e36a12 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6166,6 +6166,12 @@ "config_flow": false, "iot_class": "local_polling", "name": "TP-Link LTE" + }, + "tplink_tapo": { + "integration_type": "virtual", + "config_flow": false, + "supported_by": "tplink", + "name": "Tapo" } }, "iot_standards": [ From b420c650160415b5941477096183a159e6323522 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 7 Feb 2024 08:14:55 +0100 Subject: [PATCH 0370/1367] Fix hue fallback onoff colormode (#109856) Co-authored-by: Marcel van der Veldt --- homeassistant/components/hue/v2/light.py | 3 +++ tests/components/hue/test_light_v2.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 348d60d8de2..32a9832f69b 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -94,6 +94,9 @@ class HueLight(HueBaseEntity, LightEntity): self._supported_color_modes.add(ColorMode.BRIGHTNESS) # support transition if brightness control self._attr_supported_features |= LightEntityFeature.TRANSITION + if len(self._supported_color_modes) == 0: + # only add onoff colormode as fallback + self._supported_color_modes.add(ColorMode.ONOFF) self._last_brightness: float | None = None self._color_temp_active: bool = False # get list of supported effects (combine effects and timed_effects) diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 55b0c194781..0c79933a246 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -72,7 +72,7 @@ async def test_lights( assert light_4.attributes["friendly_name"] == "Hue on/off light" assert light_4.state == "off" assert light_4.attributes["mode"] == "normal" - assert light_4.attributes["supported_color_modes"] == [] + assert light_4.attributes["supported_color_modes"] == [ColorMode.ONOFF] async def test_light_turn_on_service( From 5521a3986699b8af6dbc34bd422f7b9583b6ab8f Mon Sep 17 00:00:00 2001 From: Vilppu Vuorinen <8420095+vilppuvuorinen@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:45:27 +0200 Subject: [PATCH 0371/1367] Disable energy report based operations with API lib upgrade (#109832) Co-authored-by: Franck Nijhof --- homeassistant/components/melcloud/__init__.py | 5 ----- .../components/melcloud/manifest.json | 2 +- homeassistant/components/melcloud/sensor.py | 20 ------------------- .../components/melcloud/strings.json | 3 --- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 3 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index baa601d4ab0..2db3e79dfe9 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -123,11 +123,6 @@ class MelCloudDevice: via_device=(DOMAIN, f"{dev.mac}-{dev.serial}"), ) - @property - def daily_energy_consumed(self) -> float | None: - """Return energy consumed during the current day in kWh.""" - return self.device.daily_energy_consumed - async def mel_devices_setup( hass: HomeAssistant, token: str diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index fde248467bf..0122c840373 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/melcloud", "iot_class": "cloud_polling", "loggers": ["pymelcloud"], - "requirements": ["pymelcloud==2.5.8"] + "requirements": ["pymelcloud==2.5.9"] } diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index cf53fe42b77..d3d1f4976f6 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -58,16 +58,6 @@ ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( value_fn=lambda x: x.device.total_energy_consumed, enabled=lambda x: x.device.has_energy_consumed_meter, ), - MelcloudSensorEntityDescription( - key="daily_energy", - translation_key="daily_energy", - icon="mdi:factory", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda x: x.device.daily_energy_consumed, - enabled=lambda x: True, - ), ) ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( MelcloudSensorEntityDescription( @@ -90,16 +80,6 @@ ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( value_fn=lambda x: x.device.tank_temperature, enabled=lambda x: True, ), - MelcloudSensorEntityDescription( - key="daily_energy", - translation_key="daily_energy", - icon="mdi:factory", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda x: x.device.daily_energy_consumed, - enabled=lambda x: True, - ), ) ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( MelcloudSensorEntityDescription( diff --git a/homeassistant/components/melcloud/strings.json b/homeassistant/components/melcloud/strings.json index 3abb30bf9ac..6a98b88e2d3 100644 --- a/homeassistant/components/melcloud/strings.json +++ b/homeassistant/components/melcloud/strings.json @@ -65,9 +65,6 @@ "room_temperature": { "name": "Room temperature" }, - "daily_energy": { - "name": "Daily energy consumed" - }, "outside_temperature": { "name": "Outside temperature" }, diff --git a/requirements_all.txt b/requirements_all.txt index 2820fbb478f..4b6cb439a7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1940,7 +1940,7 @@ pymata-express==1.19 pymediaroom==0.6.5.4 # homeassistant.components.melcloud -pymelcloud==2.5.8 +pymelcloud==2.5.9 # homeassistant.components.meteoclimatic pymeteoclimatic==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 320f314508b..da3cd705b50 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1494,7 +1494,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.melcloud -pymelcloud==2.5.8 +pymelcloud==2.5.9 # homeassistant.components.meteoclimatic pymeteoclimatic==0.1.0 From 2fc56ff4e4707d3e4caa1bbfd48b66f4676a1185 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Wed, 7 Feb 2024 08:53:19 +0100 Subject: [PATCH 0372/1367] Add late PR changes to tedee (#109858) requested changes --- .../components/tedee/binary_sensor.py | 19 +++++------ homeassistant/components/tedee/config_flow.py | 33 ++++++++++++------- homeassistant/components/tedee/sensor.py | 25 +++++++------- .../tedee/snapshots/test_sensor.ambr | 8 ++--- 4 files changed, 46 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/tedee/binary_sensor.py b/homeassistant/components/tedee/binary_sensor.py index 7efa25fa245..645e25d4e85 100644 --- a/homeassistant/components/tedee/binary_sensor.py +++ b/homeassistant/components/tedee/binary_sensor.py @@ -1,4 +1,5 @@ """Tedee sensor entities.""" + from collections.abc import Callable from dataclasses import dataclass @@ -58,21 +59,17 @@ async def async_setup_entry( """Set up the Tedee sensor entity.""" coordinator = hass.data[DOMAIN][entry.entry_id] - for entity_description in ENTITIES: - async_add_entities( - [ - TedeeBinarySensorEntity(lock, coordinator, entity_description) - for lock in coordinator.data.values() - ] - ) + async_add_entities( + TedeeBinarySensorEntity(lock, coordinator, entity_description) + for lock in coordinator.data.values() + for entity_description in ENTITIES + ) def _async_add_new_lock(lock_id: int) -> None: lock = coordinator.data[lock_id] async_add_entities( - [ - TedeeBinarySensorEntity(lock, coordinator, entity_description) - for entity_description in ENTITIES - ] + TedeeBinarySensorEntity(lock, coordinator, entity_description) + for entity_description in ENTITIES ) coordinator.new_lock_callbacks.append(_async_add_new_lock) diff --git a/homeassistant/components/tedee/config_flow.py b/homeassistant/components/tedee/config_flow.py index 075a4c998ea..7c8c7b4c3ab 100644 --- a/homeassistant/components/tedee/config_flow.py +++ b/homeassistant/components/tedee/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Tedee integration.""" + from collections.abc import Mapping from typing import Any @@ -83,14 +84,24 @@ class TedeeConfigFlow(ConfigFlow, domain=DOMAIN): self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] ) - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required( - CONF_LOCAL_ACCESS_TOKEN, - default=entry_data[CONF_LOCAL_ACCESS_TOKEN], - ): str, - } - ), - ) + 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 + + if not user_input: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required( + CONF_LOCAL_ACCESS_TOKEN, + default=self.reauth_entry.data[CONF_LOCAL_ACCESS_TOKEN], + ): str, + } + ), + ) + return await self.async_step_user(user_input) diff --git a/homeassistant/components/tedee/sensor.py b/homeassistant/components/tedee/sensor.py index 9880f73746d..225686f6b18 100644 --- a/homeassistant/components/tedee/sensor.py +++ b/homeassistant/components/tedee/sensor.py @@ -1,4 +1,5 @@ """Tedee sensor entities.""" + from collections.abc import Callable from dataclasses import dataclass @@ -11,7 +12,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, UnitOfTime +from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -33,15 +34,17 @@ ENTITIES: tuple[TedeeSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda lock: lock.battery_level, + entity_category=EntityCategory.DIAGNOSTIC, ), TedeeSensorEntityDescription( key="pullspring_duration", translation_key="pullspring_duration", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, - state_class=SensorStateClass.TOTAL, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:timer-lock-open", value_fn=lambda lock: lock.duration_pullspring, + entity_category=EntityCategory.DIAGNOSTIC, ), ) @@ -54,21 +57,17 @@ async def async_setup_entry( """Set up the Tedee sensor entity.""" coordinator = hass.data[DOMAIN][entry.entry_id] - for entity_description in ENTITIES: - async_add_entities( - [ - TedeeSensorEntity(lock, coordinator, entity_description) - for lock in coordinator.data.values() - ] - ) + async_add_entities( + TedeeSensorEntity(lock, coordinator, entity_description) + for lock in coordinator.data.values() + for entity_description in ENTITIES + ) def _async_add_new_lock(lock_id: int) -> None: lock = coordinator.data[lock_id] async_add_entities( - [ - TedeeSensorEntity(lock, coordinator, entity_description) - for entity_description in ENTITIES - ] + TedeeSensorEntity(lock, coordinator, entity_description) + for entity_description in ENTITIES ) coordinator.new_lock_callbacks.append(_async_add_new_lock) diff --git a/tests/components/tedee/snapshots/test_sensor.ambr b/tests/components/tedee/snapshots/test_sensor.ambr index a74ee38bff0..a31ccea4578 100644 --- a/tests/components/tedee/snapshots/test_sensor.ambr +++ b/tests/components/tedee/snapshots/test_sensor.ambr @@ -12,7 +12,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.lock_1a2b_battery', 'has_entity_name': True, 'hidden_by': None, @@ -38,14 +38,14 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.lock_1a2b_pullspring_duration', 'has_entity_name': True, 'hidden_by': None, @@ -86,7 +86,7 @@ 'device_class': 'duration', 'friendly_name': 'Lock-1A2B Pullspring duration', 'icon': 'mdi:timer-lock-open', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , From 586b4ab93d465323d5288dd59464f97413f069d3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 7 Feb 2024 08:56:35 +0100 Subject: [PATCH 0373/1367] Simplify Hue v2 color mode calculation (#109857) --- homeassistant/components/hue/v2/light.py | 38 +++++++++++------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 32a9832f69b..81993c7627f 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -21,6 +21,7 @@ from homeassistant.components.light import ( LightEntity, LightEntityDescription, LightEntityFeature, + filter_supported_color_modes, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -70,6 +71,7 @@ async def async_setup_entry( class HueLight(HueBaseEntity, LightEntity): """Representation of a Hue light.""" + _fixed_color_mode: ColorMode | None = None entity_description = LightEntityDescription( key="hue_light", has_entity_name=True, name=None ) @@ -83,20 +85,20 @@ class HueLight(HueBaseEntity, LightEntity): self._attr_supported_features |= LightEntityFeature.FLASH self.resource = resource self.controller = controller - self._supported_color_modes: set[ColorMode | str] = set() + supported_color_modes = {ColorMode.ONOFF} if self.resource.supports_color: - self._supported_color_modes.add(ColorMode.XY) + supported_color_modes.add(ColorMode.XY) if self.resource.supports_color_temperature: - self._supported_color_modes.add(ColorMode.COLOR_TEMP) + supported_color_modes.add(ColorMode.COLOR_TEMP) if self.resource.supports_dimming: - if len(self._supported_color_modes) == 0: - # only add color mode brightness if no color variants - self._supported_color_modes.add(ColorMode.BRIGHTNESS) + supported_color_modes.add(ColorMode.BRIGHTNESS) # support transition if brightness control self._attr_supported_features |= LightEntityFeature.TRANSITION - if len(self._supported_color_modes) == 0: - # only add onoff colormode as fallback - self._supported_color_modes.add(ColorMode.ONOFF) + supported_color_modes = filter_supported_color_modes(supported_color_modes) + self._attr_supported_color_modes = supported_color_modes + if len(self._attr_supported_color_modes) == 1: + # If the light supports only a single color mode, set it now + self._fixed_color_mode = next(iter(self._attr_supported_color_modes)) self._last_brightness: float | None = None self._color_temp_active: bool = False # get list of supported effects (combine effects and timed_effects) @@ -131,14 +133,15 @@ class HueLight(HueBaseEntity, LightEntity): @property def color_mode(self) -> ColorMode: """Return the color mode of the light.""" + if self._fixed_color_mode: + # The light supports only a single color mode, return it + return self._fixed_color_mode + + # The light supports both color temperature and XY, determine which + # mode the light is in if self.color_temp_active: return ColorMode.COLOR_TEMP - if self.resource.supports_color: - return ColorMode.XY - if self.resource.supports_dimming: - return ColorMode.BRIGHTNESS - # fallback to on_off - return ColorMode.ONOFF + return ColorMode.XY @property def color_temp_active(self) -> bool: @@ -183,11 +186,6 @@ class HueLight(HueBaseEntity, LightEntity): # return a fallback value to prevent issues with mired->kelvin conversions return FALLBACK_MAX_MIREDS - @property - def supported_color_modes(self) -> set | None: - """Flag supported features.""" - return self._supported_color_modes - @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the optional state attributes.""" From 6f3be3e50548db071a05dfb8d8a65cce14c7cafa Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 7 Feb 2024 03:13:51 -0500 Subject: [PATCH 0374/1367] Move Roborock map retrieval to coordinator and made map always diagnostic (#104680) Co-authored-by: Robert Resch --- homeassistant/components/roborock/__init__.py | 5 ++ .../components/roborock/coordinator.py | 9 +++ homeassistant/components/roborock/image.py | 77 ++++++++----------- tests/components/roborock/conftest.py | 3 + tests/components/roborock/test_init.py | 14 ++++ 5 files changed, 64 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index e0dfb2b271f..f8ceb121fe4 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -122,6 +122,11 @@ async def setup_device( # Verify we can communicate locally - if we can't, switch to cloud api await coordinator.verify_api() coordinator.api.is_available = True + try: + await coordinator.get_maps() + except RoborockException as err: + _LOGGER.warning("Failed to get map data") + _LOGGER.debug(err) try: await coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady as ex: diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index cd08cf871d4..3864a90b16d 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -59,6 +59,8 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): if mac := self.roborock_device_info.network_info.mac: self.device_info[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, mac)} + # Maps from map flag to map name + self.maps: dict[int, str] = {} async def verify_api(self) -> None: """Verify that the api is reachable. If it is not, switch clients.""" @@ -107,3 +109,10 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): self.current_map = ( self.roborock_device_info.props.status.map_status - 3 ) // 4 + + async def get_maps(self) -> None: + """Add a map to the coordinators mapping.""" + maps = await self.api.get_multi_maps_list() + if maps and maps.map_info: + for roborock_map in maps.map_info: + self.maps[roborock_map.mapFlag] = roborock_map.name diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index b2a14b57819..66957232679 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -66,13 +66,7 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): self._attr_image_last_updated = dt_util.utcnow() self.map_flag = map_flag self.cached_map = self._create_image(starting_map) - - @property - def entity_category(self) -> EntityCategory | None: - """Return diagnostic entity category for any non-selected maps.""" - if not self.is_selected: - return EntityCategory.DIAGNOSTIC - return None + self._attr_entity_category = EntityCategory.DIAGNOSTIC @property def is_selected(self) -> bool: @@ -127,42 +121,37 @@ async def create_coordinator_maps( Only one map can be loaded at a time per device. """ entities = [] - maps = await coord.cloud_api.get_multi_maps_list() - if maps is not None and maps.map_info is not None: - cur_map = coord.current_map - # This won't be None at this point as the coordinator will have run first. - assert cur_map is not None - # Sort the maps so that we start with the current map and we can skip the - # load_multi_map call. - maps_info = sorted( - maps.map_info, key=lambda data: data.mapFlag == cur_map, reverse=True + + cur_map = coord.current_map + # This won't be None at this point as the coordinator will have run first. + assert cur_map is not None + # Sort the maps so that we start with the current map and we can skip the + # load_multi_map call. + maps_info = sorted( + coord.maps.items(), key=lambda data: data[0] == cur_map, reverse=True + ) + for map_flag, map_name in maps_info: + # Load the map - so we can access it with get_map_v1 + if map_flag != cur_map: + # Only change the map and sleep if we have multiple maps. + await coord.api.send_command(RoborockCommand.LOAD_MULTI_MAP, [map_flag]) + # We cannot get the map until the roborock servers fully process the + # map change. + await asyncio.sleep(MAP_SLEEP) + # Get the map data + api_data: bytes = await coord.cloud_api.get_map_v1() + entities.append( + RoborockMap( + f"{slugify(coord.roborock_device_info.device.duid)}_map_{map_name}", + coord, + map_flag, + api_data, + map_name, + ) ) - for roborock_map in maps_info: - # Load the map - so we can access it with get_map_v1 - if roborock_map.mapFlag != cur_map: - # Only change the map and sleep if we have multiple maps. - await coord.api.send_command( - RoborockCommand.LOAD_MULTI_MAP, [roborock_map.mapFlag] - ) - # We cannot get the map until the roborock servers fully process the - # map change. - await asyncio.sleep(MAP_SLEEP) - # Get the map data - api_data: bytes = await coord.cloud_api.get_map_v1() - entities.append( - RoborockMap( - f"{slugify(coord.roborock_device_info.device.duid)}_map_{roborock_map.name}", - coord, - roborock_map.mapFlag, - api_data, - roborock_map.name, - ) - ) - if len(maps.map_info) != 1: - # Set the map back to the map the user previously had selected so that it - # does not change the end user's app. - # Only needs to happen when we changed maps above. - await coord.cloud_api.send_command( - RoborockCommand.LOAD_MULTI_MAP, [cur_map] - ) + if len(coord.maps) != 1: + # Set the map back to the map the user previously had selected so that it + # does not change the end user's app. + # Only needs to happen when we changed maps above. + await coord.cloud_api.send_command(RoborockCommand.LOAD_MULTI_MAP, [cur_map]) return entities diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 711ae203e0f..efbc2ea7f9d 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -45,6 +45,9 @@ def bypass_api_fixture() -> None: ), patch( "homeassistant.components.roborock.coordinator.RoborockMqttClient.get_multi_maps_list", return_value=MULTI_MAP_LIST, + ), patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_multi_maps_list", + return_value=MULTI_MAP_LIST, ), patch( "homeassistant.components.roborock.image.RoborockMapDataParser.parse", return_value=MAP_DATA, diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index 5d1afaf8f84..608263a3496 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -107,6 +107,20 @@ async def test_local_client_fails_props( assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY +async def test_fails_maps_continue( + hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture +) -> None: + """Test that if we fail to get the maps, we still setup.""" + with patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_multi_maps_list", + side_effect=RoborockException(), + ): + await async_setup_component(hass, DOMAIN, {}) + assert mock_roborock_entry.state is ConfigEntryState.LOADED + # No map data means no images + assert len(hass.states.async_all("image")) == 0 + + async def test_reauth_started( hass: HomeAssistant, bypass_api_fixture, mock_roborock_entry: MockConfigEntry ) -> None: From 6d4ab6c758f268cd12edbe4652c4193a12f78784 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:27:04 +0100 Subject: [PATCH 0375/1367] Add Husqvarna Automower integration (#109073) * Add Husqvarna Automower * Update homeassistant/components/husqvarna_automower/__init__.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/husqvarna_automower/config_flow.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/husqvarna_automower/entity.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/husqvarna_automower/entity.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/husqvarna_automower/lawn_mower.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/husqvarna_automower/lawn_mower.py Co-authored-by: Joost Lekkerkerker * address review * add test_config_non_unique_profile * add missing const * WIP tests * tests * tests * Update homeassistant/components/husqvarna_automower/api.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/husqvarna_automower/config_flow.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/husqvarna_automower/config_flow.py Co-authored-by: Joost Lekkerkerker * Update tests/components/husqvarna_automower/conftest.py Co-authored-by: Joost Lekkerkerker * . * loop through test * Update homeassistant/components/husqvarna_automower/entity.py * Update homeassistant/components/husqvarna_automower/coordinator.py * Update homeassistant/components/husqvarna_automower/coordinator.py * Apply suggestions from code review * ruff --------- Co-authored-by: Joost Lekkerkerker --- CODEOWNERS | 2 + .../husqvarna_automower/__init__.py | 62 ++++++++ .../components/husqvarna_automower/api.py | 29 ++++ .../application_credentials.py | 14 ++ .../husqvarna_automower/config_flow.py | 43 ++++++ .../components/husqvarna_automower/const.py | 7 + .../husqvarna_automower/coordinator.py | 47 ++++++ .../components/husqvarna_automower/entity.py | 41 ++++++ .../husqvarna_automower/lawn_mower.py | 126 ++++++++++++++++ .../husqvarna_automower/manifest.json | 10 ++ .../husqvarna_automower/strings.json | 21 +++ .../generated/application_credentials.py | 1 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../husqvarna_automower/__init__.py | 11 ++ .../husqvarna_automower/conftest.py | 85 +++++++++++ tests/components/husqvarna_automower/const.py | 4 + .../husqvarna_automower/fixtures/jwt | 1 + .../husqvarna_automower/fixtures/mower.json | 139 ++++++++++++++++++ .../husqvarna_automower/test_config_flow.py | 129 ++++++++++++++++ .../husqvarna_automower/test_init.py | 68 +++++++++ .../husqvarna_automower/test_lawn_mower.py | 88 +++++++++++ 24 files changed, 941 insertions(+) create mode 100644 homeassistant/components/husqvarna_automower/__init__.py create mode 100644 homeassistant/components/husqvarna_automower/api.py create mode 100644 homeassistant/components/husqvarna_automower/application_credentials.py create mode 100644 homeassistant/components/husqvarna_automower/config_flow.py create mode 100644 homeassistant/components/husqvarna_automower/const.py create mode 100644 homeassistant/components/husqvarna_automower/coordinator.py create mode 100644 homeassistant/components/husqvarna_automower/entity.py create mode 100644 homeassistant/components/husqvarna_automower/lawn_mower.py create mode 100644 homeassistant/components/husqvarna_automower/manifest.json create mode 100644 homeassistant/components/husqvarna_automower/strings.json create mode 100644 tests/components/husqvarna_automower/__init__.py create mode 100644 tests/components/husqvarna_automower/conftest.py create mode 100644 tests/components/husqvarna_automower/const.py create mode 100644 tests/components/husqvarna_automower/fixtures/jwt create mode 100644 tests/components/husqvarna_automower/fixtures/mower.json create mode 100644 tests/components/husqvarna_automower/test_config_flow.py create mode 100644 tests/components/husqvarna_automower/test_init.py create mode 100644 tests/components/husqvarna_automower/test_lawn_mower.py diff --git a/CODEOWNERS b/CODEOWNERS index 144883db68f..7e53ae3058b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -584,6 +584,8 @@ build.json @home-assistant/supervisor /tests/components/humidifier/ @home-assistant/core @Shulyaka /homeassistant/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock /tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock +/homeassistant/components/husqvarna_automower/ @Thomas55555 +/tests/components/husqvarna_automower/ @Thomas55555 /homeassistant/components/huum/ @frwickst /tests/components/huum/ @frwickst /homeassistant/components/hvv_departures/ @vigonotion diff --git a/homeassistant/components/husqvarna_automower/__init__.py b/homeassistant/components/husqvarna_automower/__init__.py new file mode 100644 index 00000000000..057c1fcc617 --- /dev/null +++ b/homeassistant/components/husqvarna_automower/__init__.py @@ -0,0 +1,62 @@ +"""The Husqvarna Automower integration.""" + +import logging + +from aioautomower.session import AutomowerSession +from aiohttp import ClientError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow + +from . import api +from .const import DOMAIN +from .coordinator import AutomowerDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +PLATFORMS: list[Platform] = [ + Platform.LAWN_MOWER, +] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up this integration using UI.""" + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + api_api = api.AsyncConfigEntryAuth( + aiohttp_client.async_get_clientsession(hass), + session, + ) + automower_api = AutomowerSession(api_api) + try: + await api_api.async_get_access_token() + except ClientError as err: + raise ConfigEntryNotReady from err + coordinator = AutomowerDataUpdateCoordinator(hass, automower_api) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown) + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Handle unload of an entry.""" + coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + await coordinator.shutdown() + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/husqvarna_automower/api.py b/homeassistant/components/husqvarna_automower/api.py new file mode 100644 index 00000000000..e5dc00ad7cb --- /dev/null +++ b/homeassistant/components/husqvarna_automower/api.py @@ -0,0 +1,29 @@ +"""API for Husqvarna Automower bound to Home Assistant OAuth.""" + +import logging + +from aioautomower.auth import AbstractAuth +from aioautomower.const import API_BASE_URL +from aiohttp import ClientSession + +from homeassistant.helpers import config_entry_oauth2_flow + +_LOGGER = logging.getLogger(__name__) + + +class AsyncConfigEntryAuth(AbstractAuth): + """Provide Husqvarna Automower authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + websession: ClientSession, + oauth_session: config_entry_oauth2_flow.OAuth2Session, + ) -> None: + """Initialize Husqvarna Automower auth.""" + super().__init__(websession, API_BASE_URL) + self._oauth_session = oauth_session + + async def async_get_access_token(self) -> str: + """Return a valid access token.""" + await self._oauth_session.async_ensure_token_valid() + return self._oauth_session.token["access_token"] diff --git a/homeassistant/components/husqvarna_automower/application_credentials.py b/homeassistant/components/husqvarna_automower/application_credentials.py new file mode 100644 index 00000000000..f201130ab22 --- /dev/null +++ b/homeassistant/components/husqvarna_automower/application_credentials.py @@ -0,0 +1,14 @@ +"""Application credentials platform for Husqvarna Automower.""" + +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + +from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + authorize_url=OAUTH2_AUTHORIZE, + token_url=OAUTH2_TOKEN, + ) diff --git a/homeassistant/components/husqvarna_automower/config_flow.py b/homeassistant/components/husqvarna_automower/config_flow.py new file mode 100644 index 00000000000..cafe942a894 --- /dev/null +++ b/homeassistant/components/husqvarna_automower/config_flow.py @@ -0,0 +1,43 @@ +"""Config flow to add the integration via the UI.""" +import logging +from typing import Any + +from aioautomower.utils import async_structure_token + +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import DOMAIN, NAME + +_LOGGER = logging.getLogger(__name__) +CONF_USER_ID = "user_id" + + +class HusqvarnaConfigFlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, + domain=DOMAIN, +): + """Handle a config flow.""" + + VERSION = 1 + DOMAIN = DOMAIN + + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: + """Create an entry for the flow.""" + token = data[CONF_TOKEN] + user_id = token[CONF_USER_ID] + structured_token = await async_structure_token(token[CONF_ACCESS_TOKEN]) + first_name = structured_token.user.first_name + last_name = structured_token.user.last_name + await self.async_set_unique_id(user_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=f"{NAME} of {first_name} {last_name}", + data=data, + ) + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) diff --git a/homeassistant/components/husqvarna_automower/const.py b/homeassistant/components/husqvarna_automower/const.py new file mode 100644 index 00000000000..ab30bae45f2 --- /dev/null +++ b/homeassistant/components/husqvarna_automower/const.py @@ -0,0 +1,7 @@ +"""The constants for the Husqvarna Automower integration.""" + +DOMAIN = "husqvarna_automower" +NAME = "Husqvarna Automower" +HUSQVARNA_URL = "https://developer.husqvarnagroup.cloud/login" +OAUTH2_AUTHORIZE = "https://api.authentication.husqvarnagroup.dev/v1/oauth2/authorize" +OAUTH2_TOKEN = "https://api.authentication.husqvarnagroup.dev/v1/oauth2/token" diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py new file mode 100644 index 00000000000..8409643ee7c --- /dev/null +++ b/homeassistant/components/husqvarna_automower/coordinator.py @@ -0,0 +1,47 @@ +"""Data UpdateCoordinator for the Husqvarna Automower integration.""" +from datetime import timedelta +import logging +from typing import Any + +from aioautomower.model import MowerAttributes, MowerList + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .api import AsyncConfigEntryAuth +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttributes]]): + """Class to manage fetching Husqvarna data.""" + + def __init__(self, hass: HomeAssistant, api: AsyncConfigEntryAuth) -> None: + """Initialize data updater.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(minutes=5), + ) + self.api = api + + self.ws_connected: bool = False + + async def _async_update_data(self) -> dict[str, MowerAttributes]: + """Subscribe for websocket and poll data from the API.""" + if not self.ws_connected: + await self.api.connect() + self.api.register_data_callback(self.callback) + self.ws_connected = True + return await self.api.get_status() + + async def shutdown(self, *_: Any) -> None: + """Close resources.""" + await self.api.close() + + @callback + def callback(self, ws_data: MowerList) -> None: + """Process websocket callbacks and write them to the DataUpdateCoordinator.""" + self.async_set_updated_data(ws_data) diff --git a/homeassistant/components/husqvarna_automower/entity.py b/homeassistant/components/husqvarna_automower/entity.py new file mode 100644 index 00000000000..e91e3c89ab2 --- /dev/null +++ b/homeassistant/components/husqvarna_automower/entity.py @@ -0,0 +1,41 @@ +"""Platform for Husqvarna Automower base entity.""" + +import logging + +from aioautomower.model import MowerAttributes + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import AutomowerDataUpdateCoordinator +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class AutomowerBaseEntity(CoordinatorEntity[AutomowerDataUpdateCoordinator]): + """Defining the Automower base Entity.""" + + _attr_has_entity_name = True + _attr_should_poll = False + + def __init__( + self, + mower_id: str, + coordinator: AutomowerDataUpdateCoordinator, + ) -> None: + """Initialize AutomowerEntity.""" + super().__init__(coordinator) + self.mower_id = mower_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, mower_id)}, + name=self.mower_attributes.system.name, + manufacturer="Husqvarna", + model=self.mower_attributes.system.model, + suggested_area="Garden", + ) + + @property + def mower_attributes(self) -> MowerAttributes: + """Get the mower attributes of the current mower.""" + return self.coordinator.data[self.mower_id] diff --git a/homeassistant/components/husqvarna_automower/lawn_mower.py b/homeassistant/components/husqvarna_automower/lawn_mower.py new file mode 100644 index 00000000000..e44f8b98c47 --- /dev/null +++ b/homeassistant/components/husqvarna_automower/lawn_mower.py @@ -0,0 +1,126 @@ +"""Husqvarna Automower lawn mower entity.""" +import logging + +from aioautomower.exceptions import ApiException +from aioautomower.model import MowerActivities, MowerStates + +from homeassistant.components.lawn_mower import ( + LawnMowerActivity, + LawnMowerEntity, + LawnMowerEntityFeature, +) +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 AutomowerDataUpdateCoordinator +from .entity import AutomowerBaseEntity + +SUPPORT_STATE_SERVICES = ( + LawnMowerEntityFeature.DOCK + | LawnMowerEntityFeature.PAUSE + | LawnMowerEntityFeature.START_MOWING +) + +DOCKED_ACTIVITIES = (MowerActivities.PARKED_IN_CS, MowerActivities.CHARGING) +ERROR_ACTIVITIES = ( + MowerActivities.STOPPED_IN_GARDEN, + MowerActivities.UNKNOWN, + MowerActivities.NOT_APPLICABLE, +) +ERROR_STATES = [ + MowerStates.FATAL_ERROR, + MowerStates.ERROR, + MowerStates.ERROR_AT_POWER_UP, + MowerStates.NOT_APPLICABLE, + MowerStates.UNKNOWN, + MowerStates.STOPPED, + MowerStates.OFF, +] +MOWING_ACTIVITIES = ( + MowerActivities.MOWING, + MowerActivities.LEAVING, + MowerActivities.GOING_HOME, +) +PAUSED_STATES = [ + MowerStates.PAUSED, + MowerStates.WAIT_UPDATING, + MowerStates.WAIT_POWER_UP, +] + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up lawn mower platform.""" + coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + AutomowerLawnMowerEntity(mower_id, coordinator) for mower_id in coordinator.data + ) + + +class AutomowerLawnMowerEntity(LawnMowerEntity, AutomowerBaseEntity): + """Defining each mower Entity.""" + + _attr_name = None + _attr_supported_features = SUPPORT_STATE_SERVICES + + def __init__( + self, + mower_id: str, + coordinator: AutomowerDataUpdateCoordinator, + ) -> None: + """Set up HusqvarnaAutomowerEntity.""" + super().__init__(mower_id, coordinator) + self._attr_unique_id = mower_id + + @property + def available(self) -> bool: + """Return True if the device is available.""" + return super().available and self.mower_attributes.metadata.connected + + @property + def activity(self) -> LawnMowerActivity: + """Return the state of the mower.""" + mower_attributes = self.mower_attributes + if mower_attributes.mower.state in PAUSED_STATES: + return LawnMowerActivity.PAUSED + if mower_attributes.mower.activity in MOWING_ACTIVITIES: + return LawnMowerActivity.MOWING + if (mower_attributes.mower.state == "RESTRICTED") or ( + mower_attributes.mower.activity in DOCKED_ACTIVITIES + ): + return LawnMowerActivity.DOCKED + return LawnMowerActivity.ERROR + + async def async_start_mowing(self) -> None: + """Resume schedule.""" + try: + await self.coordinator.api.resume_schedule(self.mower_id) + except ApiException as exception: + raise HomeAssistantError( + f"Command couldn't be sent to the command queue: {exception}" + ) from exception + + async def async_pause(self) -> None: + """Pauses the mower.""" + try: + await self.coordinator.api.pause_mowing(self.mower_id) + except ApiException as exception: + raise HomeAssistantError( + f"Command couldn't be sent to the command queue: {exception}" + ) from exception + + async def async_dock(self) -> None: + """Parks the mower until next schedule.""" + try: + await self.coordinator.api.park_until_next_schedule(self.mower_id) + except ApiException as exception: + raise HomeAssistantError( + f"Command couldn't be sent to the command queue: {exception}" + ) from exception diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json new file mode 100644 index 00000000000..b5c40e7cf5a --- /dev/null +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "husqvarna_automower", + "name": "Husqvarna Automower", + "codeowners": ["@Thomas55555"], + "config_flow": true, + "dependencies": ["application_credentials"], + "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", + "iot_class": "cloud_push", + "requirements": ["aioautomower==2024.1.5"] +} diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json new file mode 100644 index 00000000000..569e148a5a3 --- /dev/null +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + } + }, + "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%]" + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + } + } +} diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index 586aa64ce18..851474d8481 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -13,6 +13,7 @@ APPLICATION_CREDENTIALS = [ "google_sheets", "google_tasks", "home_connect", + "husqvarna_automower", "lametric", "lyric", "myuplink", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index aa3efde99bc..0a4683d724a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -228,6 +228,7 @@ FLOWS = { "hue", "huisbaasje", "hunterdouglas_powerview", + "husqvarna_automower", "huum", "hvv_departures", "hydrawise", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index ccf21e36a12..38f1dfe070b 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2618,6 +2618,12 @@ "integration_type": "virtual", "supported_by": "motion_blinds" }, + "husqvarna_automower": { + "name": "Husqvarna Automower", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_push" + }, "huum": { "name": "Huum", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 4b6cb439a7e..109de7320b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -205,6 +205,9 @@ aioaseko==0.0.2 # homeassistant.components.asuswrt aioasuswrt==1.4.0 +# homeassistant.components.husqvarna_automower +aioautomower==2024.1.5 + # homeassistant.components.azure_devops aioazuredevops==1.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da3cd705b50..ed62d2c1b02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -184,6 +184,9 @@ aioaseko==0.0.2 # homeassistant.components.asuswrt aioasuswrt==1.4.0 +# homeassistant.components.husqvarna_automower +aioautomower==2024.1.5 + # homeassistant.components.azure_devops aioazuredevops==1.3.5 diff --git a/tests/components/husqvarna_automower/__init__.py b/tests/components/husqvarna_automower/__init__.py new file mode 100644 index 00000000000..069fa0d7372 --- /dev/null +++ b/tests/components/husqvarna_automower/__init__.py @@ -0,0 +1,11 @@ +"""Tests for the Husqvarna Automower integration.""" +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/husqvarna_automower/conftest.py b/tests/components/husqvarna_automower/conftest.py new file mode 100644 index 00000000000..89c0133cd0b --- /dev/null +++ b/tests/components/husqvarna_automower/conftest.py @@ -0,0 +1,85 @@ +"""Test helpers for Husqvarna Automower.""" +from collections.abc import Generator +import time +from unittest.mock import AsyncMock, patch + +from aioautomower.utils import mower_list_to_dictionary_dataclass +import pytest + +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import CLIENT_ID, CLIENT_SECRET, USER_ID + +from tests.common import MockConfigEntry, load_fixture, load_json_value_fixture + + +@pytest.fixture(name="jwt") +def load_jwt_fixture(): + """Load Fixture data.""" + return load_fixture("jwt", DOMAIN) + + +@pytest.fixture(name="expires_at") +def mock_expires_at() -> float: + """Fixture to set the oauth token expiration time.""" + return time.time() + 3600 + + +@pytest.fixture +def mock_config_entry(jwt, expires_at: int) -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + version=1, + domain=DOMAIN, + title="Husqvarna Automower of Erika Mustermann", + data={ + "auth_implementation": DOMAIN, + "token": { + "access_token": jwt, + "scope": "iam:read amc:api", + "expires_in": 86399, + "refresh_token": "3012bc9f-7a65-4240-b817-9154ffdcc30f", + "provider": "husqvarna", + "user_id": USER_ID, + "token_type": "Bearer", + "expires_at": expires_at, + }, + }, + unique_id=USER_ID, + entry_id="automower_test", + ) + + +@pytest.fixture(autouse=True) +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, + ), + DOMAIN, + ) + + +@pytest.fixture +def mock_automower_client() -> Generator[AsyncMock, None, None]: + """Mock a Husqvarna Automower client.""" + with patch( + "homeassistant.components.husqvarna_automower.AutomowerSession", + autospec=True, + ) as mock_client: + client = mock_client.return_value + client.get_status.return_value = mower_list_to_dictionary_dataclass( + load_json_value_fixture("mower.json", DOMAIN) + ) + yield client diff --git a/tests/components/husqvarna_automower/const.py b/tests/components/husqvarna_automower/const.py new file mode 100644 index 00000000000..7a00937291a --- /dev/null +++ b/tests/components/husqvarna_automower/const.py @@ -0,0 +1,4 @@ +"""Constants for Husqvarna Automower tests.""" +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" +USER_ID = "123" diff --git a/tests/components/husqvarna_automower/fixtures/jwt b/tests/components/husqvarna_automower/fixtures/jwt new file mode 100644 index 00000000000..b30ec36082e --- /dev/null +++ b/tests/components/husqvarna_automower/fixtures/jwt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVlZDU2ZDUzLTEyNWYtNDExZi04ZTFlLTNlNDRkMGVkOGJmOCJ9.eyJqdGkiOiI2MGYxNGQ1OS1iY2M4LTQwMzktYmMzOC0yNWRiMzc2MGQwNDciLCJpc3MiOiJodXNxdmFybmEiLCJyb2xlcyI6W10sImdyb3VwcyI6WyJhbWMiLCJkZXZlbG9wZXItcG9ydGFsIiwiZmQ3OGIzYTQtYTdmOS00Yzc2LWJlZjktYWE1YTUwNTgzMzgyIiwiZ2FyZGVuYS1teWFjY291bnQiLCJodXNxdmFybmEtY29ubmVjdCIsImh1c3F2YXJuYS1teXBhZ2VzIiwic21hcnRnYXJkZW4iXSwic2NvcGVzIjpbImlhbTpyZWFkIiwiYW1jOmFwaSJdLCJzY29wZSI6ImlhbTpyZWFkIGFtYzphcGkiLCJjbGllbnRfaWQiOiI0MzNlNWZkZi01MTI5LTQ1MmMteHh4eC1mYWRjZTMyMTMwNDIiLCJjdXN0b21lcl9pZCI6IjQ3NTU5OTc3MjA0NTh4eHh4IiwidXNlciI6eyJmaXJzdF9uYW1lIjoiSm9obiIsImxhc3RfbmFtZSI6IkRvZSIsImN1c3RvbV9hdHRyaWJ1dGVzIjp7ImhjX2NvdW50cnkiOiJERSJ9LCJjdXN0b21lcl9pZCI6IjQ3NTU5OTc3MjA0NTh4eHh4In0sImlhdCI6MTY5NzY2Njk0NywiZXhwIjoxNjk3NzUzMzQ3LCJzdWIiOiI1YTkzMTQxZS01NWE3LTQ3OWYtOTZlMi04YTYzMTg4YzA1NGYifQ.1O3FOoWHaWpo-PrW88097ai6nsUGlK2NWyqIDLkUl1BTatQoFhIA1nKmCthf6A9CAYeoPS4c8CBhqqLj-5VrJXfNc7pFZ1nAw69pT33Ku7_S9QqonPf_JRvWX8-A7sTCKXEkCTso6v_jbmiePK6C9_psClJx_PUgFFOoNaROZhSsAlq9Gftvzs9UTcd2UO9ohsku_Kpx480C0QCKRjm4LTrFTBpgijRPc3F0BnyfgW8rT3Trl290f3CyEzLk8k9bgGA0qDlAanKuNNKK1j7hwRsiq_28A7bWJzlLc6Wgrq8Pc2CnnMada_eXavkTu-VzB-q8_PGFkLyeG16CR-NXlox9mEB6NxTn5stYSMUkiTApAfgCwLuj4c_WCXnxUZn0VdnsswvaIZON3bTSOMATXLG8PFUyDOcDxHBV4LEDyTVspo-QblanTTBLFWMTfWIWApBmRO9OkiJrcq9g7T8hKNNImeN4skk2vIZVXkCq_cEOdVAG4099b1V8zXCBgtDc \ No newline at end of file diff --git a/tests/components/husqvarna_automower/fixtures/mower.json b/tests/components/husqvarna_automower/fixtures/mower.json new file mode 100644 index 00000000000..eec43698bf0 --- /dev/null +++ b/tests/components/husqvarna_automower/fixtures/mower.json @@ -0,0 +1,139 @@ +{ + "data": [ + { + "type": "mower", + "id": "c7233734-b219-4287-a173-08e3643f89f0", + "attributes": { + "system": { + "name": "Test Mower 1", + "model": "450XH-TEST", + "serialNumber": 123 + }, + "battery": { + "batteryPercent": 100 + }, + "capabilities": { + "headlights": true, + "workAreas": false, + "position": true, + "stayOutZones": false + }, + "mower": { + "mode": "MAIN_AREA", + "activity": "PARKED_IN_CS", + "state": "RESTRICTED", + "errorCode": 0, + "errorCodeTimestamp": 0 + }, + "calendar": { + "tasks": [ + { + "start": 1140, + "duration": 300, + "monday": true, + "tuesday": false, + "wednesday": true, + "thursday": false, + "friday": true, + "saturday": false, + "sunday": false + }, + { + "start": 0, + "duration": 480, + "monday": false, + "tuesday": true, + "wednesday": false, + "thursday": true, + "friday": false, + "saturday": true, + "sunday": false + } + ] + }, + "planner": { + "nextStartTimestamp": 1685991600000, + "override": { + "action": "NOT_ACTIVE" + }, + "restrictedReason": "WEEK_SCHEDULE" + }, + "metadata": { + "connected": true, + "statusTimestamp": 1697669932683 + }, + "positions": [ + { + "latitude": 35.5402913, + "longitude": -82.5527055 + }, + { + "latitude": 35.5407693, + "longitude": -82.5521503 + }, + { + "latitude": 35.5403241, + "longitude": -82.5522924 + }, + { + "latitude": 35.5406973, + "longitude": -82.5518579 + }, + { + "latitude": 35.5404659, + "longitude": -82.5516567 + }, + { + "latitude": 35.5406318, + "longitude": -82.5515709 + }, + { + "latitude": 35.5402477, + "longitude": -82.5519437 + }, + { + "latitude": 35.5403503, + "longitude": -82.5516889 + }, + { + "latitude": 35.5401429, + "longitude": -82.551536 + }, + { + "latitude": 35.5405489, + "longitude": -82.5512195 + }, + { + "latitude": 35.5404005, + "longitude": -82.5512115 + }, + { + "latitude": 35.5405969, + "longitude": -82.551418 + }, + { + "latitude": 35.5403437, + "longitude": -82.5523917 + }, + { + "latitude": 35.5403481, + "longitude": -82.5520054 + } + ], + "cuttingHeight": 4, + "headlight": { + "mode": "EVENING_ONLY" + }, + "statistics": { + "numberOfChargingCycles": 1380, + "numberOfCollisions": 11396, + "totalChargingTime": 4334400, + "totalCuttingTime": 4194000, + "totalDriveDistance": 1780272, + "totalRunningTime": 4564800, + "totalSearchingTime": 370800 + } + } + } + ] +} diff --git a/tests/components/husqvarna_automower/test_config_flow.py b/tests/components/husqvarna_automower/test_config_flow.py new file mode 100644 index 00000000000..fcf9fbffa0c --- /dev/null +++ b/tests/components/husqvarna_automower/test_config_flow.py @@ -0,0 +1,129 @@ +"""Test the Husqvarna Automower config flow.""" + +from unittest.mock import AsyncMock, patch + +from homeassistant import config_entries +from homeassistant.components.husqvarna_automower.const import ( + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import config_entry_oauth2_flow + +from . import setup_integration +from .const import CLIENT_ID, USER_ID + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker +from tests.typing import ClientSessionGenerator + + +async def test_full_flow( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock: AiohttpClientMocker, + current_request_with_host, + jwt, +) -> None: + """Check full flow.""" + result = await hass.config_entries.flow.async_init( + "husqvarna_automower", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.clear_requests() + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": jwt, + "scope": "iam:read amc:api", + "expires_in": 86399, + "refresh_token": "mock-refresh-token", + "provider": "husqvarna", + "user_id": "mock-user-id", + "token_type": "Bearer", + "expires_at": 1697753347, + }, + ) + + with patch( + "homeassistant.components.husqvarna_automower.async_setup_entry", + return_value=True, + ) as mock_setup: + await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 + + +async def test_config_non_unique_profile( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + current_request_with_host: None, + mock_config_entry: MockConfigEntry, + aioclient_mock: AiohttpClientMocker, + mock_automower_client: AsyncMock, + jwt, +) -> None: + """Test setup a non-unique profile.""" + await setup_integration(hass, mock_config_entry) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.clear_requests() + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": jwt, + "scope": "iam:read amc:api", + "expires_in": 86399, + "refresh_token": "mock-refresh-token", + "provider": "husqvarna", + "user_id": USER_ID, + "token_type": "Bearer", + "expires_at": 1697753347, + }, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/husqvarna_automower/test_init.py b/tests/components/husqvarna_automower/test_init.py new file mode 100644 index 00000000000..14460ad5d21 --- /dev/null +++ b/tests/components/husqvarna_automower/test_init.py @@ -0,0 +1,68 @@ +"""Tests for init module.""" +import http +import time +from unittest.mock import AsyncMock + +import pytest + +from homeassistant.components.husqvarna_automower.const import DOMAIN, OAUTH2_TOKEN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_load_unload_entry( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test load and unload entry.""" + await setup_integration(hass, mock_config_entry) + entry = hass.config_entries.async_entries(DOMAIN)[0] + + assert entry.state == ConfigEntryState.LOADED + + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + ("expires_at", "status", "expected_state"), + [ + ( + time.time() - 3600, + http.HTTPStatus.UNAUTHORIZED, + ConfigEntryState.SETUP_RETRY, # Will trigger reauth in the future + ), + ( + time.time() - 3600, + http.HTTPStatus.INTERNAL_SERVER_ERROR, + ConfigEntryState.SETUP_RETRY, + ), + ], + ids=["unauthorized", "internal_server_error"], +) +async def test_expired_token_refresh_failure( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + aioclient_mock: AiohttpClientMocker, + status: http.HTTPStatus, + expected_state: ConfigEntryState, +) -> None: + """Test failure while refreshing token with a transient error.""" + + aioclient_mock.clear_requests() + aioclient_mock.post( + OAUTH2_TOKEN, + status=status, + ) + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is expected_state diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py new file mode 100644 index 00000000000..38b8f2901ce --- /dev/null +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -0,0 +1,88 @@ +"""Tests for lawn_mower module.""" +from datetime import timedelta +from unittest.mock import AsyncMock + +from aioautomower.exceptions import ApiException +from aioautomower.utils import mower_list_to_dictionary_dataclass +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.components.lawn_mower import LawnMowerActivity +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import setup_integration + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_value_fixture, +) + +TEST_MOWER_ID = "c7233734-b219-4287-a173-08e3643f89f0" + + +async def test_lawn_mower_states( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test lawn_mower state.""" + values = mower_list_to_dictionary_dataclass( + load_json_value_fixture("mower.json", DOMAIN) + ) + await setup_integration(hass, mock_config_entry) + state = hass.states.get("lawn_mower.test_mower_1") + assert state is not None + assert state.state == LawnMowerActivity.DOCKED + + for activity, state, expected_state in [ + ("UNKNOWN", "PAUSED", LawnMowerActivity.PAUSED), + ("MOWING", "NOT_APPLICABLE", LawnMowerActivity.MOWING), + ("NOT_APPLICABLE", "ERROR", LawnMowerActivity.ERROR), + ]: + values[TEST_MOWER_ID].mower.activity = activity + values[TEST_MOWER_ID].mower.state = state + mock_automower_client.get_status.return_value = values + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("lawn_mower.test_mower_1") + assert state.state == expected_state + + +@pytest.mark.parametrize( + ("aioautomower_command", "service"), + [ + ("resume_schedule", "start_mowing"), + ("pause_mowing", "pause"), + ("park_until_next_schedule", "dock"), + ], +) +async def test_lawn_mower_commands( + hass: HomeAssistant, + aioautomower_command: str, + service: str, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test lawn_mower commands.""" + await setup_integration(hass, mock_config_entry) + + getattr(mock_automower_client, aioautomower_command).side_effect = ApiException( + "Test error" + ) + + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + domain="lawn_mower", + service=service, + service_data={"entity_id": "lawn_mower.test_mower_1"}, + blocking=True, + ) + assert ( + str(exc_info.value) + == "Command couldn't be sent to the command queue: Test error" + ) From 4e2f5997203f3dc80c3fe81c9396a4d39ef1930b Mon Sep 17 00:00:00 2001 From: Jiayi Chen Date: Wed, 7 Feb 2024 09:35:50 +0100 Subject: [PATCH 0376/1367] Update Growatt server URLs (#109122) --- .coveragerc | 1 + homeassistant/components/growatt_server/const.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index fa0bf2fbd4c..d8330e13fdf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -484,6 +484,7 @@ omit = homeassistant/components/gpsd/sensor.py homeassistant/components/greenwave/light.py homeassistant/components/growatt_server/__init__.py + homeassistant/components/growatt_server/const.py homeassistant/components/growatt_server/sensor.py homeassistant/components/growatt_server/sensor_types/* homeassistant/components/gstreamer/media_player.py diff --git a/homeassistant/components/growatt_server/const.py b/homeassistant/components/growatt_server/const.py index 4e548ef2c2a..e4e7c638fa3 100644 --- a/homeassistant/components/growatt_server/const.py +++ b/homeassistant/components/growatt_server/const.py @@ -8,13 +8,16 @@ DEFAULT_PLANT_ID = "0" DEFAULT_NAME = "Growatt" SERVER_URLS = [ - "https://server-api.growatt.com/", - "https://server-us.growatt.com/", - "http://server.smten.com/", + "https://openapi.growatt.com/", # Other regional server + "https://openapi-cn.growatt.com/", # Chinese server + "https://openapi-us.growatt.com/", # North American server + "http://server.smten.com/", # smten server ] DEPRECATED_URLS = [ "https://server.growatt.com/", + "https://server-api.growatt.com/", + "https://server-us.growatt.com/", ] DEFAULT_URL = SERVER_URLS[0] From 2ae019bfc24e18fc805e207b93d8078d32e351da Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:56:42 +0100 Subject: [PATCH 0377/1367] Update syrupy to 4.6.1 (#109860) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 1f9dda7cc44..5b970b68a13 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -31,7 +31,7 @@ pytest-xdist==3.3.1 pytest==7.4.4 requests-mock==1.11.0 respx==0.20.2 -syrupy==4.6.0 +syrupy==4.6.1 tqdm==4.66.1 types-aiofiles==23.2.0.20240106 types-atomicwrites==1.4.5.1 From 059c251194252788eeb86dc27072f532151d9cae Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 7 Feb 2024 20:27:10 +1100 Subject: [PATCH 0378/1367] Bump aio-georss-gdacs to 0.9 (#109859) --- homeassistant/components/gdacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gdacs/manifest.json b/homeassistant/components/gdacs/manifest.json index b6fb3d8cee3..d743dd00424 100644 --- a/homeassistant/components/gdacs/manifest.json +++ b/homeassistant/components/gdacs/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aio_georss_gdacs", "aio_georss_client"], "quality_scale": "platinum", - "requirements": ["aio-georss-gdacs==0.8"] + "requirements": ["aio-georss-gdacs==0.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 109de7320b3..dc8cdbe7c94 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aio-geojson-nsw-rfs-incidents==0.7 aio-geojson-usgs-earthquakes==0.2 # homeassistant.components.gdacs -aio-georss-gdacs==0.8 +aio-georss-gdacs==0.9 # homeassistant.components.airq aioairq==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed62d2c1b02..a8166cd8a57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ aio-geojson-nsw-rfs-incidents==0.7 aio-geojson-usgs-earthquakes==0.2 # homeassistant.components.gdacs -aio-georss-gdacs==0.8 +aio-georss-gdacs==0.9 # homeassistant.components.airq aioairq==0.3.2 From 1a037da9f5fda9f67472c05c42a7077434303803 Mon Sep 17 00:00:00 2001 From: dcmeglio <21957250+dcmeglio@users.noreply.github.com> Date: Wed, 7 Feb 2024 04:47:40 -0500 Subject: [PATCH 0379/1367] Add sensor for Ecowitt raw soil moisture value (#109849) --- homeassistant/components/ecowitt/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/ecowitt/sensor.py b/homeassistant/components/ecowitt/sensor.py index 6d048cc423d..4bcdd2461cd 100644 --- a/homeassistant/components/ecowitt/sensor.py +++ b/homeassistant/components/ecowitt/sensor.py @@ -176,6 +176,12 @@ ECOWITT_SENSORS_MAPPING: Final = { native_unit_of_measurement=UnitOfLength.MILES, state_class=SensorStateClass.MEASUREMENT, ), + EcoWittSensorTypes.SOIL_RAWADC: SensorEntityDescription( + key="SOIL_RAWADC", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), EcoWittSensorTypes.SPEED_KPH: SensorEntityDescription( key="SPEED_KPH", device_class=SensorDeviceClass.WIND_SPEED, From 521e9eb869fdd41c853cb8231b5b8632f14b966c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 7 Feb 2024 12:29:06 +0100 Subject: [PATCH 0380/1367] Update frontend to 20240207.0 (#109871) --- 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 1af6a9da7b0..d998871a60b 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==20240205.0"] + "requirements": ["home-assistant-frontend==20240207.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 63f424b5116..dd1ca886b4e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ habluetooth==2.4.0 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240205.0 +home-assistant-frontend==20240207.0 home-assistant-intents==2024.2.2 httpx==0.26.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index dc8cdbe7c94..bfebc4817c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1062,7 +1062,7 @@ hole==0.8.0 holidays==0.42 # homeassistant.components.frontend -home-assistant-frontend==20240205.0 +home-assistant-frontend==20240207.0 # homeassistant.components.conversation home-assistant-intents==2024.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a8166cd8a57..bda6e5ba53b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -858,7 +858,7 @@ hole==0.8.0 holidays==0.42 # homeassistant.components.frontend -home-assistant-frontend==20240205.0 +home-assistant-frontend==20240207.0 # homeassistant.components.conversation home-assistant-intents==2024.2.2 From 34220200c10bc35d85c68d0c091634cf2cf80dbf Mon Sep 17 00:00:00 2001 From: Matrix Date: Wed, 7 Feb 2024 20:42:33 +0800 Subject: [PATCH 0381/1367] Fix YoLink SpeakerHub support (#107925) * improve * Fix when hub offline/online message pushing * fix as suggestion * check config entry load state * Add exception translation --- homeassistant/components/yolink/__init__.py | 14 ++++++++++++-- homeassistant/components/yolink/number.py | 18 ++++++++++++++++-- homeassistant/components/yolink/services.py | 16 ++++++++++++++-- homeassistant/components/yolink/services.yaml | 2 -- homeassistant/components/yolink/strings.json | 5 +++++ 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 01395fd5f5f..270bd550038 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -19,8 +19,10 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, config_entry_oauth2_flow, + config_validation as cv, device_registry as dr, ) +from homeassistant.helpers.typing import ConfigType from . import api from .const import DOMAIN, YOLINK_EVENT @@ -30,6 +32,8 @@ from .services import async_register_services SCAN_INTERVAL = timedelta(minutes=5) +CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) + PLATFORMS = [ Platform.BINARY_SENSOR, @@ -96,6 +100,14 @@ class YoLinkHomeStore: device_coordinators: dict[str, YoLinkCoordinator] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up YoLink.""" + + async_register_services(hass) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up yolink from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -147,8 +159,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - async_register_services(hass, entry) - async def async_yolink_unload(event) -> None: """Unload yolink.""" await yolink_home.async_unload() diff --git a/homeassistant/components/yolink/number.py b/homeassistant/components/yolink/number.py index 1ec20cd4d17..a7ba89e1f6c 100644 --- a/homeassistant/components/yolink/number.py +++ b/homeassistant/components/yolink/number.py @@ -4,6 +4,7 @@ from __future__ import annotations 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_SPEAKER_HUB @@ -30,6 +31,7 @@ class YoLinkNumberTypeConfigEntityDescription(NumberEntityDescription): """YoLink NumberEntity description.""" exists_fn: Callable[[YoLinkDevice], bool] + should_update_entity: Callable value: Callable @@ -37,6 +39,14 @@ NUMBER_TYPE_CONF_SUPPORT_DEVICES = [ATTR_DEVICE_SPEAKER_HUB] SUPPORT_SET_VOLUME_DEVICES = [ATTR_DEVICE_SPEAKER_HUB] + +def get_volume_value(state: dict[str, Any]) -> int | None: + """Get volume option.""" + if (options := state.get("options")) is not None: + return options.get("volume") + return None + + DEVICE_CONFIG_DESCRIPTIONS: tuple[YoLinkNumberTypeConfigEntityDescription, ...] = ( YoLinkNumberTypeConfigEntityDescription( key=OPTIONS_VALUME, @@ -48,7 +58,8 @@ DEVICE_CONFIG_DESCRIPTIONS: tuple[YoLinkNumberTypeConfigEntityDescription, ...] native_unit_of_measurement=None, icon="mdi:volume-high", exists_fn=lambda device: device.device_type in SUPPORT_SET_VOLUME_DEVICES, - value=lambda state: state["options"]["volume"], + should_update_entity=lambda value: value is not None, + value=get_volume_value, ), ) @@ -98,7 +109,10 @@ class YoLinkNumberTypeConfigEntity(YoLinkEntity, NumberEntity): @callback def update_entity_state(self, state: dict) -> None: """Update HA Entity State.""" - attr_val = self.entity_description.value(state) + if ( + attr_val := self.entity_description.value(state) + ) is None and self.entity_description.should_update_entity(attr_val) is False: + return self._attr_native_value = attr_val self.async_write_ha_state() diff --git a/homeassistant/components/yolink/services.py b/homeassistant/components/yolink/services.py index bb2c660ef56..e41e3dce260 100644 --- a/homeassistant/components/yolink/services.py +++ b/homeassistant/components/yolink/services.py @@ -3,8 +3,9 @@ import voluptuous as vol from yolink.client_request import ClientRequest -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv, device_registry as dr from .const import ( @@ -19,7 +20,7 @@ from .const import ( SERVICE_PLAY_ON_SPEAKER_HUB = "play_on_speaker_hub" -def async_register_services(hass: HomeAssistant, entry: ConfigEntry) -> None: +def async_register_services(hass: HomeAssistant) -> None: """Register services for YoLink integration.""" async def handle_speaker_hub_play_call(service_call: ServiceCall) -> None: @@ -28,6 +29,17 @@ def async_register_services(hass: HomeAssistant, entry: ConfigEntry) -> None: device_registry = dr.async_get(hass) device_entry = device_registry.async_get(service_data[ATTR_TARGET_DEVICE]) if device_entry is not None: + for entry_id in device_entry.config_entries: + if (entry := hass.config_entries.async_get_entry(entry_id)) is None: + continue + if entry.domain == DOMAIN: + break + if entry is None or entry.state == ConfigEntryState.NOT_LOADED: + raise ServiceValidationError( + "Config entry not found or not loaded!", + translation_domain=DOMAIN, + translation_key="invalid_config_entry", + ) home_store = hass.data[DOMAIN][entry.entry_id] for identifier in device_entry.identifiers: if ( diff --git a/homeassistant/components/yolink/services.yaml b/homeassistant/components/yolink/services.yaml index 939eba3e7f5..5f7a3ec3122 100644 --- a/homeassistant/components/yolink/services.yaml +++ b/homeassistant/components/yolink/services.yaml @@ -7,9 +7,7 @@ play_on_speaker_hub: device: filter: - integration: yolink - manufacturer: YoLink model: SpeakerHub - message: required: true example: hello, yolink diff --git a/homeassistant/components/yolink/strings.json b/homeassistant/components/yolink/strings.json index 9661abe096c..83e712328f9 100644 --- a/homeassistant/components/yolink/strings.json +++ b/homeassistant/components/yolink/strings.json @@ -37,6 +37,11 @@ "button_4_long_press": "Button_4 (long press)" } }, + "exceptions": { + "invalid_config_entry": { + "message": "Config entry not found or not loaded!" + } + }, "entity": { "switch": { "usb_ports": { "name": "USB ports" }, From 5d1da0b3e4f52e2eb8d58d2d78ec5d73d252f1f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 7 Feb 2024 15:26:00 +0100 Subject: [PATCH 0382/1367] Remove soft hyphens from myuplink sensor names (#109845) Remove soft hyphens from sensor names --- homeassistant/components/myuplink/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/myuplink/sensor.py b/homeassistant/components/myuplink/sensor.py index 916cf723866..6206fc3b759 100644 --- a/homeassistant/components/myuplink/sensor.py +++ b/homeassistant/components/myuplink/sensor.py @@ -91,7 +91,7 @@ class MyUplinkDevicePointSensor(MyUplinkEntity, SensorEntity): # Internal properties self.point_id = device_point.parameter_id - self._attr_name = device_point.parameter_name + self._attr_name = device_point.parameter_name.replace("\u002d", "") if entity_description is not None: self.entity_description = entity_description From 2e194c4ec3c342aa195f3be7a58bb90166efeb58 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 7 Feb 2024 15:39:36 +0100 Subject: [PATCH 0383/1367] Fix light color mode in tplink (#109831) --- homeassistant/components/tplink/light.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 87d30e4f76a..e27ee7de49f 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -163,6 +163,7 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): _attr_supported_features = LightEntityFeature.TRANSITION _attr_name = None + _fixed_color_mode: ColorMode | None = None device: SmartBulb @@ -193,6 +194,9 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): if device.is_dimmable: modes.add(ColorMode.BRIGHTNESS) self._attr_supported_color_modes = filter_supported_color_modes(modes) + if len(self._attr_supported_color_modes) == 1: + # If the light supports only a single color mode, set it now + self._fixed_color_mode = next(iter(self._attr_supported_color_modes)) self._async_update_attrs() @callback @@ -273,14 +277,14 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): def _determine_color_mode(self) -> ColorMode: """Return the active color mode.""" - if self.device.is_color: - if self.device.is_variable_color_temp and self.device.color_temp: - return ColorMode.COLOR_TEMP - return ColorMode.HS - if self.device.is_variable_color_temp: - return ColorMode.COLOR_TEMP + if self._fixed_color_mode: + # The light supports only a single color mode, return it + return self._fixed_color_mode - return ColorMode.BRIGHTNESS + # The light supports both color temp and color, determine which on is active + if self.device.is_variable_color_temp and self.device.color_temp: + return ColorMode.COLOR_TEMP + return ColorMode.HS @callback def _async_update_attrs(self) -> None: From aea81a180c21bf5dd7d5de17a35046811f0d85b3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 7 Feb 2024 15:39:49 +0100 Subject: [PATCH 0384/1367] Fix Shelly white light test (#109855) --- tests/components/shelly/conftest.py | 18 ++++++++++++++++++ tests/components/shelly/test_light.py | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 9d7bb9404f8..0ad32409945 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -65,6 +65,24 @@ def mock_light_set_state( } +def mock_white_light_set_state( + turn="on", + temp=4050, + gain=19, + brightness=128, + transition=0, +): + """Mock white light block set_state.""" + return { + "ison": turn == "on", + "mode": "white", + "gain": gain, + "temp": temp, + "brightness": brightness, + "transition": transition, + } + + MOCK_BLOCKS = [ Mock( sensor_ids={ diff --git a/tests/components/shelly/test_light.py b/tests/components/shelly/test_light.py index 77b65ad3bb5..c27a1199d4b 100644 --- a/tests/components/shelly/test_light.py +++ b/tests/components/shelly/test_light.py @@ -1,4 +1,6 @@ """Tests for Shelly light platform.""" +from unittest.mock import AsyncMock + from aioshelly.const import ( MODEL_BULB, MODEL_BULB_RGBW, @@ -35,6 +37,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from . import init_integration, mutate_rpc_device_status +from .conftest import mock_white_light_set_state RELAY_BLOCK_ID = 0 LIGHT_BLOCK_ID = 2 @@ -227,6 +230,11 @@ async def test_block_device_white_bulb( monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "mode") monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "colorTemp") monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "effect") + monkeypatch.setattr( + mock_block_device.blocks[LIGHT_BLOCK_ID], + "set_state", + AsyncMock(side_effect=mock_white_light_set_state), + ) await init_integration(hass, 1, model=MODEL_VINTAGE_V2) # Test initial From 1ea9b1a158d1aaf356101edb587260a9ec164fe1 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 7 Feb 2024 15:19:42 +0000 Subject: [PATCH 0385/1367] Add support for air purifiers to HomeKit Device (#109880) --- .../components/homekit_controller/const.py | 3 + .../components/homekit_controller/fan.py | 1 + .../components/homekit_controller/select.py | 15 +- .../components/homekit_controller/sensor.py | 29 +++- .../homekit_controller/strings.json | 13 ++ .../snapshots/test_init.ambr | 136 ++++++++++++++++++ 6 files changed, 192 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 939657eb8a5..aea5a6661ee 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -55,6 +55,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { ServicesTypes.DOORBELL: "event", ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH: "event", ServicesTypes.SERVICE_LABEL: "event", + ServicesTypes.AIR_PURIFIER: "fan", } CHARACTERISTIC_PLATFORMS = { @@ -104,6 +105,8 @@ CHARACTERISTIC_PLATFORMS = { CharacteristicsTypes.FILTER_LIFE_LEVEL: "sensor", CharacteristicsTypes.VENDOR_AIRVERSA_SLEEP_MODE: "switch", CharacteristicsTypes.TEMPERATURE_UNITS: "select", + CharacteristicsTypes.AIR_PURIFIER_STATE_CURRENT: "sensor", + CharacteristicsTypes.AIR_PURIFIER_STATE_TARGET: "select", } STARTUP_EXCEPTIONS = ( diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index d87b6ab3e39..1b2d572f2b6 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -206,6 +206,7 @@ class HomeKitFanV2(BaseHomeKitFan): ENTITY_TYPES = { ServicesTypes.FAN: HomeKitFanV1, ServicesTypes.FAN_V2: HomeKitFanV2, + ServicesTypes.AIR_PURIFIER: HomeKitFanV2, } diff --git a/homeassistant/components/homekit_controller/select.py b/homeassistant/components/homekit_controller/select.py index e6eae1c51ca..c3185bcba55 100644 --- a/homeassistant/components/homekit_controller/select.py +++ b/homeassistant/components/homekit_controller/select.py @@ -5,7 +5,10 @@ from dataclasses import dataclass from enum import IntEnum from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes -from aiohomekit.model.characteristics.const import TemperatureDisplayUnits +from aiohomekit.model.characteristics.const import ( + TargetAirPurifierStateValues, + TemperatureDisplayUnits, +) from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry @@ -47,6 +50,16 @@ SELECT_ENTITIES: dict[str, HomeKitSelectEntityDescription] = { "fahrenheit": TemperatureDisplayUnits.FAHRENHEIT, }, ), + CharacteristicsTypes.AIR_PURIFIER_STATE_TARGET: HomeKitSelectEntityDescription( + key="air_purifier_state_target", + translation_key="air_purifier_state_target", + name="Air Purifier Mode", + entity_category=EntityCategory.CONFIG, + choices={ + "automatic": TargetAirPurifierStateValues.AUTOMATIC, + "manual": TargetAirPurifierStateValues.MANUAL, + }, + ), } _ECOBEE_MODE_TO_TEXT = { diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index ebfba110e48..26476417a56 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -3,10 +3,15 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from enum import IntEnum from aiohomekit.model import Accessory, Transport from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes -from aiohomekit.model.characteristics.const import ThreadNodeCapabilities, ThreadStatus +from aiohomekit.model.characteristics.const import ( + CurrentAirPurifierStateValues, + ThreadNodeCapabilities, + ThreadStatus, +) from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.bluetooth import ( @@ -52,6 +57,7 @@ class HomeKitSensorEntityDescription(SensorEntityDescription): probe: Callable[[Characteristic], bool] | None = None format: Callable[[Characteristic], str] | None = None + enum: dict[IntEnum, str] | None = None def thread_node_capability_to_str(char: Characteristic) -> str: @@ -324,6 +330,18 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { ], translation_key="thread_status", ), + CharacteristicsTypes.AIR_PURIFIER_STATE_CURRENT: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.AIR_PURIFIER_STATE_CURRENT, + name="Air Purifier Status", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + enum={ + CurrentAirPurifierStateValues.INACTIVE: "inactive", + CurrentAirPurifierStateValues.IDLE: "idle", + CurrentAirPurifierStateValues.ACTIVE: "purifying", + }, + translation_key="air_purifier_state_current", + ), CharacteristicsTypes.VENDOR_NETATMO_NOISE: HomeKitSensorEntityDescription( key=CharacteristicsTypes.VENDOR_NETATMO_NOISE, name="Noise", @@ -535,6 +553,8 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): ) -> None: """Initialise a secondary HomeKit characteristic sensor.""" self.entity_description = description + if self.entity_description.enum: + self._attr_options = list(self.entity_description.enum.values()) super().__init__(conn, info, char) def get_characteristic_types(self) -> list[str]: @@ -551,10 +571,11 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): @property def native_value(self) -> str | int | float: """Return the current sensor value.""" - val = self._char.value + if self.entity_description.enum: + return self.entity_description.enum[self._char.value] if self.entity_description.format: - return self.entity_description.format(val) - return val + return self.entity_description.format(self._char) + return self._char.value ENTITY_TYPES = { diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index 998c375aac1..d1205645fd3 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -108,6 +108,12 @@ "celsius": "Celsius", "fahrenheit": "Fahrenheit" } + }, + "air_purifier_state_target": { + "state": { + "automatic": "Automatic", + "manual": "Manual" + } } }, "sensor": { @@ -131,6 +137,13 @@ "leader": "Leader", "router": "Router" } + }, + "air_purifier_state_current": { + "state": { + "inactive": "Inactive", + "idle": "Idle", + "purifying": "Purifying" + } } } } diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 29b71d18422..1007bd70370 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -101,6 +101,142 @@ 'state': 'unknown', }), }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': None, + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.airversa_ap2_1808_airpurifier', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Airversa AP2 1808 AirPurifier', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_32832', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'Airversa AP2 1808 AirPurifier', + 'percentage': 0, + 'percentage_step': 20.0, + 'preset_mode': None, + 'preset_modes': None, + 'supported_features': , + }), + 'entity_id': 'fan.airversa_ap2_1808_airpurifier', + 'state': 'off', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'automatic', + 'manual', + ]), + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airversa_ap2_1808_air_purifier_mode', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Airversa AP2 1808 Air Purifier Mode', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'air_purifier_state_target', + 'unique_id': '00:00:00:00:00:00_1_32832_32837', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'Airversa AP2 1808 Air Purifier Mode', + 'options': list([ + 'automatic', + 'manual', + ]), + }), + 'entity_id': 'select.airversa_ap2_1808_air_purifier_mode', + 'state': 'automatic', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'inactive', + 'idle', + 'purifying', + ]), + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airversa_ap2_1808_air_purifier_status', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Airversa AP2 1808 Air Purifier Status', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'air_purifier_state_current', + 'unique_id': '00:00:00:00:00:00_1_32832_32836', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'enum', + 'friendly_name': 'Airversa AP2 1808 Air Purifier Status', + 'options': list([ + 'inactive', + 'idle', + 'purifying', + ]), + }), + 'entity_id': 'sensor.airversa_ap2_1808_air_purifier_status', + 'state': 'inactive', + }), + }), dict({ 'entry': dict({ 'aliases': list([ From 8dd1e741b2b69831918122a61e826211c52beca2 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Thu, 8 Feb 2024 02:24:25 +1100 Subject: [PATCH 0386/1367] Bump aio-geojson-geonetnz-quakes to 0.16 (#109873) --- homeassistant/components/geonetnz_quakes/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index 9ed59b2bc97..2314dabcf0f 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aio_geojson_geonetnz_quakes"], "quality_scale": "platinum", - "requirements": ["aio-geojson-geonetnz-quakes==0.15"] + "requirements": ["aio-geojson-geonetnz-quakes==0.16"] } diff --git a/requirements_all.txt b/requirements_all.txt index bfebc4817c9..91e62aa514d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -170,7 +170,7 @@ agent-py==0.0.23 aio-geojson-generic-client==0.4 # homeassistant.components.geonetnz_quakes -aio-geojson-geonetnz-quakes==0.15 +aio-geojson-geonetnz-quakes==0.16 # homeassistant.components.geonetnz_volcano aio-geojson-geonetnz-volcano==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bda6e5ba53b..04361c87f40 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -149,7 +149,7 @@ agent-py==0.0.23 aio-geojson-generic-client==0.4 # homeassistant.components.geonetnz_quakes -aio-geojson-geonetnz-quakes==0.15 +aio-geojson-geonetnz-quakes==0.16 # homeassistant.components.geonetnz_volcano aio-geojson-geonetnz-volcano==0.8 From d0384480f52797b650e37164ed488152133b7bd1 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 7 Feb 2024 17:18:00 +0100 Subject: [PATCH 0387/1367] Test unique IDs for Shelly entities (#109879) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- tests/components/shelly/conftest.py | 2 + tests/components/shelly/test_binary_sensor.py | 54 +++++-- tests/components/shelly/test_button.py | 30 +++- tests/components/shelly/test_climate.py | 6 +- tests/components/shelly/test_cover.py | 49 +++--- tests/components/shelly/test_event.py | 22 +-- tests/components/shelly/test_light.py | 133 +++++++++++----- tests/components/shelly/test_number.py | 13 +- tests/components/shelly/test_sensor.py | 84 +++++++++-- tests/components/shelly/test_switch.py | 12 +- tests/components/shelly/test_update.py | 142 +++++++++--------- 11 files changed, 359 insertions(+), 188 deletions(-) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 0ad32409945..194ee57e13b 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -104,6 +104,7 @@ MOCK_BLOCKS = [ sensor_ids={"roller": "stop", "rollerPos": 0}, channel="1", type="roller", + description="roller_0", set_state=AsyncMock( side_effect=lambda go, roller_pos=0: { "current_pos": roller_pos, @@ -118,6 +119,7 @@ MOCK_BLOCKS = [ colorTemp=mock_light_set_state()["temp"], **mock_light_set_state(), type="light", + description="light_0", set_state=AsyncMock(side_effect=mock_light_set_state), ), Mock( diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index 8a5e0108ad7..8a6b5acb971 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -6,7 +6,6 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI from homeassistant.components.shelly.const import SLEEP_PERIOD_MULTIPLIER from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State -from homeassistant.helpers.entity_registry import async_get from . import ( init_integration, @@ -23,7 +22,7 @@ SENSOR_BLOCK_ID = 3 async def test_block_binary_sensor( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, mock_block_device, monkeypatch, entity_registry ) -> None: """Test block binary sensor.""" entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_channel_1_overpowering" @@ -36,9 +35,13 @@ async def test_block_binary_sensor( assert hass.states.get(entity_id).state == STATE_ON + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-relay_0-overpower" + async def test_block_binary_sensor_extra_state_attr( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, mock_block_device, monkeypatch, entity_registry ) -> None: """Test block binary sensor extra state attributes.""" entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_gas" @@ -55,9 +58,17 @@ async def test_block_binary_sensor_extra_state_attr( assert state.state == STATE_OFF assert state.attributes.get("detected") == "none" + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-sensor_0-gas" + async def test_block_rest_binary_sensor( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_block_device, + monkeypatch, + entity_registry, ) -> None: """Test block REST binary sensor.""" entity_id = register_entity(hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud") @@ -71,9 +82,17 @@ async def test_block_rest_binary_sensor( assert hass.states.get(entity_id).state == STATE_ON + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-cloud" + async def test_block_rest_binary_sensor_connected_battery_devices( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_block_device, + monkeypatch, + entity_registry, ) -> None: """Test block REST binary sensor for connected battery devices.""" entity_id = register_entity(hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud") @@ -94,9 +113,13 @@ async def test_block_rest_binary_sensor_connected_battery_devices( await mock_rest_update(hass, freezer, seconds=SLEEP_PERIOD_MULTIPLIER * 3600) assert hass.states.get(entity_id).state == STATE_ON + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-cloud" + async def test_block_sleeping_binary_sensor( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, mock_block_device, monkeypatch, entity_registry ) -> None: """Test block sleeping binary sensor.""" entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_motion" @@ -116,6 +139,10 @@ async def test_block_sleeping_binary_sensor( assert hass.states.get(entity_id).state == STATE_ON + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-sensor_0-motion" + async def test_block_restored_sleeping_binary_sensor( hass: HomeAssistant, mock_block_device, device_reg, monkeypatch @@ -165,7 +192,7 @@ async def test_block_restored_sleeping_binary_sensor_no_last_state( async def test_rpc_binary_sensor( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, mock_rpc_device, monkeypatch, entity_registry ) -> None: """Test RPC binary sensor.""" entity_id = f"{BINARY_SENSOR_DOMAIN}.test_cover_0_overpowering" @@ -180,12 +207,15 @@ async def test_rpc_binary_sensor( assert hass.states.get(entity_id).state == STATE_ON + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-cover:0-overpower" + async def test_rpc_binary_sensor_removal( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, mock_rpc_device, monkeypatch, entity_registry ) -> None: """Test RPC binary sensor is removed due to removal_condition.""" - entity_registry = async_get(hass) entity_id = register_entity( hass, BINARY_SENSOR_DOMAIN, "test_cover_0_input", "input:0-input" ) @@ -199,7 +229,7 @@ async def test_rpc_binary_sensor_removal( async def test_rpc_sleeping_binary_sensor( - hass: HomeAssistant, mock_rpc_device, device_reg, monkeypatch + hass: HomeAssistant, mock_rpc_device, monkeypatch, entity_registry ) -> None: """Test RPC online sleeping binary sensor.""" entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_cloud" @@ -226,6 +256,10 @@ async def test_rpc_sleeping_binary_sensor( assert state assert state.state == STATE_ON + entry = entity_registry.async_get("binary_sensor.test_name_external_power") + assert entry + assert entry.unique_id == "123456789ABC-devicepower:0-external_power" + async def test_rpc_restored_sleeping_binary_sensor( hass: HomeAssistant, mock_rpc_device, device_reg, monkeypatch diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py index 42fa83b32a1..cf46038b85c 100644 --- a/tests/components/shelly/test_button.py +++ b/tests/components/shelly/test_button.py @@ -12,33 +12,49 @@ from homeassistant.helpers import entity_registry as er from . import init_integration -async def test_block_button(hass: HomeAssistant, mock_block_device) -> None: +async def test_block_button( + hass: HomeAssistant, mock_block_device, entity_registry +) -> None: """Test block device reboot button.""" await init_integration(hass, 1) + entity_id = "button.test_name_reboot" + # reboot button - assert hass.states.get("button.test_name_reboot").state == STATE_UNKNOWN + assert hass.states.get(entity_id).state == STATE_UNKNOWN + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC_reboot" await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.test_name_reboot"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) assert mock_block_device.trigger_reboot.call_count == 1 -async def test_rpc_button(hass: HomeAssistant, mock_rpc_device) -> None: +async def test_rpc_button( + hass: HomeAssistant, mock_rpc_device, entity_registry +) -> None: """Test rpc device OTA button.""" await init_integration(hass, 2) + entity_id = "button.test_name_reboot" + # reboot button - assert hass.states.get("button.test_name_reboot").state == STATE_UNKNOWN + assert hass.states.get(entity_id).state == STATE_UNKNOWN + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC_reboot" await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.test_name_reboot"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) assert mock_rpc_device.trigger_reboot.call_count == 1 @@ -56,6 +72,7 @@ async def test_migrate_unique_id( hass: HomeAssistant, mock_block_device, mock_rpc_device, + entity_registry, caplog: pytest.LogCaptureFixture, gen: int, old_unique_id: str, @@ -65,7 +82,6 @@ async def test_migrate_unique_id( """Test migration of unique_id.""" entry = await init_integration(hass, gen, skip_setup=True) - entity_registry = er.async_get(hass) entity: er.RegistryEntry = entity_registry.async_get_or_create( suggested_object_id="test_name_reboot", disabled_by=None, diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 28235325af4..1f2e1728508 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -43,7 +43,7 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.test_name" async def test_climate_hvac_mode( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, mock_block_device, monkeypatch, entity_registry ) -> None: """Test climate hvac mode service.""" monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") @@ -65,6 +65,10 @@ async def test_climate_hvac_mode( state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.OFF + entry = entity_registry.async_get(ENTITY_ID) + assert entry + assert entry.unique_id == "123456789ABC-sensor_0" + # Test set hvac mode heat await hass.services.async_call( CLIMATE_DOMAIN, diff --git a/tests/components/shelly/test_cover.py b/tests/components/shelly/test_cover.py index 08c0c76d35e..4ffe1f19818 100644 --- a/tests/components/shelly/test_cover.py +++ b/tests/components/shelly/test_cover.py @@ -25,44 +25,49 @@ ROLLER_BLOCK_ID = 1 async def test_block_device_services( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, mock_block_device, monkeypatch, entity_registry ) -> None: """Test block device cover services.""" + entity_id = "cover.test_name" monkeypatch.setitem(mock_block_device.settings, "mode", "roller") await init_integration(hass, 1) await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.test_name", ATTR_POSITION: 50}, + {ATTR_ENTITY_ID: entity_id, ATTR_POSITION: 50}, blocking=True, ) - state = hass.states.get("cover.test_name") + state = hass.states.get(entity_id) assert state.attributes[ATTR_CURRENT_POSITION] == 50 await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.test_name"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - assert hass.states.get("cover.test_name").state == STATE_OPENING + assert hass.states.get(entity_id).state == STATE_OPENING await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.test_name"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - assert hass.states.get("cover.test_name").state == STATE_CLOSING + assert hass.states.get(entity_id).state == STATE_CLOSING await hass.services.async_call( COVER_DOMAIN, SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.test_name"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - assert hass.states.get("cover.test_name").state == STATE_CLOSED + assert hass.states.get(entity_id).state == STATE_CLOSED + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-roller_0" async def test_block_device_update( @@ -89,18 +94,22 @@ async def test_block_device_no_roller_blocks( async def test_rpc_device_services( - hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, + entity_registry, ) -> None: """Test RPC device cover services.""" + entity_id = "cover.test_cover_0" await init_integration(hass, 2) await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.test_cover_0", ATTR_POSITION: 50}, + {ATTR_ENTITY_ID: entity_id, ATTR_POSITION: 50}, blocking=True, ) - state = hass.states.get("cover.test_cover_0") + state = hass.states.get(entity_id) assert state.attributes[ATTR_CURRENT_POSITION] == 50 mutate_rpc_device_status( @@ -109,11 +118,11 @@ async def test_rpc_device_services( await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.test_cover_0"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("cover.test_cover_0").state == STATE_OPENING + assert hass.states.get(entity_id).state == STATE_OPENING mutate_rpc_device_status( monkeypatch, mock_rpc_device, "cover:0", "state", "closing" @@ -121,21 +130,25 @@ async def test_rpc_device_services( await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.test_cover_0"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("cover.test_cover_0").state == STATE_CLOSING + assert hass.states.get(entity_id).state == STATE_CLOSING mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed") await hass.services.async_call( COVER_DOMAIN, SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.test_cover_0"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED + assert hass.states.get(entity_id).state == STATE_CLOSED + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-cover:0" async def test_rpc_device_no_cover_keys( diff --git a/tests/components/shelly/test_event.py b/tests/components/shelly/test_event.py index 09439adc6f7..30c2ff45166 100644 --- a/tests/components/shelly/test_event.py +++ b/tests/components/shelly/test_event.py @@ -12,18 +12,18 @@ from homeassistant.components.event import ( ) from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import async_get from . import init_integration, inject_rpc_device_event, register_entity DEVICE_BLOCK_ID = 4 -async def test_rpc_button(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> None: +async def test_rpc_button( + hass: HomeAssistant, mock_rpc_device, entity_registry, monkeypatch +) -> None: """Test RPC device event.""" await init_integration(hass, 2) entity_id = "event.test_name_input_0" - registry = async_get(hass) state = hass.states.get(entity_id) assert state @@ -34,7 +34,7 @@ async def test_rpc_button(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> assert state.attributes.get(ATTR_EVENT_TYPE) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == EventDeviceClass.BUTTON - entry = registry.async_get(entity_id) + entry = entity_registry.async_get(entity_id) assert entry assert entry.unique_id == "123456789ABC-input:0" @@ -59,25 +59,25 @@ async def test_rpc_button(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> async def test_rpc_event_removal( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, mock_rpc_device, entity_registry, monkeypatch ) -> None: """Test RPC event entity is removed due to removal_condition.""" - registry = async_get(hass) entity_id = register_entity(hass, EVENT_DOMAIN, "test_name_input_0", "input:0") - assert registry.async_get(entity_id) is not None + assert entity_registry.async_get(entity_id) is not None monkeypatch.setitem(mock_rpc_device.config, "input:0", {"id": 0, "type": "switch"}) await init_integration(hass, 2) - assert registry.async_get(entity_id) is None + assert entity_registry.async_get(entity_id) is None -async def test_block_event(hass: HomeAssistant, monkeypatch, mock_block_device) -> None: +async def test_block_event( + hass: HomeAssistant, monkeypatch, mock_block_device, entity_registry +) -> None: """Test block device event.""" await init_integration(hass, 1) entity_id = "event.test_name_channel_1" - registry = async_get(hass) state = hass.states.get(entity_id) assert state @@ -86,7 +86,7 @@ async def test_block_event(hass: HomeAssistant, monkeypatch, mock_block_device) assert state.attributes.get(ATTR_EVENT_TYPE) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == EventDeviceClass.BUTTON - entry = registry.async_get(entity_id) + entry = entity_registry.async_get(entity_id) assert entry assert entry.unique_id == "123456789ABC-relay_0-1" diff --git a/tests/components/shelly/test_light.py b/tests/components/shelly/test_light.py index c27a1199d4b..487fa6e053b 100644 --- a/tests/components/shelly/test_light.py +++ b/tests/components/shelly/test_light.py @@ -43,12 +43,15 @@ RELAY_BLOCK_ID = 0 LIGHT_BLOCK_ID = 2 -async def test_block_device_rgbw_bulb(hass: HomeAssistant, mock_block_device) -> None: +async def test_block_device_rgbw_bulb( + hass: HomeAssistant, mock_block_device, entity_registry +) -> None: """Test block device RGBW bulb.""" + entity_id = "light.test_name_channel_1" await init_integration(hass, 1, model=MODEL_BULB) # Test initial - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) attributes = state.attributes assert state.state == STATE_ON assert attributes[ATTR_RGBW_COLOR] == (45, 55, 65, 70) @@ -66,13 +69,13 @@ async def test_block_device_rgbw_bulb(hass: HomeAssistant, mock_block_device) -> await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_name_channel_1"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( turn="off" ) - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) assert state.state == STATE_OFF # Turn on, RGBW = [70, 80, 90, 20], brightness = 33, effect = Flash @@ -81,7 +84,7 @@ async def test_block_device_rgbw_bulb(hass: HomeAssistant, mock_block_device) -> LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_name_channel_1", + ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: [70, 80, 90, 30], ATTR_BRIGHTNESS: 33, ATTR_EFFECT: "Flash", @@ -91,7 +94,7 @@ async def test_block_device_rgbw_bulb(hass: HomeAssistant, mock_block_device) -> mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( turn="on", gain=13, brightness=13, red=70, green=80, blue=90, white=30, effect=3 ) - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) attributes = state.attributes assert state.state == STATE_ON assert attributes[ATTR_COLOR_MODE] == ColorMode.RGBW @@ -104,31 +107,40 @@ async def test_block_device_rgbw_bulb(hass: HomeAssistant, mock_block_device) -> await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_name_channel_1", ATTR_COLOR_TEMP_KELVIN: 3500}, + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 3500}, blocking=True, ) mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( turn="on", temp=3500, mode="white" ) - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) attributes = state.attributes assert state.state == STATE_ON assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP assert attributes[ATTR_COLOR_TEMP_KELVIN] == 3500 + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-light_0" + async def test_block_device_rgb_bulb( hass: HomeAssistant, mock_block_device, monkeypatch, + entity_registry, caplog: pytest.LogCaptureFixture, ) -> None: """Test block device RGB bulb.""" + entity_id = "light.test_name_channel_1" monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "mode") + monkeypatch.setattr( + mock_block_device.blocks[LIGHT_BLOCK_ID], "description", "light_1" + ) await init_integration(hass, 1, model=MODEL_BULB_RGBW) # Test initial - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) attributes = state.attributes assert state.state == STATE_ON assert attributes[ATTR_RGB_COLOR] == (45, 55, 65) @@ -149,13 +161,13 @@ async def test_block_device_rgb_bulb( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_name_channel_1"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( turn="off" ) - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) assert state.state == STATE_OFF # Turn on, RGB = [70, 80, 90], brightness = 33, effect = Flash @@ -164,7 +176,7 @@ async def test_block_device_rgb_bulb( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_name_channel_1", + ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: [70, 80, 90], ATTR_BRIGHTNESS: 33, ATTR_EFFECT: "Flash", @@ -174,7 +186,7 @@ async def test_block_device_rgb_bulb( mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( turn="on", gain=13, brightness=13, red=70, green=80, blue=90, effect=3 ) - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) attributes = state.attributes assert state.state == STATE_ON assert attributes[ATTR_COLOR_MODE] == ColorMode.RGB @@ -187,13 +199,13 @@ async def test_block_device_rgb_bulb( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_name_channel_1", ATTR_COLOR_TEMP_KELVIN: 3500}, + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 3500}, blocking=True, ) mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( turn="on", temp=3500, mode="white" ) - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) attributes = state.attributes assert state.state == STATE_ON assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP @@ -204,32 +216,40 @@ async def test_block_device_rgb_bulb( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_name_channel_1", ATTR_EFFECT: "Breath"}, + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "Breath"}, blocking=True, ) mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( turn="on", mode="color" ) - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) attributes = state.attributes assert state.state == STATE_ON assert attributes[ATTR_EFFECT] == "Off" assert "Effect 'Breath' not supported" in caplog.text + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-light_1" + async def test_block_device_white_bulb( hass: HomeAssistant, mock_block_device, + entity_registry, monkeypatch, - caplog: pytest.LogCaptureFixture, ) -> None: """Test block device white bulb.""" + entity_id = "light.test_name_channel_1" monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "red") monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "green") monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "blue") monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "mode") monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "colorTemp") monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "effect") + monkeypatch.setattr( + mock_block_device.blocks[LIGHT_BLOCK_ID], "description", "light_1" + ) monkeypatch.setattr( mock_block_device.blocks[LIGHT_BLOCK_ID], "set_state", @@ -238,7 +258,7 @@ async def test_block_device_white_bulb( await init_integration(hass, 1, model=MODEL_VINTAGE_V2) # Test initial - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) attributes = state.attributes assert state.state == STATE_ON assert attributes[ATTR_BRIGHTNESS] == 128 @@ -250,13 +270,13 @@ async def test_block_device_white_bulb( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_name_channel_1"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( turn="off" ) - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) assert state.state == STATE_OFF # Turn on, brightness = 33 @@ -264,17 +284,21 @@ async def test_block_device_white_bulb( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_name_channel_1", ATTR_BRIGHTNESS: 33}, + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 33}, blocking=True, ) mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( turn="on", gain=13, brightness=13 ) - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) attributes = state.attributes assert state.state == STATE_ON assert attributes[ATTR_BRIGHTNESS] == 33 + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-light_1" + @pytest.mark.parametrize( "model", @@ -288,16 +312,20 @@ async def test_block_device_white_bulb( ], ) async def test_block_device_support_transition( - hass: HomeAssistant, mock_block_device, model, monkeypatch + hass: HomeAssistant, mock_block_device, entity_registry, model, monkeypatch ) -> None: """Test block device supports transition.""" + entity_id = "light.test_name_channel_1" monkeypatch.setitem( mock_block_device.settings, "fw", "20220809-122808/v1.12-g99f7e0b" ) + monkeypatch.setattr( + mock_block_device.blocks[LIGHT_BLOCK_ID], "description", "light_1" + ) await init_integration(hass, 1, model=model) # Test initial - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) attributes = state.attributes assert attributes[ATTR_SUPPORTED_FEATURES] & LightEntityFeature.TRANSITION @@ -306,13 +334,13 @@ async def test_block_device_support_transition( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_name_channel_1", ATTR_TRANSITION: 4}, + {ATTR_ENTITY_ID: entity_id, ATTR_TRANSITION: 4}, blocking=True, ) mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( turn="on", transition=4000 ) - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) assert state.state == STATE_ON # Turn off, TRANSITION = 6, limit to 5000ms @@ -320,20 +348,25 @@ async def test_block_device_support_transition( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_name_channel_1", ATTR_TRANSITION: 6}, + {ATTR_ENTITY_ID: entity_id, ATTR_TRANSITION: 6}, blocking=True, ) mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( turn="off", transition=5000 ) - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) assert state.state == STATE_OFF + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-light_1" + async def test_block_device_relay_app_type_light( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, mock_block_device, entity_registry, monkeypatch ) -> None: """Test block device relay in app type set to light mode.""" + entity_id = "light.test_name_channel_1" monkeypatch.delattr(mock_block_device.blocks[RELAY_BLOCK_ID], "red") monkeypatch.delattr(mock_block_device.blocks[RELAY_BLOCK_ID], "green") monkeypatch.delattr(mock_block_device.blocks[RELAY_BLOCK_ID], "blue") @@ -345,11 +378,14 @@ async def test_block_device_relay_app_type_light( monkeypatch.setitem( mock_block_device.settings["relays"][RELAY_BLOCK_ID], "appliance_type", "light" ) + monkeypatch.setattr( + mock_block_device.blocks[RELAY_BLOCK_ID], "description", "relay_1" + ) await init_integration(hass, 1) assert hass.states.get("switch.test_name_channel_1") is None # Test initial - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) attributes = state.attributes assert state.state == STATE_ON assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.ONOFF] @@ -360,13 +396,13 @@ async def test_block_device_relay_app_type_light( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_name_channel_1"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) mock_block_device.blocks[RELAY_BLOCK_ID].set_state.assert_called_once_with( turn="off" ) - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) assert state.state == STATE_OFF # Turn on @@ -374,15 +410,19 @@ async def test_block_device_relay_app_type_light( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_name_channel_1"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) mock_block_device.blocks[RELAY_BLOCK_ID].set_state.assert_called_once_with( turn="on" ) - state = hass.states.get("light.test_name_channel_1") + state = hass.states.get(entity_id) assert state.state == STATE_ON + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-relay_1" + async def test_block_device_no_light_blocks( hass: HomeAssistant, mock_block_device, monkeypatch @@ -394,9 +434,10 @@ async def test_block_device_no_light_blocks( async def test_rpc_device_switch_type_lights_mode( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, mock_rpc_device, entity_registry, monkeypatch ) -> None: """Test RPC device with switch in consumption type lights mode.""" + entity_id = "light.test_switch_0" monkeypatch.setitem( mock_rpc_device.config["sys"]["ui_data"], "consumption_types", ["lights"] ) @@ -405,23 +446,29 @@ async def test_rpc_device_switch_type_lights_mode( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_switch_0"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - assert hass.states.get("light.test_switch_0").state == STATE_ON + assert hass.states.get(entity_id).state == STATE_ON mutate_rpc_device_status(monkeypatch, mock_rpc_device, "switch:0", "output", False) await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_switch_0"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("light.test_switch_0").state == STATE_OFF + assert hass.states.get(entity_id).state == STATE_OFF + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-switch:0" -async def test_rpc_light(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> None: +async def test_rpc_light( + hass: HomeAssistant, mock_rpc_device, entity_registry, monkeypatch +) -> None: """Test RPC light.""" entity_id = f"{LIGHT_DOMAIN}.test_light_0" monkeypatch.delitem(mock_rpc_device.status, "switch:0") @@ -476,3 +523,7 @@ async def test_rpc_light(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> N state = hass.states.get(entity_id) assert state.state == STATE_ON assert state.attributes[ATTR_BRIGHTNESS] == 33 + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-light:0" diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index a072c7638a1..b97d2c914a3 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -23,23 +23,28 @@ DEVICE_BLOCK_ID = 4 async def test_block_number_update( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, mock_block_device, entity_registry, monkeypatch ) -> None: """Test block device number update.""" + entity_id = "number.test_name_valve_position" await init_integration(hass, 1, sleep_period=1000) - assert hass.states.get("number.test_name_valve_position") is None + assert hass.states.get(entity_id) is None # Make device online mock_block_device.mock_update() await hass.async_block_till_done() - assert hass.states.get("number.test_name_valve_position").state == "50" + assert hass.states.get(entity_id).state == "50" monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valvePos", 30) mock_block_device.mock_update() - assert hass.states.get("number.test_name_valve_position").state == "30" + assert hass.states.get(entity_id).state == "30" + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-device_0-valvePos" async def test_block_restored_number( diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 86c6356191b..79776c00b41 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -43,7 +43,7 @@ DEVICE_BLOCK_ID = 4 async def test_block_sensor( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, mock_block_device, entity_registry, monkeypatch ) -> None: """Test block sensor.""" entity_id = f"{SENSOR_DOMAIN}.test_name_channel_1_power" @@ -56,8 +56,14 @@ async def test_block_sensor( assert hass.states.get(entity_id).state == "60.1" + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-relay_0-power" -async def test_energy_sensor(hass: HomeAssistant, mock_block_device) -> None: + +async def test_energy_sensor( + hass: HomeAssistant, mock_block_device, entity_registry +) -> None: """Test energy sensor.""" entity_id = f"{SENSOR_DOMAIN}.test_name_channel_1_energy" await init_integration(hass, 1) @@ -68,13 +74,16 @@ async def test_energy_sensor(hass: HomeAssistant, mock_block_device) -> None: # suggested unit is KWh assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-relay_0-energy" + async def test_power_factory_unit_migration( - hass: HomeAssistant, mock_block_device + hass: HomeAssistant, mock_block_device, entity_registry ) -> None: """Test migration unit of the power factory sensor.""" - registry = async_get(hass) - registry.async_get_or_create( + entity_registry.async_get_or_create( SENSOR_DOMAIN, DOMAIN, "123456789ABC-emeter_0-powerFactor", @@ -90,9 +99,13 @@ async def test_power_factory_unit_migration( assert state.state == "98.0" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-emeter_0-powerFactor" + async def test_power_factory_without_unit_migration( - hass: HomeAssistant, mock_block_device + hass: HomeAssistant, mock_block_device, entity_registry ) -> None: """Test unit and value of the power factory sensor without unit migration.""" entity_id = f"{SENSOR_DOMAIN}.test_name_power_factor" @@ -102,6 +115,10 @@ async def test_power_factory_without_unit_migration( assert state.state == "0.98" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-emeter_0-powerFactor" + async def test_block_rest_sensor( hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch @@ -119,7 +136,7 @@ async def test_block_rest_sensor( async def test_block_sleeping_sensor( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, mock_block_device, entity_registry, monkeypatch ) -> None: """Test block sleeping sensor.""" monkeypatch.setattr( @@ -142,6 +159,10 @@ async def test_block_sleeping_sensor( assert hass.states.get(entity_id).state == "23.4" + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-sensor_0-temp" + async def test_block_restored_sleeping_sensor( hass: HomeAssistant, mock_block_device, device_reg, monkeypatch @@ -197,7 +218,7 @@ async def test_block_restored_sleeping_sensor_no_last_state( async def test_block_sensor_error( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, mock_block_device, entity_registry, monkeypatch ) -> None: """Test block sensor unavailable on sensor error.""" entity_id = f"{SENSOR_DOMAIN}.test_name_battery" @@ -210,12 +231,15 @@ async def test_block_sensor_error( assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-device_0-battery" + async def test_block_sensor_removal( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, mock_block_device, entity_registry, monkeypatch ) -> None: """Test block sensor is removed due to removal_condition.""" - entity_registry = async_get(hass) entity_id = register_entity( hass, SENSOR_DOMAIN, "test_name_battery", "device_0-battery" ) @@ -300,7 +324,7 @@ async def test_rpc_sensor(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> async def test_rpc_illuminance_sensor( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, mock_rpc_device, entity_registry, monkeypatch ) -> None: """Test RPC illuminacne sensor.""" entity_id = f"{SENSOR_DOMAIN}.test_name_illuminance" @@ -308,9 +332,13 @@ async def test_rpc_illuminance_sensor( assert hass.states.get(entity_id).state == "345" + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-illuminance:0-illuminance" + async def test_rpc_sensor_error( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, mock_rpc_device, entity_registry, monkeypatch ) -> None: """Test RPC sensor unavailable on sensor error.""" entity_id = f"{SENSOR_DOMAIN}.test_name_voltmeter" @@ -323,9 +351,17 @@ async def test_rpc_sensor_error( assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-voltmeter-voltmeter" + async def test_rpc_polling_sensor( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device, monkeypatch + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_rpc_device, + entity_registry, + monkeypatch, ) -> None: """Test RPC polling sensor.""" entity_id = register_entity(hass, SENSOR_DOMAIN, "test_name_rssi", "wifi-rssi") @@ -338,6 +374,10 @@ async def test_rpc_polling_sensor( assert hass.states.get(entity_id).state == "-70" + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-wifi-rssi" + async def test_rpc_sleeping_sensor( hass: HomeAssistant, mock_rpc_device, device_reg, monkeypatch @@ -470,7 +510,10 @@ async def test_rpc_em1_sensors( async def test_rpc_sleeping_update_entity_service( - hass: HomeAssistant, mock_rpc_device, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + mock_rpc_device, + entity_registry, + caplog: pytest.LogCaptureFixture, ) -> None: """Test RPC sleeping device when the update_entity service is used.""" await async_setup_component(hass, "homeassistant", {}) @@ -500,6 +543,10 @@ async def test_rpc_sleeping_update_entity_service( state = hass.states.get(entity_id) assert state.state == "22.9" + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-temperature:0-temperature_0" + assert ( "Entity sensor.test_name_temperature comes from a sleeping device" in caplog.text @@ -507,7 +554,10 @@ async def test_rpc_sleeping_update_entity_service( async def test_block_sleeping_update_entity_service( - hass: HomeAssistant, mock_block_device, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + mock_block_device, + entity_registry, + caplog: pytest.LogCaptureFixture, ) -> None: """Test block sleeping device when the update_entity service is used.""" await async_setup_component(hass, "homeassistant", {}) @@ -536,6 +586,10 @@ async def test_block_sleeping_update_entity_service( state = hass.states.get(entity_id) assert state.state == "22.1" + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-sensor_0-temp" + assert ( "Entity sensor.test_name_temperature comes from a sleeping device" in caplog.text diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index 555533cc817..8a0cefe3ae0 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -54,12 +54,13 @@ async def test_block_device_services(hass: HomeAssistant, mock_block_device) -> assert hass.states.get("switch.test_name_channel_1").state == STATE_OFF -async def test_block_device_unique_ids(hass: HomeAssistant, mock_block_device) -> None: +async def test_block_device_unique_ids( + hass: HomeAssistant, entity_registry, mock_block_device +) -> None: """Test block device unique_ids.""" await init_integration(hass, 1) - registry = er.async_get(hass) - entry = registry.async_get("switch.test_name_channel_1") + entry = entity_registry.async_get("switch.test_name_channel_1") assert entry assert entry.unique_id == "123456789ABC-relay_0" @@ -187,13 +188,12 @@ async def test_rpc_device_services( async def test_rpc_device_unique_ids( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, mock_rpc_device, entity_registry ) -> None: """Test RPC device unique_ids.""" await init_integration(hass, 2) - registry = er.async_get(hass) - entry = registry.async_get("switch.test_switch_0") + entry = entity_registry.async_get("switch.test_switch_0") assert entry assert entry.unique_id == "123456789ABC-switch:0" diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 06eac49e293..70503db0e0d 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -29,10 +29,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity_registry import async_get from . import ( - MOCK_MAC, init_integration, inject_rpc_device_event, mock_rest_update, @@ -44,22 +42,21 @@ from tests.common import mock_restore_cache async def test_block_update( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_block_device, + entity_registry, + monkeypatch, + entity_registry_enabled_by_default: None, ) -> None: """Test block device update entity.""" - entity_registry = async_get(hass) - entity_registry.async_get_or_create( - UPDATE_DOMAIN, - DOMAIN, - f"{MOCK_MAC}-fwupdate", - suggested_object_id="test_name_firmware_update", - disabled_by=None, - ) + entity_id = "update.test_name_firmware_update" monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1") monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2") + monkeypatch.setitem(mock_block_device.status, "cloud", {"connected": False}) await init_integration(hass, 1) - state = hass.states.get("update.test_name_firmware_update") + state = hass.states.get(entity_id) assert state.state == STATE_ON assert state.attributes[ATTR_INSTALLED_VERSION] == "1" assert state.attributes[ATTR_LATEST_VERSION] == "2" @@ -70,12 +67,12 @@ async def test_block_update( await hass.services.async_call( UPDATE_DOMAIN, SERVICE_INSTALL, - {ATTR_ENTITY_ID: "update.test_name_firmware_update"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) assert mock_block_device.trigger_ota_update.call_count == 1 - state = hass.states.get("update.test_name_firmware_update") + state = hass.states.get(entity_id) assert state.state == STATE_ON assert state.attributes[ATTR_INSTALLED_VERSION] == "1" assert state.attributes[ATTR_LATEST_VERSION] == "2" @@ -85,31 +82,34 @@ async def test_block_update( monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2") await mock_rest_update(hass, freezer) - state = hass.states.get("update.test_name_firmware_update") + state = hass.states.get(entity_id) assert state.state == STATE_OFF assert state.attributes[ATTR_INSTALLED_VERSION] == "2" assert state.attributes[ATTR_LATEST_VERSION] == "2" assert state.attributes[ATTR_IN_PROGRESS] is False + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-fwupdate" + async def test_block_beta_update( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_block_device, + entity_registry, + monkeypatch, + entity_registry_enabled_by_default: None, ) -> None: """Test block device beta update entity.""" - entity_registry = async_get(hass) - entity_registry.async_get_or_create( - UPDATE_DOMAIN, - DOMAIN, - f"{MOCK_MAC}-fwupdate_beta", - suggested_object_id="test_name_beta_firmware_update", - disabled_by=None, - ) + entity_id = "update.test_name_beta_firmware_update" monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1") monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2") monkeypatch.setitem(mock_block_device.status["update"], "beta_version", "") + monkeypatch.setitem(mock_block_device.status, "cloud", {"connected": False}) await init_integration(hass, 1) - state = hass.states.get("update.test_name_beta_firmware_update") + state = hass.states.get(entity_id) assert state.state == STATE_OFF assert state.attributes[ATTR_INSTALLED_VERSION] == "1" assert state.attributes[ATTR_LATEST_VERSION] == "1" @@ -118,7 +118,7 @@ async def test_block_beta_update( monkeypatch.setitem(mock_block_device.status["update"], "beta_version", "2b") await mock_rest_update(hass, freezer) - state = hass.states.get("update.test_name_beta_firmware_update") + state = hass.states.get(entity_id) assert state.state == STATE_ON assert state.attributes[ATTR_INSTALLED_VERSION] == "1" assert state.attributes[ATTR_LATEST_VERSION] == "2b" @@ -128,12 +128,12 @@ async def test_block_beta_update( await hass.services.async_call( UPDATE_DOMAIN, SERVICE_INSTALL, - {ATTR_ENTITY_ID: "update.test_name_beta_firmware_update"}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) assert mock_block_device.trigger_ota_update.call_count == 1 - state = hass.states.get("update.test_name_beta_firmware_update") + state = hass.states.get(entity_id) assert state.state == STATE_ON assert state.attributes[ATTR_INSTALLED_VERSION] == "1" assert state.attributes[ATTR_LATEST_VERSION] == "2b" @@ -142,28 +142,25 @@ async def test_block_beta_update( monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2b") await mock_rest_update(hass, freezer) - state = hass.states.get("update.test_name_beta_firmware_update") + state = hass.states.get(entity_id) assert state.state == STATE_OFF assert state.attributes[ATTR_INSTALLED_VERSION] == "2b" assert state.attributes[ATTR_LATEST_VERSION] == "2b" assert state.attributes[ATTR_IN_PROGRESS] is False + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-fwupdate_beta" + async def test_block_update_connection_error( hass: HomeAssistant, mock_block_device, monkeypatch, caplog: pytest.LogCaptureFixture, + entity_registry_enabled_by_default: None, ) -> None: """Test block device update connection error.""" - entity_registry = async_get(hass) - entity_registry.async_get_or_create( - UPDATE_DOMAIN, - DOMAIN, - f"{MOCK_MAC}-fwupdate", - suggested_object_id="test_name_firmware_update", - disabled_by=None, - ) monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1") monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2") monkeypatch.setattr( @@ -184,17 +181,12 @@ async def test_block_update_connection_error( async def test_block_update_auth_error( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, + mock_block_device, + monkeypatch, + entity_registry_enabled_by_default: None, ) -> None: """Test block device update authentication error.""" - entity_registry = async_get(hass) - entity_registry.async_get_or_create( - UPDATE_DOMAIN, - DOMAIN, - f"{MOCK_MAC}-fwupdate", - suggested_object_id="test_name_firmware_update", - disabled_by=None, - ) monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1") monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2") monkeypatch.setattr( @@ -228,7 +220,9 @@ async def test_block_update_auth_error( assert flow["context"].get("entry_id") == entry.entry_id -async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> None: +async def test_rpc_update( + hass: HomeAssistant, mock_rpc_device, entity_registry, monkeypatch +) -> None: """Test RPC device update entity.""" entity_id = "update.test_name_firmware_update" monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1") @@ -320,9 +314,13 @@ async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> assert state.attributes[ATTR_LATEST_VERSION] == "2" assert state.attributes[ATTR_IN_PROGRESS] is False + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-sys-fwupdate" + async def test_rpc_sleeping_update( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, mock_rpc_device, entity_registry, monkeypatch ) -> None: """Test RPC sleeping device update entity.""" monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1") @@ -361,6 +359,10 @@ async def test_rpc_sleeping_update( assert state.attributes[ATTR_IN_PROGRESS] is False assert state.attributes[ATTR_SUPPORTED_FEATURES] == UpdateEntityFeature(0) + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-sys-fwupdate" + async def test_rpc_restored_sleeping_update( hass: HomeAssistant, mock_rpc_device, device_reg, monkeypatch @@ -448,17 +450,14 @@ async def test_rpc_restored_sleeping_update_no_last_state( async def test_rpc_beta_update( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device, monkeypatch + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_rpc_device, + entity_registry, + monkeypatch, + entity_registry_enabled_by_default: None, ) -> None: """Test RPC device beta update entity.""" - entity_registry = async_get(hass) - entity_registry.async_get_or_create( - UPDATE_DOMAIN, - DOMAIN, - f"{MOCK_MAC}-sys-fwupdate_beta", - suggested_object_id="test_name_beta_firmware_update", - disabled_by=None, - ) entity_id = "update.test_name_beta_firmware_update" monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1") monkeypatch.setitem( @@ -564,6 +563,10 @@ async def test_rpc_beta_update( assert state.attributes[ATTR_LATEST_VERSION] == "2b" assert state.attributes[ATTR_IN_PROGRESS] is False + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-sys-fwupdate_beta" + @pytest.mark.parametrize( ("exc", "error"), @@ -572,23 +575,16 @@ async def test_rpc_beta_update( (RpcCallError(-1, "error"), "OTA update request error"), ], ) -async def test_rpc_update__errors( +async def test_rpc_update_errors( hass: HomeAssistant, exc, error, mock_rpc_device, monkeypatch, caplog: pytest.LogCaptureFixture, + entity_registry_enabled_by_default: None, ) -> None: """Test RPC device update connection/call errors.""" - entity_registry = async_get(hass) - entity_registry.async_get_or_create( - UPDATE_DOMAIN, - DOMAIN, - f"{MOCK_MAC}-sys-fwupdate", - suggested_object_id="test_name_firmware_update", - disabled_by=None, - ) monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1") monkeypatch.setitem( mock_rpc_device.status["sys"], @@ -614,17 +610,13 @@ async def test_rpc_update__errors( async def test_rpc_update_auth_error( - hass: HomeAssistant, mock_rpc_device, monkeypatch, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + mock_rpc_device, + entity_registry, + monkeypatch, + entity_registry_enabled_by_default: None, ) -> None: """Test RPC device update authentication error.""" - entity_registry = async_get(hass) - entity_registry.async_get_or_create( - UPDATE_DOMAIN, - DOMAIN, - f"{MOCK_MAC}-sys-fwupdate", - suggested_object_id="test_name_firmware_update", - disabled_by=None, - ) monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1") monkeypatch.setitem( mock_rpc_device.status["sys"], From b5b09446a1d8c095300cb0a9a568c76e710b6e7a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 7 Feb 2024 17:22:00 +0100 Subject: [PATCH 0388/1367] Add analog input value sensor for Shelly (#109312) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> Co-authored-by: J. Nick Koston --- homeassistant/components/shelly/sensor.py | 5 +++++ tests/components/shelly/conftest.py | 2 +- tests/components/shelly/test_sensor.py | 10 ++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index e46800963a3..824b7264f26 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -960,6 +960,11 @@ RPC_SENSORS: Final = { native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), + "analoginput_xpercent": RpcSensorDescription( + key="input", + sub_key="xpercent", + name="Analog value", + ), } diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 194ee57e13b..45e1e551b23 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -219,7 +219,7 @@ MOCK_STATUS_COAP = { MOCK_STATUS_RPC = { "switch:0": {"output": True}, - "input:0": {"id": 0, "state": None}, + "input:0": {"id": 0, "state": None, "xpercent": 8.9}, "light:0": {"output": True, "brightness": 53.0}, "cloud": {"connected": False}, "cover:0": { diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 79776c00b41..87d0c1baa14 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -594,3 +594,13 @@ async def test_block_sleeping_update_entity_service( "Entity sensor.test_name_temperature comes from a sleeping device" in caplog.text ) + + +async def test_rpc_analog_input_xpercent_sensor( + hass: HomeAssistant, mock_rpc_device +) -> None: + """Test RPC analog input xpercent sensor.""" + entity_id = f"{SENSOR_DOMAIN}.test_name_input_0_analog_value" + await init_integration(hass, 2) + + assert hass.states.get(entity_id).state == "8.9" From de066c7fc08239f9b53289247fc63964dab2a188 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 7 Feb 2024 17:22:10 +0100 Subject: [PATCH 0389/1367] Reset log level in script tests (#109881) --- tests/scripts/test_auth.py | 10 ++++++++++ tests/scripts/test_check_config.py | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index 82a8a408c53..3d9f26d1204 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -1,4 +1,5 @@ """Test the auth script to manage local users.""" +import logging from typing import Any from unittest.mock import Mock, patch @@ -11,6 +12,15 @@ from homeassistant.scripts import auth as script_auth from tests.common import register_auth_provider +@pytest.fixture(autouse=True) +def reset_log_level(): + """Reset log level after each test case.""" + logger = logging.getLogger("homeassistant.core") + orig_level = logger.level + yield + logger.setLevel(orig_level) + + @pytest.fixture def provider(hass): """Home Assistant auth provider.""" diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 425ad561f50..9e2f6708c99 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -1,4 +1,5 @@ """Test check_config script.""" +import logging from unittest.mock import patch import pytest @@ -22,6 +23,15 @@ BASE_CONFIG = ( BAD_CORE_CONFIG = "homeassistant:\n unit_system: bad\n\n\n" +@pytest.fixture(autouse=True) +def reset_log_level(): + """Reset log level after each test case.""" + logger = logging.getLogger("homeassistant.loader") + orig_level = logger.level + yield + logger.setLevel(orig_level) + + @pytest.fixture(autouse=True) async def apply_stop_hass(stop_hass: None) -> None: """Make sure all hass are stopped.""" From 8fd51fcbef0a2e88600781a6d67fdc6db40b9575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 7 Feb 2024 18:07:53 +0100 Subject: [PATCH 0390/1367] Bump myuplink dependency to 0.1.1 (#109878) Bump myuplink dependeny to 0.1.1 --- homeassistant/components/myuplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myuplink/manifest.json b/homeassistant/components/myuplink/manifest.json index 303af547335..c46e4216ee5 100644 --- a/homeassistant/components/myuplink/manifest.json +++ b/homeassistant/components/myuplink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/myuplink", "iot_class": "cloud_polling", - "requirements": ["myuplink==0.0.9"] + "requirements": ["myuplink==0.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 91e62aa514d..434f8b9bdec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1334,7 +1334,7 @@ mutesync==0.0.1 mypermobil==0.1.8 # homeassistant.components.myuplink -myuplink==0.0.9 +myuplink==0.1.1 # homeassistant.components.nad nad-receiver==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04361c87f40..43e7c4d0b95 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1070,7 +1070,7 @@ mutesync==0.0.1 mypermobil==0.1.8 # homeassistant.components.myuplink -myuplink==0.0.9 +myuplink==0.1.1 # homeassistant.components.keenetic_ndms2 ndms2-client==0.1.2 From b276a7863b057cce0cf1572fd0cdf065b4e7e1d9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 7 Feb 2024 18:20:53 +0100 Subject: [PATCH 0391/1367] Add missing `unique_id` check for Shelly Analog Input sensor (#109888) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- tests/components/shelly/test_sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 87d0c1baa14..4f93b298557 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -597,10 +597,14 @@ async def test_block_sleeping_update_entity_service( async def test_rpc_analog_input_xpercent_sensor( - hass: HomeAssistant, mock_rpc_device + hass: HomeAssistant, mock_rpc_device, entity_registry ) -> None: """Test RPC analog input xpercent sensor.""" entity_id = f"{SENSOR_DOMAIN}.test_name_input_0_analog_value" await init_integration(hass, 2) assert hass.states.get(entity_id).state == "8.9" + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-input:0-analoginput_xpercent" From 1750f54da4ee9baa1ac383665a1f67f9a2698393 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 7 Feb 2024 15:13:42 -0600 Subject: [PATCH 0392/1367] Assist fixes (#109889) * Don't pass entity ids in hassil slot lists * Use first completed response * Add more tests --- homeassistant/components/climate/intent.py | 34 ++++-- .../components/conversation/default_agent.py | 36 +++--- homeassistant/components/intent/__init__.py | 7 +- homeassistant/helpers/intent.py | 15 ++- tests/components/climate/test_intent.py | 70 ++++++++++- .../conversation/snapshots/test_init.ambr | 8 +- .../conversation/test_default_agent.py | 93 +++++++++++++- tests/components/conversation/test_trigger.py | 115 +++++++++++++++++- tests/components/intent/test_init.py | 25 ++++ 9 files changed, 354 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/climate/intent.py b/homeassistant/components/climate/intent.py index 4152fb5ee2d..db263451f0b 100644 --- a/homeassistant/components/climate/intent.py +++ b/homeassistant/components/climate/intent.py @@ -1,4 +1,5 @@ """Intents for the client integration.""" + from __future__ import annotations import voluptuous as vol @@ -36,24 +37,34 @@ class GetTemperatureIntent(intent.IntentHandler): if not entities: raise intent.IntentHandleError("No climate entities") - if "area" in slots: - # Filter by area - area_name = slots["area"]["value"] + name_slot = slots.get("name", {}) + entity_name: str | None = name_slot.get("value") + entity_text: str | None = name_slot.get("text") + + area_slot = slots.get("area", {}) + area_id = area_slot.get("value") + + if area_id: + # Filter by area and optionally name + area_name = area_slot.get("text") for maybe_climate in intent.async_match_states( - hass, area_name=area_name, domains=[DOMAIN] + hass, name=entity_name, area_name=area_id, domains=[DOMAIN] ): climate_state = maybe_climate break if climate_state is None: - raise intent.IntentHandleError(f"No climate entity in area {area_name}") + raise intent.NoStatesMatchedError( + name=entity_text or entity_name, + area=area_name or area_id, + domains={DOMAIN}, + device_classes=None, + ) climate_entity = component.get_entity(climate_state.entity_id) - elif "name" in slots: + elif entity_name: # Filter by name - entity_name = slots["name"]["value"] - for maybe_climate in intent.async_match_states( hass, name=entity_name, domains=[DOMAIN] ): @@ -61,7 +72,12 @@ class GetTemperatureIntent(intent.IntentHandler): break if climate_state is None: - raise intent.IntentHandleError(f"No climate entity named {entity_name}") + raise intent.NoStatesMatchedError( + name=entity_name, + area=None, + domains={DOMAIN}, + device_classes=None, + ) climate_entity = component.get_entity(climate_state.entity_id) else: diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index fb33d87e107..52925fbc241 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -223,22 +223,22 @@ class DefaultAgent(AbstractConversationAgent): # Check if a trigger matched if isinstance(result, SentenceTriggerResult): # Gather callback responses in parallel - trigger_responses = await asyncio.gather( - *( - self._trigger_sentences[trigger_id].callback( - result.sentence, trigger_result - ) - for trigger_id, trigger_result in result.matched_triggers.items() + trigger_callbacks = [ + self._trigger_sentences[trigger_id].callback( + result.sentence, trigger_result ) - ) + for trigger_id, trigger_result in result.matched_triggers.items() + ] # Use last non-empty result as response. # # There may be multiple copies of a trigger running when editing in # the UI, so it's critical that we filter out empty responses here. response_text: str | None = None - for trigger_response in trigger_responses: - response_text = response_text or trigger_response + for trigger_future in asyncio.as_completed(trigger_callbacks): + if trigger_response := await trigger_future: + response_text = trigger_response + break # Convert to conversation result response = intent.IntentResponse(language=language) @@ -724,7 +724,12 @@ class DefaultAgent(AbstractConversationAgent): if async_should_expose(self.hass, DOMAIN, state.entity_id) ] - # Gather exposed entity names + # Gather exposed entity names. + # + # NOTE: We do not pass entity ids in here because multiple entities may + # have the same name. The intent matcher doesn't gather all matching + # values for a list, just the first. So we will need to match by name no + # matter what. entity_names = [] for state in states: # Checked against "requires_context" and "excludes_context" in hassil @@ -740,7 +745,7 @@ class DefaultAgent(AbstractConversationAgent): if not entity: # Default name - entity_names.append((state.name, state.entity_id, context)) + entity_names.append((state.name, state.name, context)) continue if entity.aliases: @@ -748,12 +753,15 @@ class DefaultAgent(AbstractConversationAgent): if not alias.strip(): continue - entity_names.append((alias, state.entity_id, context)) + entity_names.append((alias, state.name, context)) # Default name - entity_names.append((state.name, state.entity_id, context)) + entity_names.append((state.name, state.name, context)) - # Expose all areas + # Expose all areas. + # + # We pass in area id here with the expectation that no two areas will + # share the same name or alias. areas = ar.async_get(self.hass) area_names = [] for area in areas.async_list_areas(): diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index 5756b78b4de..d032f535b06 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -1,4 +1,5 @@ """The Intent integration.""" + from __future__ import annotations import logging @@ -155,7 +156,7 @@ class GetStateIntentHandler(intent.IntentHandler): slots = self.async_validate_slots(intent_obj.slots) # Entity name to match - name: str | None = slots.get("name", {}).get("value") + entity_name: str | None = slots.get("name", {}).get("value") # Look up area first to fail early area_name = slots.get("area", {}).get("value") @@ -186,7 +187,7 @@ class GetStateIntentHandler(intent.IntentHandler): states = list( intent.async_match_states( hass, - name=name, + name=entity_name, area=area, domains=domains, device_classes=device_classes, @@ -197,7 +198,7 @@ class GetStateIntentHandler(intent.IntentHandler): _LOGGER.debug( "Found %s state(s) that matched: name=%s, area=%s, domains=%s, device_classes=%s, assistant=%s", len(states), - name, + entity_name, area, domains, device_classes, diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index d932332b4c0..c828b86264c 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -403,11 +403,11 @@ class ServiceIntentHandler(IntentHandler): slots = self.async_validate_slots(intent_obj.slots) name_slot = slots.get("name", {}) - entity_id: str | None = name_slot.get("value") - entity_name: str | None = name_slot.get("text") - if entity_id == "all": + entity_name: str | None = name_slot.get("value") + entity_text: str | None = name_slot.get("text") + if entity_name == "all": # Don't match on name if targeting all entities - entity_id = None + entity_name = None # Look up area first to fail early area_slot = slots.get("area", {}) @@ -436,7 +436,7 @@ class ServiceIntentHandler(IntentHandler): states = list( async_match_states( hass, - name=entity_id, + name=entity_name, area=area, domains=domains, device_classes=device_classes, @@ -447,7 +447,7 @@ class ServiceIntentHandler(IntentHandler): if not states: # No states matched constraints raise NoStatesMatchedError( - name=entity_name or entity_id, + name=entity_text or entity_name, area=area_name or area_id, domains=domains, device_classes=device_classes, @@ -455,6 +455,9 @@ class ServiceIntentHandler(IntentHandler): response = await self.async_handle_states(intent_obj, states, area) + # Make the matched states available in the response + response.async_set_states(matched_states=states, unmatched_states=[]) + return response async def async_handle_states( diff --git a/tests/components/climate/test_intent.py b/tests/components/climate/test_intent.py index 6473eca1b88..e4f92759793 100644 --- a/tests/components/climate/test_intent.py +++ b/tests/components/climate/test_intent.py @@ -1,4 +1,5 @@ """Test climate intents.""" + from collections.abc import Generator from unittest.mock import patch @@ -135,8 +136,10 @@ async def test_get_temperature( # Add climate entities to different areas: # climate_1 => living room # climate_2 => bedroom + # nothing in office living_room_area = area_registry.async_create(name="Living Room") bedroom_area = area_registry.async_create(name="Bedroom") + office_area = area_registry.async_create(name="Office") entity_registry.async_update_entity( climate_1.entity_id, area_id=living_room_area.id @@ -158,7 +161,7 @@ async def test_get_temperature( hass, "test", climate_intent.INTENT_GET_TEMPERATURE, - {"area": {"value": "Bedroom"}}, + {"area": {"value": bedroom_area.name}}, ) assert response.response_type == intent.IntentResponseType.QUERY_ANSWER assert len(response.matched_states) == 1 @@ -179,6 +182,52 @@ async def test_get_temperature( state = response.matched_states[0] assert state.attributes["current_temperature"] == 22.0 + # Check area with no climate entities + with pytest.raises(intent.NoStatesMatchedError) as error: + response = await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"area": {"value": office_area.name}}, + ) + + # Exception should contain details of what we tried to match + assert isinstance(error.value, intent.NoStatesMatchedError) + assert error.value.name is None + assert error.value.area == office_area.name + assert error.value.domains == {DOMAIN} + assert error.value.device_classes is None + + # Check wrong name + with pytest.raises(intent.NoStatesMatchedError) as error: + response = await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"name": {"value": "Does not exist"}}, + ) + + assert isinstance(error.value, intent.NoStatesMatchedError) + assert error.value.name == "Does not exist" + assert error.value.area is None + assert error.value.domains == {DOMAIN} + assert error.value.device_classes is None + + # Check wrong name with area + with pytest.raises(intent.NoStatesMatchedError) as error: + response = await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"name": {"value": "Climate 1"}, "area": {"value": bedroom_area.name}}, + ) + + assert isinstance(error.value, intent.NoStatesMatchedError) + assert error.value.name == "Climate 1" + assert error.value.area == bedroom_area.name + assert error.value.domains == {DOMAIN} + assert error.value.device_classes is None + async def test_get_temperature_no_entities( hass: HomeAssistant, @@ -216,19 +265,28 @@ async def test_get_temperature_no_state( climate_1.entity_id, area_id=living_room_area.id ) - with patch("homeassistant.core.StateMachine.get", return_value=None), pytest.raises( - intent.IntentHandleError + with ( + patch("homeassistant.core.StateMachine.get", return_value=None), + pytest.raises(intent.IntentHandleError), ): await intent.async_handle( hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {} ) - with patch( - "homeassistant.core.StateMachine.async_all", return_value=[] - ), pytest.raises(intent.IntentHandleError): + with ( + patch("homeassistant.core.StateMachine.async_all", return_value=[]), + pytest.raises(intent.NoStatesMatchedError) as error, + ): await intent.async_handle( hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {"area": {"value": "Living Room"}}, ) + + # Exception should contain details of what we tried to match + assert isinstance(error.value, intent.NoStatesMatchedError) + assert error.value.name is None + assert error.value.area == "Living Room" + assert error.value.domains == {DOMAIN} + assert error.value.device_classes is None diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 034bfafc1f5..f4478941473 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -1397,7 +1397,7 @@ 'name': dict({ 'name': 'name', 'text': 'my cool light', - 'value': 'light.kitchen', + 'value': 'kitchen', }), }), 'intent': dict({ @@ -1422,7 +1422,7 @@ 'name': dict({ 'name': 'name', 'text': 'my cool light', - 'value': 'light.kitchen', + 'value': 'kitchen', }), }), 'intent': dict({ @@ -1572,7 +1572,7 @@ 'name': dict({ 'name': 'name', 'text': 'test light', - 'value': 'light.demo_1234', + 'value': 'test light', }), }), 'intent': dict({ @@ -1604,7 +1604,7 @@ 'name': dict({ 'name': 'name', 'text': 'test light', - 'value': 'light.demo_1234', + 'value': 'test light', }), }), 'intent': dict({ diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 0cf343a3e20..d8a256608c8 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -101,7 +101,7 @@ async def test_exposed_areas( device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id) kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234") - entity_registry.async_update_entity( + kitchen_light = entity_registry.async_update_entity( kitchen_light.entity_id, device_id=kitchen_device.id ) hass.states.async_set( @@ -109,7 +109,7 @@ async def test_exposed_areas( ) bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678") - entity_registry.async_update_entity( + bedroom_light = entity_registry.async_update_entity( bedroom_light.entity_id, area_id=area_bedroom.id ) hass.states.async_set( @@ -206,14 +206,14 @@ async def test_unexposed_entities_skipped( # Both lights are in the kitchen exposed_light = entity_registry.async_get_or_create("light", "demo", "1234") - entity_registry.async_update_entity( + exposed_light = entity_registry.async_update_entity( exposed_light.entity_id, area_id=area_kitchen.id, ) hass.states.async_set(exposed_light.entity_id, "off") unexposed_light = entity_registry.async_get_or_create("light", "demo", "5678") - entity_registry.async_update_entity( + unexposed_light = entity_registry.async_update_entity( unexposed_light.entity_id, area_id=area_kitchen.id, ) @@ -336,7 +336,9 @@ async def test_device_area_context( light_entity = entity_registry.async_get_or_create( "light", "demo", f"{area.name}-light-{i}" ) - entity_registry.async_update_entity(light_entity.entity_id, area_id=area.id) + light_entity = entity_registry.async_update_entity( + light_entity.entity_id, area_id=area.id + ) hass.states.async_set( light_entity.entity_id, "off", @@ -692,7 +694,7 @@ async def test_empty_aliases( names = slot_lists["name"] assert len(names.values) == 1 - assert names.values[0].value_out == kitchen_light.entity_id + assert names.values[0].value_out == kitchen_light.name assert names.values[0].text_in.text == kitchen_light.name @@ -713,3 +715,82 @@ async def test_all_domains_loaded(hass: HomeAssistant, init_components) -> None: result.response.speech["plain"]["speech"] == "Sorry, I am not aware of any device called test light" ) + + +async def test_same_named_entities_in_different_areas( + hass: HomeAssistant, + init_components, + area_registry: ar.AreaRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test that entities with the same name in different areas can be targeted.""" + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") + + area_bedroom = area_registry.async_get_or_create("bedroom_id") + area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom") + + # Both lights have the same name, but are in different areas + kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234") + kitchen_light = entity_registry.async_update_entity( + kitchen_light.entity_id, + area_id=area_kitchen.id, + name="overhead light", + ) + hass.states.async_set( + kitchen_light.entity_id, + "off", + attributes={ATTR_FRIENDLY_NAME: kitchen_light.name}, + ) + + bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678") + bedroom_light = entity_registry.async_update_entity( + bedroom_light.entity_id, + area_id=area_bedroom.id, + name="overhead light", + ) + hass.states.async_set( + bedroom_light.entity_id, + "off", + attributes={ATTR_FRIENDLY_NAME: bedroom_light.name}, + ) + + # Target kitchen light + calls = async_mock_service(hass, "light", "turn_on") + result = await conversation.async_converse( + hass, "turn on overhead light in the kitchen", None, Context(), None + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.intent is not None + assert ( + result.response.intent.slots.get("name", {}).get("value") == kitchen_light.name + ) + assert ( + result.response.intent.slots.get("name", {}).get("text") == kitchen_light.name + ) + assert len(result.response.matched_states) == 1 + assert result.response.matched_states[0].entity_id == kitchen_light.entity_id + assert calls[0].data.get("entity_id") == [kitchen_light.entity_id] + + # Target bedroom light + calls.clear() + result = await conversation.async_converse( + hass, "turn on overhead light in the bedroom", None, Context(), None + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.intent is not None + assert ( + result.response.intent.slots.get("name", {}).get("value") == bedroom_light.name + ) + assert ( + result.response.intent.slots.get("name", {}).get("text") == bedroom_light.name + ) + assert len(result.response.matched_states) == 1 + assert result.response.matched_states[0].entity_id == bedroom_light.entity_id + assert calls[0].data.get("entity_id") == [bedroom_light.entity_id] diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index 74df1b7f8a6..78c30c05e7b 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -1,4 +1,7 @@ """Test conversation triggers.""" + +import logging + import pytest import voluptuous as vol @@ -70,7 +73,7 @@ async def test_if_fires_on_event(hass: HomeAssistant, calls, setup_comp) -> None async def test_response(hass: HomeAssistant, setup_comp) -> None: - """Test the firing of events.""" + """Test the conversation response action.""" response = "I'm sorry, Dave. I'm afraid I can't do that" assert await async_setup_component( hass, @@ -100,6 +103,116 @@ async def test_response(hass: HomeAssistant, setup_comp) -> None: assert service_response["response"]["speech"]["plain"]["speech"] == response +async def test_response_same_sentence(hass: HomeAssistant, calls, setup_comp) -> None: + """Test the conversation response action with multiple triggers using the same sentence.""" + assert await async_setup_component( + hass, + "automation", + { + "automation": [ + { + "trigger": { + "id": "trigger1", + "platform": "conversation", + "command": ["test sentence"], + }, + "action": [ + # Add delay so this response will not be the first + {"delay": "0:0:0.100"}, + { + "service": "test.automation", + "data_template": {"data": "{{ trigger }}"}, + }, + {"set_conversation_response": "response 2"}, + ], + }, + { + "trigger": { + "id": "trigger2", + "platform": "conversation", + "command": ["test sentence"], + }, + "action": {"set_conversation_response": "response 1"}, + }, + ] + }, + ) + + service_response = await hass.services.async_call( + "conversation", + "process", + {"text": "test sentence"}, + blocking=True, + return_response=True, + ) + await hass.async_block_till_done() + + # Should only get first response + assert service_response["response"]["speech"]["plain"]["speech"] == "response 1" + + # Service should still have been called + assert len(calls) == 1 + assert calls[0].data["data"] == { + "alias": None, + "id": "trigger1", + "idx": "0", + "platform": "conversation", + "sentence": "test sentence", + "slots": {}, + "details": {}, + } + + +async def test_response_same_sentence_with_error( + hass: HomeAssistant, calls, setup_comp, caplog: pytest.LogCaptureFixture +) -> None: + """Test the conversation response action with multiple triggers using the same sentence and an error.""" + caplog.set_level(logging.ERROR) + assert await async_setup_component( + hass, + "automation", + { + "automation": [ + { + "trigger": { + "id": "trigger1", + "platform": "conversation", + "command": ["test sentence"], + }, + "action": [ + # Add delay so this will not finish first + {"delay": "0:0:0.100"}, + {"service": "fake_domain.fake_service"}, + ], + }, + { + "trigger": { + "id": "trigger2", + "platform": "conversation", + "command": ["test sentence"], + }, + "action": {"set_conversation_response": "response 1"}, + }, + ] + }, + ) + + service_response = await hass.services.async_call( + "conversation", + "process", + {"text": "test sentence"}, + blocking=True, + return_response=True, + ) + await hass.async_block_till_done() + + # Should still get first response + assert service_response["response"]["speech"]["plain"]["speech"] == "response 1" + + # Error should have been logged + assert "Error executing script" in caplog.text + + async def test_subscribe_trigger_does_not_interfere_with_responses( hass: HomeAssistant, setup_comp, hass_ws_client: WebSocketGenerator ) -> None: diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index d80add2a441..4c327a237c7 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -1,4 +1,5 @@ """Tests for Intent component.""" + import pytest from homeassistant.components.cover import SERVICE_OPEN_COVER @@ -225,6 +226,30 @@ async def test_turn_on_multiple_intent(hass: HomeAssistant) -> None: assert call.data == {"entity_id": ["light.test_lights_2"]} +async def test_turn_on_all(hass: HomeAssistant) -> None: + """Test HassTurnOn intent with "all" name.""" + result = await async_setup_component(hass, "homeassistant", {}) + result = await async_setup_component(hass, "intent", {}) + assert result + + hass.states.async_set("light.test_light", "off") + hass.states.async_set("light.test_light_2", "off") + calls = async_mock_service(hass, "light", SERVICE_TURN_ON) + + await intent.async_handle(hass, "test", "HassTurnOn", {"name": {"value": "all"}}) + await hass.async_block_till_done() + + # All lights should be on now + assert len(calls) == 2 + entity_ids = set() + for call in calls: + assert call.domain == "light" + assert call.service == "turn_on" + entity_ids.update(call.data.get("entity_id", [])) + + assert entity_ids == {"light.test_light", "light.test_light_2"} + + async def test_get_state_intent( hass: HomeAssistant, area_registry: ar.AreaRegistry, From fc5f4bd5b8dd12a6c42af9c40b026c1695cb5921 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 8 Feb 2024 08:42:22 +0100 Subject: [PATCH 0393/1367] Bump aioelectricitymaps to 0.4.0 (#109895) --- homeassistant/components/co2signal/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json index 4f22ee68910..ff6d5bdb18b 100644 --- a/homeassistant/components/co2signal/manifest.json +++ b/homeassistant/components/co2signal/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aioelectricitymaps"], - "requirements": ["aioelectricitymaps==0.3.1"] + "requirements": ["aioelectricitymaps==0.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 434f8b9bdec..c2b79b5a9de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -236,7 +236,7 @@ aioeagle==1.1.0 aioecowitt==2024.2.0 # homeassistant.components.co2signal -aioelectricitymaps==0.3.1 +aioelectricitymaps==0.4.0 # homeassistant.components.emonitor aioemonitor==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 43e7c4d0b95..8f8e51ed281 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -215,7 +215,7 @@ aioeagle==1.1.0 aioecowitt==2024.2.0 # homeassistant.components.co2signal -aioelectricitymaps==0.3.1 +aioelectricitymaps==0.4.0 # homeassistant.components.emonitor aioemonitor==1.0.5 From d6e617eff879c289e66be3826725e81864dc7bdf Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 8 Feb 2024 08:59:57 +0100 Subject: [PATCH 0394/1367] Bump Python matter server to 5.5.0 (#109894) --- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/matter/test_api.py | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index d3d0568342e..801704c25c5 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==5.4.1"] + "requirements": ["python-matter-server==5.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index c2b79b5a9de..f9c5d48ed96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2241,7 +2241,7 @@ python-kasa[speedups]==0.6.2.1 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==5.4.1 +python-matter-server==5.5.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f8e51ed281..9ab268117c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1714,7 +1714,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.6.2.1 # homeassistant.components.matter -python-matter-server==5.4.1 +python-matter-server==5.5.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/tests/components/matter/test_api.py b/tests/components/matter/test_api.py index 892f935ebab..8e463800f98 100644 --- a/tests/components/matter/test_api.py +++ b/tests/components/matter/test_api.py @@ -229,6 +229,7 @@ async def test_node_diagnostics( mac_address="00:11:22:33:44:55", available=True, active_fabrics=[MatterFabricData(2, 4939, 1, vendor_name="Nabu Casa")], + active_fabric_index=0, ) matter_client.node_diagnostics = AsyncMock(return_value=mock_diagnostics) From 2d88b77813ac61adcb333f366aa9c61a6e0820ba Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 8 Feb 2024 09:01:48 +0100 Subject: [PATCH 0395/1367] Skip polling of unavailable Matter nodes (#109917) --- 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 61535d990db..5c3f65d903c 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -129,6 +129,9 @@ class MatterEntity(Entity): async def async_update(self) -> None: """Call when the entity needs to be updated.""" + if not self._endpoint.node.available: + # skip poll when the node is not (yet) available + return # manually poll/refresh the primary value await self.matter_client.refresh_attribute( self._endpoint.node.node_id, From 168657b724c59c0436df41a0026228eaced09ea6 Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 8 Feb 2024 04:51:20 -0500 Subject: [PATCH 0396/1367] Catch APIRateLimit in Honeywell (#107806) --- homeassistant/components/honeywell/climate.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 9d2768334ff..61ccdd00e49 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -6,6 +6,7 @@ from typing import Any from aiohttp import ClientConnectionError from aiosomecomfort import ( + APIRateLimited, AuthError, ConnectionError as AscConnectionError, SomeComfortError, @@ -504,10 +505,11 @@ class HoneywellUSThermostat(ClimateEntity): await self._device.refresh() except ( + TimeoutError, + AscConnectionError, + APIRateLimited, AuthError, ClientConnectionError, - AscConnectionError, - TimeoutError, ): self._retry += 1 self._attr_available = self._retry <= RETRY @@ -522,8 +524,12 @@ class HoneywellUSThermostat(ClimateEntity): except UnauthorizedError: await _login() return - - except (AscConnectionError, ClientConnectionError, TimeoutError): + except ( + TimeoutError, + AscConnectionError, + APIRateLimited, + ClientConnectionError, + ): self._retry += 1 self._attr_available = self._retry <= RETRY return From 55f10656a7daf8d750b29136b4d58f69a2f03de5 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 8 Feb 2024 12:40:32 +0100 Subject: [PATCH 0397/1367] Allow modbus "scale" to be negative. (#109965) --- homeassistant/components/modbus/__init__.py | 2 +- tests/components/modbus/test_sensor.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 0f674d4d0df..36e841f7859 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -186,7 +186,7 @@ BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend( ] ), vol.Optional(CONF_STRUCTURE): cv.string, - vol.Optional(CONF_SCALE, default=1): cv.positive_float, + vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), vol.Optional(CONF_PRECISION): cv.positive_int, vol.Optional( diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 97571041482..5ca38873c42 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -688,6 +688,16 @@ async def test_config_wrong_struct_sensor( False, "112594", ), + ( + { + CONF_DATA_TYPE: DataType.INT16, + CONF_SCALE: -1, + CONF_OFFSET: 0, + }, + [0x000A], + False, + "-10", + ), ], ) async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: From c7957f8e94fe7780051ba4fde5c2c0f221fb8e38 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 Feb 2024 13:00:45 +0100 Subject: [PATCH 0398/1367] Prevent network access in emulated_hue tests (#109991) --- tests/components/emulated_hue/test_init.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 8f35997f176..9a872d66946 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,7 +1,9 @@ """Test the Emulated Hue component.""" from datetime import timedelta from typing import Any -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch + +from aiohttp import web from homeassistant.components.emulated_hue.config import ( DATA_KEY, @@ -135,6 +137,9 @@ async def test_setup_works(hass: HomeAssistant) -> None: AsyncMock(), ) as mock_create_upnp_datagram_endpoint, patch( "homeassistant.components.emulated_hue.async_get_source_ip" + ), patch( + "homeassistant.components.emulated_hue.web.TCPSite", + return_value=Mock(spec_set=web.TCPSite), ): mock_create_upnp_datagram_endpoint.return_value = AsyncMock( spec=UPNPResponderProtocol From e7213a419247d44b794ac2b4aee65b9f512a2cf2 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 8 Feb 2024 13:14:10 +0100 Subject: [PATCH 0399/1367] Handle Matter nodes that become available after startup is done (#109956) --- homeassistant/components/matter/adapter.py | 21 +++++++++++++++++++++ tests/components/matter/test_adapter.py | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index 5690996841d..6d7d437a206 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -52,11 +52,27 @@ class MatterAdapter: async def setup_nodes(self) -> None: """Set up all existing nodes and subscribe to new nodes.""" + initialized_nodes: set[int] = set() for node in self.matter_client.get_nodes(): + if not node.available: + # ignore un-initialized nodes at startup + # catch them later when they become available. + continue + initialized_nodes.add(node.node_id) self._setup_node(node) def node_added_callback(event: EventType, node: MatterNode) -> None: """Handle node added event.""" + initialized_nodes.add(node.node_id) + self._setup_node(node) + + def node_updated_callback(event: EventType, node: MatterNode) -> None: + """Handle node updated event.""" + if node.node_id in initialized_nodes: + return + if not node.available: + return + initialized_nodes.add(node.node_id) self._setup_node(node) def endpoint_added_callback(event: EventType, data: dict[str, int]) -> None: @@ -116,6 +132,11 @@ class MatterAdapter: callback=node_added_callback, event_filter=EventType.NODE_ADDED ) ) + self.config_entry.async_on_unload( + self.matter_client.subscribe_events( + callback=node_updated_callback, event_filter=EventType.NODE_UPDATED + ) + ) def _setup_node(self, node: MatterNode) -> None: """Set up an node.""" diff --git a/tests/components/matter/test_adapter.py b/tests/components/matter/test_adapter.py index 35e6673114e..0cc3e360ab6 100644 --- a/tests/components/matter/test_adapter.py +++ b/tests/components/matter/test_adapter.py @@ -144,10 +144,10 @@ async def test_node_added_subscription( integration: MagicMock, ) -> None: """Test subscription to new devices work.""" - assert matter_client.subscribe_events.call_count == 4 + assert matter_client.subscribe_events.call_count == 5 assert ( matter_client.subscribe_events.call_args.kwargs["event_filter"] - == EventType.NODE_ADDED + == EventType.NODE_UPDATED ) node_added_callback = matter_client.subscribe_events.call_args.kwargs["callback"] From e9684865946cf9b31d61b97d54c9020e3f5cc63b Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 8 Feb 2024 13:16:50 +0100 Subject: [PATCH 0400/1367] Bump pymodbus to v3.6.4 (#109980) --- homeassistant/components/modbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 194eb56757e..b90f5663643 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["pymodbus"], "quality_scale": "gold", - "requirements": ["pymodbus==3.6.3"] + "requirements": ["pymodbus==3.6.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index f9c5d48ed96..37e9d30b341 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1955,7 +1955,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.3 +pymodbus==3.6.4 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ab268117c6..6e6f121a0fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1506,7 +1506,7 @@ pymeteoclimatic==0.1.0 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.3 +pymodbus==3.6.4 # homeassistant.components.monoprice pymonoprice==0.4 From 29d3e174613c3b7bd900c6da868c8b4a5bdaa9ea Mon Sep 17 00:00:00 2001 From: wilburCforce <109390391+wilburCforce@users.noreply.github.com> Date: Thu, 8 Feb 2024 06:32:57 -0600 Subject: [PATCH 0401/1367] Update pylutron to 0.2.11 (#109853) --- homeassistant/components/lutron/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index 6444aa306a2..67ebebcc25b 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/lutron", "iot_class": "local_polling", "loggers": ["pylutron"], - "requirements": ["pylutron==0.2.8"] + "requirements": ["pylutron==0.2.11"] } diff --git a/requirements_all.txt b/requirements_all.txt index 37e9d30b341..382e238a9bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1931,7 +1931,7 @@ pylitterbot==2023.4.9 pylutron-caseta==0.19.0 # homeassistant.components.lutron -pylutron==0.2.8 +pylutron==0.2.11 # homeassistant.components.mailgun pymailgunner==1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e6f121a0fb..c87893a7dcb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1488,7 +1488,7 @@ pylitterbot==2023.4.9 pylutron-caseta==0.19.0 # homeassistant.components.lutron -pylutron==0.2.8 +pylutron==0.2.11 # homeassistant.components.mailgun pymailgunner==1.4 From 9f50153c8b6b01e315767851c87d7160ed42f2d8 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 8 Feb 2024 13:48:33 +0100 Subject: [PATCH 0402/1367] Allow modbus min/max temperature to be negative. (#109977) --- homeassistant/components/modbus/__init__.py | 4 ++-- tests/components/modbus/test_climate.py | 26 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 36e841f7859..0ceb1a2523f 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -241,8 +241,8 @@ CLIMATE_SCHEMA = vol.All( { vol.Required(CONF_TARGET_TEMP): cv.positive_int, vol.Optional(CONF_TARGET_TEMP_WRITE_REGISTERS, default=False): cv.boolean, - vol.Optional(CONF_MAX_TEMP, default=35): cv.positive_float, - vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_float, + vol.Optional(CONF_MAX_TEMP, default=35): vol.Coerce(float), + vol.Optional(CONF_MIN_TEMP, default=5): vol.Coerce(float), vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float), vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string, vol.Optional(CONF_HVAC_ONOFF_REGISTER): cv.positive_int, diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 3ff9aa37bcf..b885e6452d8 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -42,6 +42,8 @@ from homeassistant.components.modbus.const import ( CONF_HVAC_MODE_REGISTER, CONF_HVAC_MODE_VALUES, CONF_HVAC_ONOFF_REGISTER, + CONF_MAX_TEMP, + CONF_MIN_TEMP, CONF_TARGET_TEMP, CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_WRITE_REGISTERS, @@ -170,6 +172,30 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") } ], }, + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_MIN_TEMP: 23, + CONF_MAX_TEMP: 57, + } + ], + }, + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_MIN_TEMP: -57, + CONF_MAX_TEMP: -23, + } + ], + }, ], ) async def test_config_climate(hass: HomeAssistant, mock_modbus) -> None: From b3293972880b69b749d08af2a7b704e189e54a52 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 8 Feb 2024 15:29:55 +0100 Subject: [PATCH 0403/1367] Fix callable import (#110003) --- homeassistant/components/renson/button.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/renson/button.py b/homeassistant/components/renson/button.py index 117fadb502b..5cdf0e4787b 100644 --- a/homeassistant/components/renson/button.py +++ b/homeassistant/components/renson/button.py @@ -1,9 +1,9 @@ """Renson ventilation unit buttons.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass -from _collections_abc import Callable from renson_endura_delta.renson import RensonVentilation from homeassistant.components.button import ( From 97c6fd0f8d8785fc3dd2b12b73b893496eb5adb2 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 8 Feb 2024 15:34:43 +0100 Subject: [PATCH 0404/1367] Allow modbus negative min/max value. (#109995) --- homeassistant/components/modbus/__init__.py | 4 ++-- tests/components/modbus/test_sensor.py | 22 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 0ceb1a2523f..1151a5f1f01 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -342,8 +342,8 @@ SENSOR_SCHEMA = vol.All( vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Exclusive(CONF_VIRTUAL_COUNT, "vir_sen_count"): cv.positive_int, vol.Exclusive(CONF_SLAVE_COUNT, "vir_sen_count"): cv.positive_int, - vol.Optional(CONF_MIN_VALUE): cv.positive_float, - vol.Optional(CONF_MAX_VALUE): cv.positive_float, + vol.Optional(CONF_MIN_VALUE): vol.Coerce(float), + vol.Optional(CONF_MAX_VALUE): vol.Coerce(float), vol.Optional(CONF_NAN_VALUE): nan_validator, vol.Optional(CONF_ZERO_SUPPRESS): cv.positive_float, } diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 5ca38873c42..aa8b15585dc 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -185,6 +185,28 @@ SLAVE_UNIQUE_ID = "ground_floor_sensor" } ] }, + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_DATA_TYPE: DataType.INT16, + CONF_MIN_VALUE: 1, + CONF_MAX_VALUE: 3, + } + ] + }, + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_DATA_TYPE: DataType.INT16, + CONF_MIN_VALUE: -3, + CONF_MAX_VALUE: -1, + } + ] + }, ], ) async def test_config_sensor(hass: HomeAssistant, mock_modbus) -> None: From 2f15053311dd383ab81c3d637a867d2fb649d58c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 Feb 2024 15:39:01 +0100 Subject: [PATCH 0405/1367] Don't blow up if config entries have unhashable unique IDs (#109966) * Don't blow up if config entries have unhashable unique IDs * Add test * Add comment on when we remove the guard * Don't stringify hashable non string unique_id --- homeassistant/config_entries.py | 38 +++++++++++++++++--- tests/test_config_entries.py | 61 +++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 4debdc8e495..cfb18505813 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -7,6 +7,7 @@ from collections.abc import ( Callable, Coroutine, Generator, + Hashable, Iterable, Mapping, ValuesView, @@ -49,6 +50,7 @@ from .helpers.event import ( ) from .helpers.frame import report from .helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType, UndefinedType +from .loader import async_suggest_report_issue from .setup import DATA_SETUP_DONE, async_process_deps_reqs, async_setup_component from .util import uuid as uuid_util from .util.decorator import Registry @@ -1124,9 +1126,10 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]): - domain -> unique_id -> ConfigEntry """ - def __init__(self) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize the container.""" super().__init__() + self._hass = hass self._domain_index: dict[str, list[ConfigEntry]] = {} self._domain_unique_id_index: dict[str, dict[str, ConfigEntry]] = {} @@ -1145,8 +1148,27 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]): data[entry_id] = entry self._domain_index.setdefault(entry.domain, []).append(entry) if entry.unique_id is not None: + unique_id_hash = entry.unique_id + # Guard against integrations using unhashable unique_id + # In HA Core 2024.9, we should remove the guard and instead fail + if not isinstance(entry.unique_id, Hashable): + unique_id_hash = str(entry.unique_id) # type: ignore[unreachable] + report_issue = async_suggest_report_issue( + self._hass, integration_domain=entry.domain + ) + _LOGGER.error( + ( + "Config entry '%s' from integration %s has an invalid unique_id" + " '%s', please %s" + ), + entry.title, + entry.domain, + entry.unique_id, + report_issue, + ) + self._domain_unique_id_index.setdefault(entry.domain, {})[ - entry.unique_id + unique_id_hash ] = entry def _unindex_entry(self, entry_id: str) -> None: @@ -1157,6 +1179,9 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]): if not self._domain_index[domain]: del self._domain_index[domain] if (unique_id := entry.unique_id) is not None: + # Check type first to avoid expensive isinstance call + if type(unique_id) is not str and not isinstance(unique_id, Hashable): # noqa: E721 + unique_id = str(entry.unique_id) # type: ignore[unreachable] del self._domain_unique_id_index[domain][unique_id] if not self._domain_unique_id_index[domain]: del self._domain_unique_id_index[domain] @@ -1174,6 +1199,9 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]): self, domain: str, unique_id: str ) -> ConfigEntry | None: """Get entry by domain and unique id.""" + # Check type first to avoid expensive isinstance call + if type(unique_id) is not str and not isinstance(unique_id, Hashable): # noqa: E721 + unique_id = str(unique_id) # type: ignore[unreachable] return self._domain_unique_id_index.get(domain, {}).get(unique_id) @@ -1189,7 +1217,7 @@ class ConfigEntries: self.flow = ConfigEntriesFlowManager(hass, self, hass_config) self.options = OptionsFlowManager(hass) self._hass_config = hass_config - self._entries = ConfigEntryItems() + self._entries = ConfigEntryItems(hass) self._store = storage.Store[dict[str, list[dict[str, Any]]]]( hass, STORAGE_VERSION, STORAGE_KEY ) @@ -1314,10 +1342,10 @@ class ConfigEntries: self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) if config is None: - self._entries = ConfigEntryItems() + self._entries = ConfigEntryItems(self.hass) return - entries: ConfigEntryItems = ConfigEntryItems() + entries: ConfigEntryItems = ConfigEntryItems(self.hass) for entry in config["entries"]: pref_disable_new_entities = entry.get("pref_disable_new_entities") diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 1c67534d5df..609f80e1a60 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -4257,3 +4257,64 @@ async def test_update_entry_and_reload( assert entry.state == config_entries.ConfigEntryState.LOADED assert task["type"] == FlowResultType.ABORT assert task["reason"] == "reauth_successful" + + +@pytest.mark.parametrize("unique_id", [["blah", "bleh"], {"key": "value"}]) +async def test_unhashable_unique_id( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, unique_id: Any +) -> None: + """Test the ConfigEntryItems user dict handles unhashable unique_id.""" + entries = config_entries.ConfigEntryItems(hass) + entry = config_entries.ConfigEntry( + version=1, + minor_version=1, + domain="test", + entry_id="mock_id", + title="title", + data={}, + source="test", + unique_id=unique_id, + ) + + entries[entry.entry_id] = entry + assert ( + "Config entry 'title' from integration test has an invalid unique_id " + f"'{str(unique_id)}'" + ) in caplog.text + + assert entry.entry_id in entries + assert entries[entry.entry_id] is entry + assert entries.get_entry_by_domain_and_unique_id("test", unique_id) == entry + del entries[entry.entry_id] + assert not entries + assert entries.get_entry_by_domain_and_unique_id("test", unique_id) is None + + +@pytest.mark.parametrize("unique_id", [123]) +async def test_hashable_non_string_unique_id( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, unique_id: Any +) -> None: + """Test the ConfigEntryItems user dict handles hashable non string unique_id.""" + entries = config_entries.ConfigEntryItems(hass) + entry = config_entries.ConfigEntry( + version=1, + minor_version=1, + domain="test", + entry_id="mock_id", + title="title", + data={}, + source="test", + unique_id=unique_id, + ) + + entries[entry.entry_id] = entry + assert ( + "Config entry 'title' from integration test has an invalid unique_id" + ) not in caplog.text + + assert entry.entry_id in entries + assert entries[entry.entry_id] is entry + assert entries.get_entry_by_domain_and_unique_id("test", unique_id) == entry + del entries[entry.entry_id] + assert not entries + assert entries.get_entry_by_domain_and_unique_id("test", unique_id) is None From fa0260a5d5ed15c45e1af50bf344cafdb1d859b8 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 8 Feb 2024 15:41:19 +0100 Subject: [PATCH 0406/1367] Bump aioecowitt to 2024.2.1 (#109999) --- homeassistant/components/ecowitt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json index d3dfe0331ef..175960ab57d 100644 --- a/homeassistant/components/ecowitt/manifest.json +++ b/homeassistant/components/ecowitt/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["webhook"], "documentation": "https://www.home-assistant.io/integrations/ecowitt", "iot_class": "local_push", - "requirements": ["aioecowitt==2024.2.0"] + "requirements": ["aioecowitt==2024.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 382e238a9bc..b0a8a5366e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -233,7 +233,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2024.2.0 +aioecowitt==2024.2.1 # homeassistant.components.co2signal aioelectricitymaps==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c87893a7dcb..57337535278 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2024.2.0 +aioecowitt==2024.2.1 # homeassistant.components.co2signal aioelectricitymaps==0.4.0 From 69af00b36054032b3ffee063f0ec7f7effd4ab9e Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 8 Feb 2024 15:41:37 +0100 Subject: [PATCH 0407/1367] Bump deebot-client to 5.1.1 (#109994) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 34760ea6aca..3fcb2b3211e 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.8", "deebot-client==5.1.0"] + "requirements": ["py-sucks==0.9.8", "deebot-client==5.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index b0a8a5366e8..1aea1db5a68 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -687,7 +687,7 @@ debugpy==1.8.0 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==5.1.0 +deebot-client==5.1.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57337535278..1ae7ef77878 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,7 +562,7 @@ dbus-fast==2.21.1 debugpy==1.8.0 # homeassistant.components.ecovacs -deebot-client==5.1.0 +deebot-client==5.1.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 1adbddb754498d993d8929d73e23a292620d3d85 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 8 Feb 2024 17:40:30 +0100 Subject: [PATCH 0408/1367] Update pylint-per-file-ignores to 1.3.2 (#110014) --- pyproject.toml | 12 ++++++------ requirements_test.txt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a404669de91..ef3e150d4ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -383,6 +383,12 @@ enable = [ #"useless-suppression", # temporarily every now and then to clean them up "use-symbolic-message-instead", ] +per-file-ignores = [ + # hass-component-root-import: Tests test non-public APIs + # protected-access: Tests do often test internals a lot + # redefined-outer-name: Tests reference fixtures in the test function + "/tests/:hass-component-root-import,protected-access,redefined-outer-name", +] [tool.pylint.REPORTS] score = false @@ -409,12 +415,6 @@ runtime-typing = false [tool.pylint.CODE_STYLE] max-line-length-suggestions = 72 -[tool.pylint-per-file-ignores] -# hass-component-root-import: Tests test non-public APIs -# protected-access: Tests do often test internals a lot -# redefined-outer-name: Tests reference fixtures in the test function -"/tests/"="hass-component-root-import,protected-access,redefined-outer-name" - [tool.pytest.ini_options] testpaths = [ "tests", diff --git a/requirements_test.txt b/requirements_test.txt index 5b970b68a13..f823bf05fb0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -15,7 +15,7 @@ mypy==1.8.0 pre-commit==3.6.0 pydantic==1.10.12 pylint==3.0.3 -pylint-per-file-ignores==1.2.1 +pylint-per-file-ignores==1.3.2 pipdeptree==2.13.2 pytest-asyncio==0.23.4 pytest-aiohttp==1.0.5 From 35cb37ffb0d15c4ee139ff956c492a103db2fe75 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 8 Feb 2024 20:03:41 +0100 Subject: [PATCH 0409/1367] Update frontend to 20240207.1 (#110039) --- 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 d998871a60b..21f4df79568 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==20240207.0"] + "requirements": ["home-assistant-frontend==20240207.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dd1ca886b4e..3dadd4717c8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ habluetooth==2.4.0 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240207.0 +home-assistant-frontend==20240207.1 home-assistant-intents==2024.2.2 httpx==0.26.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1aea1db5a68..df84d0dc6cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1062,7 +1062,7 @@ hole==0.8.0 holidays==0.42 # homeassistant.components.frontend -home-assistant-frontend==20240207.0 +home-assistant-frontend==20240207.1 # homeassistant.components.conversation home-assistant-intents==2024.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ae7ef77878..6f3c1d48773 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -858,7 +858,7 @@ hole==0.8.0 holidays==0.42 # homeassistant.components.frontend -home-assistant-frontend==20240207.0 +home-assistant-frontend==20240207.1 # homeassistant.components.conversation home-assistant-intents==2024.2.2 From a0e515df1a327ab0c8dccaa3d3c45aab1594d40e Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Thu, 8 Feb 2024 14:09:53 -0500 Subject: [PATCH 0410/1367] Allow disabling home assistant watchdog (#109818) --- homeassistant/components/hassio/handler.py | 1 - tests/components/hassio/test_init.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 37b4b0ded9c..9381b7951d6 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -506,7 +506,6 @@ class HassIO: options = { "ssl": CONF_SSL_CERTIFICATE in http_config, "port": port, - "watchdog": True, "refresh_token": refresh_token.token, } diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index fe8eeb0b0f6..1c1197131c0 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -293,7 +293,7 @@ async def test_setup_api_push_api_data( assert aioclient_mock.call_count == 19 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 - assert aioclient_mock.mock_calls[1][2]["watchdog"] + assert "watchdog" not in aioclient_mock.mock_calls[1][2] async def test_setup_api_push_api_data_server_host( From 23fa9103d78be88c2c68ecd2c6c969aba94ad98f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 20:14:49 +0100 Subject: [PATCH 0411/1367] Bump Wandalen/wretry.action from 1.3.0 to 1.4.0 (#109950) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 522544bba80..c48e29db315 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1079,7 +1079,7 @@ jobs: uses: actions/download-artifact@v3 - name: Upload coverage to Codecov (full coverage) if: needs.info.outputs.test_full_suite == 'true' - uses: Wandalen/wretry.action@v1.3.0 + uses: Wandalen/wretry.action@v1.4.0 with: action: codecov/codecov-action@v3.1.3 with: | @@ -1090,7 +1090,7 @@ jobs: attempt_delay: 30000 - name: Upload coverage to Codecov (partial coverage) if: needs.info.outputs.test_full_suite == 'false' - uses: Wandalen/wretry.action@v1.3.0 + uses: Wandalen/wretry.action@v1.4.0 with: action: codecov/codecov-action@v3.1.3 with: | From 326c7c0495c5b717b1ff87420be2a7869b1b4c8c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Thu, 8 Feb 2024 20:19:23 +0100 Subject: [PATCH 0412/1367] Add option-icons for Plugwise Select (#109986) --- homeassistant/components/plugwise/icons.json | 30 +++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/plugwise/icons.json b/homeassistant/components/plugwise/icons.json index 4af2c0b4c75..2a57dd4350f 100644 --- a/homeassistant/components/plugwise/icons.json +++ b/homeassistant/components/plugwise/icons.json @@ -64,16 +64,38 @@ }, "select": { "dhw_mode": { - "default": "mdi:shower" + "default": "mdi:shower", + "state": { + "comfort": "mdi:sofa", + "eco": "mdi:leaf", + "off": "mdi:circle-off-outline", + "boost": "mdi:rocket-launch", + "auto": "mdi:auto-mode" + } }, "gateway_mode": { - "default": "mdi:cog-outline" + "default": "mdi:cog-outline", + "state": { + "away": "mdi:pause", + "full": "mdi:home", + "vacation": "mdi:beach" + } }, "regulation_mode": { - "default": "mdi:hvac" + "default": "mdi:hvac", + "state": { + "bleeding_hot": "mdi:fire-circle", + "bleeding_cold": "mdi:water-circle", + "off": "mdi:circle-off-outline", + "heating": "mdi:radiator", + "cooling": "mdi:snowflake" + } }, "select_schedule": { - "default": "mdi:calendar-clock" + "default": "mdi:calendar-clock", + "state": { + "off": "mdi:circle-off-outline" + } } }, "sensor": { From 531f2e8443af6b5d3b53a723a624f4c7a45a0ecb Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 8 Feb 2024 23:15:22 +0100 Subject: [PATCH 0413/1367] bump wretry.action@v1.4.4 (#110053) --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c48e29db315..e3ced9b9692 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1079,7 +1079,7 @@ jobs: uses: actions/download-artifact@v3 - name: Upload coverage to Codecov (full coverage) if: needs.info.outputs.test_full_suite == 'true' - uses: Wandalen/wretry.action@v1.4.0 + uses: Wandalen/wretry.action@v1.4.4 with: action: codecov/codecov-action@v3.1.3 with: | @@ -1090,7 +1090,7 @@ jobs: attempt_delay: 30000 - name: Upload coverage to Codecov (partial coverage) if: needs.info.outputs.test_full_suite == 'false' - uses: Wandalen/wretry.action@v1.4.0 + uses: Wandalen/wretry.action@v1.4.4 with: action: codecov/codecov-action@v3.1.3 with: | From 02efe41564e2f16aa3f12f563b44b13c04a61938 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Feb 2024 16:31:17 -0600 Subject: [PATCH 0414/1367] Avoid directly changing config entry state in tests (#110048) --- tests/common.py | 21 +++++++++++++++++++ tests/components/blink/test_services.py | 4 ++-- tests/components/hue/test_light_v1.py | 2 +- tests/components/opower/test_config_flow.py | 4 ++-- .../smartthings/test_binary_sensor.py | 2 +- 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_scene.py | 2 +- tests/components/smartthings/test_sensor.py | 2 +- tests/components/smartthings/test_switch.py | 2 +- 12 files changed, 34 insertions(+), 13 deletions(-) diff --git a/tests/common.py b/tests/common.py index 1b40904d5e2..24b9134c32e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -944,6 +944,27 @@ class MockConfigEntry(config_entries.ConfigEntry): """Test helper to add entry to entry manager.""" manager._entries[self.entry_id] = self + def mock_state( + self, + hass: HomeAssistant, + state: config_entries.ConfigEntryState, + reason: str | None = None, + ) -> None: + """Mock the state of a config entry to be used in tests. + + Currently this is a wrapper around _async_set_state, but it may + change in the future. + + It is preferable to get the config entry into the desired state + by using the normal config entry methods, and this helper + is only intended to be used in cases where that is not possible. + + When in doubt, this helper should not be used in new code + and is only intended for backwards compatibility with existing + tests. + """ + self._async_set_state(hass, state, reason) + def patch_yaml_files(files_dict, endswith=True): """Patch load_yaml with a dictionary of yaml files.""" diff --git a/tests/components/blink/test_services.py b/tests/components/blink/test_services.py index 1c2faa32d04..c5777ecb2f0 100644 --- a/tests/components/blink/test_services.py +++ b/tests/components/blink/test_services.py @@ -179,7 +179,7 @@ async def test_service_pin_called_with_unloaded_entry( 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() - mock_config_entry.state = ConfigEntryState.SETUP_ERROR + mock_config_entry.mock_state(hass, ConfigEntryState.SETUP_ERROR) hass.config.is_allowed_path = Mock(return_value=True) mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()} @@ -207,7 +207,7 @@ async def test_service_update_called_with_unloaded_entry( assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - mock_config_entry.state = ConfigEntryState.SETUP_ERROR + mock_config_entry.mock_state(hass, ConfigEntryState.SETUP_ERROR) hass.config.is_allowed_path = Mock(return_value=True) mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()} diff --git a/tests/components/hue/test_light_v1.py b/tests/components/hue/test_light_v1.py index d1fd9cdc62f..90f8b0adfbf 100644 --- a/tests/components/hue/test_light_v1.py +++ b/tests/components/hue/test_light_v1.py @@ -179,7 +179,7 @@ async def setup_bridge(hass, mock_bridge_v1): hass.config.components.add(hue.DOMAIN) config_entry = create_config_entry() config_entry.add_to_hass(hass) - config_entry.state = ConfigEntryState.LOADED + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.options = {CONF_ALLOW_HUE_GROUPS: True} mock_bridge_v1.config_entry = config_entry hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge_v1} diff --git a/tests/components/opower/test_config_flow.py b/tests/components/opower/test_config_flow.py index f9ae457a80e..0e96af200df 100644 --- a/tests/components/opower/test_config_flow.py +++ b/tests/components/opower/test_config_flow.py @@ -277,7 +277,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.mock_state(hass, ConfigEntryState.LOADED) mock_config_entry.async_start_reauth(hass) await hass.async_block_till_done() @@ -326,7 +326,7 @@ async def test_form_valid_reauth_with_mfa( "utility": "Consolidated Edison (ConEd)", }, ) - mock_config_entry.state = ConfigEntryState.LOADED + mock_config_entry.mock_state(hass, ConfigEntryState.LOADED) mock_config_entry.async_start_reauth(hass) await hass.async_block_till_done() diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index d6fe0bd40fc..1c222b3ca78 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -106,7 +106,7 @@ async def test_unload_config_entry(hass: HomeAssistant, device_factory) -> None: "Motion Sensor 1", [Capability.motion_sensor], {Attribute.motion: "inactive"} ) config_entry = await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device]) - config_entry.state = ConfigEntryState.LOADED + config_entry.mock_state(hass, ConfigEntryState.LOADED) # Act await hass.config_entries.async_forward_entry_unload(config_entry, "binary_sensor") # Assert diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index bf781c71c4e..081b40e57a9 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -243,7 +243,7 @@ async def test_unload_config_entry(hass: HomeAssistant, device_factory) -> None: "Garage", [Capability.garage_door_control], {Attribute.door: "open"} ) config_entry = await setup_platform(hass, COVER_DOMAIN, devices=[device]) - config_entry.state = ConfigEntryState.LOADED + config_entry.mock_state(hass, ConfigEntryState.LOADED) # Act await hass.config_entries.async_forward_entry_unload(config_entry, COVER_DOMAIN) # Assert diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 751646580d9..ca2f97b2909 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -301,7 +301,7 @@ async def test_unload_config_entry(hass: HomeAssistant, device_factory) -> None: status={Attribute.switch: "off", Attribute.fan_speed: 0}, ) config_entry = await setup_platform(hass, FAN_DOMAIN, devices=[device]) - config_entry.state = ConfigEntryState.LOADED + config_entry.mock_state(hass, ConfigEntryState.LOADED) # Act await hass.config_entries.async_forward_entry_unload(config_entry, "fan") # Assert diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index d2d0a133859..b6e4e5a107b 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -319,7 +319,7 @@ async def test_unload_config_entry(hass: HomeAssistant, device_factory) -> None: }, ) config_entry = await setup_platform(hass, LIGHT_DOMAIN, devices=[device]) - config_entry.state = ConfigEntryState.LOADED + config_entry.mock_state(hass, ConfigEntryState.LOADED) # Act await hass.config_entries.async_forward_entry_unload(config_entry, "light") # Assert diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 58111087848..10981433c1d 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -120,7 +120,7 @@ async def test_unload_config_entry(hass: HomeAssistant, device_factory) -> None: # Arrange device = device_factory("Lock_1", [Capability.lock], {Attribute.lock: "locked"}) config_entry = await setup_platform(hass, LOCK_DOMAIN, devices=[device]) - config_entry.state = ConfigEntryState.LOADED + config_entry.mock_state(hass, ConfigEntryState.LOADED) # Act await hass.config_entries.async_forward_entry_unload(config_entry, "lock") # Assert diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py index 39e387c7658..489ca87371c 100644 --- a/tests/components/smartthings/test_scene.py +++ b/tests/components/smartthings/test_scene.py @@ -44,7 +44,7 @@ async def test_unload_config_entry(hass: HomeAssistant, scene) -> None: """Test the scene is removed when the config entry is unloaded.""" # Arrange config_entry = await setup_platform(hass, SCENE_DOMAIN, scenes=[scene]) - config_entry.state = ConfigEntryState.LOADED + config_entry.mock_state(hass, ConfigEntryState.LOADED) # Act await hass.config_entries.async_forward_entry_unload(config_entry, SCENE_DOMAIN) # Assert diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index ab163360778..35d38dd33de 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -296,7 +296,7 @@ async def test_unload_config_entry(hass: HomeAssistant, device_factory) -> None: # Arrange device = device_factory("Sensor 1", [Capability.battery], {Attribute.battery: 100}) config_entry = await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) - config_entry.state = ConfigEntryState.LOADED + config_entry.mock_state(hass, ConfigEntryState.LOADED) # Act await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") # Assert diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 437acb04f56..e9dd8ad1b68 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -106,7 +106,7 @@ async def test_unload_config_entry(hass: HomeAssistant, device_factory) -> None: # Arrange device = device_factory("Switch 1", [Capability.switch], {Attribute.switch: "on"}) config_entry = await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) - config_entry.state = ConfigEntryState.LOADED + config_entry.mock_state(hass, ConfigEntryState.LOADED) # Act await hass.config_entries.async_forward_entry_unload(config_entry, "switch") # Assert From 2681dae60cef666989089d9e24697f65d2b420c5 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 8 Feb 2024 19:38:03 -0600 Subject: [PATCH 0415/1367] Matching duplicate named entities is now an error in Assist (#110050) * Matching duplicate named entities is now an error * Update snapshot * Only use area id --- .../components/conversation/default_agent.py | 30 ++- homeassistant/components/intent/__init__.py | 21 +- homeassistant/helpers/intent.py | 24 +- .../conversation/snapshots/test_init.ambr | 4 +- .../conversation/test_default_agent.py | 218 ++++++++++++++++++ 5 files changed, 283 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 52925fbc241..cd371ff0630 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -316,6 +316,20 @@ class DefaultAgent(AbstractConversationAgent): ), conversation_id, ) + except intent.DuplicateNamesMatchedError as duplicate_names_error: + # Intent was valid, but two or more entities with the same name matched. + ( + error_response_type, + error_response_args, + ) = _get_duplicate_names_matched_response(duplicate_names_error) + return _make_error_result( + language, + intent.IntentResponseErrorCode.NO_VALID_TARGETS, + self._get_error_text( + error_response_type, lang_intents, **error_response_args + ), + conversation_id, + ) except intent.IntentHandleError: # Intent was valid and entities matched constraints, but an error # occurred during handling. @@ -753,7 +767,7 @@ class DefaultAgent(AbstractConversationAgent): if not alias.strip(): continue - entity_names.append((alias, state.name, context)) + entity_names.append((alias, alias, context)) # Default name entity_names.append((state.name, state.name, context)) @@ -992,6 +1006,20 @@ def _get_no_states_matched_response( return ErrorKey.NO_INTENT, {} +def _get_duplicate_names_matched_response( + duplicate_names_error: intent.DuplicateNamesMatchedError, +) -> tuple[ErrorKey, dict[str, Any]]: + """Return key and template arguments for error when intent returns duplicate matches.""" + + if duplicate_names_error.area: + return ErrorKey.DUPLICATE_ENTITIES_IN_AREA, { + "entity": duplicate_names_error.name, + "area": duplicate_names_error.area, + } + + return ErrorKey.DUPLICATE_ENTITIES, {"entity": duplicate_names_error.name} + + def _collect_list_references(expression: Expression, list_names: set[str]) -> None: """Collect list reference names recursively.""" if isinstance(expression, Sequence): diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index d032f535b06..e960b5616cb 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -156,16 +156,18 @@ class GetStateIntentHandler(intent.IntentHandler): slots = self.async_validate_slots(intent_obj.slots) # Entity name to match - entity_name: str | None = slots.get("name", {}).get("value") + name_slot = slots.get("name", {}) + entity_name: str | None = name_slot.get("value") + entity_text: str | None = name_slot.get("text") # Look up area first to fail early - area_name = slots.get("area", {}).get("value") + area_slot = slots.get("area", {}) + area_id = area_slot.get("value") + area_name = area_slot.get("text") area: ar.AreaEntry | None = None - if area_name is not None: + if area_id is not None: areas = ar.async_get(hass) - area = areas.async_get_area(area_name) or areas.async_get_area_by_name( - area_name - ) + area = areas.async_get_area(area_id) if area is None: raise intent.IntentHandleError(f"No area named {area_name}") @@ -205,6 +207,13 @@ class GetStateIntentHandler(intent.IntentHandler): intent_obj.assistant, ) + if entity_name and (len(states) > 1): + # Multiple entities matched for the same name + raise intent.DuplicateNamesMatchedError( + name=entity_text or entity_name, + area=area_name or area_id, + ) + # Create response response = intent_obj.create_response() response.response_type = intent.IntentResponseType.QUERY_ANSWER diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index c828b86264c..5217a55bec5 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -155,6 +155,17 @@ class NoStatesMatchedError(IntentError): self.device_classes = device_classes +class DuplicateNamesMatchedError(IntentError): + """Error when two or more entities with the same name matched.""" + + def __init__(self, name: str, area: str | None) -> None: + """Initialize error.""" + super().__init__() + + self.name = name + self.area = area + + def _is_device_class( state: State, entity: entity_registry.RegistryEntry | None, @@ -318,8 +329,6 @@ def async_match_states( for state, entity in states_and_entities: if _has_name(state, entity, name): yield state - break - else: # Not filtered by name for state, _entity in states_and_entities: @@ -416,9 +425,7 @@ class ServiceIntentHandler(IntentHandler): area: area_registry.AreaEntry | None = None if area_id is not None: areas = area_registry.async_get(hass) - area = areas.async_get_area(area_id) or areas.async_get_area_by_name( - area_name - ) + area = areas.async_get_area(area_id) if area is None: raise IntentHandleError(f"No area named {area_name}") @@ -453,6 +460,13 @@ class ServiceIntentHandler(IntentHandler): device_classes=device_classes, ) + if entity_name and (len(states) > 1): + # Multiple entities matched for the same name + raise DuplicateNamesMatchedError( + name=entity_text or entity_name, + area=area_name or area_id, + ) + response = await self.async_handle_states(intent_obj, states, area) # Make the matched states available in the response diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index f4478941473..6af9d197e01 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -1397,7 +1397,7 @@ 'name': dict({ 'name': 'name', 'text': 'my cool light', - 'value': 'kitchen', + 'value': 'my cool light', }), }), 'intent': dict({ @@ -1422,7 +1422,7 @@ 'name': dict({ 'name': 'name', 'text': 'my cool light', - 'value': 'kitchen', + 'value': 'my cool light', }), }), 'intent': dict({ diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index d8a256608c8..4b4f9ade3eb 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -614,6 +614,115 @@ async def test_error_no_intent(hass: HomeAssistant, init_components) -> None: ) +async def test_error_duplicate_names( + hass: HomeAssistant, init_components, entity_registry: er.EntityRegistry +) -> None: + """Test error message when multiple devices have the same name (or alias).""" + kitchen_light_1 = entity_registry.async_get_or_create("light", "demo", "1234") + kitchen_light_2 = entity_registry.async_get_or_create("light", "demo", "5678") + + # Same name and alias + for light in (kitchen_light_1, kitchen_light_2): + light = entity_registry.async_update_entity( + light.entity_id, + name="kitchen light", + aliases={"overhead light"}, + ) + hass.states.async_set( + light.entity_id, + "off", + attributes={ATTR_FRIENDLY_NAME: light.name}, + ) + + # Check name and alias + for name in ("kitchen light", "overhead light"): + # command + result = await conversation.async_converse( + hass, f"turn on {name}", None, Context(), None + ) + assert result.response.response_type == intent.IntentResponseType.ERROR + assert ( + result.response.error_code + == intent.IntentResponseErrorCode.NO_VALID_TARGETS + ) + assert ( + result.response.speech["plain"]["speech"] + == f"Sorry, there are multiple devices called {name}" + ) + + # question + result = await conversation.async_converse( + hass, f"is {name} on?", None, Context(), None + ) + assert result.response.response_type == intent.IntentResponseType.ERROR + assert ( + result.response.error_code + == intent.IntentResponseErrorCode.NO_VALID_TARGETS + ) + assert ( + result.response.speech["plain"]["speech"] + == f"Sorry, there are multiple devices called {name}" + ) + + +async def test_error_duplicate_names_in_area( + hass: HomeAssistant, + init_components, + area_registry: ar.AreaRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test error message when multiple devices have the same name (or alias).""" + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") + + kitchen_light_1 = entity_registry.async_get_or_create("light", "demo", "1234") + kitchen_light_2 = entity_registry.async_get_or_create("light", "demo", "5678") + + # Same name and alias + for light in (kitchen_light_1, kitchen_light_2): + light = entity_registry.async_update_entity( + light.entity_id, + name="kitchen light", + area_id=area_kitchen.id, + aliases={"overhead light"}, + ) + hass.states.async_set( + light.entity_id, + "off", + attributes={ATTR_FRIENDLY_NAME: light.name}, + ) + + # Check name and alias + for name in ("kitchen light", "overhead light"): + # command + result = await conversation.async_converse( + hass, f"turn on {name} in {area_kitchen.name}", None, Context(), None + ) + assert result.response.response_type == intent.IntentResponseType.ERROR + assert ( + result.response.error_code + == intent.IntentResponseErrorCode.NO_VALID_TARGETS + ) + assert ( + result.response.speech["plain"]["speech"] + == f"Sorry, there are multiple devices called {name} in the {area_kitchen.name} area" + ) + + # question + result = await conversation.async_converse( + hass, f"is {name} on in the {area_kitchen.name}?", None, Context(), None + ) + assert result.response.response_type == intent.IntentResponseType.ERROR + assert ( + result.response.error_code + == intent.IntentResponseErrorCode.NO_VALID_TARGETS + ) + assert ( + result.response.speech["plain"]["speech"] + == f"Sorry, there are multiple devices called {name} in the {area_kitchen.name} area" + ) + + async def test_no_states_matched_default_error( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: @@ -794,3 +903,112 @@ async def test_same_named_entities_in_different_areas( assert len(result.response.matched_states) == 1 assert result.response.matched_states[0].entity_id == bedroom_light.entity_id assert calls[0].data.get("entity_id") == [bedroom_light.entity_id] + + # Targeting a duplicate name should fail + result = await conversation.async_converse( + hass, "turn on overhead light", None, Context(), None + ) + assert result.response.response_type == intent.IntentResponseType.ERROR + + # Querying a duplicate name should also fail + result = await conversation.async_converse( + hass, "is the overhead light on?", None, Context(), None + ) + assert result.response.response_type == intent.IntentResponseType.ERROR + + # But we can still ask questions that don't rely on the name + result = await conversation.async_converse( + hass, "how many lights are on?", None, Context(), None + ) + assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER + + +async def test_same_aliased_entities_in_different_areas( + hass: HomeAssistant, + init_components, + area_registry: ar.AreaRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test that entities with the same alias (but different names) in different areas can be targeted.""" + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") + + area_bedroom = area_registry.async_get_or_create("bedroom_id") + area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom") + + # Both lights have the same alias, but are in different areas + kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234") + kitchen_light = entity_registry.async_update_entity( + kitchen_light.entity_id, + area_id=area_kitchen.id, + name="kitchen overhead light", + aliases={"overhead light"}, + ) + hass.states.async_set( + kitchen_light.entity_id, + "off", + attributes={ATTR_FRIENDLY_NAME: kitchen_light.name}, + ) + + bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678") + bedroom_light = entity_registry.async_update_entity( + bedroom_light.entity_id, + area_id=area_bedroom.id, + name="bedroom overhead light", + aliases={"overhead light"}, + ) + hass.states.async_set( + bedroom_light.entity_id, + "off", + attributes={ATTR_FRIENDLY_NAME: bedroom_light.name}, + ) + + # Target kitchen light + calls = async_mock_service(hass, "light", "turn_on") + result = await conversation.async_converse( + hass, "turn on overhead light in the kitchen", None, Context(), None + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.intent is not None + assert result.response.intent.slots.get("name", {}).get("value") == "overhead light" + assert result.response.intent.slots.get("name", {}).get("text") == "overhead light" + assert len(result.response.matched_states) == 1 + assert result.response.matched_states[0].entity_id == kitchen_light.entity_id + assert calls[0].data.get("entity_id") == [kitchen_light.entity_id] + + # Target bedroom light + calls.clear() + result = await conversation.async_converse( + hass, "turn on overhead light in the bedroom", None, Context(), None + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.intent is not None + assert result.response.intent.slots.get("name", {}).get("value") == "overhead light" + assert result.response.intent.slots.get("name", {}).get("text") == "overhead light" + assert len(result.response.matched_states) == 1 + assert result.response.matched_states[0].entity_id == bedroom_light.entity_id + assert calls[0].data.get("entity_id") == [bedroom_light.entity_id] + + # Targeting a duplicate alias should fail + result = await conversation.async_converse( + hass, "turn on overhead light", None, Context(), None + ) + assert result.response.response_type == intent.IntentResponseType.ERROR + + # Querying a duplicate alias should also fail + result = await conversation.async_converse( + hass, "is the overhead light on?", None, Context(), None + ) + assert result.response.response_type == intent.IntentResponseType.ERROR + + # But we can still ask questions that don't rely on the alias + result = await conversation.async_converse( + hass, "how many lights are on?", None, Context(), None + ) + assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER From 2d8d6ce6424b23758c6d43c5417c54593a8943d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Feb 2024 21:27:04 -0600 Subject: [PATCH 0416/1367] Bump aiodiscover 1.6.1 (#110059) fixes decoding idna encoding hostnames changelog: https://github.com/bdraco/aiodiscover/compare/v1.6.0...v1.6.1 --- homeassistant/components/dhcp/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/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index f190f0ab10e..70469a93678 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -9,7 +9,7 @@ "quality_scale": "internal", "requirements": [ "scapy==2.5.0", - "aiodiscover==1.6.0", + "aiodiscover==1.6.1", "cached_ipaddress==0.3.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3dadd4717c8..addf71bd68f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ # Automatically generated by gen_requirements_all.py, do not edit -aiodiscover==1.6.0 +aiodiscover==1.6.1 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 aiohttp==3.9.3 diff --git a/requirements_all.txt b/requirements_all.txt index df84d0dc6cf..2e454e37f23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -221,7 +221,7 @@ aiobotocore==2.9.1 aiocomelit==0.8.3 # homeassistant.components.dhcp -aiodiscover==1.6.0 +aiodiscover==1.6.1 # homeassistant.components.dnsip aiodns==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f3c1d48773..ddea8936102 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -200,7 +200,7 @@ aiobotocore==2.9.1 aiocomelit==0.8.3 # homeassistant.components.dhcp -aiodiscover==1.6.0 +aiodiscover==1.6.1 # homeassistant.components.dnsip aiodns==3.0.0 From 261f9c5d624c8b90b5633b2a48871183bce0166a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 8 Feb 2024 19:52:40 -0800 Subject: [PATCH 0417/1367] Cleanup test config entry setup to use best practices (#110070) * Cleanup test config entry setup to use best practices * Add missed files --- tests/components/caldav/test_calendar.py | 4 ++-- tests/components/caldav/test_init.py | 4 ++-- tests/components/caldav/test_todo.py | 24 +++++++++---------- .../components/rainbird/test_binary_sensor.py | 4 ++-- tests/components/rainbird/test_calendar.py | 6 ++--- tests/components/rainbird/test_config_flow.py | 4 ++-- tests/components/rainbird/test_number.py | 4 ++-- tests/components/rainbird/test_sensor.py | 4 ++-- tests/components/rainbird/test_switch.py | 4 ++-- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index df5428121ee..11f1524b4b0 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -1090,7 +1090,7 @@ async def test_setup_config_entry( ) -> None: """Test a calendar entity from a config entry.""" config_entry.add_to_hass(hass) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) state = hass.states.get(TEST_ENTITY) assert state @@ -1124,7 +1124,7 @@ async def test_config_entry_supported_components( ) -> None: """Test that calendars are only created for VEVENT types when using a config entry.""" config_entry.add_to_hass(hass) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) state = hass.states.get("calendar.calendar_1") assert state diff --git a/tests/components/caldav/test_init.py b/tests/components/caldav/test_init.py index 8e832e24d2d..192c18ef81a 100644 --- a/tests/components/caldav/test_init.py +++ b/tests/components/caldav/test_init.py @@ -26,7 +26,7 @@ async def test_load_unload( assert config_entry.state == ConfigEntryState.NOT_LOADED with patch("homeassistant.components.caldav.config_flow.caldav.DAVClient"): - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED @@ -63,7 +63,7 @@ async def test_client_failure( "homeassistant.components.caldav.config_flow.caldav.DAVClient" ) as mock_client: mock_client.return_value.principal.side_effect = side_effect - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state == expected_state diff --git a/tests/components/caldav/test_todo.py b/tests/components/caldav/test_todo.py index 6056cac5fa9..7b67a1af714 100644 --- a/tests/components/caldav/test_todo.py +++ b/tests/components/caldav/test_todo.py @@ -188,7 +188,7 @@ async def test_todo_list_state( expected_state: str, ) -> None: """Test a calendar entity from a config entry.""" - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) state = hass.states.get(TEST_ENTITY) assert state @@ -210,7 +210,7 @@ async def test_supported_components( has_entity: bool, ) -> None: """Test a calendar supported components matches VTODO.""" - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) state = hass.states.get(TEST_ENTITY) assert (state is not None) == has_entity @@ -266,7 +266,7 @@ async def test_add_item( ) -> None: """Test adding an item to the list.""" calendar.search.return_value = [] - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) state = hass.states.get(TEST_ENTITY) assert state @@ -298,7 +298,7 @@ async def test_add_item_failure( calendar: Mock, ) -> None: """Test failure when adding an item to the list.""" - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) calendar.save_todo.side_effect = DAVError() @@ -488,7 +488,7 @@ async def test_update_item( item = Todo(dav_client, None, TODO_ALL_FIELDS, calendar, "2") calendar.search = MagicMock(return_value=[item]) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) state = hass.states.get(TEST_ENTITY) assert state @@ -539,7 +539,7 @@ async def test_update_item_failure( item = Todo(dav_client, None, TODO_NEEDS_ACTION, calendar, "2") calendar.search = MagicMock(return_value=[item]) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) calendar.todo_by_uid = MagicMock(return_value=item) dav_client.put.side_effect = DAVError() @@ -574,7 +574,7 @@ async def test_update_item_lookup_failure( item = Todo(dav_client, None, TODO_NEEDS_ACTION, calendar, "2") calendar.search = MagicMock(return_value=[item]) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) calendar.todo_by_uid.side_effect = side_effect @@ -616,7 +616,7 @@ async def test_remove_item( item2 = Todo(dav_client, None, TODO_COMPLETED, calendar, "3") calendar.search = MagicMock(return_value=[item1, item2]) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) state = hass.states.get(TEST_ENTITY) assert state @@ -660,7 +660,7 @@ async def test_remove_item_lookup_failure( ) -> None: """Test failure while removing an item from the list.""" - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) calendar.todo_by_uid.side_effect = side_effect @@ -685,7 +685,7 @@ async def test_remove_item_failure( item = Todo(dav_client, "2.ics", TODO_NEEDS_ACTION, calendar, "2") calendar.search = MagicMock(return_value=[item]) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) def lookup(uid: str) -> Mock: return item @@ -714,7 +714,7 @@ async def test_remove_item_not_found( item = Todo(dav_client, "2.ics", TODO_NEEDS_ACTION, calendar, "2") calendar.search = MagicMock(return_value=[item]) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) def lookup(uid: str) -> Mock: return item @@ -743,7 +743,7 @@ async def test_subscribe( item = Todo(dav_client, None, TODO_NEEDS_ACTION, calendar, "2") calendar.search = MagicMock(return_value=[item]) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) # Subscribe and get the initial list client = await hass_ws_client(hass) diff --git a/tests/components/rainbird/test_binary_sensor.py b/tests/components/rainbird/test_binary_sensor.py index afe18337377..826a7635c53 100644 --- a/tests/components/rainbird/test_binary_sensor.py +++ b/tests/components/rainbird/test_binary_sensor.py @@ -32,7 +32,7 @@ async def setup_config_entry( hass: HomeAssistant, config_entry: MockConfigEntry ) -> list[Platform]: """Fixture to setup the config entry.""" - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED @@ -74,7 +74,7 @@ async def test_no_unique_id( # Failure to migrate config entry to a unique id responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED rainsensor = hass.states.get("binary_sensor.rain_bird_controller_rainsensor") diff --git a/tests/components/rainbird/test_calendar.py b/tests/components/rainbird/test_calendar.py index 44baf09fd55..673d32998d5 100644 --- a/tests/components/rainbird/test_calendar.py +++ b/tests/components/rainbird/test_calendar.py @@ -87,7 +87,7 @@ async def setup_config_entry( hass: HomeAssistant, config_entry: MockConfigEntry ) -> list[Platform]: """Fixture to setup the config entry.""" - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED @@ -191,7 +191,7 @@ async def test_event_state( """Test calendar upcoming event state.""" freezer.move_to(freeze_time) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED state = hass.states.get(TEST_ENTITY) @@ -298,7 +298,7 @@ async def test_no_unique_id( # Failure to migrate config entry to a unique id responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED state = hass.states.get(TEST_ENTITY) diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py index 7a4dc2a55d4..09db734f1ad 100644 --- a/tests/components/rainbird/test_config_flow.py +++ b/tests/components/rainbird/test_config_flow.py @@ -158,7 +158,7 @@ async def test_multiple_config_entries( expected_config_entry: dict[str, Any] | None, ) -> None: """Test setting up multiple config entries that refer to different devices.""" - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED responses.clear() @@ -233,7 +233,7 @@ async def test_duplicate_config_entries( expected_config_entry_data: dict[str, Any], ) -> None: """Test that a device can not be registered twice.""" - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED responses.clear() diff --git a/tests/components/rainbird/test_number.py b/tests/components/rainbird/test_number.py index 79b8fd5ec37..b0c1856819e 100644 --- a/tests/components/rainbird/test_number.py +++ b/tests/components/rainbird/test_number.py @@ -37,7 +37,7 @@ async def setup_config_entry( hass: HomeAssistant, config_entry: MockConfigEntry ) -> list[Platform]: """Fixture to setup the config entry.""" - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED @@ -155,7 +155,7 @@ async def test_no_unique_id( # Failure to migrate config entry to a unique id responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED raindelay = hass.states.get("number.rain_bird_controller_rain_delay") diff --git a/tests/components/rainbird/test_sensor.py b/tests/components/rainbird/test_sensor.py index 2a0195f8d97..ebe852ccf46 100644 --- a/tests/components/rainbird/test_sensor.py +++ b/tests/components/rainbird/test_sensor.py @@ -31,7 +31,7 @@ async def setup_config_entry( hass: HomeAssistant, config_entry: MockConfigEntry ) -> list[Platform]: """Fixture to setup the config entry.""" - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED @@ -85,7 +85,7 @@ async def test_sensor_no_unique_id( # Failure to migrate config entry to a unique id responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED raindelay = hass.states.get("sensor.rain_bird_controller_raindelay") diff --git a/tests/components/rainbird/test_switch.py b/tests/components/rainbird/test_switch.py index f9c03f63dd3..0f9a139a69d 100644 --- a/tests/components/rainbird/test_switch.py +++ b/tests/components/rainbird/test_switch.py @@ -43,7 +43,7 @@ async def setup_config_entry( hass: HomeAssistant, config_entry: MockConfigEntry ) -> list[Platform]: """Fixture to setup the config entry.""" - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED @@ -293,7 +293,7 @@ async def test_no_unique_id( # Failure to migrate config entry to a unique id responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED zone = hass.states.get("switch.rain_bird_sprinkler_3") From 122ac059bcd91ebe16f0257c6aed4723d8ca24e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Feb 2024 22:23:42 -0600 Subject: [PATCH 0418/1367] Convert dhcp watcher to asyncio (#109938) --- homeassistant/components/dhcp/__init__.py | 244 ++++---------------- homeassistant/components/dhcp/manifest.json | 10 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/dhcp/test_init.py | 111 ++++----- 6 files changed, 111 insertions(+), 268 deletions(-) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index ad0446543db..ac09c908927 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -3,19 +3,17 @@ from __future__ import annotations from abc import ABC, abstractmethod import asyncio -from collections.abc import Callable, Iterable -import contextlib +from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta from fnmatch import translate from functools import lru_cache import itertools import logging -import os import re -import threading -from typing import TYPE_CHECKING, Any, Final, cast +from typing import Any, Final +import aiodhcpwatcher from aiodiscover import DiscoverHosts from aiodiscover.discovery import ( HOSTNAME as DISCOVERY_HOSTNAME, @@ -23,8 +21,6 @@ from aiodiscover.discovery import ( MAC_ADDRESS as DISCOVERY_MAC_ADDRESS, ) from cached_ipaddress import cached_ip_addresses -from scapy.config import conf -from scapy.error import Scapy_Exception from homeassistant import config_entries from homeassistant.components.device_tracker import ( @@ -61,20 +57,13 @@ from homeassistant.loader import DHCPMatcher, async_get_dhcp from .const import DOMAIN -if TYPE_CHECKING: - from scapy.packet import Packet - from scapy.sendrecv import AsyncSniffer - CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) FILTER = "udp and (port 67 or 68)" -REQUESTED_ADDR = "requested_addr" -MESSAGE_TYPE = "message-type" HOSTNAME: Final = "hostname" MAC_ADDRESS: Final = "macaddress" IP_ADDRESS: Final = "ip" REGISTERED_DEVICES: Final = "registered_devices" -DHCP_REQUEST = 3 SCAN_INTERVAL = timedelta(minutes=60) @@ -144,22 +133,25 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # everything else starts up or we will miss events for passive_cls in (DeviceTrackerRegisteredWatcher, DeviceTrackerWatcher): passive_watcher = passive_cls(hass, address_data, integration_matchers) - await passive_watcher.async_start() + passive_watcher.async_start() watchers.append(passive_watcher) - async def _initialize(event: Event) -> None: + async def _async_initialize(event: Event) -> None: + await aiodhcpwatcher.async_init() + for active_cls in (DHCPWatcher, NetworkWatcher): active_watcher = active_cls(hass, address_data, integration_matchers) - await active_watcher.async_start() + active_watcher.async_start() watchers.append(active_watcher) - async def _async_stop(event: Event) -> None: + @callback + def _async_stop(event: Event) -> None: for watcher in watchers: - await watcher.async_stop() + watcher.async_stop() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _initialize) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_initialize) return True @@ -178,21 +170,20 @@ class WatcherBase(ABC): self.hass = hass self._integration_matchers = integration_matchers self._address_data = address_data + self._unsub: Callable[[], None] | None = None + + @callback + def async_stop(self) -> None: + """Stop scanning for new devices on the network.""" + if self._unsub: + self._unsub() + self._unsub = None @abstractmethod - async def async_stop(self) -> None: - """Stop the watcher.""" - - @abstractmethod - async def async_start(self) -> None: + @callback + def async_start(self) -> None: """Start the watcher.""" - def process_client(self, ip_address: str, hostname: str, mac_address: str) -> None: - """Process a client.""" - self.hass.loop.call_soon_threadsafe( - self.async_process_client, ip_address, hostname, mac_address - ) - @callback def async_process_client( self, ip_address: str, hostname: str, mac_address: str @@ -291,20 +282,19 @@ class NetworkWatcher(WatcherBase): ) -> None: """Initialize class.""" super().__init__(hass, address_data, integration_matchers) - self._unsub: Callable[[], None] | None = None self._discover_hosts: DiscoverHosts | None = None self._discover_task: asyncio.Task | None = None - async def async_stop(self) -> None: + @callback + def async_stop(self) -> None: """Stop scanning for new devices on the network.""" - if self._unsub: - self._unsub() - self._unsub = None + super().async_stop() if self._discover_task: self._discover_task.cancel() self._discover_task = None - async def async_start(self) -> None: + @callback + def async_start(self) -> None: """Start scanning for new devices on the network.""" self._discover_hosts = DiscoverHosts() self._unsub = async_track_time_interval( @@ -336,23 +326,8 @@ class NetworkWatcher(WatcherBase): class DeviceTrackerWatcher(WatcherBase): """Class to watch dhcp data from routers.""" - def __init__( - self, - hass: HomeAssistant, - address_data: dict[str, dict[str, str]], - integration_matchers: DhcpMatchers, - ) -> None: - """Initialize class.""" - super().__init__(hass, address_data, integration_matchers) - self._unsub: Callable[[], None] | None = None - - async def async_stop(self) -> None: - """Stop watching for new device trackers.""" - if self._unsub: - self._unsub() - self._unsub = None - - async def async_start(self) -> None: + @callback + def async_start(self) -> None: """Stop watching for new device trackers.""" self._unsub = async_track_state_added_domain( self.hass, [DEVICE_TRACKER_DOMAIN], self._async_process_device_event @@ -391,23 +366,8 @@ class DeviceTrackerWatcher(WatcherBase): class DeviceTrackerRegisteredWatcher(WatcherBase): """Class to watch data from device tracker registrations.""" - def __init__( - self, - hass: HomeAssistant, - address_data: dict[str, dict[str, str]], - integration_matchers: DhcpMatchers, - ) -> None: - """Initialize class.""" - super().__init__(hass, address_data, integration_matchers) - self._unsub: Callable[[], None] | None = None - - async def async_stop(self) -> None: - """Stop watching for device tracker registrations.""" - if self._unsub: - self._unsub() - self._unsub = None - - async def async_start(self) -> None: + @callback + def async_start(self) -> None: """Stop watching for device tracker registrations.""" self._unsub = async_dispatcher_connect( self.hass, CONNECTED_DEVICE_REGISTERED, self._async_process_device_data @@ -429,114 +389,17 @@ class DeviceTrackerRegisteredWatcher(WatcherBase): class DHCPWatcher(WatcherBase): """Class to watch dhcp requests.""" - def __init__( - self, - hass: HomeAssistant, - address_data: dict[str, dict[str, str]], - integration_matchers: DhcpMatchers, - ) -> None: - """Initialize class.""" - super().__init__(hass, address_data, integration_matchers) - self._sniffer: AsyncSniffer | None = None - self._started = threading.Event() - - async def async_stop(self) -> None: - """Stop watching for new device trackers.""" - await self.hass.async_add_executor_job(self._stop) - - def _stop(self) -> None: - """Stop the thread.""" - if self._started.is_set(): - assert self._sniffer is not None - self._sniffer.stop() - - async def async_start(self) -> None: - """Start watching for dhcp packets.""" - await self.hass.async_add_executor_job(self._start) - - def _start(self) -> None: - """Start watching for dhcp packets.""" - # Local import because importing from scapy has side effects such as opening - # sockets - from scapy import arch # pylint: disable=import-outside-toplevel # noqa: F401 - from scapy.layers.dhcp import DHCP # pylint: disable=import-outside-toplevel - from scapy.layers.inet import IP # pylint: disable=import-outside-toplevel - from scapy.layers.l2 import Ether # pylint: disable=import-outside-toplevel - - # - # Importing scapy.sendrecv will cause a scapy resync which will - # import scapy.arch.read_routes which will import scapy.sendrecv - # - # We avoid this circular import by importing arch above to ensure - # the module is loaded and avoid the problem - # - from scapy.sendrecv import ( # pylint: disable=import-outside-toplevel - AsyncSniffer, + @callback + def _async_process_dhcp_request(self, response: aiodhcpwatcher.DHCPRequest) -> None: + """Process a dhcp request.""" + self.async_process_client( + response.ip_address, response.hostname, _format_mac(response.mac_address) ) - def _handle_dhcp_packet(packet: Packet) -> None: - """Process a dhcp packet.""" - if DHCP not in packet: - return - - options_dict = _dhcp_options_as_dict(packet[DHCP].options) - if options_dict.get(MESSAGE_TYPE) != DHCP_REQUEST: - # Not a DHCP request - return - - ip_address = options_dict.get(REQUESTED_ADDR) or cast(str, packet[IP].src) - assert isinstance(ip_address, str) - hostname = "" - if (hostname_bytes := options_dict.get(HOSTNAME)) and isinstance( - hostname_bytes, bytes - ): - with contextlib.suppress(AttributeError, UnicodeDecodeError): - hostname = hostname_bytes.decode() - mac_address = _format_mac(cast(str, packet[Ether].src)) - - if ip_address is not None and mac_address is not None: - self.process_client(ip_address, hostname, mac_address) - - # disable scapy promiscuous mode as we do not need it - conf.sniff_promisc = 0 - - try: - _verify_l2socket_setup(FILTER) - except (Scapy_Exception, OSError) as ex: - if os.geteuid() == 0: - _LOGGER.error("Cannot watch for dhcp packets: %s", ex) - else: - _LOGGER.debug( - "Cannot watch for dhcp packets without root or CAP_NET_RAW: %s", ex - ) - return - - try: - _verify_working_pcap(FILTER) - except (Scapy_Exception, ImportError) as ex: - _LOGGER.error( - "Cannot watch for dhcp packets without a functional packet filter: %s", - ex, - ) - return - - self._sniffer = AsyncSniffer( - filter=FILTER, - started_callback=self._started.set, - prn=_handle_dhcp_packet, - store=0, - ) - - self._sniffer.start() - if self._sniffer.thread: - self._sniffer.thread.name = self.__class__.__name__ - - -def _dhcp_options_as_dict( - dhcp_options: Iterable[tuple[str, int | bytes | None]], -) -> dict[str, str | int | bytes | None]: - """Extract data from packet options as a dict.""" - return {option[0]: option[1] for option in dhcp_options if len(option) >= 2} + @callback + def async_start(self) -> None: + """Start watching for dhcp packets.""" + self._unsub = aiodhcpwatcher.start(self._async_process_dhcp_request) def _format_mac(mac_address: str) -> str: @@ -544,33 +407,6 @@ def _format_mac(mac_address: str) -> str: return format_mac(mac_address).replace(":", "") -def _verify_l2socket_setup(cap_filter: str) -> None: - """Create a socket using the scapy configured l2socket. - - Try to create the socket - to see if we have permissions - since AsyncSniffer will do it another - thread so we will not be able to capture - any permission or bind errors. - """ - conf.L2socket(filter=cap_filter) - - -def _verify_working_pcap(cap_filter: str) -> None: - """Verify we can create a packet filter. - - If we cannot create a filter we will be listening for - all traffic which is too intensive. - """ - # Local import because importing from scapy has side effects such as opening - # sockets - from scapy.arch.common import ( # pylint: disable=import-outside-toplevel - compile_filter, - ) - - compile_filter(cap_filter) - - @lru_cache(maxsize=4096, typed=True) def _compile_fnmatch(pattern: str) -> re.Pattern: """Compile a fnmatch pattern.""" diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 70469a93678..142aab52cc8 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -5,10 +5,16 @@ "documentation": "https://www.home-assistant.io/integrations/dhcp", "integration_type": "system", "iot_class": "local_push", - "loggers": ["aiodiscover", "dnspython", "pyroute2", "scapy"], + "loggers": [ + "aiodiscover", + "aiodhcpwatcher", + "dnspython", + "pyroute2", + "scapy" + ], "quality_scale": "internal", "requirements": [ - "scapy==2.5.0", + "aiodhcpwatcher==0.8.0", "aiodiscover==1.6.1", "cached_ipaddress==0.3.0" ] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index addf71bd68f..2ebac8f2bcc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,5 +1,6 @@ # Automatically generated by gen_requirements_all.py, do not edit +aiodhcpwatcher==0.8.0 aiodiscover==1.6.1 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 @@ -51,7 +52,6 @@ PyTurboJPEG==1.7.1 pyudev==0.23.2 PyYAML==6.0.1 requests==2.31.0 -scapy==2.5.0 SQLAlchemy==2.0.25 typing-extensions>=4.9.0,<5.0 ulid-transform==0.9.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2e454e37f23..e723cc09e23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -220,6 +220,9 @@ aiobotocore==2.9.1 # homeassistant.components.comelit aiocomelit==0.8.3 +# homeassistant.components.dhcp +aiodhcpwatcher==0.8.0 + # homeassistant.components.dhcp aiodiscover==1.6.1 @@ -2485,9 +2488,6 @@ samsungtvws[async,encrypted]==2.6.0 # homeassistant.components.satel_integra satel-integra==0.3.7 -# homeassistant.components.dhcp -scapy==2.5.0 - # homeassistant.components.screenlogic screenlogicpy==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddea8936102..54cbb09a5b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -199,6 +199,9 @@ aiobotocore==2.9.1 # homeassistant.components.comelit aiocomelit==0.8.3 +# homeassistant.components.dhcp +aiodhcpwatcher==0.8.0 + # homeassistant.components.dhcp aiodiscover==1.6.1 @@ -1895,9 +1898,6 @@ samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv samsungtvws[async,encrypted]==2.6.0 -# homeassistant.components.dhcp -scapy==2.5.0 - # homeassistant.components.screenlogic screenlogicpy==0.10.0 diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index 18d213a7029..487435ef3f5 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -3,10 +3,14 @@ from collections.abc import Awaitable, Callable import datetime import threading from typing import Any, cast -from unittest.mock import MagicMock, patch +from unittest.mock import patch +import aiodhcpwatcher import pytest -from scapy import arch # noqa: F401 +from scapy import ( + arch, # noqa: F401 + interfaces, +) from scapy.error import Scapy_Exception from scapy.layers.dhcp import DHCP from scapy.layers.l2 import Ether @@ -140,29 +144,18 @@ async def _async_get_handle_dhcp_packet( {}, integration_matchers, ) - async_handle_dhcp_packet: Callable[[Any], Awaitable[None]] | None = None + with patch("aiodhcpwatcher.start"): + dhcp_watcher.async_start() - def _mock_sniffer(*args, **kwargs): - nonlocal async_handle_dhcp_packet - callback = kwargs["prn"] + def _async_handle_dhcp_request(request: aiodhcpwatcher.DHCPRequest) -> None: + dhcp_watcher._async_process_dhcp_request(request) - async def _async_handle_dhcp_packet(packet): - await hass.async_add_executor_job(callback, packet) + handler = aiodhcpwatcher.make_packet_handler(_async_handle_dhcp_request) - async_handle_dhcp_packet = _async_handle_dhcp_packet - return MagicMock() + async def _async_handle_dhcp_packet(packet): + handler(packet) - with patch( - "homeassistant.components.dhcp._verify_l2socket_setup", - ), patch( - "scapy.arch.common.compile_filter", - ), patch( - "scapy.sendrecv.AsyncSniffer", - _mock_sniffer, - ): - await dhcp_watcher.async_start() - - return cast("Callable[[Any], Awaitable[None]]", async_handle_dhcp_packet) + return cast("Callable[[Any], Awaitable[None]]", _async_handle_dhcp_packet) async def test_dhcp_match_hostname_and_macaddress(hass: HomeAssistant) -> None: @@ -541,9 +534,10 @@ async def test_setup_and_stop(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - with patch("scapy.sendrecv.AsyncSniffer.start") as start_call, patch( - "homeassistant.components.dhcp._verify_l2socket_setup", - ), patch("scapy.arch.common.compile_filter"), patch( + with patch.object( + interfaces, + "resolve_iface", + ) as resolve_iface_call, patch("scapy.arch.common.compile_filter"), patch( "homeassistant.components.dhcp.DiscoverHosts.async_discover" ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -552,7 +546,7 @@ async def test_setup_and_stop(hass: HomeAssistant) -> None: hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - start_call.assert_called_once() + resolve_iface_call.assert_called_once() async def test_setup_fails_as_root( @@ -569,8 +563,9 @@ async def test_setup_fails_as_root( wait_event = threading.Event() - with patch("os.geteuid", return_value=0), patch( - "homeassistant.components.dhcp._verify_l2socket_setup", + with patch("os.geteuid", return_value=0), patch.object( + interfaces, + "resolve_iface", side_effect=Scapy_Exception, ), patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -595,7 +590,10 @@ async def test_setup_fails_non_root( await hass.async_block_till_done() with patch("os.geteuid", return_value=10), patch( - "homeassistant.components.dhcp._verify_l2socket_setup", + "scapy.arch.common.compile_filter" + ), patch.object( + interfaces, + "resolve_iface", side_effect=Scapy_Exception, ), patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -618,10 +616,13 @@ async def test_setup_fails_with_broken_libpcap( ) await hass.async_block_till_done() - with patch("homeassistant.components.dhcp._verify_l2socket_setup"), patch( + with patch( "scapy.arch.common.compile_filter", side_effect=ImportError, - ) as compile_filter, patch("scapy.sendrecv.AsyncSniffer") as async_sniffer, patch( + ) as compile_filter, patch.object( + interfaces, + "resolve_iface", + ) as resolve_iface_call, patch( "homeassistant.components.dhcp.DiscoverHosts.async_discover" ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -630,7 +631,7 @@ async def test_setup_fails_with_broken_libpcap( await hass.async_block_till_done() assert compile_filter.called - assert not async_sniffer.called + assert not resolve_iface_call.called assert ( "Cannot watch for dhcp packets without a functional packet filter" in caplog.text @@ -666,9 +667,9 @@ async def test_device_tracker_hostname_and_macaddress_exists_before_start( ] ), ) - await device_tracker_watcher.async_start() + device_tracker_watcher.async_start() await hass.async_block_till_done() - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() assert len(mock_init.mock_calls) == 1 @@ -699,7 +700,7 @@ async def test_device_tracker_registered(hass: HomeAssistant) -> None: ] ), ) - await device_tracker_watcher.async_start() + device_tracker_watcher.async_start() await hass.async_block_till_done() async_dispatcher_send( hass, @@ -718,7 +719,7 @@ async def test_device_tracker_registered(hass: HomeAssistant) -> None: hostname="connect", macaddress="b8b7f16db533", ) - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() @@ -738,7 +739,7 @@ async def test_device_tracker_registered_hostname_none(hass: HomeAssistant) -> N ] ), ) - await device_tracker_watcher.async_start() + device_tracker_watcher.async_start() await hass.async_block_till_done() async_dispatcher_send( hass, @@ -748,7 +749,7 @@ async def test_device_tracker_registered_hostname_none(hass: HomeAssistant) -> N await hass.async_block_till_done() assert len(mock_init.mock_calls) == 0 - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() @@ -771,7 +772,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start( ] ), ) - await device_tracker_watcher.async_start() + device_tracker_watcher.async_start() await hass.async_block_till_done() hass.states.async_set( "device_tracker.august_connect", @@ -784,7 +785,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start( }, ) await hass.async_block_till_done() - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() assert len(mock_init.mock_calls) == 1 @@ -818,7 +819,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_not_home( ] ), ) - await device_tracker_watcher.async_start() + device_tracker_watcher.async_start() await hass.async_block_till_done() hass.states.async_set( "device_tracker.august_connect", @@ -831,7 +832,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_not_home( }, ) await hass.async_block_till_done() - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() assert len(mock_init.mock_calls) == 0 @@ -848,7 +849,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_not_router( {}, [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], ) - await device_tracker_watcher.async_start() + device_tracker_watcher.async_start() await hass.async_block_till_done() hass.states.async_set( "device_tracker.august_connect", @@ -861,7 +862,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_not_router( }, ) await hass.async_block_till_done() - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() assert len(mock_init.mock_calls) == 0 @@ -878,7 +879,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_hostname_missi {}, [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], ) - await device_tracker_watcher.async_start() + device_tracker_watcher.async_start() await hass.async_block_till_done() hass.states.async_set( "device_tracker.august_connect", @@ -890,7 +891,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_hostname_missi }, ) await hass.async_block_till_done() - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() assert len(mock_init.mock_calls) == 0 @@ -907,7 +908,7 @@ async def test_device_tracker_invalid_ip_address( {}, [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], ) - await device_tracker_watcher.async_start() + device_tracker_watcher.async_start() await hass.async_block_till_done() hass.states.async_set( "device_tracker.august_connect", @@ -919,7 +920,7 @@ async def test_device_tracker_invalid_ip_address( }, ) await hass.async_block_till_done() - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() assert "Ignoring invalid IP Address: invalid" in caplog.text @@ -955,9 +956,9 @@ async def test_device_tracker_ignore_self_assigned_ips_before_start( ] ), ) - await device_tracker_watcher.async_start() + device_tracker_watcher.async_start() await hass.async_block_till_done() - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() assert len(mock_init.mock_calls) == 0 @@ -988,9 +989,9 @@ async def test_aiodiscover_finds_new_hosts(hass: HomeAssistant) -> None: ] ), ) - await device_tracker_watcher.async_start() + device_tracker_watcher.async_start() await hass.async_block_till_done() - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() assert len(mock_init.mock_calls) == 1 @@ -1047,9 +1048,9 @@ async def test_aiodiscover_does_not_call_again_on_shorter_hostname( ] ), ) - await device_tracker_watcher.async_start() + device_tracker_watcher.async_start() await hass.async_block_till_done() - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() assert len(mock_init.mock_calls) == 2 @@ -1092,7 +1093,7 @@ async def test_aiodiscover_finds_new_hosts_after_interval(hass: HomeAssistant) - ] ), ) - await device_tracker_watcher.async_start() + device_tracker_watcher.async_start() await hass.async_block_till_done() assert len(mock_init.mock_calls) == 0 @@ -1109,7 +1110,7 @@ async def test_aiodiscover_finds_new_hosts_after_interval(hass: HomeAssistant) - ): async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=65)) await hass.async_block_till_done() - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() assert len(mock_init.mock_calls) == 1 From 4f404881dd5f1c218d6bee0146c6e91cb91ac0f4 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 9 Feb 2024 07:05:08 +0000 Subject: [PATCH 0419/1367] Remove homekit_controller entity registry entries when backing char or service is gone (#109952) --- .../homekit_controller/connection.py | 51 +++++++++++++++++++ .../components/homekit_controller/utils.py | 25 +++++++++ .../specific_devices/test_ecobee3.py | 15 ++++++ .../test_fan_that_changes_features.py | 2 + .../homekit_controller/test_utils.py | 15 ++++++ 5 files changed, 108 insertions(+) create mode 100644 tests/components/homekit_controller/test_utils.py diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index c127c6dd95e..299b01e5b00 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -46,6 +46,7 @@ from .const import ( SUBSCRIBE_COOLDOWN, ) from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry +from .utils import IidTuple, unique_id_to_iids RETRY_INTERVAL = 60 # seconds MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3 @@ -513,6 +514,54 @@ class HKDevice: device_registry.async_update_device(device.id, new_identifiers=identifiers) + @callback + def async_reap_stale_entity_registry_entries(self) -> None: + """Delete entity registry entities for removed characteristics, services and accessories.""" + _LOGGER.debug( + "Removing stale entity registry entries for pairing %s", + self.unique_id, + ) + + reg = er.async_get(self.hass) + + # For the current config entry only, visit all registry entity entries + # Build a set of (unique_id, aid, sid, iid) + # For services, (unique_id, aid, sid, None) + # For accessories, (unique_id, aid, None, None) + entries = er.async_entries_for_config_entry(reg, self.config_entry.entry_id) + existing_entities = { + iids: entry.entity_id + for entry in entries + if (iids := unique_id_to_iids(entry.unique_id)) + } + + # Process current entity map and produce a similar set + current_unique_id: set[IidTuple] = set() + for accessory in self.entity_map.accessories: + current_unique_id.add((accessory.aid, None, None)) + + for service in accessory.services: + current_unique_id.add((accessory.aid, service.iid, None)) + + for char in service.characteristics: + current_unique_id.add( + ( + accessory.aid, + service.iid, + char.iid, + ) + ) + + # Remove the difference + if stale := existing_entities.keys() - current_unique_id: + for parts in stale: + _LOGGER.debug( + "Removing stale entity registry entry %s for pairing %s", + existing_entities[parts], + self.unique_id, + ) + reg.async_remove(existing_entities[parts]) + @callback def async_migrate_ble_unique_id(self) -> None: """Config entries from step_bluetooth used incorrect identifier for unique_id.""" @@ -615,6 +664,8 @@ class HKDevice: self.async_migrate_ble_unique_id() + self.async_reap_stale_entity_registry_entries() + self.async_create_devices() # Load any triggers for this config entry diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py index 33a08504724..489dee5584c 100644 --- a/homeassistant/components/homekit_controller/utils.py +++ b/homeassistant/components/homekit_controller/utils.py @@ -11,6 +11,31 @@ from homeassistant.core import Event, HomeAssistant from .const import CONTROLLER from .storage import async_get_entity_storage +IidTuple = tuple[int, int | None, int | None] + + +def unique_id_to_iids(unique_id: str) -> IidTuple | None: + """Convert a unique_id to a tuple of accessory id, service iid and characteristic iid. + + Depending on the field in the accessory map that is referenced, some of these may be None. + + Returns None if this unique_id doesn't follow the homekit_controller scheme and is invalid. + """ + try: + match unique_id.split("_"): + case (unique_id, aid, sid, cid): + return (int(aid), int(sid), int(cid)) + case (unique_id, aid, sid): + return (int(aid), int(sid), None) + case (unique_id, aid): + return (int(aid), None, None) + except ValueError: + # One of the int conversions failed - this can't be a valid homekit_controller unique id + # Fall through and return None + pass + + return None + @lru_cache def folded_name(name: str) -> str: diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index a4bcf7e962e..99ece418c7b 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -284,8 +284,13 @@ async def test_ecobee3_remove_sensors_at_runtime( await device_config_changed(hass, accessories) assert hass.states.get("binary_sensor.kitchen") is None + assert entity_registry.async_get("binary_sensor.kitchen") is None + assert hass.states.get("binary_sensor.porch") is None + assert entity_registry.async_get("binary_sensor.porch") is None + assert hass.states.get("binary_sensor.basement") is None + assert entity_registry.async_get("binary_sensor.basement") is None # Now add the sensors back accessories = await setup_accessories_from_file(hass, "ecobee3.json") @@ -302,8 +307,13 @@ async def test_ecobee3_remove_sensors_at_runtime( # Ensure the sensors are back assert hass.states.get("binary_sensor.kitchen") is not None + assert occ1.id == entity_registry.async_get("binary_sensor.kitchen").id + assert hass.states.get("binary_sensor.porch") is not None + assert occ2.id == entity_registry.async_get("binary_sensor.porch").id + assert hass.states.get("binary_sensor.basement") is not None + assert occ3.id == entity_registry.async_get("binary_sensor.basement").id async def test_ecobee3_services_and_chars_removed( @@ -333,10 +343,15 @@ async def test_ecobee3_services_and_chars_removed( # Make sure the climate entity is still there assert hass.states.get("climate.homew") is not None + assert entity_registry.async_get("climate.homew") is not None # Make sure the basement temperature sensor is gone assert hass.states.get("sensor.basement_temperature") is None + assert entity_registry.async_get("select.basement_temperature") is None # Make sure the current mode select and clear hold button are gone assert hass.states.get("select.homew_current_mode") is None + assert entity_registry.async_get("select.homew_current_mode") is None + assert hass.states.get("button.homew_clear_hold") is None + assert entity_registry.async_get("button.homew_clear_hold") is None diff --git a/tests/components/homekit_controller/specific_devices/test_fan_that_changes_features.py b/tests/components/homekit_controller/specific_devices/test_fan_that_changes_features.py index 1dc8e9ace68..9921808c371 100644 --- a/tests/components/homekit_controller/specific_devices/test_fan_that_changes_features.py +++ b/tests/components/homekit_controller/specific_devices/test_fan_that_changes_features.py @@ -136,6 +136,7 @@ async def test_bridge_with_two_fans_one_removed( # Verify the first fan is still there fan_state = hass.states.get("fan.living_room_fan") + assert entity_registry.async_get("fan.living_room_fan") is not None assert ( fan_state.attributes[ATTR_SUPPORTED_FEATURES] is FanEntityFeature.SET_SPEED @@ -144,3 +145,4 @@ async def test_bridge_with_two_fans_one_removed( ) # The second fan should have been removed assert not hass.states.get("fan.ceiling_fan") + assert not entity_registry.async_get("fan.ceiling_fan") diff --git a/tests/components/homekit_controller/test_utils.py b/tests/components/homekit_controller/test_utils.py new file mode 100644 index 00000000000..57dd98669fb --- /dev/null +++ b/tests/components/homekit_controller/test_utils.py @@ -0,0 +1,15 @@ +"""Checks for basic helper utils.""" +from homeassistant.components.homekit_controller.utils import unique_id_to_iids + + +def test_unique_id_to_iids(): + """Check that unique_id_to_iids is safe against different invalid ids.""" + assert unique_id_to_iids("pairingid_1_2_3") == (1, 2, 3) + assert unique_id_to_iids("pairingid_1_2") == (1, 2, None) + assert unique_id_to_iids("pairingid_1") == (1, None, None) + + assert unique_id_to_iids("pairingid") is None + assert unique_id_to_iids("pairingid_1_2_3_4") is None + assert unique_id_to_iids("pairingid_a") is None + assert unique_id_to_iids("pairingid_1_a") is None + assert unique_id_to_iids("pairingid_1_2_a") is None From e7043f5dda21cab67785dac16da465b2f612e2da Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 9 Feb 2024 08:15:21 +0100 Subject: [PATCH 0420/1367] Update sentry-sdk to 1.40.2 (#110049) --- 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 e0a2d5f75c4..e5acf81eaae 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.40.0"] + "requirements": ["sentry-sdk==1.40.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index e723cc09e23..ecf41c29f8b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2514,7 +2514,7 @@ sensorpro-ble==0.5.3 sensorpush-ble==1.6.2 # homeassistant.components.sentry -sentry-sdk==1.40.0 +sentry-sdk==1.40.2 # homeassistant.components.sfr_box sfrbox-api==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54cbb09a5b5..d936e2bab1c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1918,7 +1918,7 @@ sensorpro-ble==0.5.3 sensorpush-ble==1.6.2 # homeassistant.components.sentry -sentry-sdk==1.40.0 +sentry-sdk==1.40.2 # homeassistant.components.sfr_box sfrbox-api==0.0.8 From a3dbe42fa19e087dd9ea2b8731a0de7acc7971e6 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler <2292715+bdr99@users.noreply.github.com> Date: Fri, 9 Feb 2024 02:25:32 -0500 Subject: [PATCH 0421/1367] Bump py-aosmith to 1.0.8 (#110061) --- homeassistant/components/aosmith/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aosmith/manifest.json b/homeassistant/components/aosmith/manifest.json index 436918ae772..21580b87286 100644 --- a/homeassistant/components/aosmith/manifest.json +++ b/homeassistant/components/aosmith/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aosmith", "iot_class": "cloud_polling", - "requirements": ["py-aosmith==1.0.6"] + "requirements": ["py-aosmith==1.0.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index ecf41c29f8b..9126ef5116c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1585,7 +1585,7 @@ pushover_complete==1.1.1 pvo==2.1.1 # homeassistant.components.aosmith -py-aosmith==1.0.6 +py-aosmith==1.0.8 # homeassistant.components.canary py-canary==0.5.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d936e2bab1c..d3cdbe45270 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1238,7 +1238,7 @@ pushover_complete==1.1.1 pvo==2.1.1 # homeassistant.components.aosmith -py-aosmith==1.0.6 +py-aosmith==1.0.8 # homeassistant.components.canary py-canary==0.5.3 From d3da6cbcfc88103288a2d081644a66089183985a Mon Sep 17 00:00:00 2001 From: spycle <48740594+spycle@users.noreply.github.com> Date: Fri, 9 Feb 2024 07:33:52 +0000 Subject: [PATCH 0422/1367] Bump pyMicrobot to 0.0.12 (#109970) --- homeassistant/components/keymitt_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/keymitt_ble/manifest.json b/homeassistant/components/keymitt_ble/manifest.json index ee07881a01e..05e06d819f1 100644 --- a/homeassistant/components/keymitt_ble/manifest.json +++ b/homeassistant/components/keymitt_ble/manifest.json @@ -16,5 +16,5 @@ "integration_type": "hub", "iot_class": "assumed_state", "loggers": ["keymitt_ble"], - "requirements": ["PyMicroBot==0.0.10"] + "requirements": ["PyMicroBot==0.0.12"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9126ef5116c..a09fcde9b56 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -76,7 +76,7 @@ PyMetEireann==2021.8.0 PyMetno==0.11.0 # homeassistant.components.keymitt_ble -PyMicroBot==0.0.10 +PyMicroBot==0.0.12 # homeassistant.components.nina PyNINA==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3cdbe45270..8ae413405d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -64,7 +64,7 @@ PyMetEireann==2021.8.0 PyMetno==0.11.0 # homeassistant.components.keymitt_ble -PyMicroBot==0.0.10 +PyMicroBot==0.0.12 # homeassistant.components.nina PyNINA==0.3.3 From d049928be79c144a001070d3a2408a9ba105911b Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 9 Feb 2024 08:35:12 +0100 Subject: [PATCH 0423/1367] Log error and continue on parsing issues of translated strings (#110046) --- homeassistant/helpers/translation.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index ab9d5f576fe..be3e0464361 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -273,7 +273,13 @@ class _TranslationCache: for key, value in updated_resources.items(): if key not in cached_resources: continue - tuples = list(string.Formatter().parse(value)) + try: + tuples = list(string.Formatter().parse(value)) + except ValueError: + _LOGGER.error( + ("Error while parsing localized (%s) string %s"), language, key + ) + continue updated_placeholders = {tup[1] for tup in tuples if tup[1] is not None} tuples = list(string.Formatter().parse(cached_resources[key])) From a0abc276124e6ac545799f3b6c368dcb6f378dbe Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 9 Feb 2024 08:39:08 +0100 Subject: [PATCH 0424/1367] Avoid key_error in modbus climate with non-defined fan_mode. (#110017) --- homeassistant/components/modbus/climate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 637478fffd4..d31323a27e9 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -364,7 +364,9 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): # Translate the value received if fan_mode is not None: - self._attr_fan_mode = self._fan_mode_mapping_from_modbus[int(fan_mode)] + self._attr_fan_mode = self._fan_mode_mapping_from_modbus.get( + int(fan_mode), self._attr_fan_mode + ) # Read the on/off register if defined. If the value in this # register is "OFF", it will take precedence over the value From 720fb7da59865bfc4acd7e8a2dccc0fd74d34938 Mon Sep 17 00:00:00 2001 From: Christophe Gagnier Date: Fri, 9 Feb 2024 02:41:48 -0500 Subject: [PATCH 0425/1367] Update pytechnove to 1.2.2 (#110074) --- .../components/technove/manifest.json | 2 +- .../components/technove/strings.json | 4 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../technove/fixtures/station_bad_status.json | 27 +++++++++++++++ .../technove/snapshots/test_sensor.ambr | 4 +++ tests/components/technove/test_sensor.py | 33 +++++++++++++++++-- 7 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 tests/components/technove/fixtures/station_bad_status.json diff --git a/homeassistant/components/technove/manifest.json b/homeassistant/components/technove/manifest.json index 33739bbd867..c63151560f8 100644 --- a/homeassistant/components/technove/manifest.json +++ b/homeassistant/components/technove/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/technove", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["python-technove==1.2.1"], + "requirements": ["python-technove==1.2.2"], "zeroconf": ["_technove-stations._tcp.local."] } diff --git a/homeassistant/components/technove/strings.json b/homeassistant/components/technove/strings.json index 8a850ee610c..f38bf61d8ed 100644 --- a/homeassistant/components/technove/strings.json +++ b/homeassistant/components/technove/strings.json @@ -63,7 +63,9 @@ "state": { "unplugged": "Unplugged", "plugged_waiting": "Plugged, waiting", - "plugged_charging": "Plugged, charging" + "plugged_charging": "Plugged, charging", + "out_of_activation_period": "Out of activation period", + "high_charge_period": "High charge period" } } } diff --git a/requirements_all.txt b/requirements_all.txt index a09fcde9b56..2a7bc11bd9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2290,7 +2290,7 @@ python-songpal==0.16.1 python-tado==0.17.4 # homeassistant.components.technove -python-technove==1.2.1 +python-technove==1.2.2 # homeassistant.components.telegram_bot python-telegram-bot==13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ae413405d9..b8f45dd0891 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1757,7 +1757,7 @@ python-songpal==0.16.1 python-tado==0.17.4 # homeassistant.components.technove -python-technove==1.2.1 +python-technove==1.2.2 # homeassistant.components.telegram_bot python-telegram-bot==13.1 diff --git a/tests/components/technove/fixtures/station_bad_status.json b/tests/components/technove/fixtures/station_bad_status.json new file mode 100644 index 00000000000..ad24ad43211 --- /dev/null +++ b/tests/components/technove/fixtures/station_bad_status.json @@ -0,0 +1,27 @@ +{ + "voltageIn": 238, + "voltageOut": 238, + "maxStationCurrent": 32, + "maxCurrent": 24, + "current": 23.75, + "network_ssid": "Connecting...", + "id": "AA:AA:AA:AA:AA:BB", + "auto_charge": true, + "highChargePeriodActive": false, + "normalPeriodActive": false, + "maxChargePourcentage": 0.9, + "isBatteryProtected": false, + "inSharingMode": true, + "energySession": 12.34, + "energyTotal": 1234, + "version": "1.82", + "rssi": -82, + "name": "TechnoVE Station", + "lastCharge": "1701072080,0,17.39\n", + "time": 1701000000, + "isUpToDate": true, + "isSessionActive": true, + "conflictInSharingConfig": false, + "isStaticIp": false, + "status": 12345 +} diff --git a/tests/components/technove/snapshots/test_sensor.ambr b/tests/components/technove/snapshots/test_sensor.ambr index d38b08631cc..cbaf8813604 100644 --- a/tests/components/technove/snapshots/test_sensor.ambr +++ b/tests/components/technove/snapshots/test_sensor.ambr @@ -297,6 +297,8 @@ 'unplugged', 'plugged_waiting', 'plugged_charging', + 'out_of_activation_period', + 'high_charge_period', ]), }), 'config_entry_id': , @@ -333,6 +335,8 @@ 'unplugged', 'plugged_waiting', 'plugged_charging', + 'out_of_activation_period', + 'high_charge_period', ]), }), 'context': , diff --git a/tests/components/technove/test_sensor.py b/tests/components/technove/test_sensor.py index 5215f62c517..c44aab8ecc4 100644 --- a/tests/components/technove/test_sensor.py +++ b/tests/components/technove/test_sensor.py @@ -5,15 +5,20 @@ from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion -from technove import Status, TechnoVEError +from technove import Station, Status, TechnoVEError +from homeassistant.components.technove.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import setup_with_selected_platforms -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_object_fixture, +) @pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_technove") @@ -93,3 +98,27 @@ async def test_sensor_update_failure( await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +@pytest.mark.usefixtures("init_integration") +async def test_sensor_unknown_status( + hass: HomeAssistant, + mock_technove: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test coordinator update failure.""" + entity_id = "sensor.technove_station_status" + + assert hass.states.get(entity_id).state == Status.PLUGGED_CHARGING.value + + mock_technove.update.return_value = Station( + load_json_object_fixture("station_bad_status.json", DOMAIN) + ) + + freezer.tick(timedelta(minutes=5, seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_UNKNOWN + # Other sensors should still be available + assert hass.states.get("sensor.technove_station_total_energy_usage").state == "1234" From b5afdf34f4b8a3d8119994f0f44e46287c6eefbd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Feb 2024 01:44:14 -0600 Subject: [PATCH 0426/1367] Improve ability to debug one time listeners blocking the event loop (#110064) --- homeassistant/core.py | 12 ++++++++++-- tests/test_core.py | 13 +++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 047f46afdf5..6e8d45bd0bd 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -22,6 +22,7 @@ from dataclasses import dataclass import datetime import enum import functools +import inspect import logging import os import pathlib @@ -1158,7 +1159,7 @@ class _OneTimeListener: remove: CALLBACK_TYPE | None = None @callback - def async_call(self, event: Event) -> None: + def __call__(self, event: Event) -> None: """Remove listener from event bus and then fire listener.""" if not self.remove: # If the listener was already removed, we don't need to do anything @@ -1167,6 +1168,13 @@ class _OneTimeListener: self.remove = None self.hass.async_run_job(self.listener, event) + def __repr__(self) -> str: + """Return the representation of the listener and source module.""" + module = inspect.getmodule(self.listener) + if module: + return f"<_OneTimeListener {module.__name__}:{self.listener}>" + return f"<_OneTimeListener {self.listener}>" + class EventBus: """Allow the firing of and listening for events.""" @@ -1364,7 +1372,7 @@ class EventBus: event_type, ( HassJob( - one_time_listener.async_call, + one_time_listener, f"onetime listen {event_type} {listener}", job_type=HassJobType.Callback, ), diff --git a/tests/test_core.py b/tests/test_core.py index eb1e6418476..466d4578c7e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2818,3 +2818,16 @@ def test_deprecated_constants( ) -> None: """Test deprecated constants.""" import_and_test_deprecated_constant_enum(caplog, ha, enum, "SOURCE_", "2025.1") + + +def test_one_time_listener_repr(hass: HomeAssistant) -> None: + """Test one time listener repr.""" + + def _listener(event: ha.Event): + """Test listener.""" + + one_time_listener = ha._OneTimeListener(hass, _listener) + repr_str = repr(one_time_listener) + assert "OneTimeListener" in repr_str + assert "test_core" in repr_str + assert "_listener" in repr_str From eef5b442829b42b15095243fb384c013534581f4 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 9 Feb 2024 18:45:55 +1100 Subject: [PATCH 0427/1367] Bump aio-geojson-geonetnz-volcano to 0.9 (#109940) --- homeassistant/components/geonetnz_volcano/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geonetnz_volcano/manifest.json b/homeassistant/components/geonetnz_volcano/manifest.json index 6e9503e0243..421222bb810 100644 --- a/homeassistant/components/geonetnz_volcano/manifest.json +++ b/homeassistant/components/geonetnz_volcano/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aio_geojson_geonetnz_volcano"], - "requirements": ["aio-geojson-geonetnz-volcano==0.8"] + "requirements": ["aio-geojson-geonetnz-volcano==0.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2a7bc11bd9e..bbec620495f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -173,7 +173,7 @@ aio-geojson-generic-client==0.4 aio-geojson-geonetnz-quakes==0.16 # homeassistant.components.geonetnz_volcano -aio-geojson-geonetnz-volcano==0.8 +aio-geojson-geonetnz-volcano==0.9 # homeassistant.components.nsw_rural_fire_service_feed aio-geojson-nsw-rfs-incidents==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b8f45dd0891..2ab202f688e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aio-geojson-generic-client==0.4 aio-geojson-geonetnz-quakes==0.16 # homeassistant.components.geonetnz_volcano -aio-geojson-geonetnz-volcano==0.8 +aio-geojson-geonetnz-volcano==0.9 # homeassistant.components.nsw_rural_fire_service_feed aio-geojson-nsw-rfs-incidents==0.7 From 86e6fdb57c0d1f7702b01d901e1a2c499deeac41 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 9 Feb 2024 07:49:09 +0000 Subject: [PATCH 0428/1367] Bump evohome-async to 0.4.18 (#110056) --- homeassistant/components/evohome/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 9d32ba98e92..0c9bb44d06a 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/evohome", "iot_class": "cloud_polling", "loggers": ["evohomeasync", "evohomeasync2"], - "requirements": ["evohome-async==0.4.17"] + "requirements": ["evohome-async==0.4.18"] } diff --git a/requirements_all.txt b/requirements_all.txt index bbec620495f..a763d11efff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -824,7 +824,7 @@ eufylife-ble-client==0.1.8 # evdev==1.6.1 # homeassistant.components.evohome -evohome-async==0.4.17 +evohome-async==0.4.18 # homeassistant.components.faa_delays faadelays==2023.9.1 From ec3af2462b4985ae1b0fa2a9919d4f7bed920b04 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Fri, 9 Feb 2024 09:53:22 +0100 Subject: [PATCH 0429/1367] Address late review on husqvarna_automower (#109896) * Address late review on husqvarna_automower * Add missing credentials string --- .../components/husqvarna_automower/coordinator.py | 4 ++-- homeassistant/components/husqvarna_automower/entity.py | 1 - homeassistant/components/husqvarna_automower/lawn_mower.py | 2 +- homeassistant/components/husqvarna_automower/strings.json | 7 ++++++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py index 8409643ee7c..70d69f90549 100644 --- a/homeassistant/components/husqvarna_automower/coordinator.py +++ b/homeassistant/components/husqvarna_automower/coordinator.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging from typing import Any -from aioautomower.model import MowerAttributes, MowerList +from aioautomower.model import MowerAttributes from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -42,6 +42,6 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib await self.api.close() @callback - def callback(self, ws_data: MowerList) -> None: + def callback(self, ws_data: dict[str, MowerAttributes]) -> None: """Process websocket callbacks and write them to the DataUpdateCoordinator.""" self.async_set_updated_data(ws_data) diff --git a/homeassistant/components/husqvarna_automower/entity.py b/homeassistant/components/husqvarna_automower/entity.py index e91e3c89ab2..25951aad1e3 100644 --- a/homeassistant/components/husqvarna_automower/entity.py +++ b/homeassistant/components/husqvarna_automower/entity.py @@ -17,7 +17,6 @@ class AutomowerBaseEntity(CoordinatorEntity[AutomowerDataUpdateCoordinator]): """Defining the Automower base Entity.""" _attr_has_entity_name = True - _attr_should_poll = False def __init__( self, diff --git a/homeassistant/components/husqvarna_automower/lawn_mower.py b/homeassistant/components/husqvarna_automower/lawn_mower.py index e44f8b98c47..b14f9e5d72c 100644 --- a/homeassistant/components/husqvarna_automower/lawn_mower.py +++ b/homeassistant/components/husqvarna_automower/lawn_mower.py @@ -64,7 +64,7 @@ async def async_setup_entry( ) -class AutomowerLawnMowerEntity(LawnMowerEntity, AutomowerBaseEntity): +class AutomowerLawnMowerEntity(AutomowerBaseEntity, LawnMowerEntity): """Defining each mower Entity.""" _attr_name = None diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index 569e148a5a3..6a5b28153b4 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -9,10 +9,15 @@ "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%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "missing_credentials": "[%key:common::config_flow::abort::oauth2_missing_credentials%]", "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%]" + "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" From 7caf78a9269cf65377122a1564d117b80c016caf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 9 Feb 2024 10:08:23 +0100 Subject: [PATCH 0430/1367] Update debugpy to 1.8.1 (#110076) --- homeassistant/components/debugpy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index d3ed3564344..fc52557fa5a 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -6,5 +6,5 @@ "integration_type": "service", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["debugpy==1.8.0"] + "requirements": ["debugpy==1.8.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index a763d11efff..7459bb92245 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -681,7 +681,7 @@ datadog==0.15.0 dbus-fast==2.21.1 # homeassistant.components.debugpy -debugpy==1.8.0 +debugpy==1.8.1 # homeassistant.components.decora_wifi # decora-wifi==1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ab202f688e..f0b08ce7ad7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,7 +562,7 @@ datadog==0.15.0 dbus-fast==2.21.1 # homeassistant.components.debugpy -debugpy==1.8.0 +debugpy==1.8.1 # homeassistant.components.ecovacs deebot-client==5.1.1 From 793b6aa97da9055df9d23f7deb3a02b139e82a74 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 9 Feb 2024 10:10:25 +0100 Subject: [PATCH 0431/1367] Allow passing version to ConfigEntry.async_update_entry (#110077) Allow passing minor_version and version to ConfigEntry.async_update_entry --- homeassistant/config_entries.py | 10 +++++++--- tests/test_config_entries.py | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index cfb18505813..770693289b1 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1496,12 +1496,14 @@ class ConfigEntries: self, entry: ConfigEntry, *, - unique_id: str | None | UndefinedType = UNDEFINED, - title: str | UndefinedType = UNDEFINED, data: Mapping[str, Any] | UndefinedType = UNDEFINED, + minor_version: int | UndefinedType = UNDEFINED, options: Mapping[str, Any] | UndefinedType = UNDEFINED, pref_disable_new_entities: bool | UndefinedType = UNDEFINED, pref_disable_polling: bool | UndefinedType = UNDEFINED, + title: str | UndefinedType = UNDEFINED, + unique_id: str | None | UndefinedType = UNDEFINED, + version: int | UndefinedType = UNDEFINED, ) -> bool: """Update a config entry. @@ -1522,9 +1524,11 @@ class ConfigEntries: changed = True for attr, value in ( - ("title", title), + ("minor_version", minor_version), ("pref_disable_new_entities", pref_disable_new_entities), ("pref_disable_polling", pref_disable_polling), + ("title", title), + ("version", version), ): if value is UNDEFINED or getattr(entry, attr) == value: continue diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 609f80e1a60..e7115957e13 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3199,13 +3199,18 @@ async def test_updating_entry_with_and_without_changes( for change in ( {"data": {"second": True, "third": 456}}, {"data": {"second": True}}, + {"minor_version": 2}, {"options": {"hello": True}}, {"pref_disable_new_entities": True}, {"pref_disable_polling": True}, {"title": "sometitle"}, {"unique_id": "abcd1234"}, + {"version": 2}, ): assert manager.async_update_entry(entry, **change) is True + key = next(iter(change)) + value = next(iter(change.values())) + assert getattr(entry, key) == value assert manager.async_update_entry(entry, **change) is False assert manager.async_entry_for_domain_unique_id("test", "abc123") is None From 8aa4157290f74047b9ff36252f9d3f04af43ced1 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 9 Feb 2024 11:30:27 +0100 Subject: [PATCH 0432/1367] Add entry diagnostics to imap integration (#109460) --- homeassistant/components/imap/coordinator.py | 22 ++++++ homeassistant/components/imap/diagnostics.py | 38 +++++++++ tests/components/imap/test_diagnostics.py | 83 ++++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 homeassistant/components/imap/diagnostics.py create mode 100644 tests/components/imap/test_diagnostics.py diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index 3b2a3601eec..f0c9099863a 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -30,6 +30,7 @@ from homeassistant.exceptions import ( from homeassistant.helpers.json import json_bytes from homeassistant.helpers.template import Template from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util from homeassistant.util.ssl import ( SSLCipherList, client_context, @@ -57,6 +58,8 @@ EVENT_IMAP = "imap_content" MAX_ERRORS = 3 MAX_EVENT_DATA_BYTES = 32168 +DIAGNOSTICS_ATTRIBUTES = ["date", "initial"] + async def connect_to_server(data: Mapping[str, Any]) -> IMAP4_SSL: """Connect to imap server and return client.""" @@ -220,6 +223,7 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): self._last_message_uid: str | None = None self._last_message_id: str | None = None self.custom_event_template = None + self._diagnostics_data: dict[str, Any] = {} _custom_event_template = entry.data.get(CONF_CUSTOM_EVENT_DATA_TEMPLATE) if _custom_event_template is not None: self.custom_event_template = Template(_custom_event_template, hass=hass) @@ -287,6 +291,7 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): CONF_MAX_MESSAGE_SIZE, DEFAULT_MAX_MESSAGE_SIZE ) ] + self._update_diagnostics(data) if (size := len(json_bytes(data))) > MAX_EVENT_DATA_BYTES: _LOGGER.warning( "Custom imap_content event skipped, size (%s) exceeds " @@ -357,6 +362,23 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): """Close resources.""" await self._cleanup(log_error=True) + def _update_diagnostics(self, data: dict[str, Any]) -> None: + """Update the diagnostics.""" + self._diagnostics_data.update( + {key: value for key, value in data.items() if key in DIAGNOSTICS_ATTRIBUTES} + ) + custom: Any | None = data.get("custom") + self._diagnostics_data["custom_template_data_type"] = str(type(custom)) + self._diagnostics_data["custom_template_result_length"] = ( + None if custom is None else len(f"{custom}") + ) + self._diagnostics_data["event_time"] = dt_util.now().isoformat() + + @property + def diagnostics_data(self) -> dict[str, Any]: + """Return diagnostics info.""" + return self._diagnostics_data + class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator): """Class for imap client.""" diff --git a/homeassistant/components/imap/diagnostics.py b/homeassistant/components/imap/diagnostics.py new file mode 100644 index 00000000000..c7d5151ba49 --- /dev/null +++ b/homeassistant/components/imap/diagnostics.py @@ -0,0 +1,38 @@ +"""Diagnostics support for IMAP.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN +from .coordinator import ImapDataUpdateCoordinator + +REDACT_CONFIG = {CONF_PASSWORD, CONF_USERNAME} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + return _async_get_diagnostics(hass, entry) + + +@callback +def _async_get_diagnostics( + hass: HomeAssistant, + entry: ConfigEntry, +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + redacted_config = async_redact_data(entry.data, REDACT_CONFIG) + coordinator: ImapDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + data = { + "config": redacted_config, + "event": coordinator.diagnostics_data, + } + + return data diff --git a/tests/components/imap/test_diagnostics.py b/tests/components/imap/test_diagnostics.py new file mode 100644 index 00000000000..68b6831fa5b --- /dev/null +++ b/tests/components/imap/test_diagnostics.py @@ -0,0 +1,83 @@ +"""Test IMAP diagnostics.""" +from datetime import timedelta +from typing import Any +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components import imap +from homeassistant.components.sensor.const import SensorStateClass +from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util + +from .const import TEST_FETCH_RESPONSE_TEXT_PLAIN, TEST_SEARCH_RESPONSE +from .test_config_flow import MOCK_CONFIG + +from tests.common import MockConfigEntry, async_capture_events, async_fire_time_changed +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +@pytest.mark.parametrize("imap_search", [TEST_SEARCH_RESPONSE]) +@pytest.mark.parametrize("imap_fetch", [TEST_FETCH_RESPONSE_TEXT_PLAIN]) +@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) +async def test_entry_diagnostics( + hass: HomeAssistant, + mock_imap_protocol: MagicMock, + hass_client: ClientSessionGenerator, +) -> None: + """Test receiving a message successfully.""" + event_called = async_capture_events(hass, "imap_content") + + template = "{{ 4 * 4 }}" + config = MOCK_CONFIG.copy() + config["custom_event_data_template"] = template + config_entry = MockConfigEntry(domain=imap.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, dt_util.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" + + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + + expected_config = { + "username": "**REDACTED**", + "password": "**REDACTED**", + "server": "imap.server.com", + "port": 993, + "charset": "utf-8", + "folder": "INBOX", + "search": "UnSeen UnDeleted", + "custom_event_data_template": "{{ 4 * 4 }}", + } + expected_event_data = { + "date": "2023-03-24T13:52:00+01:00", + "initial": True, + "custom_template_data_type": "", + "custom_template_result_length": 2, + } + diagnostics = await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) + assert diagnostics["config"] == expected_config + event_data = diagnostics["event"] + assert event_data.pop("event_time") is not None + assert event_data == expected_event_data From 6499be852842130770ef6c2e9c9a2c0eaa52aae5 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 9 Feb 2024 21:31:16 +1100 Subject: [PATCH 0433/1367] Bump aio-geojson-usgs-earthquakes to 0.3 (#110084) --- homeassistant/components/usgs_earthquakes_feed/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index 6dbe43cb4e3..ffb9412703f 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -6,5 +6,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aio_geojson_usgs_earthquakes"], - "requirements": ["aio-geojson-usgs-earthquakes==0.2"] + "requirements": ["aio-geojson-usgs-earthquakes==0.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7459bb92245..ba6f46f691c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -179,7 +179,7 @@ aio-geojson-geonetnz-volcano==0.9 aio-geojson-nsw-rfs-incidents==0.7 # homeassistant.components.usgs_earthquakes_feed -aio-geojson-usgs-earthquakes==0.2 +aio-geojson-usgs-earthquakes==0.3 # homeassistant.components.gdacs aio-georss-gdacs==0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0b08ce7ad7..f6c7f59b6b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -158,7 +158,7 @@ aio-geojson-geonetnz-volcano==0.9 aio-geojson-nsw-rfs-incidents==0.7 # homeassistant.components.usgs_earthquakes_feed -aio-geojson-usgs-earthquakes==0.2 +aio-geojson-usgs-earthquakes==0.3 # homeassistant.components.gdacs aio-georss-gdacs==0.9 From 3ca202a338dd25e3c8e33aa7044546dc674c3afe Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 9 Feb 2024 14:10:59 +0100 Subject: [PATCH 0434/1367] Fix scene tests (#110097) --- tests/components/scene/test_init.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 26d9294dedb..e0b2af87187 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -240,8 +240,10 @@ async def setup_lights(hass, entities): await hass.async_block_till_done() light_1, light_2 = entities - light_1.supported_color_modes = ["brightness"] - light_2.supported_color_modes = ["brightness"] + light_1._attr_supported_color_modes = {"brightness"} + light_2._attr_supported_color_modes = {"brightness"} + light_1._attr_color_mode = "brightness" + light_2._attr_color_mode = "brightness" await turn_off_lights(hass, [light_1.entity_id, light_2.entity_id]) assert not light.is_on(hass, light_1.entity_id) From ae5bef6ffacdb9ad0d19e163bba943779b405153 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 9 Feb 2024 14:11:27 +0100 Subject: [PATCH 0435/1367] Fix color mode in flux_led light (#110096) --- homeassistant/components/flux_led/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/flux_led/util.py b/homeassistant/components/flux_led/util.py index 7aa2d91de4e..8db12cb6e32 100644 --- a/homeassistant/components/flux_led/util.py +++ b/homeassistant/components/flux_led/util.py @@ -12,6 +12,8 @@ from .const import FLUX_COLOR_MODE_TO_HASS, MIN_RGB_BRIGHTNESS def _hass_color_modes(device: AIOWifiLedBulb) -> set[str]: color_modes = device.color_modes + if not color_modes: + return {ColorMode.ONOFF} return {_flux_color_mode_to_hass(mode, color_modes) for mode in color_modes} From ced922bb1a84da9c06ec356bd70defb07dc0bc35 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Feb 2024 07:50:30 -0600 Subject: [PATCH 0436/1367] Improve apple_tv error reporting when setup fails (#110071) * Improve apple_tv error reporting when setup fails * Improve apple_tv error reporting when setup fails * Update homeassistant/components/apple_tv/__init__.py * ensure cleaned up --- homeassistant/components/apple_tv/__init__.py | 55 ++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 273acf12329..875a23c3244 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -26,7 +26,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceInfo @@ -57,10 +57,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: manager = AppleTVManager(hass, entry) if manager.is_on: - await manager.connect_once(raise_missing_credentials=True) - if not manager.atv: - address = entry.data[CONF_ADDRESS] - raise ConfigEntryNotReady(f"Not found at {address}, waiting for discovery") + address = entry.data[CONF_ADDRESS] + + try: + await manager.async_first_connect() + except ( + exceptions.AuthenticationError, + exceptions.InvalidCredentialsError, + exceptions.NoCredentialsError, + ) as ex: + raise ConfigEntryAuthFailed( + f"{address}: Authentication failed, try reconfiguring device: {ex}" + ) from ex + except ( + asyncio.CancelledError, + exceptions.ConnectionLostError, + exceptions.ConnectionFailedError, + ) as ex: + raise ConfigEntryNotReady(f"{address}: {ex}") from ex + except ( + exceptions.ProtocolError, + exceptions.NoServiceError, + exceptions.PairingError, + exceptions.BackOffError, + exceptions.DeviceIdMissingError, + ) as ex: + _LOGGER.debug( + "Error setting up apple_tv at %s: %s", address, ex, exc_info=ex + ) + raise ConfigEntryNotReady(f"{address}: {ex}") from ex hass.data.setdefault(DOMAIN, {})[entry.unique_id] = manager @@ -228,11 +253,25 @@ class AppleTVManager(DeviceListener): "Not starting connect loop (%s, %s)", self.atv is None, self.is_on ) + async def _connect_once(self, raise_missing_credentials: bool) -> None: + """Connect to device once.""" + if conf := await self._scan(): + await self._connect(conf, raise_missing_credentials) + + async def async_first_connect(self): + """Connect to device for the first time.""" + connect_ok = False + try: + await self._connect_once(raise_missing_credentials=True) + connect_ok = True + finally: + if not connect_ok: + await self.disconnect() + async def connect_once(self, raise_missing_credentials: bool) -> None: """Try to connect once.""" try: - if conf := await self._scan(): - await self._connect(conf, raise_missing_credentials) + await self._connect_once(raise_missing_credentials) except exceptions.AuthenticationError: self.config_entry.async_start_reauth(self.hass) await self.disconnect() @@ -245,7 +284,7 @@ class AppleTVManager(DeviceListener): pass except Exception: # pylint: disable=broad-except _LOGGER.exception("Failed to connect") - self.atv = None + await self.disconnect() async def _connect_loop(self): """Connect loop background task function.""" From 6e134b325dafd3b33b430db3ee7b4d58f597e049 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Feb 2024 07:51:02 -0600 Subject: [PATCH 0437/1367] Make ConfigEntryItems responsible for updating unique ids (#110018) * Make ConfigEntryItems responsible for updating unique ids * Make ConfigEntryItems responsible for updating unique ids * Make ConfigEntryItems responsible for updating unique ids * Make ConfigEntryItems responsible for updating unique ids * Make ConfigEntryItems responsible for updating unique ids --- homeassistant/config_entries.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 770693289b1..e4cd2205671 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1146,6 +1146,10 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]): _LOGGER.error("An entry with the id %s already exists", entry_id) self._unindex_entry(entry_id) data[entry_id] = entry + self._index_entry(entry) + + def _index_entry(self, entry: ConfigEntry) -> None: + """Index an entry.""" self._domain_index.setdefault(entry.domain, []).append(entry) if entry.unique_id is not None: unique_id_hash = entry.unique_id @@ -1191,6 +1195,16 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]): self._unindex_entry(entry_id) super().__delitem__(entry_id) + def update_unique_id(self, entry: ConfigEntry, new_unique_id: str | None) -> None: + """Update unique id for an entry. + + This method mutates the entry with the new unique id and updates the indexes. + """ + entry_id = entry.entry_id + self._unindex_entry(entry_id) + entry.unique_id = new_unique_id + self._index_entry(entry) + def get_entries_for_domain(self, domain: str) -> list[ConfigEntry]: """Get entries for a domain.""" return self._domain_index.get(domain, []) @@ -1517,10 +1531,7 @@ class ConfigEntries: if unique_id is not UNDEFINED and entry.unique_id != unique_id: # Reindex the entry if the unique_id has changed - entry_id = entry.entry_id - del self._entries[entry_id] - entry.unique_id = unique_id - self._entries[entry_id] = entry + self._entries.update_unique_id(entry, unique_id) changed = True for attr, value in ( From 8e4714c5630e557570726dbb887b70aed81cb676 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Feb 2024 08:05:27 -0600 Subject: [PATCH 0438/1367] Avoid delaying startup in dlna_dmr (#109836) * Avoid delaying startup in dlna_dmr fixes #109834 * make sure device info is linked up at startup * fixes * update tests * startup only * override device info if we have it * fixes * make sure its set right away when adding the device * revert test changes * coverage * coverage * coverage * coverage * adjust * fixes * more fixes * coverage * coverage * coverage * tweaks * tweaks * Revert "revert test changes" This reverts commit 014d29297dac9dda45a50ec9eb2fa63a735538ac. * coverage * coverage --- .../components/dlna_dmr/media_player.py | 127 +++++++---- tests/components/dlna_dmr/test_init.py | 5 + .../components/dlna_dmr/test_media_player.py | 213 +++++++++++++++++- 3 files changed, 300 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 749f2c887eb..c8c70486854 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -19,6 +19,7 @@ from homeassistant import config_entries from homeassistant.components import media_source, ssdp from homeassistant.components.media_player import ( ATTR_MEDIA_EXTRA, + DOMAIN as MEDIA_PLAYER_DOMAIN, BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -28,7 +29,7 @@ from homeassistant.components.media_player import ( async_process_play_media_url, ) from homeassistant.const import CONF_DEVICE_ID, CONF_MAC, CONF_TYPE, CONF_URL -from homeassistant.core import HomeAssistant +from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -37,6 +38,7 @@ from .const import ( CONF_CALLBACK_URL_OVERRIDE, CONF_LISTEN_PORT, CONF_POLL_AVAILABILITY, + DOMAIN, LOGGER as _LOGGER, MEDIA_METADATA_DIDL, MEDIA_TYPE_MAP, @@ -87,9 +89,32 @@ async def async_setup_entry( """Set up the DlnaDmrEntity from a config entry.""" _LOGGER.debug("media_player.async_setup_entry %s (%s)", entry.entry_id, entry.title) + udn = entry.data[CONF_DEVICE_ID] + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + + if ( + ( + existing_entity_id := ent_reg.async_get_entity_id( + domain=MEDIA_PLAYER_DOMAIN, platform=DOMAIN, unique_id=udn + ) + ) + and (existing_entry := ent_reg.async_get(existing_entity_id)) + and (device_id := existing_entry.device_id) + and (device_entry := dev_reg.async_get(device_id)) + and (dr.CONNECTION_UPNP, udn) not in device_entry.connections + ): + # If the existing device is missing the udn connection, add it + # now to ensure that when the entity gets added it is linked to + # the correct device. + dev_reg.async_update_device( + device_id, + merge_connections={(dr.CONNECTION_UPNP, udn)}, + ) + # Create our own device-wrapping entity entity = DlnaDmrEntity( - udn=entry.data[CONF_DEVICE_ID], + udn=udn, device_type=entry.data[CONF_TYPE], name=entry.title, event_port=entry.options.get(CONF_LISTEN_PORT) or 0, @@ -98,6 +123,7 @@ async def async_setup_entry( location=entry.data[CONF_URL], mac_address=entry.data.get(CONF_MAC), browse_unfiltered=entry.options.get(CONF_BROWSE_UNFILTERED, False), + config_entry=entry, ) async_add_entities([entity]) @@ -143,6 +169,7 @@ class DlnaDmrEntity(MediaPlayerEntity): location: str, mac_address: str | None, browse_unfiltered: bool, + config_entry: config_entries.ConfigEntry, ) -> None: """Initialize DLNA DMR entity.""" self.udn = udn @@ -154,25 +181,17 @@ class DlnaDmrEntity(MediaPlayerEntity): self.mac_address = mac_address self.browse_unfiltered = browse_unfiltered self._device_lock = asyncio.Lock() + self._background_setup_task: asyncio.Task[None] | None = None + self._updated_registry: bool = False + self._config_entry = config_entry + self._attr_device_info = dr.DeviceInfo(connections={(dr.CONNECTION_UPNP, udn)}) async def async_added_to_hass(self) -> None: """Handle addition.""" # Update this entity when the associated config entry is modified - if self.registry_entry and self.registry_entry.config_entry_id: - config_entry = self.hass.config_entries.async_get_entry( - self.registry_entry.config_entry_id - ) - assert config_entry is not None - self.async_on_remove( - config_entry.add_update_listener(self.async_config_update_listener) - ) - - # Try to connect to the last known location, but don't worry if not available - if not self._device: - try: - await self._device_connect(self.location) - except UpnpError as err: - _LOGGER.debug("Couldn't connect immediately: %r", err) + self.async_on_remove( + self._config_entry.add_update_listener(self.async_config_update_listener) + ) # Get SSDP notifications for only this device self.async_on_remove( @@ -193,8 +212,29 @@ class DlnaDmrEntity(MediaPlayerEntity): ) ) + if not self._device: + if self.hass.state is CoreState.running: + await self._async_setup() + else: + self._background_setup_task = self.hass.async_create_background_task( + self._async_setup(), f"dlna_dmr {self.name} setup" + ) + + async def _async_setup(self) -> None: + # Try to connect to the last known location, but don't worry if not available + try: + await self._device_connect(self.location) + except UpnpError as err: + _LOGGER.debug("Couldn't connect immediately: %r", err) + async def async_will_remove_from_hass(self) -> None: """Handle removal.""" + if self._background_setup_task: + self._background_setup_task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await self._background_setup_task + self._background_setup_task = None + await self._device_disconnect() async def async_ssdp_callback( @@ -351,25 +391,28 @@ class DlnaDmrEntity(MediaPlayerEntity): def _update_device_registry(self, set_mac: bool = False) -> None: """Update the device registry with new information about the DMR.""" - if not self._device: - return # Can't get all the required information without a connection + if ( + # Can't get all the required information without a connection + not self._device + or + # No new information + (not set_mac and self._updated_registry) + ): + return - if not self.registry_entry or not self.registry_entry.config_entry_id: - return # No config registry entry to link to - - if self.registry_entry.device_id and not set_mac: - return # No new information - - connections = set() # Connections based on the root device's UDN, and the DMR embedded # device's UDN. They may be the same, if the DMR is the root device. - connections.add( + connections = { ( dr.CONNECTION_UPNP, self._device.profile_device.root_device.udn, - ) - ) - connections.add((dr.CONNECTION_UPNP, self._device.udn)) + ), + (dr.CONNECTION_UPNP, self._device.udn), + ( + dr.CONNECTION_UPNP, + self.udn, + ), + } if self.mac_address: # Connection based on MAC address, if known @@ -378,23 +421,27 @@ class DlnaDmrEntity(MediaPlayerEntity): (dr.CONNECTION_NETWORK_MAC, self.mac_address) ) - # Create linked HA DeviceEntry now the information is known. - dev_reg = dr.async_get(self.hass) - device_entry = dev_reg.async_get_or_create( - config_entry_id=self.registry_entry.config_entry_id, + device_info = dr.DeviceInfo( connections=connections, default_manufacturer=self._device.manufacturer, default_model=self._device.model_name, default_name=self._device.name, ) + self._attr_device_info = device_info + + self._updated_registry = True + # Create linked HA DeviceEntry now the information is known. + device_entry = dr.async_get(self.hass).async_get_or_create( + config_entry_id=self._config_entry.entry_id, **device_info + ) # Update entity registry to link to the device - ent_reg = er.async_get(self.hass) - ent_reg.async_get_or_create( - self.registry_entry.domain, - self.registry_entry.platform, + er.async_get(self.hass).async_get_or_create( + MEDIA_PLAYER_DOMAIN, + DOMAIN, self.unique_id, device_id=device_entry.id, + config_entry=self._config_entry, ) async def _device_disconnect(self) -> None: @@ -419,6 +466,10 @@ class DlnaDmrEntity(MediaPlayerEntity): async def async_update(self) -> None: """Retrieve the latest data.""" + if self._background_setup_task: + await self._background_setup_task + self._background_setup_task = None + if not self._device: if not self.poll_availability: return diff --git a/tests/components/dlna_dmr/test_init.py b/tests/components/dlna_dmr/test_init.py index f1c3151fb28..38160f117b4 100644 --- a/tests/components/dlna_dmr/test_init.py +++ b/tests/components/dlna_dmr/test_init.py @@ -6,6 +6,7 @@ from homeassistant.components import media_player from homeassistant.components.dlna_dmr.const import DOMAIN as DLNA_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -31,6 +32,10 @@ async def test_resource_lifecycle( ) assert len(entries) == 1 entity_id = entries[0].entity_id + + await async_update_entity(hass, entity_id) + await hass.async_block_till_done() + mock_state = hass.states.get(entity_id) assert mock_state is not None assert mock_state.state == media_player.STATE_IDLE diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index 51128b161fb..65670b48ab1 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -26,6 +26,7 @@ from homeassistant.components.dlna_dmr.const import ( CONF_CALLBACK_URL_OVERRIDE, CONF_LISTEN_PORT, CONF_POLL_AVAILABILITY, + DOMAIN, ) from homeassistant.components.dlna_dmr.data import EventListenAddr from homeassistant.components.dlna_dmr.media_player import DlnaDmrEntity @@ -46,7 +47,7 @@ from homeassistant.const import ( CONF_TYPE, CONF_URL, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, CONNECTION_UPNP, @@ -216,6 +217,9 @@ async def test_setup_entry_no_options( """ config_entry_mock.options = MappingProxyType({}) mock_entity_id = await setup_mock_component(hass, config_entry_mock) + await async_update_entity(hass, mock_entity_id) + await hass.async_block_till_done() + mock_state = hass.states.get(mock_entity_id) assert mock_state is not None @@ -266,17 +270,23 @@ async def test_setup_entry_no_options( assert mock_state.state == ha_const.STATE_UNAVAILABLE +@pytest.mark.parametrize( + "core_state", + (CoreState.not_running, CoreState.running), +) async def test_setup_entry_with_options( hass: HomeAssistant, domain_data_mock: Mock, ssdp_scanner_mock: Mock, config_entry_mock: MockConfigEntry, dmr_device_mock: Mock, + core_state: CoreState, ) -> None: """Test setting options leads to a DlnaDmrEntity with custom event_handler. Check that the device is constructed properly as part of the test. """ + hass.set_state(core_state) config_entry_mock.options = MappingProxyType( { CONF_LISTEN_PORT: 2222, @@ -285,6 +295,8 @@ async def test_setup_entry_with_options( } ) mock_entity_id = await setup_mock_component(hass, config_entry_mock) + await async_update_entity(hass, mock_entity_id) + await hass.async_block_till_done() mock_state = hass.states.get(mock_entity_id) assert mock_state is not None @@ -343,8 +355,9 @@ async def test_setup_entry_mac_address( dmr_device_mock: Mock, ) -> None: """Entry with a MAC address will set up and set the device registry connection.""" - await setup_mock_component(hass, config_entry_mock) - + mock_entity_id = await setup_mock_component(hass, config_entry_mock) + await async_update_entity(hass, mock_entity_id) + await hass.async_block_till_done() # Check the device registry connections for MAC address dev_reg = async_get_dr(hass) device = dev_reg.async_get_device( @@ -363,8 +376,9 @@ async def test_setup_entry_no_mac_address( dmr_device_mock: Mock, ) -> None: """Test setting up an entry without a MAC address will succeed.""" - await setup_mock_component(hass, config_entry_mock_no_mac) - + mock_entity_id = await setup_mock_component(hass, config_entry_mock_no_mac) + await async_update_entity(hass, mock_entity_id) + await hass.async_block_till_done() # Check the device registry connections does not include the MAC address dev_reg = async_get_dr(hass) device = dev_reg.async_get_device( @@ -382,6 +396,8 @@ async def test_event_subscribe_failure( dmr_device_mock.async_subscribe_services.side_effect = UpnpError mock_entity_id = await setup_mock_component(hass, config_entry_mock) + await async_update_entity(hass, mock_entity_id) + await hass.async_block_till_done() mock_state = hass.states.get(mock_entity_id) assert mock_state is not None @@ -412,6 +428,8 @@ async def test_event_subscribe_rejected( dmr_device_mock.async_subscribe_services.side_effect = UpnpResponseError(status=501) mock_entity_id = await setup_mock_component(hass, config_entry_mock) + await async_update_entity(hass, mock_entity_id) + await hass.async_block_till_done() mock_state = hass.states.get(mock_entity_id) assert mock_state is not None @@ -432,6 +450,8 @@ async def test_available_device( ) -> None: """Test a DlnaDmrEntity with a connected DmrDevice.""" # Check hass device information is filled in + await async_update_entity(hass, mock_entity_id) + await hass.async_block_till_done() dev_reg = async_get_dr(hass) device = dev_reg.async_get_device( connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, @@ -1235,14 +1255,20 @@ async def test_playback_update_state( dmr_device_mock.async_update.assert_not_awaited() +@pytest.mark.parametrize( + "core_state", + (CoreState.not_running, CoreState.running), +) async def test_unavailable_device( hass: HomeAssistant, domain_data_mock: Mock, ssdp_scanner_mock: Mock, config_entry_mock: MockConfigEntry, + core_state: CoreState, ) -> None: """Test a DlnaDmrEntity with out a connected DmrDevice.""" # Cause connection attempts to fail + hass.set_state(core_state) domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpConnectionError with patch( @@ -1336,7 +1362,9 @@ async def test_unavailable_device( connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, identifiers=set(), ) - assert device is None + assert device is not None + assert device.name is None + assert device.manufacturer is None # Unload config entry to clean up assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { @@ -1355,15 +1383,21 @@ async def test_unavailable_device( assert mock_state.state == ha_const.STATE_UNAVAILABLE +@pytest.mark.parametrize( + "core_state", + (CoreState.not_running, CoreState.running), +) async def test_become_available( hass: HomeAssistant, domain_data_mock: Mock, ssdp_scanner_mock: Mock, config_entry_mock: MockConfigEntry, dmr_device_mock: Mock, + core_state: CoreState, ) -> None: """Test a device becoming available after the entity is constructed.""" # Cause connection attempts to fail before adding entity + hass.set_state(core_state) domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpConnectionError mock_entity_id = await setup_mock_component(hass, config_entry_mock) mock_state = hass.states.get(mock_entity_id) @@ -1376,7 +1410,7 @@ async def test_become_available( connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, identifiers=set(), ) - assert device is None + assert device is not None # Mock device is now available. domain_data_mock.upnp_factory.async_create_device.side_effect = None @@ -1440,13 +1474,19 @@ async def test_become_available( assert mock_state.state == ha_const.STATE_UNAVAILABLE +@pytest.mark.parametrize( + "core_state", + (CoreState.not_running, CoreState.running), +) async def test_alive_but_gone( hass: HomeAssistant, domain_data_mock: Mock, ssdp_scanner_mock: Mock, mock_disconnected_entity_id: str, + core_state: CoreState, ) -> None: """Test a device sending an SSDP alive announcement, but not being connectable.""" + hass.set_state(core_state) domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError # Send an SSDP notification from the still missing device @@ -2275,3 +2315,162 @@ async def test_config_update_mac_address( ) assert device is not None assert (CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) in device.connections + + +@pytest.mark.parametrize( + "core_state", + (CoreState.not_running, CoreState.running), +) +async def test_connections_restored( + hass: HomeAssistant, + domain_data_mock: Mock, + ssdp_scanner_mock: Mock, + config_entry_mock: MockConfigEntry, + dmr_device_mock: Mock, + core_state: CoreState, +) -> None: + """Test previous connections restored.""" + # Cause connection attempts to fail before adding entity + hass.set_state(core_state) + domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpConnectionError + mock_entity_id = await setup_mock_component(hass, config_entry_mock) + mock_state = hass.states.get(mock_entity_id) + assert mock_state is not None + assert mock_state.state == ha_const.STATE_UNAVAILABLE + + # Check hass device information has not been filled in yet + dev_reg = async_get_dr(hass) + device = dev_reg.async_get_device( + connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + identifiers=set(), + ) + assert device is not None + + # Mock device is now available. + domain_data_mock.upnp_factory.async_create_device.side_effect = None + domain_data_mock.upnp_factory.async_create_device.reset_mock() + + # Send an SSDP notification from the now alive device + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.ALIVE, + ) + await hass.async_block_till_done() + + # Check device was created from the supplied URL + domain_data_mock.upnp_factory.async_create_device.assert_awaited_once_with( + NEW_DEVICE_LOCATION + ) + # Check event notifiers are acquired + domain_data_mock.async_get_event_notifier.assert_awaited_once_with( + EventListenAddr(LOCAL_IP, 0, None), hass + ) + # Check UPnP services are subscribed + dmr_device_mock.async_subscribe_services.assert_awaited_once_with( + auto_resubscribe=True + ) + assert dmr_device_mock.on_event is not None + # Quick check of the state to verify the entity has a connected DmrDevice + mock_state = hass.states.get(mock_entity_id) + assert mock_state is not None + 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( + connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + identifiers=set(), + ) + assert device is not None + previous_connections = device.connections + assert device.manufacturer == "device_manufacturer" + assert device.model == "device_model_name" + assert device.name == "device_name" + + # Reload the config entry + assert await hass.config_entries.async_reload(config_entry_mock.entry_id) + await async_update_entity(hass, mock_entity_id) + + # Confirm SSDP notifications unregistered + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 2 + + # Confirm the entity has disconnected from the device + domain_data_mock.async_release_event_notifier.assert_awaited_once() + dmr_device_mock.async_unsubscribe_services.assert_awaited_once() + + # Check hass device information has not been filled in yet + dev_reg = async_get_dr(hass) + device = dev_reg.async_get_device( + connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + identifiers=set(), + ) + assert device is not None + assert device.connections == previous_connections + + # Verify the entity remains linked to the device + ent_reg = async_get_er(hass) + entry = ent_reg.async_get(mock_entity_id) + assert entry is not None + assert entry.device_id == device.id + + # Verify the entity has an idle state + mock_state = hass.states.get(mock_entity_id) + assert mock_state is not None + assert mock_state.state == MediaPlayerState.IDLE + + # Unload config entry to clean up + assert await hass.config_entries.async_unload(config_entry_mock.entry_id) + + +async def test_udn_upnp_connection_added_if_missing( + hass: HomeAssistant, + domain_data_mock: Mock, + ssdp_scanner_mock: Mock, + config_entry_mock: MockConfigEntry, + dmr_device_mock: Mock, +) -> None: + """Test missing upnp connection added. + + We did not always add the upnp connection to the device registry, so we need to + check that it is added if missing as otherwise we might end up creating a new + device entry. + """ + config_entry_mock.add_to_hass(hass) + + # Cause connection attempts to fail before adding entity + ent_reg = async_get_er(hass) + entry = ent_reg.async_get_or_create( + MP_DOMAIN, + DOMAIN, + MOCK_DEVICE_UDN, + config_entry=config_entry_mock, + ) + mock_entity_id = entry.entity_id + + dev_reg = async_get_dr(hass) + device = dev_reg.async_get_or_create( + config_entry_id=config_entry_mock.entry_id, + connections={(CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS)}, + identifiers=set(), + ) + + ent_reg.async_update_entity(mock_entity_id, device_id=device.id) + + domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpConnectionError + assert await hass.config_entries.async_setup(config_entry_mock.entry_id) is True + await hass.async_block_till_done() + + mock_state = hass.states.get(mock_entity_id) + assert mock_state is not None + assert mock_state.state == ha_const.STATE_UNAVAILABLE + + # Check hass device information has not been filled in yet + dev_reg = async_get_dr(hass) + device = dev_reg.async_get(device.id) + assert device is not None + assert (CONNECTION_UPNP, MOCK_DEVICE_UDN) in device.connections From 206aaac700bbcab18673f14fed48529e6d867f40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Feb 2024 08:33:21 -0600 Subject: [PATCH 0439/1367] Reduce complexity in the homekit config flow filters (#109850) * Add typing to entity filters * Add typing to entity filters * Add typing to entity filters * Add typing to entity filters * tweaks * tweaks * tweaks * tweaks * tweaks --- .../components/homekit/config_flow.py | 214 +++++++++--------- tests/components/homekit/test_config_flow.py | 24 +- 2 files changed, 125 insertions(+), 113 deletions(-) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index a6984ae2121..d7c8ea65e2d 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -7,7 +7,7 @@ from operator import itemgetter import random import re import string -from typing import Any +from typing import Any, Final, TypedDict import voluptuous as vol @@ -34,12 +34,6 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.entityfilter import ( - CONF_EXCLUDE_DOMAINS, - CONF_EXCLUDE_ENTITIES, - CONF_INCLUDE_DOMAINS, - CONF_INCLUDE_ENTITIES, -) from homeassistant.loader import async_get_integrations from .const import ( @@ -69,13 +63,13 @@ MODE_EXCLUDE = "exclude" INCLUDE_EXCLUDE_MODES = [MODE_EXCLUDE, MODE_INCLUDE] -DOMAINS_NEED_ACCESSORY_MODE = [ +DOMAINS_NEED_ACCESSORY_MODE = { CAMERA_DOMAIN, LOCK_DOMAIN, MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN, -] -NEVER_BRIDGED_DOMAINS = [CAMERA_DOMAIN] +} +NEVER_BRIDGED_DOMAINS = {CAMERA_DOMAIN} CAMERA_ENTITY_PREFIX = f"{CAMERA_DOMAIN}." @@ -124,12 +118,34 @@ DEFAULT_DOMAINS = [ "water_heater", ] -_EMPTY_ENTITY_FILTER: dict[str, list[str]] = { - CONF_INCLUDE_DOMAINS: [], - CONF_EXCLUDE_DOMAINS: [], - CONF_INCLUDE_ENTITIES: [], - CONF_EXCLUDE_ENTITIES: [], -} +CONF_INCLUDE_DOMAINS: Final = "include_domains" +CONF_INCLUDE_ENTITIES: Final = "include_entities" +CONF_EXCLUDE_DOMAINS: Final = "exclude_domains" +CONF_EXCLUDE_ENTITIES: Final = "exclude_entities" + + +class EntityFilterDict(TypedDict, total=False): + """Entity filter dict.""" + + include_domains: list[str] + include_entities: list[str] + exclude_domains: list[str] + exclude_entities: list[str] + + +def _make_entity_filter( + include_domains: list[str] | None = None, + include_entities: list[str] | None = None, + exclude_domains: list[str] | None = None, + exclude_entities: list[str] | None = None, +) -> EntityFilterDict: + """Create a filter dict.""" + return EntityFilterDict( + include_domains=include_domains or [], + include_entities=include_entities or [], + exclude_domains=exclude_domains or [], + exclude_entities=exclude_entities or [], + ) async def _async_domain_names(hass: HomeAssistant, domains: list[str]) -> str: @@ -141,19 +157,18 @@ async def _async_domain_names(hass: HomeAssistant, domains: list[str]) -> str: @callback -def _async_build_entites_filter( +def _async_build_entities_filter( domains: list[str], entities: list[str] -) -> dict[str, Any]: +) -> EntityFilterDict: """Build an entities filter from domains and entities.""" - entity_filter = deepcopy(_EMPTY_ENTITY_FILTER) - entity_filter[CONF_INCLUDE_ENTITIES] = entities # Include all of the domain if there are no entities # explicitly included as the user selected the domain - domains_with_entities_selected = _domains_set_from_entities(entities) - entity_filter[CONF_INCLUDE_DOMAINS] = [ - domain for domain in domains if domain not in domains_with_entities_selected - ] - return entity_filter + return _make_entity_filter( + include_domains=sorted( + set(domains).difference(_domains_set_from_entities(entities)) + ), + include_entities=entities, + ) def _async_cameras_from_entities(entities: list[str]) -> dict[str, str]: @@ -190,13 +205,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Choose specific domains in bridge mode.""" if user_input is not None: - entity_filter = deepcopy(_EMPTY_ENTITY_FILTER) - entity_filter[CONF_INCLUDE_DOMAINS] = user_input[CONF_INCLUDE_DOMAINS] - self.hk_data[CONF_FILTER] = entity_filter + self.hk_data[CONF_FILTER] = _make_entity_filter( + include_domains=user_input[CONF_INCLUDE_DOMAINS] + ) return await self.async_step_pairing() self.hk_data[CONF_HOMEKIT_MODE] = HOMEKIT_MODE_BRIDGE - default_domains = [] if self._async_current_names() else DEFAULT_DOMAINS + default_domains = ( + [] if self._async_current_entries(include_ignore=False) else DEFAULT_DOMAINS + ) name_to_type_map = await _async_name_to_type_map(self.hass) return self.async_show_form( step_id="user", @@ -213,24 +230,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Pairing instructions.""" + hk_data = self.hk_data + if user_input is not None: port = async_find_next_available_port(self.hass, DEFAULT_CONFIG_FLOW_PORT) await self._async_add_entries_for_accessory_mode_entities(port) - self.hk_data[CONF_PORT] = port - include_domains_filter = self.hk_data[CONF_FILTER][CONF_INCLUDE_DOMAINS] - for domain in NEVER_BRIDGED_DOMAINS: - if domain in include_domains_filter: - include_domains_filter.remove(domain) + hk_data[CONF_PORT] = port + conf_filter: EntityFilterDict = hk_data[CONF_FILTER] + conf_filter[CONF_INCLUDE_DOMAINS] = [ + domain + for domain in conf_filter[CONF_INCLUDE_DOMAINS] + if domain not in NEVER_BRIDGED_DOMAINS + ] return self.async_create_entry( - title=f"{self.hk_data[CONF_NAME]}:{self.hk_data[CONF_PORT]}", - data=self.hk_data, + title=f"{hk_data[CONF_NAME]}:{hk_data[CONF_PORT]}", + data=hk_data, ) - self.hk_data[CONF_NAME] = self._async_available_name(SHORT_BRIDGE_NAME) - self.hk_data[CONF_EXCLUDE_ACCESSORY_MODE] = True + hk_data[CONF_NAME] = self._async_available_name(SHORT_BRIDGE_NAME) + hk_data[CONF_EXCLUDE_ACCESSORY_MODE] = True return self.async_show_form( step_id="pairing", - description_placeholders={CONF_NAME: self.hk_data[CONF_NAME]}, + description_placeholders={CONF_NAME: hk_data[CONF_NAME]}, ) async def _async_add_entries_for_accessory_mode_entities( @@ -265,14 +286,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): state = self.hass.states.get(entity_id) assert state is not None name = state.attributes.get(ATTR_FRIENDLY_NAME) or state.entity_id - entity_filter = _EMPTY_ENTITY_FILTER.copy() - entity_filter[CONF_INCLUDE_ENTITIES] = [entity_id] entry_data = { CONF_PORT: port, CONF_NAME: self._async_available_name(name), CONF_HOMEKIT_MODE: HOMEKIT_MODE_ACCESSORY, - CONF_FILTER: entity_filter, + CONF_FILTER: _make_entity_filter(include_entities=[entity_id]), } if entity_id.startswith(CAMERA_ENTITY_PREFIX): entry_data[CONF_ENTITY_CONFIG] = { @@ -360,26 +379,19 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Choose advanced options.""" - if ( - not self.show_advanced_options - or user_input is not None - or self.hk_options[CONF_HOMEKIT_MODE] != HOMEKIT_MODE_BRIDGE - ): + hk_options = self.hk_options + show_advanced_options = self.show_advanced_options + bridge_mode = hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_BRIDGE + + if not show_advanced_options or user_input is not None or not bridge_mode: if user_input: - self.hk_options.update(user_input) - if ( - self.show_advanced_options - and self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_BRIDGE - ): - self.hk_options[CONF_DEVICES] = user_input[CONF_DEVICES] - - for key in (CONF_DOMAINS, CONF_ENTITIES): - if key in self.hk_options: - del self.hk_options[key] - - if CONF_INCLUDE_EXCLUDE_MODE in self.hk_options: - del self.hk_options[CONF_INCLUDE_EXCLUDE_MODE] + hk_options.update(user_input) + if show_advanced_options and bridge_mode: + hk_options[CONF_DEVICES] = user_input[CONF_DEVICES] + hk_options.pop(CONF_DOMAINS, None) + hk_options.pop(CONF_ENTITIES, None) + hk_options.pop(CONF_INCLUDE_EXCLUDE_MODE, None) return self.async_create_entry(title="", data=self.hk_options) all_supported_devices = await _async_get_supported_devices(self.hass) @@ -404,35 +416,37 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Choose camera config.""" + hk_options = self.hk_options + all_entity_config: dict[str, dict[str, Any]] + if user_input is not None: - entity_config = self.hk_options[CONF_ENTITY_CONFIG] + all_entity_config = hk_options[CONF_ENTITY_CONFIG] for entity_id in self.included_cameras: + entity_config = all_entity_config.setdefault(entity_id, {}) + if entity_id in user_input[CONF_CAMERA_COPY]: - entity_config.setdefault(entity_id, {})[ - CONF_VIDEO_CODEC - ] = VIDEO_CODEC_COPY - elif ( - entity_id in entity_config - and CONF_VIDEO_CODEC in entity_config[entity_id] - ): - del entity_config[entity_id][CONF_VIDEO_CODEC] + entity_config[CONF_VIDEO_CODEC] = VIDEO_CODEC_COPY + elif CONF_VIDEO_CODEC in entity_config: + del entity_config[CONF_VIDEO_CODEC] + if entity_id in user_input[CONF_CAMERA_AUDIO]: - entity_config.setdefault(entity_id, {})[CONF_SUPPORT_AUDIO] = True - elif ( - entity_id in entity_config - and CONF_SUPPORT_AUDIO in entity_config[entity_id] - ): - del entity_config[entity_id][CONF_SUPPORT_AUDIO] + entity_config[CONF_SUPPORT_AUDIO] = True + elif CONF_SUPPORT_AUDIO in entity_config: + del entity_config[CONF_SUPPORT_AUDIO] + + if not entity_config: + all_entity_config.pop(entity_id) + return await self.async_step_advanced() cameras_with_audio = [] cameras_with_copy = [] - entity_config = self.hk_options.setdefault(CONF_ENTITY_CONFIG, {}) + all_entity_config = hk_options.setdefault(CONF_ENTITY_CONFIG, {}) for entity in self.included_cameras: - hk_entity_config = entity_config.get(entity, {}) - if hk_entity_config.get(CONF_VIDEO_CODEC) == VIDEO_CODEC_COPY: + entity_config = all_entity_config.get(entity, {}) + if entity_config.get(CONF_VIDEO_CODEC) == VIDEO_CODEC_COPY: cameras_with_copy.append(entity) - if hk_entity_config.get(CONF_SUPPORT_AUDIO): + if entity_config.get(CONF_SUPPORT_AUDIO): cameras_with_audio.append(entity) data_schema = vol.Schema( @@ -453,18 +467,20 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Choose entity for the accessory.""" - domains = self.hk_options[CONF_DOMAINS] + hk_options = self.hk_options + domains = hk_options[CONF_DOMAINS] + entity_filter: EntityFilterDict if user_input is not None: entities = cv.ensure_list(user_input[CONF_ENTITIES]) - entity_filter = _async_build_entites_filter(domains, entities) + entity_filter = _async_build_entities_filter(domains, entities) self.included_cameras = _async_cameras_from_entities(entities) - self.hk_options[CONF_FILTER] = entity_filter + hk_options[CONF_FILTER] = entity_filter if self.included_cameras: return await self.async_step_cameras() return await self.async_step_advanced() - entity_filter = self.hk_options.get(CONF_FILTER, {}) + entity_filter = hk_options.get(CONF_FILTER, {}) entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) all_supported_entities = _async_get_matching_entities( self.hass, domains, include_entity_category=True, include_hidden=True @@ -494,24 +510,21 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Choose entities to include from the domain on the bridge.""" - domains = self.hk_options[CONF_DOMAINS] + hk_options = self.hk_options + domains = hk_options[CONF_DOMAINS] if user_input is not None: entities = cv.ensure_list(user_input[CONF_ENTITIES]) - entity_filter = _async_build_entites_filter(domains, entities) self.included_cameras = _async_cameras_from_entities(entities) - self.hk_options[CONF_FILTER] = entity_filter + hk_options[CONF_FILTER] = _async_build_entities_filter(domains, entities) if self.included_cameras: return await self.async_step_cameras() return await self.async_step_advanced() - entity_filter = self.hk_options.get(CONF_FILTER, {}) + entity_filter: EntityFilterDict = hk_options.get(CONF_FILTER, {}) entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) - all_supported_entities = _async_get_matching_entities( self.hass, domains, include_entity_category=True, include_hidden=True ) - if not entities: - entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, []) # Strip out entities that no longer exist to prevent error in the UI default_value = [ entity_id for entity_id in entities if entity_id in all_supported_entities @@ -535,15 +548,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Choose entities to exclude from the domain on the bridge.""" - domains = self.hk_options[CONF_DOMAINS] + hk_options = self.hk_options + domains = hk_options[CONF_DOMAINS] if user_input is not None: - entity_filter = deepcopy(_EMPTY_ENTITY_FILTER) - entities = cv.ensure_list(user_input[CONF_ENTITIES]) - entity_filter[CONF_INCLUDE_DOMAINS] = domains - entity_filter[CONF_EXCLUDE_ENTITIES] = entities self.included_cameras = {} - if CAMERA_DOMAIN in entity_filter[CONF_INCLUDE_DOMAINS]: + entities = cv.ensure_list(user_input[CONF_ENTITIES]) + if CAMERA_DOMAIN in domains: camera_entities = _async_get_matching_entities( self.hass, [CAMERA_DOMAIN] ) @@ -552,7 +563,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): for entity_id in camera_entities if entity_id not in entities } - self.hk_options[CONF_FILTER] = entity_filter + hk_options[CONF_FILTER] = _make_entity_filter( + include_domains=domains, exclude_entities=entities + ) if self.included_cameras: return await self.async_step_cameras() return await self.async_step_advanced() @@ -600,14 +613,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self.hk_options = deepcopy(dict(self.config_entry.options)) homekit_mode = self.hk_options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE) - entity_filter = self.hk_options.get(CONF_FILTER, {}) + entity_filter: EntityFilterDict = self.hk_options.get(CONF_FILTER, {}) include_exclude_mode = MODE_INCLUDE entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) if homekit_mode != HOMEKIT_MODE_ACCESSORY: include_exclude_mode = MODE_INCLUDE if entities else MODE_EXCLUDE domains = entity_filter.get(CONF_INCLUDE_DOMAINS, []) - include_entities = entity_filter.get(CONF_INCLUDE_ENTITIES) - if include_entities: + if include_entities := entity_filter.get(CONF_INCLUDE_ENTITIES): domains.extend(_domains_set_from_entities(include_entities)) name_to_type_map = await _async_name_to_type_map(self.hass) return self.async_show_form( @@ -708,7 +720,7 @@ def _async_get_entity_ids_for_accessory_mode( def _async_entity_ids_with_accessory_mode(hass: HomeAssistant) -> set[str]: """Return a set of entity ids that have config entries in accessory mode.""" - entity_ids = set() + entity_ids: set[str] = set() current_entries = hass.config_entries.async_entries(DOMAIN) for entry in current_entries: diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 838f72be3c6..6dff9ef896e 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -889,7 +889,7 @@ async def test_options_flow_include_mode_with_cameras( "filter": { "exclude_domains": [], "exclude_entities": [], - "include_domains": ["fan", "vacuum", "climate"], + "include_domains": ["climate", "fan", "vacuum"], "include_entities": ["camera.native_h264", "camera.transcode_h264"], }, "entity_config": {"camera.native_h264": {"video_codec": "copy"}}, @@ -904,15 +904,15 @@ async def test_options_flow_include_mode_with_cameras( assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { - "domains": ["fan", "vacuum", "climate", "camera"], + "domains": ["climate", "fan", "vacuum", "camera"], "mode": "bridge", "include_exclude_mode": "include", } schema = result["data_schema"].schema assert _get_schema_default(schema, "domains") == [ + "climate", "fan", "vacuum", - "climate", "camera", ] assert _get_schema_default(schema, "mode") == "bridge" @@ -921,7 +921,7 @@ async def test_options_flow_include_mode_with_cameras( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "domains": ["fan", "vacuum", "climate", "camera"], + "domains": ["climate", "fan", "vacuum", "camera"], "include_exclude_mode": "exclude", }, ) @@ -959,11 +959,11 @@ async def test_options_flow_include_mode_with_cameras( assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { - "entity_config": {"camera.native_h264": {}}, + "entity_config": {}, "filter": { "exclude_domains": [], "exclude_entities": ["climate.old", "camera.excluded"], - "include_domains": ["fan", "vacuum", "climate", "camera"], + "include_domains": ["climate", "fan", "vacuum", "camera"], "include_entities": [], }, "mode": "bridge", @@ -1025,7 +1025,7 @@ async def test_options_flow_with_camera_audio( "filter": { "exclude_domains": [], "exclude_entities": [], - "include_domains": ["fan", "vacuum", "climate"], + "include_domains": ["climate", "fan", "vacuum"], "include_entities": ["camera.audio", "camera.no_audio"], }, "entity_config": {"camera.audio": {"support_audio": True}}, @@ -1040,15 +1040,15 @@ async def test_options_flow_with_camera_audio( assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { - "domains": ["fan", "vacuum", "climate", "camera"], + "domains": ["climate", "fan", "vacuum", "camera"], "mode": "bridge", "include_exclude_mode": "include", } schema = result["data_schema"].schema assert _get_schema_default(schema, "domains") == [ + "climate", "fan", "vacuum", - "climate", "camera", ] assert _get_schema_default(schema, "mode") == "bridge" @@ -1058,7 +1058,7 @@ async def test_options_flow_with_camera_audio( result["flow_id"], user_input={ "include_exclude_mode": "exclude", - "domains": ["fan", "vacuum", "climate", "camera"], + "domains": ["climate", "fan", "vacuum", "camera"], }, ) @@ -1095,11 +1095,11 @@ async def test_options_flow_with_camera_audio( assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { - "entity_config": {"camera.audio": {}}, + "entity_config": {}, "filter": { "exclude_domains": [], "exclude_entities": ["climate.old", "camera.excluded"], - "include_domains": ["fan", "vacuum", "climate", "camera"], + "include_domains": ["climate", "fan", "vacuum", "camera"], "include_entities": [], }, "mode": "bridge", From 9689cb448db3ab5ce635d899fec3feacb4618875 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Feb 2024 08:42:30 -0600 Subject: [PATCH 0440/1367] Avoid linear search of entity registry in async_extract_referenced_entity_ids (#109667) * Index area_ids in the entity registry I missed that these are used in _resolve_area in search. Eventually we can make async_extract_referenced_entity_ids a bit faster with this as well * Avoid linear search of entity registry in async_extract_referenced_entity_ids needs https://github.com/home-assistant/core/pull/109660 --- homeassistant/helpers/service.py | 43 ++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index d397764c1be..b170026f375 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -496,26 +496,37 @@ def async_extract_referenced_entity_ids( if not selector.area_ids and not selected.referenced_devices: return selected - for ent_entry in ent_reg.entities.values(): + entities = ent_reg.entities + # Add indirectly referenced by area + selected.indirectly_referenced.update( + entry.entity_id + for area_id in selector.area_ids + # The entity's area matches a targeted area + for entry in entities.get_entries_for_area_id(area_id) + # Do not add entities which are hidden or which are config + # or diagnostic entities. + if entry.entity_category is None and entry.hidden_by is None + ) + # Add indirectly referenced by device + selected.indirectly_referenced.update( + entry.entity_id + for device_id in selected.referenced_devices + for entry in entities.get_entries_for_device_id(device_id) # Do not add entities which are hidden or which are config # or diagnostic entities. - if ent_entry.entity_category is not None or ent_entry.hidden_by is not None: - continue - if ( - # The entity's area matches a targeted area - ent_entry.area_id in selector.area_ids - # The entity's device matches a device referenced by an area and the entity - # has no explicitly set area - or ( - not ent_entry.area_id - and ent_entry.device_id in selected.referenced_devices + entry.entity_category is None + and entry.hidden_by is None + and ( + # The entity's device matches a device referenced + # by an area and the entity + # has no explicitly set area + not entry.area_id + # The entity's device matches a targeted device + or device_id in selector.device_ids ) - # The entity's device matches a targeted device - or ent_entry.device_id in selector.device_ids - ): - selected.indirectly_referenced.add(ent_entry.entity_id) - + ) + ) return selected From 14715c150e57192f89169c2840c14407c7a39ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=20Bj=C3=B6rck?= Date: Fri, 9 Feb 2024 16:09:45 +0100 Subject: [PATCH 0441/1367] Bump yalexs to 1.11.1, fixing camera snapshots from Yale Home (#110089) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 97963b19378..eb9d5237585 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.10.0", "yalexs-ble==2.4.1"] + "requirements": ["yalexs==1.11.1", "yalexs-ble==2.4.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index ba6f46f691c..f68003f081c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2883,7 +2883,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.1 # homeassistant.components.august -yalexs==1.10.0 +yalexs==1.11.1 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6c7f59b6b6..9ec18bcdd2b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2209,7 +2209,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.1 # homeassistant.components.august -yalexs==1.10.0 +yalexs==1.11.1 # homeassistant.components.yeelight yeelight==0.7.14 From facf9276268aeda37b24cb6799bda8e6bfb9ec74 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Feb 2024 11:11:05 -0600 Subject: [PATCH 0442/1367] Use async_update_entry in github tests (#110119) needed for #110023 --- tests/components/github/test_config_flow.py | 9 ++++--- tests/components/github/test_diagnostics.py | 5 +++- tests/components/github/test_init.py | 28 +++++++++++++++------ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index 32388fb65d1..67746d891d1 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -260,9 +260,12 @@ async def test_options_flow( mock_setup_entry: None, ) -> None: """Test options flow.""" - mock_config_entry.options = { - CONF_REPOSITORIES: ["homeassistant/core", "homeassistant/architecture"] - } + hass.config_entries.async_update_entry( + mock_config_entry, + options={ + CONF_REPOSITORIES: ["homeassistant/core", "homeassistant/architecture"] + }, + ) mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) diff --git a/tests/components/github/test_diagnostics.py b/tests/components/github/test_diagnostics.py index 4bd7563e743..c358ccc9e6d 100644 --- a/tests/components/github/test_diagnostics.py +++ b/tests/components/github/test_diagnostics.py @@ -25,7 +25,10 @@ async def test_entry_diagnostics( aioclient_mock: AiohttpClientMocker, ) -> None: """Test config entry diagnostics.""" - mock_config_entry.options = {CONF_REPOSITORIES: ["home-assistant/core"]} + hass.config_entries.async_update_entry( + mock_config_entry, + options={CONF_REPOSITORIES: ["home-assistant/core"]}, + ) response_json = json.loads(load_fixture("graphql.json", DOMAIN)) response_json["data"]["repository"]["full_name"] = "home-assistant/core" diff --git a/tests/components/github/test_init.py b/tests/components/github/test_init.py index 612c6579639..f2301056ff8 100644 --- a/tests/components/github/test_init.py +++ b/tests/components/github/test_init.py @@ -21,7 +21,10 @@ async def test_device_registry_cleanup( caplog: pytest.LogCaptureFixture, ) -> None: """Test that we remove untracked repositories from the decvice registry.""" - mock_config_entry.options = {CONF_REPOSITORIES: ["home-assistant/core"]} + hass.config_entries.async_update_entry( + mock_config_entry, + options={CONF_REPOSITORIES: ["home-assistant/core"]}, + ) await setup_github_integration(hass, mock_config_entry, aioclient_mock) devices = dr.async_entries_for_config_entry( @@ -31,7 +34,10 @@ async def test_device_registry_cleanup( assert len(devices) == 1 - mock_config_entry.options = {CONF_REPOSITORIES: []} + hass.config_entries.async_update_entry( + mock_config_entry, + options={CONF_REPOSITORIES: []}, + ) assert await hass.config_entries.async_reload(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -56,8 +62,11 @@ async def test_subscription_setup( aioclient_mock: AiohttpClientMocker, ) -> None: """Test that we setup event subscription.""" - mock_config_entry.options = {CONF_REPOSITORIES: ["home-assistant/core"]} - mock_config_entry.pref_disable_polling = False + hass.config_entries.async_update_entry( + mock_config_entry, + options={CONF_REPOSITORIES: ["home-assistant/core"]}, + pref_disable_polling=False, + ) await setup_github_integration(hass, mock_config_entry, aioclient_mock) assert ( "https://api.github.com/repos/home-assistant/core/events" in x[1] @@ -73,8 +82,11 @@ async def test_subscription_setup_polling_disabled( aioclient_mock: AiohttpClientMocker, ) -> None: """Test that we do not setup event subscription if polling is disabled.""" - mock_config_entry.options = {CONF_REPOSITORIES: ["home-assistant/core"]} - mock_config_entry.pref_disable_polling = True + hass.config_entries.async_update_entry( + mock_config_entry, + options={CONF_REPOSITORIES: ["home-assistant/core"]}, + pref_disable_polling=True, + ) await setup_github_integration(hass, mock_config_entry, aioclient_mock) assert ( "https://api.github.com/repos/home-assistant/core/events" not in x[1] @@ -82,7 +94,9 @@ async def test_subscription_setup_polling_disabled( ) # Prove that we subscribed if the user enabled polling again - mock_config_entry.pref_disable_polling = False + hass.config_entries.async_update_entry( + mock_config_entry, pref_disable_polling=False + ) assert await hass.config_entries.async_reload(mock_config_entry.entry_id) await hass.async_block_till_done() assert ( From f793fbe49230c24c3d9f0b32ccceda839968907c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 9 Feb 2024 19:27:42 +0100 Subject: [PATCH 0443/1367] Update pytest-asyncio to 0.23.5 (#110129) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index f823bf05fb0..049f2f7cbd0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -17,7 +17,7 @@ pydantic==1.10.12 pylint==3.0.3 pylint-per-file-ignores==1.3.2 pipdeptree==2.13.2 -pytest-asyncio==0.23.4 +pytest-asyncio==0.23.5 pytest-aiohttp==1.0.5 pytest-cov==4.1.0 pytest-freezer==0.4.8 From 3ac0833f8c7b6247738b1283a40b0f80b4b9f385 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 9 Feb 2024 20:01:29 +0100 Subject: [PATCH 0444/1367] Update sentry-sdk to 1.40.3 (#110109) --- 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 e5acf81eaae..425225e07ef 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.40.2"] + "requirements": ["sentry-sdk==1.40.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index f68003f081c..fd4381e7e1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2514,7 +2514,7 @@ sensorpro-ble==0.5.3 sensorpush-ble==1.6.2 # homeassistant.components.sentry -sentry-sdk==1.40.2 +sentry-sdk==1.40.3 # homeassistant.components.sfr_box sfrbox-api==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ec18bcdd2b..23c97f849e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1918,7 +1918,7 @@ sensorpro-ble==0.5.3 sensorpush-ble==1.6.2 # homeassistant.components.sentry -sentry-sdk==1.40.2 +sentry-sdk==1.40.3 # homeassistant.components.sfr_box sfrbox-api==0.0.8 From e81a9947e0fbfb3ec5e030f81eee7edf2d7c928d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 9 Feb 2024 20:05:01 +0100 Subject: [PATCH 0445/1367] Fix typo in sensor icons configuration (#110133) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/sensor/icons.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/icons.json b/homeassistant/components/sensor/icons.json index 24245d9bf03..f23826cfe95 100644 --- a/homeassistant/components/sensor/icons.json +++ b/homeassistant/components/sensor/icons.json @@ -117,7 +117,7 @@ "speed": { "default": "mdi:speedometer" }, - "sulfur_dioxide": { + "sulphur_dioxide": { "default": "mdi:molecule" }, "temperature": { From 0954e4cd736f490d6e3693c1b29b6b87488d656d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 9 Feb 2024 23:28:11 +0100 Subject: [PATCH 0446/1367] Add icon translations to GIOS (#110131) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/gios/icons.json | 30 ++++++++++++++++++++++++ homeassistant/components/gios/sensor.py | 8 ------- tests/components/gios/test_sensor.py | 2 +- 3 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/gios/icons.json diff --git a/homeassistant/components/gios/icons.json b/homeassistant/components/gios/icons.json new file mode 100644 index 00000000000..e1d848e276b --- /dev/null +++ b/homeassistant/components/gios/icons.json @@ -0,0 +1,30 @@ +{ + "entity": { + "sensor": { + "aqi": { + "default": "mdi:air-filter" + }, + "c6h6": { + "default": "mdi:molecule" + }, + "co": { + "default": "mdi:molecule" + }, + "no2_index": { + "default": "mdi:molecule" + }, + "o3_index": { + "default": "mdi:molecule" + }, + "pm10_index": { + "default": "mdi:molecule" + }, + "pm25_index": { + "default": "mdi:molecule" + }, + "so2_index": { + "default": "mdi:molecule" + } + } + } +} diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index 99c1775beef..1b13430128f 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -54,7 +54,6 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = ( GiosSensorEntityDescription( key=ATTR_AQI, value=lambda sensors: sensors.aqi.value if sensors.aqi else None, - icon="mdi:air-filter", device_class=SensorDeviceClass.ENUM, options=["very_bad", "bad", "sufficient", "moderate", "good", "very_good"], translation_key="aqi", @@ -63,7 +62,6 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = ( key=ATTR_C6H6, value=lambda sensors: sensors.c6h6.value if sensors.c6h6 else None, suggested_display_precision=0, - icon="mdi:molecule", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, translation_key="c6h6", @@ -72,7 +70,6 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = ( key=ATTR_CO, value=lambda sensors: sensors.co.value if sensors.co else None, suggested_display_precision=0, - icon="mdi:molecule", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, translation_key="co", @@ -89,7 +86,6 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = ( key=ATTR_NO2, subkey="index", value=lambda sensors: sensors.no2.index if sensors.no2 else None, - icon="mdi:molecule", device_class=SensorDeviceClass.ENUM, options=["very_bad", "bad", "sufficient", "moderate", "good", "very_good"], translation_key="no2_index", @@ -106,7 +102,6 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = ( key=ATTR_O3, subkey="index", value=lambda sensors: sensors.o3.index if sensors.o3 else None, - icon="mdi:molecule", device_class=SensorDeviceClass.ENUM, options=["very_bad", "bad", "sufficient", "moderate", "good", "very_good"], translation_key="o3_index", @@ -123,7 +118,6 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = ( key=ATTR_PM10, subkey="index", value=lambda sensors: sensors.pm10.index if sensors.pm10 else None, - icon="mdi:molecule", device_class=SensorDeviceClass.ENUM, options=["very_bad", "bad", "sufficient", "moderate", "good", "very_good"], translation_key="pm10_index", @@ -140,7 +134,6 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = ( key=ATTR_PM25, subkey="index", value=lambda sensors: sensors.pm25.index if sensors.pm25 else None, - icon="mdi:molecule", device_class=SensorDeviceClass.ENUM, options=["very_bad", "bad", "sufficient", "moderate", "good", "very_good"], translation_key="pm25_index", @@ -157,7 +150,6 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = ( key=ATTR_SO2, subkey="index", value=lambda sensors: sensors.so2.index if sensors.so2 else None, - icon="mdi:molecule", device_class=SensorDeviceClass.ENUM, options=["very_bad", "bad", "sufficient", "moderate", "good", "very_good"], translation_key="so2_index", diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index e14b4548d86..7a7a735ff42 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -43,7 +43,7 @@ async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:molecule" + assert state.attributes.get(ATTR_ICON) is None entry = entity_registry.async_get("sensor.home_benzene") assert entry From 57bec29266b56832d6ded9919bdc496823c33265 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 9 Feb 2024 23:28:24 +0100 Subject: [PATCH 0447/1367] Add icon translations to NAM (#110135) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/nam/icons.json | 27 +++++++++++++++++++++++++ homeassistant/components/nam/sensor.py | 7 ------- tests/components/nam/test_sensor.py | 14 ++++++------- 3 files changed, 34 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/nam/icons.json diff --git a/homeassistant/components/nam/icons.json b/homeassistant/components/nam/icons.json new file mode 100644 index 00000000000..5e55bf145e5 --- /dev/null +++ b/homeassistant/components/nam/icons.json @@ -0,0 +1,27 @@ +{ + "entity": { + "sensor": { + "pmsx003_caqi": { + "default": "mdi:air-filter" + }, + "pmsx003_caqi_level": { + "default": "mdi:air-filter" + }, + "sds011_caqi": { + "default": "mdi:air-filter" + }, + "sds011_caqi_level": { + "default": "mdi:air-filter" + }, + "sps30_caqi": { + "default": "mdi:air-filter" + }, + "sps30_caqi_level": { + "default": "mdi:air-filter" + }, + "sps30_pm4": { + "default": "mdi:molecule" + } + } + } +} diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 5b3c6517f64..cd1543affa2 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -180,13 +180,11 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = ( NAMSensorEntityDescription( key=ATTR_PMSX003_CAQI, translation_key="pmsx003_caqi", - icon="mdi:air-filter", value=lambda sensors: sensors.pms_caqi, ), NAMSensorEntityDescription( key=ATTR_PMSX003_CAQI_LEVEL, translation_key="pmsx003_caqi_level", - icon="mdi:air-filter", device_class=SensorDeviceClass.ENUM, options=["very_low", "low", "medium", "high", "very_high"], value=lambda sensors: sensors.pms_caqi_level, @@ -221,13 +219,11 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = ( NAMSensorEntityDescription( key=ATTR_SDS011_CAQI, translation_key="sds011_caqi", - icon="mdi:air-filter", value=lambda sensors: sensors.sds011_caqi, ), NAMSensorEntityDescription( key=ATTR_SDS011_CAQI_LEVEL, translation_key="sds011_caqi_level", - icon="mdi:air-filter", device_class=SensorDeviceClass.ENUM, options=["very_low", "low", "medium", "high", "very_high"], value=lambda sensors: sensors.sds011_caqi_level, @@ -271,13 +267,11 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = ( NAMSensorEntityDescription( key=ATTR_SPS30_CAQI, translation_key="sps30_caqi", - icon="mdi:air-filter", value=lambda sensors: sensors.sps30_caqi, ), NAMSensorEntityDescription( key=ATTR_SPS30_CAQI_LEVEL, translation_key="sps30_caqi_level", - icon="mdi:air-filter", device_class=SensorDeviceClass.ENUM, options=["very_low", "low", "medium", "high", "very_high"], value=lambda sensors: sensors.sps30_caqi_level, @@ -314,7 +308,6 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = ( translation_key="sps30_pm4", suggested_display_precision=0, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:molecule", state_class=SensorStateClass.MEASUREMENT, value=lambda sensors: sensors.sps30_p4, ), diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 50cf3aba659..80eedd5b1a3 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -241,7 +241,7 @@ async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) - "high", "very_high", ] - assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + assert state.attributes.get(ATTR_ICON) is None entry = entity_registry.async_get( "sensor.nettigo_air_monitor_pmsx003_common_air_quality_index_level" @@ -255,7 +255,7 @@ async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) - ) assert state assert state.state == "19" - assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + assert state.attributes.get(ATTR_ICON) is None entry = entity_registry.async_get( "sensor.nettigo_air_monitor_pmsx003_common_air_quality_index" @@ -324,7 +324,7 @@ async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) - ) assert state assert state.state == "19" - assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + assert state.attributes.get(ATTR_ICON) is None entry = entity_registry.async_get( "sensor.nettigo_air_monitor_sds011_common_air_quality_index" @@ -345,7 +345,7 @@ async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) - "high", "very_high", ] - assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + assert state.attributes.get(ATTR_ICON) is None entry = entity_registry.async_get( "sensor.nettigo_air_monitor_sds011_common_air_quality_index_level" @@ -371,7 +371,7 @@ async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) - state = hass.states.get("sensor.nettigo_air_monitor_sps30_common_air_quality_index") assert state assert state.state == "54" - assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + assert state.attributes.get(ATTR_ICON) is None entry = entity_registry.async_get( "sensor.nettigo_air_monitor_sps30_common_air_quality_index" @@ -392,7 +392,7 @@ async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) - "high", "very_high", ] - assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + assert state.attributes.get(ATTR_ICON) is None entry = entity_registry.async_get( "sensor.nettigo_air_monitor_sps30_common_air_quality_index_level" @@ -451,7 +451,7 @@ async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_ICON) == "mdi:molecule" + assert state.attributes.get(ATTR_ICON) is None entry = entity_registry.async_get("sensor.nettigo_air_monitor_sps30_pm4") assert entry From c2818dcb8e36ecedbf0c5c784e33ef075d3ba4cd Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 9 Feb 2024 23:28:56 +0100 Subject: [PATCH 0448/1367] Add icon translations to Tractive (#110138) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- .../components/tractive/device_tracker.py | 1 - homeassistant/components/tractive/icons.json | 58 +++++++++++++++++++ homeassistant/components/tractive/sensor.py | 9 --- homeassistant/components/tractive/switch.py | 3 - 4 files changed, 58 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/tractive/icons.json diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index 00296f3108c..c115a549fd4 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_icon = "mdi:paw" _attr_translation_key = "tracker" def __init__(self, client: TractiveClient, item: Trackables) -> None: diff --git a/homeassistant/components/tractive/icons.json b/homeassistant/components/tractive/icons.json new file mode 100644 index 00000000000..4fc4238d381 --- /dev/null +++ b/homeassistant/components/tractive/icons.json @@ -0,0 +1,58 @@ +{ + "entity": { + "device_tracker": { + "tracker": { + "default": "mdi:paw" + } + }, + "sensor": { + "activity": { + "default": "mdi:run" + }, + "activity_time": { + "default": "mdi:clock-time-eight-outline" + }, + "calories": { + "default": "mdi:fire" + }, + "daily_goal": { + "default": "mdi:flag-checkered" + }, + "minutes_day_sleep": { + "default": "mdi:sleep" + }, + "minutes_night_sleep": { + "default": "mdi:sleep" + }, + "rest_time": { + "default": "mdi:clock-time-eight-outline" + }, + "sleep": { + "default": "mdi:sleep" + }, + "tracker_state": { + "default": "mdi:radar" + } + }, + "switch": { + "tracker_buzzer": { + "default": "mdi:volume-high", + "state": { + "off": "mdi:volume-off" + } + }, + "tracker_led": { + "default": "mdi:led-on", + "state": { + "off": "mdi:led-off" + } + }, + "live_tracking": { + "default": "mdi:map-marker-path", + "state": { + "off": "mdi:map-marker-off" + } + } + } + } +} diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index ab9dad88e06..b563f536e21 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -111,7 +111,6 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( translation_key="tracker_state", signal_prefix=TRACKER_HARDWARE_STATUS_UPDATED, hardware_sensor=True, - icon="mdi:radar", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, options=[ @@ -124,7 +123,6 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( TractiveSensorEntityDescription( key=ATTR_MINUTES_ACTIVE, translation_key="activity_time", - icon="mdi:clock-time-eight-outline", native_unit_of_measurement=UnitOfTime.MINUTES, signal_prefix=TRACKER_ACTIVITY_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, @@ -132,7 +130,6 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( TractiveSensorEntityDescription( key=ATTR_MINUTES_REST, translation_key="rest_time", - icon="mdi:clock-time-eight-outline", native_unit_of_measurement=UnitOfTime.MINUTES, signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, @@ -140,7 +137,6 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( TractiveSensorEntityDescription( key=ATTR_CALORIES, translation_key="calories", - icon="mdi:fire", native_unit_of_measurement="kcal", signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, @@ -148,14 +144,12 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( TractiveSensorEntityDescription( key=ATTR_DAILY_GOAL, translation_key="daily_goal", - icon="mdi:flag-checkered", native_unit_of_measurement=UnitOfTime.MINUTES, signal_prefix=TRACKER_ACTIVITY_STATUS_UPDATED, ), TractiveSensorEntityDescription( key=ATTR_MINUTES_DAY_SLEEP, translation_key="minutes_day_sleep", - icon="mdi:sleep", native_unit_of_measurement=UnitOfTime.MINUTES, signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, @@ -163,7 +157,6 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( TractiveSensorEntityDescription( key=ATTR_MINUTES_NIGHT_SLEEP, translation_key="minutes_night_sleep", - icon="mdi:sleep", native_unit_of_measurement=UnitOfTime.MINUTES, signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, @@ -171,7 +164,6 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( TractiveSensorEntityDescription( key=ATTR_SLEEP_LABEL, translation_key="sleep", - icon="mdi:sleep", signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, value_fn=lambda state: state.lower() if isinstance(state, str) else state, device_class=SensorDeviceClass.ENUM, @@ -184,7 +176,6 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( TractiveSensorEntityDescription( key=ATTR_ACTIVITY_LABEL, translation_key="activity", - icon="mdi:run", signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, value_fn=lambda state: state.lower() if isinstance(state, str) else state, device_class=SensorDeviceClass.ENUM, diff --git a/homeassistant/components/tractive/switch.py b/homeassistant/components/tractive/switch.py index b77c35e6904..4c838e5a468 100644 --- a/homeassistant/components/tractive/switch.py +++ b/homeassistant/components/tractive/switch.py @@ -46,21 +46,18 @@ SWITCH_TYPES: tuple[TractiveSwitchEntityDescription, ...] = ( TractiveSwitchEntityDescription( key=ATTR_BUZZER, translation_key="tracker_buzzer", - icon="mdi:volume-high", method="async_set_buzzer", entity_category=EntityCategory.CONFIG, ), TractiveSwitchEntityDescription( key=ATTR_LED, translation_key="tracker_led", - icon="mdi:led-on", method="async_set_led", entity_category=EntityCategory.CONFIG, ), TractiveSwitchEntityDescription( key=ATTR_LIVE_TRACKING, translation_key="live_tracking", - icon="mdi:map-marker-path", method="async_set_live_tracking", entity_category=EntityCategory.CONFIG, ), From 327e54cbfbde03cf794b8356e3709474b2a88321 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Feb 2024 16:30:42 -0600 Subject: [PATCH 0449/1367] Bump yalexs to 1.11.2 (#110144) changelog: https://github.com/bdraco/yalexs/compare/v1.11.1...v1.11.2 --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index eb9d5237585..a1a7adb4ede 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.11.1", "yalexs-ble==2.4.1"] + "requirements": ["yalexs==1.11.2", "yalexs-ble==2.4.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index fd4381e7e1e..629cf201f76 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2883,7 +2883,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.1 # homeassistant.components.august -yalexs==1.11.1 +yalexs==1.11.2 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 23c97f849e4..cd306a65840 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2209,7 +2209,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.1 # homeassistant.components.august -yalexs==1.11.1 +yalexs==1.11.2 # homeassistant.components.yeelight yeelight==0.7.14 From b0d3cc150fd7a42d9714e3de6de54ba8bc827eae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Feb 2024 20:01:33 -0600 Subject: [PATCH 0450/1367] Use async_update_entry to update esphome options in tests (#110118) --- tests/components/esphome/test_manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index e3b5c2aa08d..9a0a2b08beb 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -91,7 +91,9 @@ async def test_esphome_device_service_calls_allowed( entity_info = [] states = [] user_service = [] - mock_config_entry.options = {CONF_ALLOW_SERVICE_CALLS: True} + hass.config_entries.async_update_entry( + mock_config_entry, options={CONF_ALLOW_SERVICE_CALLS: True} + ) device: MockESPHomeDevice = await mock_esphome_device( mock_client=mock_client, entity_info=entity_info, From 567a179084e0226a45d1db01ffb5d4e2e37f3d8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Feb 2024 20:43:46 -0600 Subject: [PATCH 0451/1367] Simplify emulated_hue exposed entities cache (#109890) Also avoids holding stale States in memory which can prevent garbage collection of linked contexts --- .../components/emulated_hue/config.py | 18 +++++++++--------- .../components/emulated_hue/hue_api.py | 17 +++++------------ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/emulated_hue/config.py b/homeassistant/components/emulated_hue/config.py index 069fc3177d6..2be89e7214c 100644 --- a/homeassistant/components/emulated_hue/config.py +++ b/homeassistant/components/emulated_hue/config.py @@ -207,25 +207,25 @@ class Config: return state.attributes.get(ATTR_EMULATED_HUE_NAME, state.name) # type: ignore[no-any-return] @cache # pylint: disable=method-cache-max-size-none - def get_exposed_states(self) -> list[State]: + def get_exposed_entity_ids(self) -> list[str]: """Return a list of exposed states.""" state_machine = self.hass.states if self.expose_by_default: return [ - state + state.entity_id for state in state_machine.async_all() if self.is_state_exposed(state) ] - states: list[State] = [] - for entity_id in self.entities: - if (state := state_machine.get(entity_id)) and self.is_state_exposed(state): - states.append(state) - return states + return [ + entity_id + for entity_id in self.entities + if (state := state_machine.get(entity_id)) and self.is_state_exposed(state) + ] @callback def _clear_exposed_cache(self, event: EventType[EventStateChangedData]) -> None: - """Clear the cache of exposed states.""" - self.get_exposed_states.cache_clear() + """Clear the cache of exposed entity ids.""" + self.get_exposed_entity_ids.cache_clear() def is_state_exposed(self, state: State) -> bool: """Cache determine if an entity should be exposed on the emulated bridge.""" diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 5da2fcab967..5e2937cae40 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -890,18 +890,11 @@ def create_config_model(config: Config, request: web.Request) -> dict[str, Any]: def create_list_of_entities(config: Config, request: web.Request) -> dict[str, Any]: """Create a list of all entities.""" hass: core.HomeAssistant = request.app["hass"] - - json_response: dict[str, Any] = {} - for cached_state in config.get_exposed_states(): - entity_id = cached_state.entity_id - state = hass.states.get(entity_id) - assert state is not None - - json_response[config.entity_id_to_number(entity_id)] = state_to_json( - config, state - ) - - return json_response + return { + config.entity_id_to_number(entity_id): state_to_json(config, state) + for entity_id in config.get_exposed_entity_ids() + if (state := hass.states.get(entity_id)) + } def hue_brightness_to_hass(value: int) -> int: From f6d4617c7ada0abefa74cd568169d81d77b8ac70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Feb 2024 00:20:15 -0600 Subject: [PATCH 0452/1367] Avoid directly changing config entry state in tests (part 2) (#110115) followup to #110048 for more places --- tests/common.py | 2 +- tests/components/automation/test_blueprint.py | 2 +- tests/components/automation/test_init.py | 2 +- tests/components/device_automation/test_init.py | 16 ++++++++-------- tests/components/powerwall/test_config_flow.py | 2 +- tests/components/script/test_blueprint.py | 2 +- tests/components/script/test_init.py | 2 +- tests/components/websocket_api/test_commands.py | 2 +- tests/components/zha/test_config_flow.py | 6 +++--- tests/components/zha/test_radio_manager.py | 6 +++--- tests/helpers/test_script.py | 2 +- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/common.py b/tests/common.py index 24b9134c32e..149fb057a97 100644 --- a/tests/common.py +++ b/tests/common.py @@ -934,7 +934,7 @@ class MockConfigEntry(config_entries.ConfigEntry): kwargs["state"] = state super().__init__(**kwargs) if reason is not None: - self.reason = reason + object.__setattr__(self, "reason", reason) def add_to_hass(self, hass: HomeAssistant) -> None: """Test helper to add entry to hass.""" diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index 2976886881d..5df33f9b4b8 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -47,7 +47,7 @@ async def test_notify_leaving_zone( ) -> None: """Test notifying leaving a zone blueprint.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device = device_registry.async_get_or_create( diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 3a8c12f735a..e75f41ad36b 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1601,7 +1601,7 @@ async def test_extraction_functions( ) -> None: """Test extraction functions.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) config_entry.add_to_hass(hass) condition_device = device_registry.async_get_or_create( diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 457b7ccbf9b..9a7d54fb690 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -976,7 +976,7 @@ async def test_automation_with_dynamically_validated_action( module.async_validate_action_config = AsyncMock() config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.mock_state(hass, 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, @@ -1078,7 +1078,7 @@ async def test_automation_with_dynamically_validated_condition( module.async_validate_condition_config = AsyncMock() config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.mock_state(hass, 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, @@ -1192,7 +1192,7 @@ async def test_automation_with_dynamically_validated_trigger( module.async_validate_trigger_config = AsyncMock(wraps=lambda hass, config: config) config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.mock_state(hass, 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, @@ -1295,7 +1295,7 @@ async def test_automation_with_bad_action( ) -> None: """Test automation with bad device action.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1329,7 +1329,7 @@ async def test_automation_with_bad_condition_action( ) -> None: """Test automation with bad device action.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1362,7 +1362,7 @@ async def test_automation_with_bad_condition( ) -> None: """Test automation with bad device condition.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1527,7 +1527,7 @@ async def test_automation_with_bad_sub_condition( ) -> None: """Test automation with bad device condition under and/or conditions.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1565,7 +1565,7 @@ async def test_automation_with_bad_trigger( ) -> None: """Test automation with bad device trigger.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index c10d8374bff..2f5ccf8ab80 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -497,7 +497,7 @@ async def test_dhcp_discovery_updates_unique_id_when_entry_is_failed( unique_id="1.2.3.4", ) entry.add_to_hass(hass) - entry.state = config_entries.ConfigEntryState.SETUP_ERROR + entry.mock_state(hass, config_entries.ConfigEntryState.SETUP_ERROR) mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") with patch( diff --git a/tests/components/script/test_blueprint.py b/tests/components/script/test_blueprint.py index b248a3d7650..6c9f17f29b7 100644 --- a/tests/components/script/test_blueprint.py +++ b/tests/components/script/test_blueprint.py @@ -47,7 +47,7 @@ async def test_confirmable_notification( ) -> None: """Test confirmable notification blueprint.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) config_entry.add_to_hass(hass) frodo = device_registry.async_get_or_create( diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 5f8e04d527a..2d21dc924dd 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -718,7 +718,7 @@ async def test_extraction_functions( ) -> None: """Test extraction functions.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_in_both = device_registry.async_get_or_create( diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 9db74b9a857..88fa914f5a7 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -2435,7 +2435,7 @@ async def test_execute_script_with_dynamically_validated_action( ) config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.mock_state(hass, 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, diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 883df4aba94..29a996d4477 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1587,7 +1587,7 @@ async def test_options_flow_defaults( mock_async_unload.assert_called_once_with(entry.entry_id) # Unload it ourselves - entry.state = config_entries.ConfigEntryState.NOT_LOADED + entry.mock_state(hass, config_entries.ConfigEntryState.NOT_LOADED) # Reconfigure ZHA assert result1["step_id"] == "prompt_migrate_or_reconfigure" @@ -1770,7 +1770,7 @@ async def test_options_flow_restarts_running_zha_if_cancelled( flow["flow_id"], user_input={} ) - entry.state = config_entries.ConfigEntryState.NOT_LOADED + entry.mock_state(hass, config_entries.ConfigEntryState.NOT_LOADED) assert result1["step_id"] == "prompt_migrate_or_reconfigure" result2 = await hass.config_entries.options.async_configure( @@ -1825,7 +1825,7 @@ async def test_options_flow_migration_reset_old_adapter( flow["flow_id"], user_input={} ) - entry.state = config_entries.ConfigEntryState.NOT_LOADED + entry.mock_state(hass, config_entries.ConfigEntryState.NOT_LOADED) assert result1["step_id"] == "prompt_migrate_or_reconfigure" result2 = await hass.config_entries.options.async_configure( diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py index 67f2d0164d3..5671c9cd465 100644 --- a/tests/components/zha/test_radio_manager.py +++ b/tests/components/zha/test_radio_manager.py @@ -224,7 +224,7 @@ async def test_migrate_matching_port_config_entry_not_loaded( title="Test", ) config_entry.add_to_hass(hass) - config_entry.state = config_entries.ConfigEntryState.SETUP_IN_PROGRESS + config_entry.mock_state(hass, config_entries.ConfigEntryState.SETUP_IN_PROGRESS) migration_data = { "new_discovery_info": { @@ -284,7 +284,7 @@ async def test_migrate_matching_port_retry( title="Test", ) config_entry.add_to_hass(hass) - config_entry.state = config_entries.ConfigEntryState.SETUP_IN_PROGRESS + config_entry.mock_state(hass, config_entries.ConfigEntryState.SETUP_IN_PROGRESS) migration_data = { "new_discovery_info": { @@ -389,7 +389,7 @@ async def test_migrate_initiate_failure( title="Test", ) config_entry.add_to_hass(hass) - config_entry.state = config_entries.ConfigEntryState.SETUP_IN_PROGRESS + config_entry.mock_state(hass, config_entries.ConfigEntryState.SETUP_IN_PROGRESS) migration_data = { "new_discovery_info": { diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index b2508dc7163..3dfe8fdbad9 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -4633,7 +4633,7 @@ async def test_validate_action_config( """Validate action config.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) config_entry.add_to_hass(hass) mock_device = device_registry.async_get_or_create( From d1f098c11fd3ea89685e7274713f5e8a7adff650 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Feb 2024 00:57:58 -0600 Subject: [PATCH 0453/1367] Use async_update_entry to update dlna_dmr options in tests (#110117) needed for #110023 --- tests/components/dlna_dmr/test_media_player.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index 65670b48ab1..b5898ff91b2 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -5,7 +5,6 @@ import asyncio from collections.abc import AsyncIterable, Mapping from dataclasses import dataclass from datetime import timedelta -from types import MappingProxyType from typing import Any from unittest.mock import ANY, DEFAULT, Mock, patch @@ -215,7 +214,7 @@ async def test_setup_entry_no_options( Check that the device is constructed properly as part of the test. """ - config_entry_mock.options = MappingProxyType({}) + hass.config_entries.async_update_entry(config_entry_mock, options={}) mock_entity_id = await setup_mock_component(hass, config_entry_mock) await async_update_entity(hass, mock_entity_id) await hass.async_block_till_done() @@ -287,12 +286,13 @@ async def test_setup_entry_with_options( Check that the device is constructed properly as part of the test. """ hass.set_state(core_state) - config_entry_mock.options = MappingProxyType( - { + hass.config_entries.async_update_entry( + config_entry_mock, + options={ CONF_LISTEN_PORT: 2222, CONF_CALLBACK_URL_OVERRIDE: "http://198.51.100.10/events", CONF_POLL_AVAILABILITY: True, - } + }, ) mock_entity_id = await setup_mock_component(hass, config_entry_mock) await async_update_entity(hass, mock_entity_id) @@ -2021,10 +2021,11 @@ async def test_poll_availability( """Test device becomes available and noticed via poll_availability.""" # Start with a disconnected device and poll_availability=True domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpConnectionError - config_entry_mock.options = MappingProxyType( - { + hass.config_entries.async_update_entry( + config_entry_mock, + options={ CONF_POLL_AVAILABILITY: True, - } + }, ) mock_entity_id = await setup_mock_component(hass, config_entry_mock) mock_state = hass.states.get(mock_entity_id) From a2f4e99994037038779802abe66d96e594d00ee6 Mon Sep 17 00:00:00 2001 From: Piotr Machowski <6118709+PiotrMachowski@users.noreply.github.com> Date: Sat, 10 Feb 2024 10:47:56 +0100 Subject: [PATCH 0454/1367] Add state_translated function to jinja templates (#96906) * Add state_translated jinja function * Add tests for load_state_translations_to_cache and get_cached_translations * Cleanup state_translated template * Add tests for state_translated jinja function * Apply black formatting * Improve code quality * Apply suggestions from code review Co-authored-by: Erik Montnemery * Apply suggestions from code review * Prevent invalid components from loading translations * Refactor loading translations to cache * Adjust code issues * Update homeassistant/helpers/translation.py Co-authored-by: Erik Montnemery * Refactor listeners that trigger translation loading * Apply suggestions from code review Co-authored-by: Erik Montnemery * Apply suggestions from code review * Adjust invalid function calls, fix code styling * Adjust code quality * Extract async_translate_state function * Apply suggestions from code review Co-authored-by: Erik Montnemery * Apply suggestions from code review * Fix tests * Fix tests --------- Co-authored-by: Piotr Machowski Co-authored-by: Erik Montnemery --- homeassistant/bootstrap.py | 2 + homeassistant/helpers/template.py | 38 ++++- homeassistant/helpers/translation.py | 183 ++++++++++++++++++++-- tests/common.py | 6 + tests/helpers/test_template.py | 124 +++++++++++++++ tests/helpers/test_translation.py | 217 ++++++++++++++++++++++++++- 6 files changed, 555 insertions(+), 15 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 985108fb9f8..83aa8cb893d 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -35,6 +35,7 @@ from .helpers import ( recorder, restore_state, template, + translation, ) from .helpers.dispatcher import async_dispatcher_send from .helpers.typing import ConfigType @@ -291,6 +292,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None: platform.uname().processor # pylint: disable=expression-not-assigned # Load the registries and cache the result of platform.uname().processor + translation.async_setup(hass) entity.async_setup(hass) template.async_setup(hass) await asyncio.gather( diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 63c24b0f9e9..86e3385a21b 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -80,6 +80,7 @@ from homeassistant.util.thread import ThreadWithException from . import area_registry, device_registry, entity_registry, location as loc_helper from .singleton import singleton +from .translation import async_translate_state from .typing import TemplateVarsType # mypy: allow-untyped-defs, no-check-untyped-defs @@ -894,6 +895,36 @@ class AllStates: return "