From 69416e7b76cec68eb3687310baf78a054967cc14 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 31 Jan 2024 20:45:31 +0100 Subject: [PATCH 001/334] Bump version to 2024.2.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8db9be36902..97f16528317 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 462d3d326d5..106a718ee8d 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.2.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From bbe4483b4a89d22370a1269e5a715e0fdd374f04 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 1 Feb 2024 02:08:32 -0600 Subject: [PATCH 002/334] 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 290a8ef9050..2f3c5100138 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 c611169af69..3e620ad8b3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1863,7 +1863,7 @@ rflink==0.0.65 ring-doorbell[listen]==0.8.5 # homeassistant.components.roku -rokuecp==0.18.1 +rokuecp==0.19.0 # homeassistant.components.romy romy==0.0.7 From ddc1c4bb27718419c2efe2f7a337ea85baa26002 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 1 Feb 2024 06:52:58 +1000 Subject: [PATCH 003/334] 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 16ad2728a6e722e37bfc58ffa506002357911b17 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 004/334] 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 f76689fb751b0df44b300b9dc6b258c03ac3071c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 1 Feb 2024 03:38:16 +0100 Subject: [PATCH 005/334] 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 ada37f558ce979cd7826f61c22bd5c8bb3a640a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 31 Jan 2024 09:47:31 -1000 Subject: [PATCH 006/334] 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 2f3c5100138..49f0494e76f 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 3e620ad8b3f..471dfa974a0 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 3f619a8022a77d13b0f9abc162df69c1b0564fab 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 007/334] 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 133b68a68df94191446dbccb42cca4fa9eeec40f 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 008/334] 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 70f0d77ba5e17e2ad53619650d80ae2ebe981f0a Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Thu, 1 Feb 2024 09:04:02 +0100 Subject: [PATCH 009/334] 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 29f4c2d5136bd2398889d9b7b7cb7038a8c54dd8 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 1 Feb 2024 08:26:39 +0100 Subject: [PATCH 010/334] 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 e34ebcb1955a8060d3af4f3463d8fa9e153c4152 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 31 Jan 2024 21:56:57 -1000 Subject: [PATCH 011/334] 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 0b6df23ee5924c2a99b242dd5f402134f118d651 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 31 Jan 2024 21:53:18 -1000 Subject: [PATCH 012/334] 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 1353c1d24cede11934e881d3ab739a043ab5cd8a Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 1 Feb 2024 11:14:33 +0100 Subject: [PATCH 013/334] 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 fc6cc45ee2c3c768925ef1ab30cf5927aa4edb84 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 1 Feb 2024 08:52:07 +0100 Subject: [PATCH 014/334] 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 74ce778691ea28a41431c64dac020bc83f4ff194 Mon Sep 17 00:00:00 2001 From: Luis Andrade Date: Thu, 1 Feb 2024 03:00:22 -0500 Subject: [PATCH 015/334] 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 0070d2171fe9255eb1d5d3c45be8c407eea98d4e 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 016/334] 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 e2bbdda0167e0b2119bfeaa79ee1303503a331e2 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 1 Feb 2024 09:30:29 +0100 Subject: [PATCH 017/334] 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 e4fc35c5631412fa8ad8a144fe4e756f56d5e8d0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 1 Feb 2024 10:20:52 +0100 Subject: [PATCH 018/334] 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 403c2d84404aad089531b9452f0b63ff52e47d68 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 019/334] 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 49f0494e76f..4b5bcad5d76 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 471dfa974a0..390bce7559d 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 c98228110a8d3d366d0f46265299b17717a64cb0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 1 Feb 2024 12:19:53 +0100 Subject: [PATCH 020/334] Bump version to 2024.2.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 97f16528317..56a6690320f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 106a718ee8d..c13f67bb130 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.0b0" +version = "2024.2.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 52a8216150fb0de9e861b11537a4bb54dfb072fb 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 021/334] 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 50dfe4dec0b32d878bec730531218fdd374bb858 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 2 Feb 2024 02:08:41 +1000 Subject: [PATCH 022/334] 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 647ac10dd9a0ca3407506e85f5a291418db2246e Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 2 Feb 2024 02:09:24 +1000 Subject: [PATCH 023/334] 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 c31dfd6d00d119bd5bd88df92305f5dccf1d31d9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 1 Feb 2024 16:53:53 +0100 Subject: [PATCH 024/334] 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 77b25553e3bb7d74434f1cb70f661fdfb8084b7d Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 1 Feb 2024 14:10:24 -0600 Subject: [PATCH 025/334] 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 c9119935213..73c177a3150 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 @@ -259,7 +252,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, ) @@ -273,9 +266,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, @@ -325,7 +316,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: @@ -333,7 +324,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, ) @@ -795,7 +786,7 @@ class DefaultAgent(AbstractConversationAgent): def _get_error_text( self, - response_type: ResponseType, + error_key: ErrorKey, lang_intents: LanguageIntents | None, **response_args, ) -> str: @@ -803,7 +794,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 ) @@ -916,59 +907,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 4b5bcad5d76..949c35cc3f6 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 390bce7559d..0ce1d0a33dc 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 a8b39ce332f0ea3373ab7b1105343ceb1997eddb 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 026/334] 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 b464e77112c32ea81743994cc10b05803a05a4e2 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 027/334] 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 949c35cc3f6..f2e2d6dc011 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 0ce1d0a33dc..9ead917d8f6 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 6aba79d7b9eea7456771554a27246cb44f8ec3af Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 1 Feb 2024 17:07:55 +0100 Subject: [PATCH 028/334] 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 faf2a90cd13c55c5ec67c0269f3943441c7e313a 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 029/334] 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 f2e2d6dc011..09793f1e3fe 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 9ead917d8f6..3228c579ea5 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 ca539630a6cf2ba8b55f37f975f72536352ca615 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 1 Feb 2024 17:07:08 +0100 Subject: [PATCH 030/334] 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 a535bda8215b5478daa21590c620c1af1560c094 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Feb 2024 12:34:23 -0600 Subject: [PATCH 031/334] 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 0015af0b3c2448b9946e27f79efc7311aae187c4 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 1 Feb 2024 13:40:29 -0600 Subject: [PATCH 032/334] 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 73c177a3150..a2cb3b68041 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -231,7 +231,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 @@ -239,7 +242,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 3d80c4f7f6e4a14fa22fc62f786b17549be0d253 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 1 Feb 2024 22:13:41 +0100 Subject: [PATCH 033/334] 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 15a1a4bfdfabb6af97a90f9bc3f8fbe24cef57f6 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 034/334] 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 fe4ad30ade1758760a35730bb560b78acceafc24 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 1 Feb 2024 22:28:02 +0100 Subject: [PATCH 035/334] 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 f77bd13cc03474860e52e01862a6536a0d86ee3d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 1 Feb 2024 22:29:58 +0100 Subject: [PATCH 036/334] Bump version to 2024.2.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 56a6690320f..84afe7ce1b2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index c13f67bb130..24a3d4a9d35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.0b1" +version = "2024.2.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 66d802b5e50952dc0a68dedf672aed97221c69ce Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Fri, 2 Feb 2024 10:37:49 +0100 Subject: [PATCH 037/334] 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 3e7dc3588d0650b98f02737bc72865393725a382 Mon Sep 17 00:00:00 2001 From: mkmer Date: Fri, 2 Feb 2024 11:31:16 -0500 Subject: [PATCH 038/334] 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 41ad3d898739ff2e4f8dc262cf996c6e6ae1f8b8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:32:50 +0100 Subject: [PATCH 039/334] 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 a2b6b0a0bcd96b9498506cb8badf0c7bd64610ad Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:33:54 +0100 Subject: [PATCH 040/334] 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 bd5bc6b83de66c6fa5e2a0eb680465b7765a0761 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 07:39:54 +0100 Subject: [PATCH 041/334] 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 8bb98b4146988bfbe67801c36b16c38a41e55430 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:31:35 +0100 Subject: [PATCH 042/334] 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 550c0bf3c329c5b1f631184feaef7ed5f24a9cd6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:33:13 +0100 Subject: [PATCH 043/334] 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 8ed0af2fb7d57c39612dd22a885dbf27692e2049 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:48:01 +0100 Subject: [PATCH 044/334] 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 e720e82fd6cb86cac1d167ebd6141fe100e3d5ef Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:31:09 +0100 Subject: [PATCH 045/334] 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 5a4f88349a9f802505502699951a749ca2951312 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Feb 2024 15:39:44 -0600 Subject: [PATCH 046/334] 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 0c82e0a6181451bb83caa325a904ca098280a4ba Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 2 Feb 2024 09:46:53 +0100 Subject: [PATCH 047/334] 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 7467d588c85b0ee1f379a38a69f37f0a835ab649 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:01:38 +0100 Subject: [PATCH 048/334] 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 05c0973937b9f9590abc9aa6c2eb00808181f313 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 09:02:41 +0100 Subject: [PATCH 049/334] 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 12e307789554193f19370b0cbeab03fca484a999 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Feb 2024 02:32:17 -0600 Subject: [PATCH 050/334] 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 1383a0c13ad84834c25abfb2ca313b8c91e78c5e 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 051/334] 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 7a7bcf1a92e3740dcd130cd2861d5313b0e644f7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Feb 2024 10:37:04 +0100 Subject: [PATCH 052/334] 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 24a3d4a9d35..069ca3564ca 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 e91e67b40006eeadfe9b4335f14919d8f5287762 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 2 Feb 2024 11:02:00 +0100 Subject: [PATCH 053/334] 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 09793f1e3fe..bef0b762232 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 3228c579ea5..ba5383086b5 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 92a3edc53638d6a2ed7fc90260f612b132019bb6 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 054/334] 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 6afc6ca126ff69992d59c7f0665833debae445bd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Feb 2024 11:43:05 +0100 Subject: [PATCH 055/334] 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 57a43ef1514a4ce29a765ef1d801fe15378da5c0 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 2 Feb 2024 14:12:26 +0100 Subject: [PATCH 056/334] 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 3c90e3d83f6d830c9604690f37536a13ed68900c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 Feb 2024 17:30:07 +0100 Subject: [PATCH 057/334] 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 bef0b762232..4f5e26d5423 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 ba5383086b5..68b56339127 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 f18f161efa021ea21190288b5165e039bff19d3f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Feb 2024 17:36:51 +0100 Subject: [PATCH 058/334] Bump version to 2024.2.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 84afe7ce1b2..4f720539b77 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 069ca3564ca..68b4d47d662 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.0b2" +version = "2024.2.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 4735aadaa8659f577547d920510dc9d12cc226e6 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 059/334] 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 39b4bd032dc..f5a7cfe182a 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -658,41 +658,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 b1bf69689e6d8cd84fa0865410b0ec0825d3cb6d Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 3 Feb 2024 02:20:10 -0600 Subject: [PATCH 060/334] 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 a57c30be88208a719791dbdce97eb162f30f82e5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Feb 2024 20:34:00 +0100 Subject: [PATCH 061/334] 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 4f5e26d5423..e826b1581da 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 68b56339127..3fa1984ce1f 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 07a1ee0baa46fa678a186a43bea4218ee158591d 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 062/334] 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 4a60d362163d803826e077185250d29140eb1dae Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 2 Feb 2024 20:26:44 -0600 Subject: [PATCH 063/334] 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 43f8731f8ad34cfd9b99e7e2a2d720e4f655c562 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 2 Feb 2024 21:24:24 +0100 Subject: [PATCH 064/334] 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 e826b1581da..a72e21147d4 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 3fa1984ce1f..2135ee94889 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 f3e8360949d165e0700707376481deff8924d4e0 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 2 Feb 2024 23:04:41 +0100 Subject: [PATCH 065/334] 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 a72e21147d4..05fb9f3a03f 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 2135ee94889..27f7eb2bad6 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 9c599f751397859bfa6049478b0c0cedcd3411b4 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 066/334] 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 404e30911b793acb0f550de6ec6a24cac3406661 Mon Sep 17 00:00:00 2001 From: Jurriaan Pruis Date: Sat, 3 Feb 2024 12:31:48 +0100 Subject: [PATCH 067/334] 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 05fb9f3a03f..75648545bd6 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 27f7eb2bad6..c2cc2c9af84 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 24b8e6097856c15cac6fbeeddebe060d3ecc042e Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 2 Feb 2024 22:42:10 +0100 Subject: [PATCH 068/334] 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 75648545bd6..00b453a9b16 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 c2cc2c9af84..d84526af85f 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 87a1482e4d743213d4e6d7d6b6d2cff673910022 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Sat, 3 Feb 2024 05:14:33 -0600 Subject: [PATCH 069/334] 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 ea0a11ae657..75d28a4e732 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.1", "home-assistant-intents==2024.2.1"] } 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 950cae8b322..d8cc1989904 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.1 diff --git a/requirements_all.txt b/requirements_all.txt index 00b453a9b16..83261dedbb4 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 d84526af85f..908edcdeee5 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 1c960d300dca1dd3869728626c41a45896b2db05 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 2 Feb 2024 19:13:17 -0600 Subject: [PATCH 070/334] 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 75d28a4e732..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.1", "home-assistant-intents==2024.2.1"] + "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.2.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d8cc1989904..7746745da6b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ hass-nabucasa==0.76.0 hassil==1.6.1 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 83261dedbb4..207e909fd60 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 908edcdeee5..fd2a866437f 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 2be71d53c55888c97822aa1bdc056dd979aed0cc Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 3 Feb 2024 06:39:35 +0000 Subject: [PATCH 071/334] 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 207e909fd60..a9dbd9b441b 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 fd2a866437f..2782bf09e16 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 14be7e4a7269327f16084c688b5351b78dc43605 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:41:25 +0100 Subject: [PATCH 072/334] 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 cd884de79e1d56363d73ef6d8d25571756c064fb Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:47:09 +0100 Subject: [PATCH 073/334] 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 f01f033b3f3abcca828e534c2c8e30280be58ead Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:11:19 -0500 Subject: [PATCH 074/334] 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 4a1a5b9e87c151c9193a599b092be2d380a51fbd Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:56:57 +0100 Subject: [PATCH 075/334] 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 66b6f81996ce69151fdd6119299266eb81d77d3f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:08:58 -0500 Subject: [PATCH 076/334] 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 966798b588e72ea85c4763648473c37e5498968b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:30:53 +0100 Subject: [PATCH 077/334] 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 f8fde71ef3fc9e721eb1804bc6aab856a9ae93c4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 04:02:19 +0100 Subject: [PATCH 078/334] 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 18a7aa20b46a76d3ee6f244b9d8e46fdc198cabb Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:06:53 -0500 Subject: [PATCH 079/334] 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 d71dd12263c708a367f0a636ca30fa2da31722dc Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:46:14 +0100 Subject: [PATCH 080/334] 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 183af926587dbc4d5f547926a6fd9723b48b5d7f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:11:39 -0500 Subject: [PATCH 081/334] 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 f1b041afbe17e7a1ba089d2d05d060b546569ffc Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 00:42:10 -0500 Subject: [PATCH 082/334] 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 c571f36c6c49f771afa6616e80f2e60ec305e4ac Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:13:20 -0500 Subject: [PATCH 083/334] 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 6c4b773bc1b39ac9ab9b38092441c02c29bdad5d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:32:47 -0500 Subject: [PATCH 084/334] 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 e99a58ad53481442281d32a3b5ad636ed5a5297c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:43:32 +0100 Subject: [PATCH 085/334] 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 0c0be6d6a1919307e14a0212d009a66aa25d40fe Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:46:27 +0100 Subject: [PATCH 086/334] 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 2a5bb66c0618184fad708883594ce8d6cb8832b5 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:33:42 -0500 Subject: [PATCH 087/334] 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 f2a9ef65917a6ce10b2b3b7ad2dcf14a003eebb6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:33:55 -0500 Subject: [PATCH 088/334] 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 63ad3ebdf422c9fe4c23bb763af35a53dac11912 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sat, 3 Feb 2024 07:50:33 +0100 Subject: [PATCH 089/334] 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 a5646e0df2484541e1aeea82931ac4172d7e99c2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:33:03 -0500 Subject: [PATCH 090/334] 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 57279b1c7bdcb485e152b7dfd7ec7697b3ac8ddf Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:13:52 -0500 Subject: [PATCH 091/334] 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 6c84e8dff0d362f76471f4d66a59e07620646f19 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:36:19 -0500 Subject: [PATCH 092/334] 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 a671d0bc6cd38d3ba77365c7fa3ad217aab8b1ec Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:40:50 -0500 Subject: [PATCH 093/334] 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 d04282a41ccbc3f8f961fcb6f46fec57dfb756be Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:37:14 -0500 Subject: [PATCH 094/334] 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 b6226acd2b88e3b6aeba9402b4a59b9d5cf4ed68 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:06:55 +0100 Subject: [PATCH 095/334] 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 13decb9b10c404f6dccffeda79a9909910502929 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:35:16 -0500 Subject: [PATCH 096/334] 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 94464f220c9dfbd592001e8518ea27d0f61bce2c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:36:43 -0500 Subject: [PATCH 097/334] 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 13fc69d8a8516d37ae7f36f4a5646ce0d8087092 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:37:49 -0500 Subject: [PATCH 098/334] 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 36bba95fd0d7014e465a4ff7a9dba492161a2f21 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:38:40 -0500 Subject: [PATCH 099/334] 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 72ffdf4f4b289a8a73d9affc81133f58c66ac39d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:39:18 -0500 Subject: [PATCH 100/334] 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 776e2da4e6f229aa9d88141e6ea38ed28c85ee65 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:39:37 -0500 Subject: [PATCH 101/334] 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 3181358484b7c94d4b6b188fcab7100be52feaae Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:40:17 -0500 Subject: [PATCH 102/334] 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 db91a40b556deda7b531cd5f1f7c263005318400 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:41:08 -0500 Subject: [PATCH 103/334] 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 39df394414cd39abd12e306304af4c4693490065 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:41:28 -0500 Subject: [PATCH 104/334] 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 cba67d15255c8b2d0e648c6b88aa23513a305a1c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:42:37 -0500 Subject: [PATCH 105/334] 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 8cfe3821da7844190c3330e51cfff09f2c286b58 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:44:04 -0500 Subject: [PATCH 106/334] 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 fecdfbfb9f7f0b16fb65cdc7ff3058795075c678 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:45:06 -0500 Subject: [PATCH 107/334] 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 5991b06574c8f54be915c5f41d793aa41a5c0249 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:51:38 -0500 Subject: [PATCH 108/334] 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 5843c93371cd14bf9a2691d66d6a1dda5af4aed2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 2 Feb 2024 22:54:01 -0500 Subject: [PATCH 109/334] 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 e98da8596a855fa83aba8c1e15cab9afa6d54089 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 06:25:52 -0500 Subject: [PATCH 110/334] 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 a77bcccbbb8ed0aab1fcd7cc344e6c3ccda7b25c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:06:21 -0500 Subject: [PATCH 111/334] 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 f3f69a810743b88b35595c223d2d3bba07c02e99 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:50:24 +0100 Subject: [PATCH 112/334] 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 1c0a6970e25e869da0c5cdfc8dbb6d2bc616afa6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:05:46 -0500 Subject: [PATCH 113/334] 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 2ac4bb8e9f4d7a2f7222322c8a3744094da71e46 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 05:18:40 -0500 Subject: [PATCH 114/334] 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 490101fa92740a22368c31e49fbaace8f9673f7d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:05:33 -0500 Subject: [PATCH 115/334] 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 49445c46a04b832f5a42b94d5baa2fd6b61bdf9c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:05:16 -0500 Subject: [PATCH 116/334] 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 bb8a74a3f43d7991d510ef4e7d8119b4ab676ef7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 02:03:38 -0500 Subject: [PATCH 117/334] 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 650ab704440153975c73ca65e1eae82d2b6b3cca Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:26:17 +0100 Subject: [PATCH 118/334] 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 3bf5fa9302a43a391aa4f72ee29f74c176f80f72 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:03:14 -0500 Subject: [PATCH 119/334] 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 463320c8eed6e4e66c9666d9dde176b6eae63d26 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:03:34 -0500 Subject: [PATCH 120/334] 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 92ebc5b43668fd83ee425966e9543431cae0756f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:03:44 -0500 Subject: [PATCH 121/334] 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 5cce878b859b6172d8b89a43a3b0ae951698e9c4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 03:26:04 +0100 Subject: [PATCH 122/334] 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 4b8cb35ba0083727cd606932bf15dffebe14322c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:03:59 -0500 Subject: [PATCH 123/334] 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 150fb151fa978c3b9a17748ee15c8b5ab2d97d85 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 05:18:00 -0500 Subject: [PATCH 124/334] 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 2724b115daca23be28c612e86a9450ad45e8b12f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:04:11 -0500 Subject: [PATCH 125/334] 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 cc8e9ac1414abf603ce74bcdfe32f1e73d434489 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 01:04:26 -0500 Subject: [PATCH 126/334] 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 93b7ffa807ca84892aa7dd9a77729902448dbbc8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 05:07:56 -0500 Subject: [PATCH 127/334] 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 280d7ef4ec06097908385d859ba049e375d4ca39 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 05:17:20 -0500 Subject: [PATCH 128/334] 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 e6300274559bb81f273265125d7c4d22e9735789 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 129/334] 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 cb03a6e29b8e7514d9c9a48801deaf2caac0f33b 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 130/334] 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 21186272bb6..2b97365b555 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6168,7 +6168,7 @@ "traccar": { "integration_type": "hub", "config_flow": true, - "iot_class": "local_polling", + "iot_class": "cloud_push", "name": "Traccar Client" }, "traccar_server": { From 0a627aed6d5734e4cab624ff43fe997d8eb9cd9c Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sat, 3 Feb 2024 11:51:23 +0100 Subject: [PATCH 131/334] 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 ef6fed506794acde85096998d3b967b5a8bc5256 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 3 Feb 2024 14:42:00 +0100 Subject: [PATCH 132/334] Bump version to 2024.2.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4f720539b77..2cbb5565ef0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 68b4d47d662..d75d8efdc08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.0b3" +version = "2024.2.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 4b2adab24dadb1e14b54ab1e6888d956f63b75e6 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 133/334] 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 5f014f42ac93f7bf8cd20525201f1b60a42b0d49 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 134/334] 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 3e2f97d105f9cfacdc276d13e474f2097c05785b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 18:47:05 -0500 Subject: [PATCH 135/334] 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 38288dd68e2b282a298d06eeee0aa13e962d0172 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:23:06 -0500 Subject: [PATCH 136/334] 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 f2de666c547708af430043faf38be2daef97d690 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 08:57:26 -0500 Subject: [PATCH 137/334] 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 af07ac120ea197db05527c9f86974cebae8dea2b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 05:40:37 -0500 Subject: [PATCH 138/334] 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 ac2e05b5c037dbd09bcef3f1008464261648c9f9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 05:39:04 -0500 Subject: [PATCH 139/334] 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 97446a5af3c7914d4ab40dddc3286f167f333c56 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 08:59:34 -0500 Subject: [PATCH 140/334] 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 e7203d6015eecc823ef20490dc61cf3cfc623f01 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 09:00:31 -0500 Subject: [PATCH 141/334] 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 855edba3a2258331e52c0c51ca33d2e413ba2b47 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 05:37:52 -0500 Subject: [PATCH 142/334] 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 500b0a9b52bad0aaaaeb9fcbe13a856c86526e24 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 4 Feb 2024 15:01:06 +0100 Subject: [PATCH 143/334] 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 1aba934aba4..9a0ecbeb9a5 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 856780ed30fdc3ed64c7c4c2ee79faf406f94284 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sat, 3 Feb 2024 20:20:17 +0100 Subject: [PATCH 144/334] 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 a9dbd9b441b..6fbd5fd0c2e 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 2782bf09e16..a436b6379c0 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 ceeef1eaccb256f8441f14ed6bda08175ce17d22 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Sun, 4 Feb 2024 11:30:30 +0100 Subject: [PATCH 145/334] 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 d99ba75ed8269caf3c5b371988b5ab9ab04747ba Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 3 Feb 2024 18:30:00 -0500 Subject: [PATCH 146/334] 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 1df5ad23efcbe6547edb8fdd5323b3b5c5f46a5b 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 147/334] 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 f16c0bd559fef9fc0736bd076ef7525fee1a5487 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 19:18:25 -0500 Subject: [PATCH 148/334] 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 9ebf985010e7edad9bdf8086c4cefaef332f2942 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:28:10 -0500 Subject: [PATCH 149/334] 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 482032cb8706ae637f2066fe6973dac0c643a369 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:22:43 -0500 Subject: [PATCH 150/334] 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 79846d5668e6074ebf29f50388987df8a9913da0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:23:23 -0500 Subject: [PATCH 151/334] 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 a62c05b983d93dc9d1c6d1aae739ae07cb6a721b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:26:53 -0500 Subject: [PATCH 152/334] 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 3a08e3bec691fbca5fb887a024b508f713649527 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:27:04 -0500 Subject: [PATCH 153/334] 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 c02e96c5c0a8fd23310c9f69a05221994aebce83 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:28:49 -0500 Subject: [PATCH 154/334] 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 8c0cd6bbabccf1f5bafbb64962dce5dc8e1648a4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:30:09 -0500 Subject: [PATCH 155/334] 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 14ad2e91f3e80ffff72aa2c0b60b1ffd4ac39a52 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:31:23 -0500 Subject: [PATCH 156/334] 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 12e32fb7998a640ef8de8c63077831ce9c8b765b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 18:46:47 -0500 Subject: [PATCH 157/334] 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 439f1766a0b36a4ea7cac3fae9abe0762359b4a3 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 21:29:29 -0500 Subject: [PATCH 158/334] 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 9cde864224a4ac3fd9a6eac22c8d9a7fb71267c0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:24:28 -0500 Subject: [PATCH 159/334] 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 38fcc88c574fb97d3c96d2035c55501dfe736c52 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:24:18 -0500 Subject: [PATCH 160/334] 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 46016004fa7339efd7d102fac71d9f02198a531b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:32:35 -0500 Subject: [PATCH 161/334] 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 384070c15857ee1a05d3249974741cd830994fe1 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:38:09 -0500 Subject: [PATCH 162/334] 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 dafdcd369c6bc6f9d10cc018727004d658d82565 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:23:59 -0500 Subject: [PATCH 163/334] 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 67362db547e6a9ca267f71dc91d74176032f35c3 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:26:44 -0500 Subject: [PATCH 164/334] 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 e1699b4d6503115cb7441ec371778bdb1205b429 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:29:56 -0500 Subject: [PATCH 165/334] 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 bf4bc9d935d94a4e70516cc4a1ec21d451835022 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:27:30 -0500 Subject: [PATCH 166/334] 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 4d7abbf8c547abee51f98e800cc186a76cad91a0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:27:42 -0500 Subject: [PATCH 167/334] 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 75c0c7bda0515ae7f2da76beef54863ca64d19dc Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:40:02 -0500 Subject: [PATCH 168/334] 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 83c487a3192292daa56bfa8314084851521b1b41 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:28:45 -0500 Subject: [PATCH 169/334] 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 32b25c7e538b5ccb81a665b648eeba2029f6dfec Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:41:07 -0500 Subject: [PATCH 170/334] 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 122652b3967a600a81ad5e03f1ecdeab0a76a81f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:28:33 -0500 Subject: [PATCH 171/334] 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 064f412da44017590a3cbb8cfc0c881a0ceb3705 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:29:40 -0500 Subject: [PATCH 172/334] 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 42bf086c97f23307fde2b8f4fcb8a924bb6546a5 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:41:53 -0500 Subject: [PATCH 173/334] 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 30b9a28502bdf9fdd9a24fd67063072571bea42c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 04:42:32 -0500 Subject: [PATCH 174/334] 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 1000fae905de03c752154ece4f3469a0fe46b5b8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:28:15 -0500 Subject: [PATCH 175/334] 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 cb02c2e6d07ae4e661ad3c1dc4f2bbecdfecdafc Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 09:02:52 -0500 Subject: [PATCH 176/334] 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 b3c257fb7912b516c5932894217079bbb503aa1f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:10:34 -0500 Subject: [PATCH 177/334] 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 25063821e1898ef49a7d0d88028655dfe026f7df Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:30:55 -0500 Subject: [PATCH 178/334] 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 514ce59a8fca64612234af792f15fee8cb60ab84 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 09:03:14 -0500 Subject: [PATCH 179/334] 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 d3aa7375f0eb71a837e0f5f4a08a653dd119eafe Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:30:08 -0500 Subject: [PATCH 180/334] 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 838b1338b80da871a22f3d2f5e4b9b04e82604a7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:31:05 -0500 Subject: [PATCH 181/334] 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 f91f98e30987febb695daf32a162107392ab9c07 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 20:11:02 -0500 Subject: [PATCH 182/334] 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 40636f22734058e93ac201e17571c40ffc3e7bad Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 09:03:48 -0500 Subject: [PATCH 183/334] 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 7be6aa455e6d7f553c3236877a9d6b2a7c5d790d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 09:15:00 -0500 Subject: [PATCH 184/334] 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 b14add591450a634c469efb8dc48aeec25a0bfa0 Mon Sep 17 00:00:00 2001 From: Matrix Date: Sun, 4 Feb 2024 15:27:57 +0800 Subject: [PATCH 185/334] 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 dfc26e45098aa512eb8f9e02eda9aa47538884bb Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 08:56:23 -0500 Subject: [PATCH 186/334] 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 94e1eaa15d00d2f872f6d52a1b105214e3b9dbf4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Feb 2024 21:52:33 -0500 Subject: [PATCH 187/334] 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 0a25788822dea66e48cb2dfc6a79588030abdc5a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Feb 2024 03:17:13 -0600 Subject: [PATCH 188/334] 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 6fbd5fd0c2e..c16f2799fd6 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 a436b6379c0..6c938651ce3 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 4fca06256bb657559a03d231a5dbb71d5ac926bb Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 4 Feb 2024 01:13:35 -0800 Subject: [PATCH 189/334] 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 ba0c065750d4c43fee386e1e9941e140cbfb0baf 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 190/334] 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 2dc630a4af3a29ca7c6f4d88e46f7f0aafa5bdd0 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 191/334] 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 d379a9aaaecc3445d92c04fdcc08a29c5b26ca42 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 4 Feb 2024 15:59:46 +0100 Subject: [PATCH 192/334] Bump version to 2024.2.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2cbb5565ef0..5517e4264b1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index d75d8efdc08..b56d0e3e217 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.0b4" +version = "2024.2.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From a7010e3e8010fa1eb75d951f0b5f6612c929d118 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 193/334] 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 fd2469e2a79b96792d8c84b169f81589bec5f089 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 4 Feb 2024 21:25:14 +0100 Subject: [PATCH 194/334] 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 bd78c44ac5c045d280aeec77bc5e945fb8219e54 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 4 Feb 2024 18:37:16 +0100 Subject: [PATCH 195/334] 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 b56d0e3e217..d927ad21280 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 f766fbfb98eb7ae00d1cbf38ad7766767cc3165b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 4 Feb 2024 18:58:09 +0100 Subject: [PATCH 196/334] 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 c16f2799fd6..6ea902e5de6 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 6c938651ce3..c08ddb04053 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 c0efec4a8435118e7fd349ea5f8ebc3f4c8421b7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 15:15:17 -0500 Subject: [PATCH 197/334] 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 7ca83a7648fc3f50254667aa9d4e03c3ce9bcd75 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Feb 2024 14:11:28 -0500 Subject: [PATCH 198/334] 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 e2695ba88fc42b067462a8460e493edde1854f74 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 4 Feb 2024 22:57:11 +0100 Subject: [PATCH 199/334] 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 ce29b4a7e3d4483e9a70169d7caa1c6f17b05338 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 4 Feb 2024 23:20:46 +0100 Subject: [PATCH 200/334] 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 d789e838797b76b8c9652bac3c29cb9ee34fa251 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 4 Feb 2024 23:20:14 +0100 Subject: [PATCH 201/334] 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 02ebf1d7f8744897f4d4cffd62337693f927af4b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 4 Feb 2024 23:23:10 +0100 Subject: [PATCH 202/334] 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 5747f8ce9d0985c81d33754c1c1225a9edfaa21e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 4 Feb 2024 22:56:22 +0100 Subject: [PATCH 203/334] 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 3934524d4a0ddc7c57212a3a1a78b08a2939df54 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sun, 4 Feb 2024 23:21:57 +0100 Subject: [PATCH 204/334] 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 4d7c96205da1747730c3e2fff3f28abc81a38b00 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 4 Feb 2024 23:21:18 +0100 Subject: [PATCH 205/334] 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 3a067d445d07bb01ba3558361113ff839a75bd6c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 4 Feb 2024 23:25:49 +0100 Subject: [PATCH 206/334] Bump version to 2024.2.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5517e4264b1..ed681efee14 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index d927ad21280..67f410c717c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.0b5" +version = "2024.2.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From bca9826e18bd2fb997df1a2bab20d6a7c7853c58 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 207/334] 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 5c1e4379a9046d57c303b36599f7c00524e0cfd4 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 208/334] 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 66d8856033fd5fdef186a8f88855a60e436c880c Mon Sep 17 00:00:00 2001 From: Leah Oswald Date: Sun, 4 Feb 2024 23:56:12 +0100 Subject: [PATCH 209/334] 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 8ab1c044bd0fa95ba1787ac7fef6c864f6341e52 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 210/334] 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 74f1b18b7388e46cc21f4f90d1aca893cc3933f0 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Mon, 5 Feb 2024 10:27:49 +1100 Subject: [PATCH 211/334] 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 6ea902e5de6..7c319de9663 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 c08ddb04053..d7de5535aad 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 91b1a8e962bcf896c31c362660145ba35bc52517 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 212/334] 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 44ecaa740b989c0a08f808e5770c8d4a908a068b 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 213/334] 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 f50afd6004756c70e716381fa3fcd049d513cea8 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 5 Feb 2024 02:58:08 -0500 Subject: [PATCH 214/334] 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 6a633c774ed..eb7785dbd61 100644 --- a/tests/components/flo/test_device.py +++ b/tests/components/flo/test_device.py @@ -90,7 +90,7 @@ async def test_device( # test error sending device ping with patch( - "homeassistant.components.flo.device.FloDeviceDataUpdateCoordinator.send_presence_ping", + "aioflo.presence.Presence.ping", side_effect=RequestError, ), pytest.raises(UpdateFailed): # simulate 4 updates failing From ecc6cc280a7e1144413ce24895d9514e7ac3a284 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Feb 2024 09:41:55 +0100 Subject: [PATCH 215/334] Bump version to 2024.2.0b7 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ed681efee14..44d046bacd0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 67f410c717c..a6deaa6937f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.0b6" +version = "2024.2.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f05ba22b5c5ef28228669d2680b48e526dcb4ba5 Mon Sep 17 00:00:00 2001 From: Myles Eftos Date: Mon, 5 Feb 2024 20:53:42 +1100 Subject: [PATCH 216/334] 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 7c319de9663..9c1d95e18bd 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 d7de5535aad..9a533250b9a 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 5930c841d7d6feb217089386e7e4195c0c017436 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 5 Feb 2024 12:40:40 +0100 Subject: [PATCH 217/334] 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 9c1d95e18bd..5a35494f074 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 9a533250b9a..4e44854e48e 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 3183cd346d4fd8e022e220ce27ce7030485e56a4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Feb 2024 12:30:09 +0100 Subject: [PATCH 218/334] 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 83a5659d570a62e1fc61b7be2035f7685af8ec86 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Feb 2024 12:41:25 +0100 Subject: [PATCH 219/334] 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 c48c8c25fa491362b7d196233882c3365a968eb4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Feb 2024 16:32:39 +0100 Subject: [PATCH 220/334] 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 dd2cc52119c48a660d76f2dc70f5751deb3879ad Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Feb 2024 16:03:14 +0100 Subject: [PATCH 221/334] 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 16266703df55832036615e33702c3e72fbd2cc9d Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 5 Feb 2024 18:52:58 +0100 Subject: [PATCH 222/334] 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 5a35494f074..afa650cb389 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 4e44854e48e..6e94b3c102a 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 2d90ee8237f766799b078bcdd425ceaca524acb3 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 223/334] 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 cc36071612cef00060f378d7ab38ec25d75447f8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 5 Feb 2024 16:09:33 +0100 Subject: [PATCH 224/334] 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 4ff23275e8f..064675fce08 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.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 afa650cb389..9eb27dc41fe 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 6e94b3c102a..11ad3d0e1c6 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 e3191d098f48fbd9822401066c2b222138567936 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Feb 2024 16:26:25 +0100 Subject: [PATCH 225/334] 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 a19aa9595a63bb774e7f20bf4a280128832e6c5f Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Mon, 5 Feb 2024 18:51:01 +0100 Subject: [PATCH 226/334] 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 9eb27dc41fe..2d4a4a087bd 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 11ad3d0e1c6..3d1c781a5eb 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 1534f99c80bc0732300f0966edce8a4e22871888 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 5 Feb 2024 20:19:38 +0100 Subject: [PATCH 227/334] 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 5a4bae22e9f..9fe394d05c3 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 532df5b5f1591aab74179a3f48f468651f42923f 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 228/334] 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 eb510e36305412a6387dac8e51ae45dd929f0142 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 5 Feb 2024 14:17:13 -0500 Subject: [PATCH 229/334] 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 d30a2e36114b3c3c57f05666848ef32711924e74 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 230/334] 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 65476914ed07f56b4d42fb6e3683eb6af467ec10 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 231/334] 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 2187cb5b8b8..909a1506eb1 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 4c6c5ee63db781a9b282e4592c5214e85edd7711 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Feb 2024 20:14:34 +0100 Subject: [PATCH 232/334] 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 df88335370544ce98c2800dfc4093e377a00357b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Feb 2024 20:27:40 +0100 Subject: [PATCH 233/334] Bump version to 2024.2.0b8 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 44d046bacd0..f6b42076575 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index a6deaa6937f..d7aea22d47a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.0b7" +version = "2024.2.0b8" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 6b354457c2c2690f1d21793551c4e5bffab33dd4 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 6 Feb 2024 01:12:56 +0100 Subject: [PATCH 234/334] 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 03953152675af99c7ee681357397d5cf85d79040 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 235/334] 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 2d4a4a087bd..cc27c58f9f2 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 3d1c781a5eb..227a587d3ee 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 3ba63fc78f03b00aa48b3ea85cba425729a4aa0d 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 236/334] 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 31c0d212048cc672b019eb072fcab41b1580b224 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 237/334] 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 c1e5b2e6cc2e01cdbcd46c32427f51a48bae6a18 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 238/334] 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 909a1506eb1..d03fed39bb3 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 9fcdfd1b168d11c16757b946e4cec3859a0ba1b8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 5 Feb 2024 15:59:02 -0500 Subject: [PATCH 239/334] 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 cc27c58f9f2..07ed634645e 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 227a587d3ee..47669fd7fb8 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 ffd5e04a29cf8deb3e0484632878f9eb48d3b02d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 6 Feb 2024 07:18:33 +0100 Subject: [PATCH 240/334] 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 5025c15165db1cdca6974834e8d01e3ea3d2746c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 5 Feb 2024 18:39:56 -0500 Subject: [PATCH 241/334] 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 8d79ac67f524061993cb36b3cdc75f2ebd20f1d7 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 242/334] 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 07ed634645e..cdd2de2a3f2 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 47669fd7fb8..91c58746342 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 e25ddf9650825338c6f6e04691b14524e8c030aa Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 6 Feb 2024 09:34:02 +0100 Subject: [PATCH 243/334] 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 3cf826dc93e75068d8598e27d5da9dcc7fb7c0d1 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 244/334] 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 cdd2de2a3f2..1090d47c0ec 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 91c58746342..7e0778e16fe 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.19.0 From 2481d146321eb939020e71374d04b04e7bef36f5 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 245/334] 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 1090d47c0ec..afd8811f28b 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 7e0778e16fe..fcc2780d65f 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 439f82a4eca559f8b45298a02f08fbeeb19d7123 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 6 Feb 2024 14:30:53 +0100 Subject: [PATCH 246/334] 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 afd8811f28b..b51efec761b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2856,10 +2856,10 @@ xbox-webapi==2.0.11 xiaomi-ble==0.23.1 # 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 fcc2780d65f..33dd796c496 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2185,10 +2185,10 @@ xbox-webapi==2.0.11 xiaomi-ble==0.23.1 # 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 c1701328278ca58b27a323a16e6715e7643c791b 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 247/334] 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 35fad529133840cfb57abd37b029e82769d8e00c Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 6 Feb 2024 15:16:15 +0100 Subject: [PATCH 248/334] 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 b51efec761b..012fcd988e0 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 33dd796c496..8e533798504 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 d099fb2a26c190786234611bb68a89e5118fd461 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 249/334] 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 064675fce08..4c4b41500e0 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 7032415528e26487e740ccd32078cf19af870fa6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Feb 2024 22:34:53 +0100 Subject: [PATCH 250/334] 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 c3532d553f4..ddaebcbf2a7 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 8569ddc5f94803de631816226ea185d21891ec47 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Feb 2024 12:41:57 -0600 Subject: [PATCH 251/334] 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 7cf7ab62495..db2760d554c 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 8aa124222120cff4ed99af60ee88e256ffec4206 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Tue, 6 Feb 2024 18:33:10 +0100 Subject: [PATCH 252/334] 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 40adb3809f73dfd5f61c7bfc6d36d178418f15f2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 6 Feb 2024 22:36:12 +0100 Subject: [PATCH 253/334] 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 2c870f9da978fd15ce301d9cdf150f64c124f7ac Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 6 Feb 2024 22:37:20 +0100 Subject: [PATCH 254/334] 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 012fcd988e0..53a39dadccd 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 8e533798504..aba70dcda19 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 5c83b774bbdecf02ea64217d23e33f1add8c51a7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Feb 2024 20:14:45 +0100 Subject: [PATCH 255/334] 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 53a39dadccd..6f073b20516 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 aba70dcda19..f0911f285e9 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 2103875ff73c2bf26d1cfa367d30fc8c0b4676e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Feb 2024 14:01:10 -0600 Subject: [PATCH 256/334] 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 6f073b20516..e3be052b1f6 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 f0911f285e9..74f58994906 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 74a75e709f1ae6f6ad4d24e0dad727c1650c30df 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 257/334] 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 4c4b41500e0..f8ba47129b0 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 d7aea22d47a..fedc663a58f 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 8c605c29c344ff5b0f05c2453eb10fd9985322e4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Feb 2024 22:49:53 +0100 Subject: [PATCH 258/334] Bump version to 2024.2.0b9 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f6b42076575..54640a8a7a8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b8" +PATCH_VERSION: Final = "0b9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index fedc663a58f..e4c0530b257 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.0b8" +version = "2024.2.0b9" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ebb1912617bab87fdbb4532e70bf49f95891fa89 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 6 Feb 2024 23:03:35 +0100 Subject: [PATCH 259/334] 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 5b4b803a8d4..7563d4c08b9 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 8c78b7dadc6..0d10051ca78 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -396,19 +396,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, @@ -418,7 +418,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", ), ], ) @@ -541,7 +541,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 d784a76d32cc58f4929dc34ce91f74b22630c66e Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 7 Feb 2024 06:29:26 +0100 Subject: [PATCH 260/334] 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 2b97365b555..ae839180729 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6156,6 +6156,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 fe94107af7fbee90c6fd5ec8db71e2937545c975 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 7 Feb 2024 06:26:33 +0100 Subject: [PATCH 261/334] 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 27691b7d48ffb1d45149c9541ed71f70da9bf6f7 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 262/334] 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 d03fed39bb3..2fa7e87d737 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 e3be052b1f6..bf8af74810b 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 74f58994906..03bc77167e9 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 031aadff00e2a2ecfae9c3f5750498d83bf3cbf8 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 7 Feb 2024 06:23:57 +0100 Subject: [PATCH 263/334] 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 bf8af74810b..a32064d625a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1310,7 +1310,7 @@ moehlenhoff-alpha2==1.3.0 mopeka-iot-ble==0.5.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 03bc77167e9..9bd5db424cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1046,7 +1046,7 @@ moehlenhoff-alpha2==1.3.0 mopeka-iot-ble==0.5.0 # homeassistant.components.motion_blinds -motionblinds==0.6.19 +motionblinds==0.6.20 # homeassistant.components.motioneye motioneye-client==0.3.14 From 40cfc31dcb5eefb22cb623f45e70638fb7d18169 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 264/334] 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 a32064d625a..e6183e425b1 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 9bd5db424cd..8bb6cd57486 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 ea4bdbb3a05c748c0330a66967e0cc8e3a18f1cc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Feb 2024 08:48:17 +0100 Subject: [PATCH 265/334] Bump version to 2024.2.0b10 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 54640a8a7a8..b0421bf6b26 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b9" +PATCH_VERSION: Final = "0b10" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index e4c0530b257..76d71ed4602 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.0b9" +version = "2024.2.0b10" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2ca3bbaea54fd3a0b41714f64c6c559143fbe9bd Mon Sep 17 00:00:00 2001 From: Jiayi Chen Date: Wed, 7 Feb 2024 09:35:50 +0100 Subject: [PATCH 266/334] 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 bcd4e349668..4be573201d6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -485,6 +485,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 881707e1fe8f77c959ebdbfbef1a0d1ba4e42f99 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 6 Feb 2024 22:06:59 +0100 Subject: [PATCH 267/334] 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 e6183e425b1..170c0331f3d 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 8bb6cd57486..a61c494026b 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 75b308c1aac3891569341890137858522cafb9c7 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 7 Feb 2024 20:27:10 +1100 Subject: [PATCH 268/334] 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 170c0331f3d..7a87c34f1ee 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 a61c494026b..907bfc5f855 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 bd21490a574c81b54be602f5c4e9e1f81c6722e6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 7 Feb 2024 12:29:06 +0100 Subject: [PATCH 269/334] 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 f8ba47129b0..e3a82474d8d 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.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 7a87c34f1ee..549c269f8a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1059,7 +1059,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 907bfc5f855..1e107e71cd7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -855,7 +855,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 e720b398d2e2f21854ee70c4b65cd2e295960ae1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Feb 2024 12:44:50 +0100 Subject: [PATCH 270/334] Bump version to 2024.2.0b11 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b0421bf6b26..6f288309837 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b10" +PATCH_VERSION: Final = "0b11" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 76d71ed4602..b5e275c7d8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.0b10" +version = "2024.2.0b11" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f61c70b6862bc18493872c3bc0e3a52b8a1e2afb Mon Sep 17 00:00:00 2001 From: Matrix Date: Wed, 7 Feb 2024 20:42:33 +0800 Subject: [PATCH 271/334] 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 473c85d563a..a1017a488d1 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 3030870de0f6125aeee3a9bb54e49846b56b6848 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 272/334] 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 31cb6715e0c..5b08b26a306 100644 --- a/homeassistant/components/myuplink/sensor.py +++ b/homeassistant/components/myuplink/sensor.py @@ -75,7 +75,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 8375fc235d6ae6d3e2bb093f7b529d4de13729c7 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Thu, 8 Feb 2024 02:24:25 +1100 Subject: [PATCH 273/334] 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 549c269f8a3..a61d360c4e0 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 1e107e71cd7..67c0775c2f7 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 f63aaf8b5a99432199bb4365b8d760ca427b6f34 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Feb 2024 16:28:11 +0100 Subject: [PATCH 274/334] Bump version to 2024.2.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6f288309837..fb6e8ef896b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0b11" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index b5e275c7d8e..6a038aa1c5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.0b11" +version = "2024.2.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 9e47d030864a8576173f68302b6d5522b368ecdc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 31 Jan 2024 22:10:32 +0100 Subject: [PATCH 275/334] 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 3b7271d597d18da1b6add30515110bf0b8e2cfb4 Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 8 Feb 2024 04:51:20 -0500 Subject: [PATCH 276/334] Catch APIRateLimit in Honeywell (#107806) --- homeassistant/components/honeywell/climate.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index efd06ba2905..6bc6169c68c 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -7,6 +7,7 @@ from typing import Any from aiohttp import ClientConnectionError from aiosomecomfort import ( + APIRateLimited, AuthError, ConnectionError as AscConnectionError, SomeComfortError, @@ -505,10 +506,11 @@ class HoneywellUSThermostat(ClimateEntity): await self._device.refresh() except ( + asyncio.TimeoutError, + AscConnectionError, + APIRateLimited, AuthError, ClientConnectionError, - AscConnectionError, - asyncio.TimeoutError, ): self._retry += 1 self._attr_available = self._retry <= RETRY @@ -524,7 +526,12 @@ class HoneywellUSThermostat(ClimateEntity): await _login() return - except (AscConnectionError, ClientConnectionError, asyncio.TimeoutError): + except ( + asyncio.TimeoutError, + AscConnectionError, + APIRateLimited, + ClientConnectionError, + ): self._retry += 1 self._attr_available = self._retry <= RETRY return From dbfee24eb751ac826d14bd1faf3b0b3d4253e826 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Thu, 8 Feb 2024 14:09:53 -0500 Subject: [PATCH 277/334] 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 ddaebcbf2a7..8d78c878cfa 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 44c9ea68eb681c3dae7a181dcda5e0ef829f565f Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 7 Feb 2024 15:13:42 -0600 Subject: [PATCH 278/334] 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 fe399659a56..8ca56b8eabe 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 26626a04079..5853d98b760 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 e320d715c7a04115dfa1af7f559565955fca718d Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 8 Feb 2024 08:59:57 +0100 Subject: [PATCH 279/334] 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 a61d360c4e0..506c913e117 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.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 67c0775c2f7..57d81e81f36 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.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 19349e1779ce638db954800921969a4204aed108 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 8 Feb 2024 08:42:22 +0100 Subject: [PATCH 280/334] 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 506c913e117..76fe94bc865 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -233,7 +233,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 57d81e81f36..818387f42d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,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 a9b3c2e2b58f9cfb8300fc19ada7c49639563ab9 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 8 Feb 2024 09:01:48 +0100 Subject: [PATCH 281/334] 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 f48d70654b2b8dc0373c3f823055f3907dac72fb Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 9 Feb 2024 18:45:55 +1100 Subject: [PATCH 282/334] 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 76fe94bc865..37845e0d0ea 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 818387f42d0..ce78bc31eff 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 7309c3c2900f2e918bb97c6e6afc8eacda6f9587 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 8 Feb 2024 13:14:10 +0100 Subject: [PATCH 283/334] 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 a9e9ec2c3d2f5f23a617f34d68f09b291c940907 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 8 Feb 2024 12:40:32 +0100 Subject: [PATCH 284/334] 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 95a800b6bc0b6b3401575908303dce5ac3670686 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 Feb 2024 15:39:01 +0100 Subject: [PATCH 285/334] 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 b0a8f952b1b..d5259ef1fc5 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 de44af2948f0030f0acacb0dbc0f08979434d135 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 286/334] 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 37845e0d0ea..326e885af99 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 ce78bc31eff..2ee08d1945d 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 c665903f9d3a426d0fa68ac08bee8339cb4434b7 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 8 Feb 2024 13:48:33 +0100 Subject: [PATCH 287/334] 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 49e5709826020abf6081370e68a0b6391ca9de5d Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 8 Feb 2024 15:41:37 +0100 Subject: [PATCH 288/334] 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 326e885af99..601e59874f7 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.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 2ee08d1945d..d8713d7083a 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.1.0 +deebot-client==5.1.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns From a18918bb73d503eb85189ca5d085390d500bac8a Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 8 Feb 2024 15:34:43 +0100 Subject: [PATCH 289/334] 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 7ff2f376d4b06a634015dbcb982d3c0870c8e30c Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 8 Feb 2024 15:41:19 +0100 Subject: [PATCH 290/334] 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 601e59874f7..38b4c0ec1fb 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==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 d8713d7083a..998e3690198 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==2024.2.0 +aioecowitt==2024.2.1 # homeassistant.components.co2signal aioelectricitymaps==0.4.0 From 4a18f592c647a8ef536453466cf23450acfb2ff3 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 9 Feb 2024 08:39:08 +0100 Subject: [PATCH 291/334] 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 56ff767969ae5e1a0afd4fe6d83651016f0fa4a8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 8 Feb 2024 20:03:41 +0100 Subject: [PATCH 292/334] 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 e3a82474d8d..6a9734ad1fa 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.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 38b4c0ec1fb..5dd3b810815 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1059,7 +1059,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 998e3690198..5e1c38a92a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -855,7 +855,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 e4382a494c20e56ba5b867d797daee221c774222 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 293/334] 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 f5884c627955b5a1f07349ba1eb48d3c01c0d4d6 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 8 Feb 2024 19:38:03 -0600 Subject: [PATCH 294/334] 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 8ca56b8eabe..295246b5e0a 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 437a2a829f4eebeacd07006e4009e4906ba72031 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 9 Feb 2024 07:49:09 +0000 Subject: [PATCH 295/334] 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 5dd3b810815..6509f2769be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -818,7 +818,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 74ea9e24df9c10e5e1867a926dd0848a76279df7 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 296/334] 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 6509f2769be..0c084d0b6b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1579,7 +1579,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 5e1c38a92a2..de460cbd0c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1232,7 +1232,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 58d46f6dec8ae2b4a8c3a3cbe695227325ac6617 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 9 Feb 2024 09:02:01 +0100 Subject: [PATCH 297/334] Bump version to 2024.2.1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index fb6e8ef896b..a19ff18d8f3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 6a038aa1c5a..89551988971 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.0" +version = "2024.2.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 5f9cc2fec1751ef1db08595564c51ce31ce7217a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 Feb 2024 13:00:45 +0100 Subject: [PATCH 298/334] 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 349d8f5c28305d817204ab6e5735c29f67f196a6 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Mon, 12 Feb 2024 03:37:37 -0500 Subject: [PATCH 299/334] Better teardown and setup of Roborock connections (#106092) Co-authored-by: Robert Resch --- homeassistant/components/roborock/__init__.py | 16 +++++----- .../components/roborock/coordinator.py | 3 +- homeassistant/components/roborock/device.py | 29 +++++++++++++++++-- homeassistant/components/roborock/select.py | 4 +-- homeassistant/components/roborock/sensor.py | 4 +-- homeassistant/components/roborock/vacuum.py | 16 +++++----- tests/components/roborock/test_init.py | 2 +- 7 files changed, 47 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index 0b4dfa29e78..a5c896f3740 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -115,6 +115,7 @@ async def setup_device( device.name, ) _LOGGER.debug(err) + await mqtt_client.async_release() raise err coordinator = RoborockDataUpdateCoordinator( hass, device, networking, product_info, mqtt_client @@ -125,6 +126,7 @@ async def setup_device( try: await coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady: + await coordinator.release() 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. " @@ -153,14 +155,10 @@ async def setup_device( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle removal of an entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - await asyncio.gather( - *( - coordinator.release() - for coordinator in hass.data[DOMAIN][entry.entry_id].values() - ) - ) + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + release_tasks = set() + for coordinator in hass.data[DOMAIN][entry.entry_id].values(): + release_tasks.add(coordinator.release()) hass.data[DOMAIN].pop(entry.entry_id) - + await asyncio.gather(*release_tasks) return unload_ok diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index cd08cf871d4..d0ed508df4c 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -77,7 +77,8 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): async def release(self) -> None: """Disconnect from API.""" - await self.api.async_disconnect() + await self.api.async_release() + await self.cloud_api.async_release() async def _update_device_prop(self) -> None: """Update device properties.""" diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 17531f6c627..2921a372e00 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -1,5 +1,4 @@ """Support for Roborock device base class.""" - from typing import Any from roborock.api import AttributeCache, RoborockClient @@ -7,6 +6,7 @@ from roborock.cloud_api import RoborockMqttClient from roborock.command_cache import CacheableAttribute from roborock.containers import Consumable, Status from roborock.exceptions import RoborockException +from roborock.roborock_message import RoborockDataProtocol from roborock.roborock_typing import RoborockCommand from homeassistant.exceptions import HomeAssistantError @@ -24,7 +24,10 @@ class RoborockEntity(Entity): _attr_has_entity_name = True def __init__( - self, unique_id: str, device_info: DeviceInfo, api: RoborockClient + self, + unique_id: str, + device_info: DeviceInfo, + api: RoborockClient, ) -> None: """Initialize the coordinated Roborock Device.""" self._attr_unique_id = unique_id @@ -75,6 +78,9 @@ class RoborockCoordinatedEntity( self, unique_id: str, coordinator: RoborockDataUpdateCoordinator, + listener_request: list[RoborockDataProtocol] + | RoborockDataProtocol + | None = None, ) -> None: """Initialize the coordinated Roborock Device.""" RoborockEntity.__init__( @@ -85,6 +91,23 @@ class RoborockCoordinatedEntity( ) CoordinatorEntity.__init__(self, coordinator=coordinator) self._attr_unique_id = unique_id + if isinstance(listener_request, RoborockDataProtocol): + listener_request = [listener_request] + self.listener_requests = listener_request or [] + + async def async_added_to_hass(self) -> None: + """Add listeners when the device is added to hass.""" + await super().async_added_to_hass() + for listener_request in self.listener_requests: + self.api.add_listener( + listener_request, self._update_from_listener, cache=self.api.cache + ) + + async def async_will_remove_from_hass(self) -> None: + """Remove listeners when the device is removed from hass.""" + for listener_request in self.listener_requests: + self.api.remove_listener(listener_request, self._update_from_listener) + await super().async_will_remove_from_hass() @property def _device_status(self) -> Status: @@ -107,7 +130,7 @@ class RoborockCoordinatedEntity( await self.coordinator.async_refresh() return res - def _update_from_listener(self, value: Status | Consumable): + def _update_from_listener(self, value: Status | Consumable) -> None: """Update the status or consumable data from a listener and then write the new entity state.""" if isinstance(value, Status): self.coordinator.roborock_device_info.props.status = value diff --git a/homeassistant/components/roborock/select.py b/homeassistant/components/roborock/select.py index ae5dd12689d..3fdd10c97d5 100644 --- a/homeassistant/components/roborock/select.py +++ b/homeassistant/components/roborock/select.py @@ -107,10 +107,8 @@ class RoborockSelectEntity(RoborockCoordinatedEntity, SelectEntity): ) -> None: """Create a select entity.""" self.entity_description = entity_description - super().__init__(unique_id, coordinator) + super().__init__(unique_id, coordinator, entity_description.protocol_listener) self._attr_options = options - if (protocol := self.entity_description.protocol_listener) is not None: - self.api.add_listener(protocol, self._update_from_listener, self.api.cache) async def async_select_option(self, option: str) -> None: """Set the option.""" diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index d5258879acb..8d723ec57cd 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -232,10 +232,8 @@ class RoborockSensorEntity(RoborockCoordinatedEntity, SensorEntity): description: RoborockSensorDescription, ) -> None: """Initialize the entity.""" - super().__init__(unique_id, coordinator) self.entity_description = description - if (protocol := self.entity_description.protocol_listener) is not None: - self.api.add_listener(protocol, self._update_from_listener, self.api.cache) + super().__init__(unique_id, coordinator, description.protocol_listener) @property def native_value(self) -> StateType | datetime.datetime: diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index 3b8f0e756b7..dafbb731bd2 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -92,14 +92,16 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): ) -> None: """Initialize a vacuum.""" StateVacuumEntity.__init__(self) - RoborockCoordinatedEntity.__init__(self, unique_id, coordinator) + RoborockCoordinatedEntity.__init__( + self, + unique_id, + coordinator, + listener_request=[ + RoborockDataProtocol.FAN_POWER, + RoborockDataProtocol.STATE, + ], + ) self._attr_fan_speed_list = self._device_status.fan_power_options - self.api.add_listener( - RoborockDataProtocol.FAN_POWER, self._update_from_listener, self.api.cache - ) - self.api.add_listener( - RoborockDataProtocol.STATE, self._update_from_listener, self.api.cache - ) @property def state(self) -> str | None: diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index 5d1afaf8f84..7546e80b003 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -18,7 +18,7 @@ async def test_unload_entry( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert setup_entry.state is ConfigEntryState.LOADED with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.async_disconnect" + "homeassistant.components.roborock.coordinator.RoborockLocalClient.async_release" ) as mock_disconnect: assert await hass.config_entries.async_unload(setup_entry.entry_id) await hass.async_block_till_done() From b54a3170f03e7fdf156a4f0247d1992cd0e6732f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Grenotton?= Date: Thu, 15 Feb 2024 12:34:29 +0100 Subject: [PATCH 300/334] Fix freebox pairing in bridge mode (#106131) --- .../components/freebox/config_flow.py | 4 +- homeassistant/components/freebox/router.py | 51 +++++++++++-------- tests/components/freebox/conftest.py | 11 ++++ tests/components/freebox/test_config_flow.py | 28 +++++++++- tests/components/freebox/test_router.py | 36 ++++++++++++- 5 files changed, 104 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 59b5d65710a..7441def7d4d 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -11,7 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN -from .router import get_api +from .router import get_api, get_hosts_list_if_supported _LOGGER = logging.getLogger(__name__) @@ -69,7 +69,7 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Check permissions await fbx.system.get_config() - await fbx.lan.get_hosts_list() + await get_hosts_list_if_supported(fbx) # Close connection await fbx.close() diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 15e3b34bd77..3b13fad0572 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -64,6 +64,33 @@ async def get_api(hass: HomeAssistant, host: str) -> Freepybox: return Freepybox(APP_DESC, token_file, API_VERSION) +async def get_hosts_list_if_supported( + fbx_api: Freepybox, +) -> tuple[bool, list[dict[str, Any]]]: + """Hosts list is not supported when freebox is configured in bridge mode.""" + supports_hosts: bool = True + fbx_devices: list[dict[str, Any]] = [] + try: + fbx_devices = await fbx_api.lan.get_hosts_list() or [] + except HttpRequestError as err: + if ( + (matcher := re.search(r"Request failed \(APIResponse: (.+)\)", str(err))) + and is_json(json_str := matcher.group(1)) + and (json_resp := json.loads(json_str)).get("error_code") == "nodev" + ): + # No need to retry, Host list not available + supports_hosts = False + _LOGGER.debug( + "Host list is not available using bridge mode (%s)", + json_resp.get("msg"), + ) + + else: + raise err + + return supports_hosts, fbx_devices + + class FreeboxRouter: """Representation of a Freebox router.""" @@ -111,27 +138,9 @@ class FreeboxRouter: # Access to Host list not available in bridge mode, API return error_code 'nodev' if self.supports_hosts: - try: - fbx_devices = await self._api.lan.get_hosts_list() - except HttpRequestError as err: - if ( - ( - matcher := re.search( - r"Request failed \(APIResponse: (.+)\)", str(err) - ) - ) - and is_json(json_str := matcher.group(1)) - and (json_resp := json.loads(json_str)).get("error_code") == "nodev" - ): - # No need to retry, Host list not available - self.supports_hosts = False - _LOGGER.debug( - "Host list is not available using bridge mode (%s)", - json_resp.get("msg"), - ) - - else: - raise err + self.supports_hosts, fbx_devices = await get_hosts_list_if_supported( + self._api + ) # Adds the Freebox itself fbx_devices.append( diff --git a/tests/components/freebox/conftest.py b/tests/components/freebox/conftest.py index 3ba175cbc75..6042248561c 100644 --- a/tests/components/freebox/conftest.py +++ b/tests/components/freebox/conftest.py @@ -112,3 +112,14 @@ def mock_router_bridge_mode(mock_device_registry_devices, router): ) return router + + +@pytest.fixture +def mock_router_bridge_mode_error(mock_device_registry_devices, router): + """Mock a failed connection to Freebox Bridge mode.""" + + router().lan.get_hosts_list = AsyncMock( + side_effect=HttpRequestError("Request failed (APIResponse: some unknown error)") + ) + + return router diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index 6a90bbd9ba8..c19b3c3f3b2 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -69,8 +69,8 @@ async def test_zeroconf(hass: HomeAssistant) -> None: assert result["step_id"] == "link" -async def test_link(hass: HomeAssistant, router: Mock) -> None: - """Test linking.""" +async def internal_test_link(hass: HomeAssistant) -> None: + """Test linking internal, common to both router modes.""" with patch( "homeassistant.components.freebox.async_setup_entry", return_value=True, @@ -91,6 +91,30 @@ async def test_link(hass: HomeAssistant, router: Mock) -> None: assert len(mock_setup_entry.mock_calls) == 1 +async def test_link(hass: HomeAssistant, router: Mock) -> None: + """Test link with standard router mode.""" + await internal_test_link(hass) + + +async def test_link_bridge_mode(hass: HomeAssistant, router_bridge_mode: Mock) -> None: + """Test linking for a freebox in bridge mode.""" + await internal_test_link(hass) + + +async def test_link_bridge_mode_error( + hass: HomeAssistant, mock_router_bridge_mode_error: Mock +) -> None: + """Test linking for a freebox in bridge mode, unknown error received from API.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + async def test_abort_if_already_setup(hass: HomeAssistant) -> None: """Test we abort if component is already setup.""" MockConfigEntry( diff --git a/tests/components/freebox/test_router.py b/tests/components/freebox/test_router.py index 572c168e665..88cf56de2bb 100644 --- a/tests/components/freebox/test_router.py +++ b/tests/components/freebox/test_router.py @@ -1,7 +1,11 @@ """Tests for the Freebox utility methods.""" import json +from unittest.mock import Mock -from homeassistant.components.freebox.router import is_json +from freebox_api.exceptions import HttpRequestError +import pytest + +from homeassistant.components.freebox.router import get_hosts_list_if_supported, is_json from .const import DATA_LAN_GET_HOSTS_LIST_MODE_BRIDGE, DATA_WIFI_GET_GLOBAL_CONFIG @@ -20,3 +24,33 @@ async def test_is_json() -> None: assert not is_json("") assert not is_json("XXX") assert not is_json("{XXX}") + + +async def test_get_hosts_list_if_supported( + router: Mock, +) -> None: + """In router mode, get_hosts_list is supported and list is filled.""" + supports_hosts, fbx_devices = await get_hosts_list_if_supported(router()) + assert supports_hosts is True + # List must not be empty; but it's content depends on how many unit tests are executed... + assert fbx_devices + assert "d633d0c8-958c-43cc-e807-d881b076924b" in str(fbx_devices) + + +async def test_get_hosts_list_if_supported_bridge( + router_bridge_mode: Mock, +) -> None: + """In bridge mode, get_hosts_list is NOT supported and list is empty.""" + supports_hosts, fbx_devices = await get_hosts_list_if_supported( + router_bridge_mode() + ) + assert supports_hosts is False + assert fbx_devices == [] + + +async def test_get_hosts_list_if_supported_bridge_error( + mock_router_bridge_mode_error: Mock, +) -> None: + """Other exceptions must be propagated.""" + with pytest.raises(HttpRequestError): + await get_hosts_list_if_supported(mock_router_bridge_mode_error()) From e825bcc28202e3b4630ee021bf0628d51a3371cb Mon Sep 17 00:00:00 2001 From: Christophe Gagnier Date: Fri, 9 Feb 2024 02:41:48 -0500 Subject: [PATCH 301/334] 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 0c084d0b6b9..e4cc1e4c382 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2284,7 +2284,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 de460cbd0c4..c112d7ea3f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1751,7 +1751,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 5a87cde71e90afec6d09762066516271b513e694 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 9 Feb 2024 21:31:16 +1100 Subject: [PATCH 302/334] 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 e4cc1e4c382..4b2ce1b59ad 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 c112d7ea3f9..618fcb9c356 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 54270df2173c7c4dd06806da4827a806f7dc9deb 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 303/334] 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 4b2ce1b59ad..f959175cfdd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2880,7 +2880,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 618fcb9c356..270fbfc1cd5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2206,7 +2206,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 c79bc17d17b6c2831c0efe0be722a242139cf4bf Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 9 Feb 2024 20:05:01 +0100 Subject: [PATCH 304/334] 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 87bd67656b4f2482caa9e0f19d2f45acc073f4ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Feb 2024 09:29:49 -0600 Subject: [PATCH 305/334] Only schedule august activity update when a new activity is seen (#110141) --- homeassistant/components/august/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 624121b8828..ea27b58d34c 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -249,10 +249,11 @@ class AugustData(AugustSubscriberMixin): device = self.get_device_detail(device_id) activities = activities_from_pubnub_message(device, date_time, message) activity_stream = self.activity_stream - if activities: - activity_stream.async_process_newer_device_activities(activities) + if activities and activity_stream.async_process_newer_device_activities( + activities + ): self.async_signal_device_id_update(device.device_id) - activity_stream.async_schedule_house_id_refresh(device.house_id) + activity_stream.async_schedule_house_id_refresh(device.house_id) @callback def async_stop(self) -> None: From 2a51377cef7194cfe71b0a35219452fdd44bd565 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Feb 2024 16:30:42 -0600 Subject: [PATCH 306/334] 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 f959175cfdd..76d0bfb292b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2880,7 +2880,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 270fbfc1cd5..9505b883aa6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2206,7 +2206,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 973a13abfa350e50f63f63d43a301b5c1a843108 Mon Sep 17 00:00:00 2001 From: Adam Goode Date: Mon, 12 Feb 2024 09:33:03 -0500 Subject: [PATCH 307/334] Properly report cover positions to prometheus (#110157) --- homeassistant/components/prometheus/__init__.py | 9 ++++++--- tests/components/prometheus/test_init.py | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index e17ae1190a4..86163704797 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -21,7 +21,10 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, HVACAction, ) -from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, +) from homeassistant.components.http import HomeAssistantView from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY from homeassistant.components.light import ATTR_BRIGHTNESS @@ -437,7 +440,7 @@ class PrometheusMetrics: float(cover_state == state.state) ) - position = state.attributes.get(ATTR_POSITION) + position = state.attributes.get(ATTR_CURRENT_POSITION) if position is not None: position_metric = self._metric( "cover_position", @@ -446,7 +449,7 @@ class PrometheusMetrics: ) position_metric.labels(**self._labels(state)).set(float(position)) - tilt_position = state.attributes.get(ATTR_TILT_POSITION) + tilt_position = state.attributes.get(ATTR_CURRENT_TILT_POSITION) if tilt_position is not None: tilt_position_metric = self._metric( "cover_tilt_position", diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index af2f2ba5784..7ee534f91ce 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -1352,7 +1352,7 @@ async def cover_fixture( suggested_object_id="position_shade", original_name="Position Shade", ) - cover_position_attributes = {cover.ATTR_POSITION: 50} + cover_position_attributes = {cover.ATTR_CURRENT_POSITION: 50} set_state_with_entry(hass, cover_position, STATE_OPEN, cover_position_attributes) data["cover_position"] = cover_position @@ -1363,7 +1363,7 @@ async def cover_fixture( suggested_object_id="tilt_position_shade", original_name="Tilt Position Shade", ) - cover_tilt_position_attributes = {cover.ATTR_TILT_POSITION: 50} + cover_tilt_position_attributes = {cover.ATTR_CURRENT_TILT_POSITION: 50} set_state_with_entry( hass, cover_tilt_position, STATE_OPEN, cover_tilt_position_attributes ) From 58b28e6df126b08a2f8f6d22f24a35de1840c79a Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sat, 10 Feb 2024 20:09:02 +0100 Subject: [PATCH 308/334] Fix device class repairs issues placeholders in Group (#110181) fix translation placeholders --- 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 47695a275fc..8e1a0a24207 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -476,7 +476,7 @@ class SensorGroup(GroupEntity, SensorEntity): translation_placeholders={ "entity_id": self.entity_id, "source_entities": ", ".join(self._entity_ids), - "state_classes:": ", ".join(state_classes), + "state_classes": ", ".join(state_classes), }, ) return None @@ -519,7 +519,7 @@ class SensorGroup(GroupEntity, SensorEntity): translation_placeholders={ "entity_id": self.entity_id, "source_entities": ", ".join(self._entity_ids), - "device_classes:": ", ".join(device_classes), + "device_classes": ", ".join(device_classes), }, ) return None From edb69fb095b36c355d439d903a0ea66dfc765d2e Mon Sep 17 00:00:00 2001 From: DustyArmstrong Date: Tue, 13 Feb 2024 16:24:08 +0000 Subject: [PATCH 309/334] Bump datapoint to 0.9.9 + re-enable Met Office Integration (#110206) --- homeassistant/components/metoffice/__init__.py | 12 +++--------- homeassistant/components/metoffice/data.py | 8 +++----- homeassistant/components/metoffice/helpers.py | 11 ++++------- homeassistant/components/metoffice/manifest.json | 3 +-- homeassistant/components/metoffice/sensor.py | 2 +- requirements_all.txt | 3 +++ requirements_test_all.txt | 3 +++ tests/components/metoffice/conftest.py | 8 +------- 8 files changed, 19 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index e00215f6073..a658de9a024 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -4,9 +4,10 @@ from __future__ import annotations import asyncio import logging import re -import sys from typing import Any +import datapoint + from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, @@ -16,7 +17,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator @@ -34,9 +35,6 @@ from .const import ( from .data import MetOfficeData from .helpers import fetch_data, fetch_site -if sys.version_info < (3, 12): - import datapoint - _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR, Platform.WEATHER] @@ -44,10 +42,6 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Met Office entry.""" - if sys.version_info >= (3, 12): - raise HomeAssistantError( - "Met Office is not supported on Python 3.12. Please use Python 3.11." - ) latitude = entry.data[CONF_LATITUDE] longitude = entry.data[CONF_LONGITUDE] diff --git a/homeassistant/components/metoffice/data.py b/homeassistant/components/metoffice/data.py index 8512dd4c7a6..c6bb2b4c01b 100644 --- a/homeassistant/components/metoffice/data.py +++ b/homeassistant/components/metoffice/data.py @@ -2,12 +2,10 @@ from __future__ import annotations from dataclasses import dataclass -import sys -if sys.version_info < (3, 12): - from datapoint.Forecast import Forecast - from datapoint.Site import Site - from datapoint.Timestep import Timestep +from datapoint.Forecast import Forecast +from datapoint.Site import Site +from datapoint.Timestep import Timestep @dataclass diff --git a/homeassistant/components/metoffice/helpers.py b/homeassistant/components/metoffice/helpers.py index 389462d573a..5b698bf19da 100644 --- a/homeassistant/components/metoffice/helpers.py +++ b/homeassistant/components/metoffice/helpers.py @@ -2,7 +2,9 @@ from __future__ import annotations import logging -import sys + +import datapoint +from datapoint.Site import Site from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.util.dt import utcnow @@ -10,11 +12,6 @@ from homeassistant.util.dt import utcnow from .const import MODE_3HOURLY from .data import MetOfficeData -if sys.version_info < (3, 12): - import datapoint - from datapoint.Site import Site - - _LOGGER = logging.getLogger(__name__) @@ -34,7 +31,7 @@ def fetch_site( def fetch_data(connection: datapoint.Manager, site: Site, mode: str) -> MetOfficeData: """Fetch weather and forecast from Datapoint API.""" try: - forecast = connection.get_forecast_for_site(site.id, mode) + forecast = connection.get_forecast_for_site(site.location_id, mode) except (ValueError, datapoint.exceptions.APIException) as err: _LOGGER.error("Check Met Office connection: %s", err.args) raise UpdateFailed from err diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index 401f2c9d265..17643d7e061 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -3,9 +3,8 @@ "name": "Met Office", "codeowners": ["@MrHarcombe", "@avee87"], "config_flow": true, - "disabled": "Integration library not compatible with Python 3.12", "documentation": "https://www.home-assistant.io/integrations/metoffice", "iot_class": "cloud_polling", "loggers": ["datapoint"], - "requirements": ["datapoint==0.9.8;python_version<'3.12'"] + "requirements": ["datapoint==0.9.9"] } diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 371c396a829..84a51a0d584 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -251,6 +251,6 @@ class MetOfficeCurrentSensor( return { ATTR_LAST_UPDATE: self.coordinator.data.now.date, ATTR_SENSOR_ID: self.entity_description.key, - ATTR_SITE_ID: self.coordinator.data.site.id, + ATTR_SITE_ID: self.coordinator.data.site.location_id, ATTR_SITE_NAME: self.coordinator.data.site.name, } diff --git a/requirements_all.txt b/requirements_all.txt index 76d0bfb292b..ed8bc12313a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -671,6 +671,9 @@ crownstone-uart==2.1.0 # homeassistant.components.datadog datadog==0.15.0 +# homeassistant.components.metoffice +datapoint==0.9.9 + # homeassistant.components.bluetooth dbus-fast==2.21.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9505b883aa6..a68230d4019 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -552,6 +552,9 @@ crownstone-uart==2.1.0 # homeassistant.components.datadog datadog==0.15.0 +# homeassistant.components.metoffice +datapoint==0.9.9 + # homeassistant.components.bluetooth dbus-fast==2.21.1 diff --git a/tests/components/metoffice/conftest.py b/tests/components/metoffice/conftest.py index 117bfe417e3..b1d1c9f508e 100644 --- a/tests/components/metoffice/conftest.py +++ b/tests/components/metoffice/conftest.py @@ -1,15 +1,9 @@ """Fixtures for Met Office weather integration tests.""" from unittest.mock import patch +from datapoint.exceptions import APIException import pytest -# All tests are marked as disabled, as the integration is disabled in the -# integration manifest. `datapoint` isn't compatible with Python 3.12 -# -# from datapoint.exceptions import APIException -APIException = Exception -collect_ignore_glob = ["test_*.py"] - @pytest.fixture def mock_simple_manager_fail(): From ad761bb2de8e19c91c3bf84fda3f85eeb01ea9da Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 11 Feb 2024 11:31:51 +0000 Subject: [PATCH 310/334] Bump evohome-async to 0.4.19 (#110225) bump client to 0.4.19 --- 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 0c9bb44d06a..6b893dc8f48 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.18"] + "requirements": ["evohome-async==0.4.19"] } diff --git a/requirements_all.txt b/requirements_all.txt index ed8bc12313a..83a7eb0310a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -821,7 +821,7 @@ eufylife-ble-client==0.1.8 # evdev==1.6.1 # homeassistant.components.evohome -evohome-async==0.4.18 +evohome-async==0.4.19 # homeassistant.components.faa_delays faadelays==2023.9.1 From a0ae18a1b6f79f5b5e212a5b2000828c6781830c Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sun, 11 Feb 2024 09:54:50 +0100 Subject: [PATCH 311/334] Fix state classes issue translation in Group (#110238) Fix state classes translation --- homeassistant/components/group/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index 25ae20da995..ba571bb1008 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -265,7 +265,7 @@ }, "state_classes_not_matching": { "title": "State classes is not correct", - "description": "Device classes `{state_classes}` on source entities `{source_entities}` needs to be same for sensor group `{entity_id}`.\n\nPlease correct the state classes on the source entities and reload the group sensor to fix this issue." + "description": "State classes `{state_classes}` on source entities `{source_entities}` needs to be same for sensor group `{entity_id}`.\n\nPlease correct the state classes on the source entities and reload the group sensor to fix this issue." } } } From 6a0c3f1b4fdcb5966e36d8d18b7bd983c95723a1 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 12 Feb 2024 15:03:29 +0100 Subject: [PATCH 312/334] Handle no data error in Electricity Maps config flow (#110259) Co-authored-by: Viktor Andersson <30777521+VIKTORVAV99@users.noreply.github.com> --- homeassistant/components/co2signal/config_flow.py | 3 +++ homeassistant/components/co2signal/strings.json | 5 +---- tests/components/co2signal/test_config_flow.py | 8 +++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index a678868ee18..a952f016671 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -8,6 +8,7 @@ from aioelectricitymaps import ( ElectricityMaps, ElectricityMapsError, ElectricityMapsInvalidTokenError, + ElectricityMapsNoDataError, ) import voluptuous as vol @@ -151,6 +152,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await fetch_latest_carbon_intensity(self.hass, em, data) except ElectricityMapsInvalidTokenError: errors["base"] = "invalid_auth" + except ElectricityMapsNoDataError: + errors["base"] = "no_data" except ElectricityMapsError: errors["base"] = "unknown" else: diff --git a/homeassistant/components/co2signal/strings.json b/homeassistant/components/co2signal/strings.json index 89289dd816d..7444cde73d7 100644 --- a/homeassistant/components/co2signal/strings.json +++ b/homeassistant/components/co2signal/strings.json @@ -28,12 +28,9 @@ "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]", - "api_ratelimit": "API Ratelimit exceeded" + "no_data": "No data is available for the location you have selected." }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "unknown": "[%key:common::config_flow::error::unknown%]", - "api_ratelimit": "[%key:component::co2signal::config::error::api_ratelimit%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index 29ce783f33a..518a747f852 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -5,6 +5,7 @@ from aioelectricitymaps import ( ElectricityMapsConnectionError, ElectricityMapsError, ElectricityMapsInvalidTokenError, + ElectricityMapsNoDataError, ) import pytest @@ -139,12 +140,9 @@ async def test_form_country(hass: HomeAssistant) -> None: ), (ElectricityMapsError("Something else"), "unknown"), (ElectricityMapsConnectionError("Boom"), "unknown"), + (ElectricityMapsNoDataError("I have no data"), "no_data"), ], - ids=[ - "invalid auth", - "generic error", - "json decode error", - ], + ids=["invalid auth", "generic error", "json decode error", "no data error"], ) async def test_form_error_handling( hass: HomeAssistant, From e7068ae134bc2b6228ce92ab44cf2594ad771295 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 11 Feb 2024 15:11:31 -0500 Subject: [PATCH 313/334] Fix cpu percentage in System Monitor (#110268) * Fix cpu percentage in System Monitor * Tests --- .../components/systemmonitor/coordinator.py | 13 ++++--- .../components/systemmonitor/sensor.py | 4 ++- tests/components/systemmonitor/test_sensor.py | 35 +++++++++++++++++++ 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/systemmonitor/coordinator.py b/homeassistant/components/systemmonitor/coordinator.py index 9143d31f163..bf625eacf9a 100644 --- a/homeassistant/components/systemmonitor/coordinator.py +++ b/homeassistant/components/systemmonitor/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinators for the System monitor integration.""" + from __future__ import annotations from abc import abstractmethod @@ -43,7 +44,8 @@ dataT = TypeVar( | sswap | VirtualMemory | tuple[float, float, float] - | sdiskusage, + | sdiskusage + | None, ) @@ -130,12 +132,15 @@ class SystemMonitorLoadCoordinator(MonitorCoordinator[tuple[float, float, float] return os.getloadavg() -class SystemMonitorProcessorCoordinator(MonitorCoordinator[float]): +class SystemMonitorProcessorCoordinator(MonitorCoordinator[float | None]): """A System monitor Processor Data Update Coordinator.""" - def update_data(self) -> float: + def update_data(self) -> float | None: """Fetch data.""" - return psutil.cpu_percent(interval=None) + cpu_percent = psutil.cpu_percent(interval=None) + if cpu_percent > 0.0: + return cpu_percent + return None class SystemMonitorBootTimeCoordinator(MonitorCoordinator[datetime]): diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index e751ffebb12..813104e2de3 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -344,7 +344,9 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription[Any]] = { native_unit_of_measurement=PERCENTAGE, icon=get_cpu_icon(), state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda entity: round(entity.coordinator.data), + value_fn=lambda entity: ( + round(entity.coordinator.data) if entity.coordinator.data else None + ), ), "processor_temperature": SysMonitorSensorEntityDescription[ dict[str, list[shwtemp]] diff --git a/tests/components/systemmonitor/test_sensor.py b/tests/components/systemmonitor/test_sensor.py index 8beeddbefdc..b1b06a378f7 100644 --- a/tests/components/systemmonitor/test_sensor.py +++ b/tests/components/systemmonitor/test_sensor.py @@ -1,4 +1,5 @@ """Test System Monitor sensor.""" + from datetime import timedelta import socket from unittest.mock import Mock, patch @@ -429,3 +430,37 @@ async def test_exception_handling_disk_sensor( assert disk_sensor is not None assert disk_sensor.state == "70.0" assert disk_sensor.attributes["unit_of_measurement"] == "%" + + +async def test_cpu_percentage_is_zero_returns_unknown( + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + mock_psutil: Mock, + mock_added_config_entry: ConfigEntry, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the sensor.""" + cpu_sensor = hass.states.get("sensor.system_monitor_processor_use") + assert cpu_sensor is not None + assert cpu_sensor.state == "10" + + mock_psutil.cpu_percent.return_value = 0.0 + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + cpu_sensor = hass.states.get("sensor.system_monitor_processor_use") + assert cpu_sensor is not None + assert cpu_sensor.state == STATE_UNKNOWN + + mock_psutil.cpu_percent.return_value = 15.0 + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + cpu_sensor = hass.states.get("sensor.system_monitor_processor_use") + assert cpu_sensor is not None + assert cpu_sensor.state == "15" From 96a10e76b88cc85e00b34cbe84c99c9ac9f127fe Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 11 Feb 2024 20:06:47 +0100 Subject: [PATCH 314/334] Bump aiopegelonline to 0.0.8 (#110274) --- homeassistant/components/pegel_online/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pegel_online/manifest.json b/homeassistant/components/pegel_online/manifest.json index 9546017d4ff..f38d320b454 100644 --- a/homeassistant/components/pegel_online/manifest.json +++ b/homeassistant/components/pegel_online/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aiopegelonline"], - "requirements": ["aiopegelonline==0.0.6"] + "requirements": ["aiopegelonline==0.0.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 83a7eb0310a..9d656109489 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -318,7 +318,7 @@ aiooncue==0.3.5 aioopenexchangerates==0.4.0 # homeassistant.components.pegel_online -aiopegelonline==0.0.6 +aiopegelonline==0.0.8 # homeassistant.components.acmeda aiopulse==0.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a68230d4019..0bc515e4d1e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -291,7 +291,7 @@ aiooncue==0.3.5 aioopenexchangerates==0.4.0 # homeassistant.components.pegel_online -aiopegelonline==0.0.6 +aiopegelonline==0.0.8 # homeassistant.components.acmeda aiopulse==0.4.4 From 159fab7025f8aaeea50a0777a85e393e0e2bd40d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Feb 2024 13:09:11 -0600 Subject: [PATCH 315/334] Bump PySwitchbot to 0.45.0 (#110275) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 2f92726a6da..401d85e7376 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -39,5 +39,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.44.0"] + "requirements": ["PySwitchbot==0.45.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9d656109489..3656e1c980e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -96,7 +96,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.44.0 +PySwitchbot==0.45.0 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0bc515e4d1e..dd8a1f4514a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.44.0 +PySwitchbot==0.45.0 # homeassistant.components.syncthru PySyncThru==0.7.10 From da6c571e6554d3f62c1140875197d0c0431525e0 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 11 Feb 2024 20:01:16 +0100 Subject: [PATCH 316/334] Update xknxproject to 3.6.0 (#110282) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 397af9ac181..6a304f7de5f 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -12,7 +12,7 @@ "quality_scale": "platinum", "requirements": [ "xknx==2.12.0", - "xknxproject==3.5.0", + "xknxproject==3.6.0", "knx-frontend==2024.1.20.105944" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 3656e1c980e..28d688fbc5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2862,7 +2862,7 @@ xiaomi-ble==0.23.1 xknx==2.12.0 # homeassistant.components.knx -xknxproject==3.5.0 +xknxproject==3.6.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd8a1f4514a..4222538dd14 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2191,7 +2191,7 @@ xiaomi-ble==0.23.1 xknx==2.12.0 # homeassistant.components.knx -xknxproject==3.5.0 +xknxproject==3.6.0 # homeassistant.components.bluesound # homeassistant.components.fritz From 003673cd29360d3ee5fb859d6d55855f539cd904 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 11 Feb 2024 21:16:06 +0100 Subject: [PATCH 317/334] Fix TDBU naming in Motionblinds (#110283) fix TDBU naming --- homeassistant/components/motion_blinds/cover.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index c987e1bb10a..9dde08af5f0 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -400,6 +400,7 @@ class MotionTDBUDevice(MotionPositionDevice): def __init__(self, coordinator, blind, device_class, motor): """Initialize the blind.""" super().__init__(coordinator, blind, device_class) + delattr(self, "_attr_name") self._motor = motor self._motor_key = motor[0] self._attr_translation_key = motor.lower() From da61564f826344f52cb623c50eff900da395264e Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Fri, 16 Feb 2024 07:56:25 -0500 Subject: [PATCH 318/334] Bump linear-garage-door to 0.2.9 (#110298) --- .../linear_garage_door/coordinator.py | 6 +- .../linear_garage_door/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../linear_garage_door/test_config_flow.py | 86 ++++++++++--------- .../linear_garage_door/test_coordinator.py | 28 +----- 6 files changed, 52 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/linear_garage_door/coordinator.py b/homeassistant/components/linear_garage_door/coordinator.py index 5a17d5a39e4..e9234327429 100644 --- a/homeassistant/components/linear_garage_door/coordinator.py +++ b/homeassistant/components/linear_garage_door/coordinator.py @@ -6,11 +6,12 @@ import logging from typing import Any from linear_garage_door import Linear -from linear_garage_door.errors import InvalidLoginError, ResponseError +from linear_garage_door.errors import InvalidLoginError from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -55,6 +56,7 @@ class LinearUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): email=self._email, password=self._password, device_id=self._device_id, + client_session=async_get_clientsession(self.hass), ) except InvalidLoginError as err: if ( @@ -63,8 +65,6 @@ class LinearUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): ): raise ConfigEntryAuthFailed from err raise ConfigEntryNotReady from err - except ResponseError as err: - raise ConfigEntryNotReady from err if not self._devices: self._devices = await linear.get_devices(self._site_id) diff --git a/homeassistant/components/linear_garage_door/manifest.json b/homeassistant/components/linear_garage_door/manifest.json index c7918e21e20..f1eb4302cf0 100644 --- a/homeassistant/components/linear_garage_door/manifest.json +++ b/homeassistant/components/linear_garage_door/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/linear_garage_door", "iot_class": "cloud_polling", - "requirements": ["linear-garage-door==0.2.7"] + "requirements": ["linear-garage-door==0.2.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 28d688fbc5c..6557249b176 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1223,7 +1223,7 @@ lightwave==0.24 limitlessled==1.1.3 # homeassistant.components.linear_garage_door -linear-garage-door==0.2.7 +linear-garage-door==0.2.9 # homeassistant.components.linode linode-api==4.1.9b1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4222538dd14..7aec584006f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -974,7 +974,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.linear_garage_door -linear-garage-door==0.2.7 +linear-garage-door==0.2.9 # homeassistant.components.lamarzocco lmcloud==0.4.35 diff --git a/tests/components/linear_garage_door/test_config_flow.py b/tests/components/linear_garage_door/test_config_flow.py index 64664745c54..5d1ed36ecb7 100644 --- a/tests/components/linear_garage_door/test_config_flow.py +++ b/tests/components/linear_garage_door/test_config_flow.py @@ -65,54 +65,58 @@ async def test_form(hass: HomeAssistant) -> None: async def test_reauth(hass: HomeAssistant) -> None: """Test reauthentication.""" - entry = await async_init_integration(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "entry_id": entry.entry_id, - "title_placeholders": {"name": entry.title}, - "unique_id": entry.unique_id, - }, - data=entry.data, - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - with patch( - "homeassistant.components.linear_garage_door.config_flow.Linear.login", + "homeassistant.components.linear_garage_door.async_setup_entry", return_value=True, - ), patch( - "homeassistant.components.linear_garage_door.config_flow.Linear.get_sites", - return_value=[{"id": "test-site-id", "name": "test-site-name"}], - ), patch( - "homeassistant.components.linear_garage_door.config_flow.Linear.close", - return_value=None, - ), patch( - "uuid.uuid4", - return_value="test-uuid", ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "new-email", - "password": "new-password", + entry = await async_init_integration(hass) + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "title_placeholders": {"name": entry.title}, + "unique_id": entry.unique_id, }, + data=entry.data, ) - await hass.async_block_till_done() + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "user" - assert result2["type"] == FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" + with patch( + "homeassistant.components.linear_garage_door.config_flow.Linear.login", + return_value=True, + ), patch( + "homeassistant.components.linear_garage_door.config_flow.Linear.get_sites", + return_value=[{"id": "test-site-id", "name": "test-site-name"}], + ), patch( + "homeassistant.components.linear_garage_door.config_flow.Linear.close", + return_value=None, + ), patch( + "uuid.uuid4", + return_value="test-uuid", + ): + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + { + "email": "new-email", + "password": "new-password", + }, + ) + await hass.async_block_till_done() - entries = hass.config_entries.async_entries() - assert len(entries) == 1 - assert entries[0].data == { - "email": "new-email", - "password": "new-password", - "site_id": "test-site-id", - "device_id": "test-uuid", - } + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + entries = hass.config_entries.async_entries() + assert len(entries) == 1 + assert entries[0].data == { + "email": "new-email", + "password": "new-password", + "site_id": "test-site-id", + "device_id": "test-uuid", + } async def test_form_invalid_login(hass: HomeAssistant) -> None: diff --git a/tests/components/linear_garage_door/test_coordinator.py b/tests/components/linear_garage_door/test_coordinator.py index fc3087db354..1e46d294f3f 100644 --- a/tests/components/linear_garage_door/test_coordinator.py +++ b/tests/components/linear_garage_door/test_coordinator.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from linear_garage_door.errors import InvalidLoginError, ResponseError +from linear_garage_door.errors import InvalidLoginError from homeassistant.components.linear_garage_door.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -45,32 +45,6 @@ async def test_invalid_password( assert flows[0]["context"]["source"] == "reauth" -async def test_response_error(hass: HomeAssistant) -> None: - """Test response error.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - "email": "test-email", - "password": "test-password", - "site_id": "test-site-id", - "device_id": "test-uuid", - }, - ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.linear_garage_door.coordinator.Linear.login", - side_effect=ResponseError, - ): - assert not await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(DOMAIN) - assert entries - assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.SETUP_RETRY - - async def test_invalid_login( hass: HomeAssistant, ) -> None: From 56ceadaeeb5e558c9dd0a3e5cc07c9ff6d44056a Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Mon, 12 Feb 2024 19:07:22 +0300 Subject: [PATCH 319/334] Fix Starline GPS count sensor (#110348) --- homeassistant/components/starline/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 1a43601940e..4f02ee1a1f6 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -139,7 +139,7 @@ class StarlineSensor(StarlineEntity, SensorEntity): if self._key == "mileage" and self._device.mileage: return self._device.mileage.get("val") if self._key == "gps_count" and self._device.position: - return self._device.position["sat_qty"] + return self._device.position.get("sat_qty") return None @property From de619e4ddce75dc8c3e55666b2df06add3e3014f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Feb 2024 12:47:34 -0600 Subject: [PATCH 320/334] Fix zone radius calculation when radius is not 0 (#110354) --- homeassistant/components/zone/__init__.py | 4 +-- tests/components/zone/test_init.py | 40 +++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 01ec041e9d8..86593c36737 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -135,7 +135,7 @@ def async_active_zone( is None # Skip zone that are outside the radius aka the # lat/long is outside the zone - or not (zone_dist - (radius := zone_attrs[ATTR_RADIUS]) < radius) + or not (zone_dist - (zone_radius := zone_attrs[ATTR_RADIUS]) < radius) ): continue @@ -144,7 +144,7 @@ def async_active_zone( zone_dist < min_dist or ( # If same distance, prefer smaller zone - zone_dist == min_dist and radius < closest.attributes[ATTR_RADIUS] + zone_dist == min_dist and zone_radius < closest.attributes[ATTR_RADIUS] ) ): continue diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index 70a399d27a4..2924e6654e2 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -228,6 +228,46 @@ async def test_in_zone_works_for_passive_zones(hass: HomeAssistant) -> None: assert zone.in_zone(hass.states.get("zone.passive_zone"), latitude, longitude) +async def test_async_active_zone_with_non_zero_radius( + hass: HomeAssistant, +) -> None: + """Test async_active_zone with a non-zero radius.""" + latitude = 32.880600 + longitude = -117.237561 + + assert await setup.async_setup_component( + hass, + zone.DOMAIN, + { + "zone": [ + { + "name": "Small Zone", + "latitude": 32.980600, + "longitude": -117.137561, + "radius": 50000, + }, + { + "name": "Big Zone", + "latitude": 32.980600, + "longitude": -117.137561, + "radius": 100000, + }, + ] + }, + ) + + home_state = hass.states.get("zone.home") + assert home_state.attributes["radius"] == 100 + assert home_state.attributes["latitude"] == 32.87336 + assert home_state.attributes["longitude"] == -117.22743 + + active = zone.async_active_zone(hass, latitude, longitude, 5000) + assert active.entity_id == "zone.home" + + active = zone.async_active_zone(hass, latitude, longitude, 0) + assert active.entity_id == "zone.small_zone" + + async def test_core_config_update(hass: HomeAssistant) -> None: """Test updating core config will update home zone.""" assert await setup.async_setup_component(hass, "zone", {}) From 5ba31290b8d8012c76780c81a836f9d05e0f71b0 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 12 Feb 2024 21:13:47 +0100 Subject: [PATCH 321/334] Bump py-sucks to 0.9.9 (#110397) bump py-sucks to 0.9.9 --- 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 3fcb2b3211e..819f1db2f69 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.1"] + "requirements": ["py-sucks==0.9.9", "deebot-client==5.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6557249b176..dc4d9874492 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1612,7 +1612,7 @@ py-nightscout==1.2.2 py-schluter==0.1.7 # homeassistant.components.ecovacs -py-sucks==0.9.8 +py-sucks==0.9.9 # homeassistant.components.synology_dsm py-synologydsm-api==2.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7aec584006f..1e0ab79946c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1262,7 +1262,7 @@ py-nextbusnext==1.0.2 py-nightscout==1.2.2 # homeassistant.components.ecovacs -py-sucks==0.9.8 +py-sucks==0.9.9 # homeassistant.components.synology_dsm py-synologydsm-api==2.1.4 From fe84e7a576a0c1d33e63f09e268f955887de2104 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 12 Feb 2024 22:31:09 +0100 Subject: [PATCH 322/334] Bump async-upnp-client to 0.38.2 (#110411) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index ab5d035dd54..128822cf289 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "iot_class": "local_push", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.38.1", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index d4a74725467..aaa6e1ee7de 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dms", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["async-upnp-client==0.38.1"], + "requirements": ["async-upnp-client==0.38.2"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 780d47e4743..00b8fec8e6a 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -39,7 +39,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.6.0", "wakeonlan==2.1.0", - "async-upnp-client==0.38.1" + "async-upnp-client==0.38.2" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 8afed8b4fd1..2737565822d 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_push", "loggers": ["async_upnp_client"], "quality_scale": "internal", - "requirements": ["async-upnp-client==0.38.1"] + "requirements": ["async-upnp-client==0.38.2"] } diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 8ce32158016..edfde84a2ac 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.38.1", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index f2a11aaf1fe..20f8ed3ed4d 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["async_upnp_client", "yeelight"], "quality_scale": "platinum", - "requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.1"], + "requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.2"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6a9734ad1fa..2e3943d9044 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiohttp-zlib-ng==0.3.1 aiohttp==3.9.3 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.38.1 +async-upnp-client==0.38.2 atomicwrites-homeassistant==1.4.1 attrs==23.2.0 awesomeversion==24.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index dc4d9874492..f1516fb967f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -478,7 +478,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.1 +async-upnp-client==0.38.2 # homeassistant.components.keyboard_remote asyncinotify==4.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e0ab79946c..171843307c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -430,7 +430,7 @@ arcam-fmj==1.4.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.1 +async-upnp-client==0.38.2 # homeassistant.components.sleepiq asyncsleepiq==1.5.2 From c7634830490649ad782af4dacca71948886705e0 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 16 Feb 2024 05:22:30 -0500 Subject: [PATCH 323/334] Mitigate session closed error in Netgear LTE (#110412) --- homeassistant/components/netgear_lte/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index 9faa2f361b9..491ee0efe59 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -212,7 +212,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: host = entry.data[CONF_HOST] password = entry.data[CONF_PASSWORD] - if DOMAIN not in hass.data: + if not (data := hass.data.get(DOMAIN)) or data.websession.closed: websession = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) hass.data[DOMAIN] = LTEData(websession) @@ -258,7 +258,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.state == ConfigEntryState.LOADED ] if len(loaded_entries) == 1: - hass.data.pop(DOMAIN) + hass.data.pop(DOMAIN, None) return unload_ok From ec7950aeda0d10444bf9617d0c8735055d300a80 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 324/334] 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 f1516fb967f..3a0346acbdd 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 171843307c3..460e103d41a 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 479ecc8b94260a401a405bb5a3ebf261694fc25d Mon Sep 17 00:00:00 2001 From: wilburCforce <109390391+wilburCforce@users.noreply.github.com> Date: Tue, 13 Feb 2024 01:37:58 -0600 Subject: [PATCH 325/334] Update pylutron to 0.2.12 (#110414) --- 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 67ebebcc25b..73f1028bb72 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.11"] + "requirements": ["pylutron==0.2.12"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3a0346acbdd..0c91bfb9e7a 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.11 +pylutron==0.2.12 # homeassistant.components.mailgun pymailgunner==1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 460e103d41a..ada2190a484 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.11 +pylutron==0.2.12 # homeassistant.components.mailgun pymailgunner==1.4 From 9309e38302af2b768e2b304ea87d51248fa9b026 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 13 Feb 2024 15:07:37 +0100 Subject: [PATCH 326/334] Fix Raspberry Pi utilities installation on Alpine 3.19 (#110463) --- machine/raspberrypi | 3 +-- machine/raspberrypi2 | 3 +-- machine/raspberrypi3 | 3 +-- machine/raspberrypi3-64 | 3 +-- machine/raspberrypi4 | 3 +-- machine/raspberrypi4-64 | 3 +-- machine/raspberrypi5-64 | 3 +-- machine/yellow | 3 +-- 8 files changed, 8 insertions(+), 16 deletions(-) diff --git a/machine/raspberrypi b/machine/raspberrypi index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/raspberrypi +++ b/machine/raspberrypi @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils diff --git a/machine/raspberrypi2 b/machine/raspberrypi2 index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/raspberrypi2 +++ b/machine/raspberrypi2 @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils diff --git a/machine/raspberrypi3 b/machine/raspberrypi3 index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/raspberrypi3 +++ b/machine/raspberrypi3 @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils diff --git a/machine/raspberrypi3-64 b/machine/raspberrypi3-64 index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/raspberrypi3-64 +++ b/machine/raspberrypi3-64 @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils diff --git a/machine/raspberrypi4 b/machine/raspberrypi4 index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/raspberrypi4 +++ b/machine/raspberrypi4 @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils diff --git a/machine/raspberrypi4-64 b/machine/raspberrypi4-64 index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/raspberrypi4-64 +++ b/machine/raspberrypi4-64 @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils diff --git a/machine/raspberrypi5-64 b/machine/raspberrypi5-64 index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/raspberrypi5-64 +++ b/machine/raspberrypi5-64 @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils diff --git a/machine/yellow b/machine/yellow index 2ed3b3c8e44..8232d3398a7 100644 --- a/machine/yellow +++ b/machine/yellow @@ -4,5 +4,4 @@ ARG \ FROM $BUILD_FROM RUN apk --no-cache add \ - raspberrypi-userland \ - raspberrypi-userland-libs + raspberrypi-utils From 393359a5466300a4b7d131a08f1caaf72bbb62df Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 15 Feb 2024 11:11:16 -0500 Subject: [PATCH 327/334] Coerce to float in Sensibo climate react custom service (#110508) --- homeassistant/components/sensibo/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index bcc851e02ae..0ad2a0a714f 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -173,9 +173,9 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_ENABLE_CLIMATE_REACT, { - vol.Required(ATTR_HIGH_TEMPERATURE_THRESHOLD): float, + vol.Required(ATTR_HIGH_TEMPERATURE_THRESHOLD): vol.Coerce(float), vol.Required(ATTR_HIGH_TEMPERATURE_STATE): dict, - vol.Required(ATTR_LOW_TEMPERATURE_THRESHOLD): float, + vol.Required(ATTR_LOW_TEMPERATURE_THRESHOLD): vol.Coerce(float), vol.Required(ATTR_LOW_TEMPERATURE_STATE): dict, vol.Required(ATTR_SMART_TYPE): vol.In( ["temperature", "feelsLike", "humidity"] From cdf67e9bb5a6394945d0aee6b6859dd9e4fe34da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Feb 2024 11:57:47 -0600 Subject: [PATCH 328/334] Bump orjson to 3.9.14 (#110552) changelog: https://github.com/ijl/orjson/compare/3.9.13...3.9.14 fixes a crasher due to buffer overread (was only partially fixed in 3.9.13) --- 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 2e3943d9044..380eb593a55 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.13 +orjson==3.9.14 packaging>=23.1 paho-mqtt==1.6.1 Pillow==10.2.0 diff --git a/pyproject.toml b/pyproject.toml index 89551988971..b5d788cc39e 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.13", + "orjson==3.9.14", "packaging>=23.1", "pip>=21.3.1", "python-slugify==8.0.1", diff --git a/requirements.txt b/requirements.txt index 63ea582eba8..76ca93d998d 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.13 +orjson==3.9.14 packaging>=23.1 pip>=21.3.1 python-slugify==8.0.1 From e5db7278e19ca332b9b9790af12f4f6ae85ed532 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Feb 2024 16:03:30 -0600 Subject: [PATCH 329/334] Fix tplink not updating IP from DHCP discovery and discovering twice (#110557) We only called format_mac on the mac address if we connected to the device during entry creation. Since the format of the mac address from DHCP discovery did not match the format saved in the unique id, the IP would not get updated and a second discovery would appear Thankfully the creation path does format the mac so we did not create any entries with an inconsistantly formatted unique id fixes #110460 --- .../components/tplink/config_flow.py | 2 +- tests/components/tplink/__init__.py | 1 + tests/components/tplink/test_config_flow.py | 60 +++++++++++++++++-- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index e1e51f19e3a..10c0c16ff7f 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -61,7 +61,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle discovery via dhcp.""" return await self._async_handle_discovery( - discovery_info.ip, discovery_info.macaddress + discovery_info.ip, dr.format_mac(discovery_info.macaddress) ) async def async_step_integration_discovery( diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 30e59014bbf..4c188fcddcc 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -36,6 +36,7 @@ IP_ADDRESS2 = "127.0.0.2" ALIAS = "My Bulb" MODEL = "HS100" MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" +DHCP_FORMATTED_MAC_ADDRESS = MAC_ADDRESS.replace(":", "") MAC_ADDRESS2 = "11:22:33:44:55:66" DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}" CREDENTIALS_HASH_LEGACY = "" diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index f5b0ba6c41f..edb19a93207 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -33,6 +33,7 @@ from . import ( DEFAULT_ENTRY_TITLE, DEVICE_CONFIG_DICT_AUTH, DEVICE_CONFIG_DICT_LEGACY, + DHCP_FORMATTED_MAC_ADDRESS, IP_ADDRESS, MAC_ADDRESS, MAC_ADDRESS2, @@ -144,6 +145,7 @@ async def test_discovery_auth( assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_ENTRY_TITLE assert result2["data"] == CREATE_ENTRY_DATA_AUTH + assert result2["context"]["unique_id"] == MAC_ADDRESS @pytest.mark.parametrize( @@ -206,6 +208,7 @@ async def test_discovery_auth_errors( ) assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == CREATE_ENTRY_DATA_AUTH + assert result3["context"]["unique_id"] == MAC_ADDRESS async def test_discovery_new_credentials( @@ -254,6 +257,7 @@ async def test_discovery_new_credentials( ) assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == CREATE_ENTRY_DATA_AUTH + assert result3["context"]["unique_id"] == MAC_ADDRESS async def test_discovery_new_credentials_invalid( @@ -309,6 +313,7 @@ async def test_discovery_new_credentials_invalid( ) assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == CREATE_ENTRY_DATA_AUTH + assert result3["context"]["unique_id"] == MAC_ADDRESS async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> None: @@ -365,6 +370,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == CREATE_ENTRY_DATA_LEGACY + assert result3["context"]["unique_id"] == MAC_ADDRESS await hass.async_block_till_done() mock_setup_entry.assert_called_once() @@ -432,6 +438,7 @@ async def test_manual(hass: HomeAssistant) -> None: assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == DEFAULT_ENTRY_TITLE assert result4["data"] == CREATE_ENTRY_DATA_LEGACY + assert result4["context"]["unique_id"] == MAC_ADDRESS # Duplicate result = await hass.config_entries.flow.async_init( @@ -470,6 +477,7 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == CREATE_ENTRY_DATA_LEGACY + assert result["context"]["unique_id"] == MAC_ADDRESS async def test_manual_auth( @@ -510,6 +518,7 @@ async def test_manual_auth( assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == CREATE_ENTRY_DATA_AUTH + assert result3["context"]["unique_id"] == MAC_ADDRESS @pytest.mark.parametrize( @@ -572,6 +581,7 @@ async def test_manual_auth_errors( ) assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["data"] == CREATE_ENTRY_DATA_AUTH + assert result4["context"]["unique_id"] == MAC_ADDRESS await hass.async_block_till_done() @@ -599,7 +609,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( - ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=ALIAS + ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS ), ) await hass.async_block_till_done() @@ -611,7 +621,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( - ip=IP_ADDRESS, macaddress="00:00:00:00:00:00", hostname="mock_hostname" + ip=IP_ADDRESS, macaddress="000000000000", hostname="mock_hostname" ), ) await hass.async_block_till_done() @@ -625,7 +635,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( - ip="1.2.3.5", macaddress="00:00:00:00:00:01", hostname="mock_hostname" + ip="1.2.3.5", macaddress="000000000001", hostname="mock_hostname" ), ) await hass.async_block_till_done() @@ -638,7 +648,9 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: [ ( config_entries.SOURCE_DHCP, - dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=ALIAS), + dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS + ), ), ( config_entries.SOURCE_INTEGRATION_DISCOVERY, @@ -675,6 +687,8 @@ async def test_discovered_by_dhcp_or_discovery( assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == CREATE_ENTRY_DATA_LEGACY + assert result2["context"]["unique_id"] == MAC_ADDRESS + assert mock_async_setup.called assert mock_async_setup_entry.called @@ -684,7 +698,9 @@ async def test_discovered_by_dhcp_or_discovery( [ ( config_entries.SOURCE_DHCP, - dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=ALIAS), + dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS + ), ), ( config_entries.SOURCE_INTEGRATION_DISCOVERY, @@ -713,7 +729,7 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device( assert result["reason"] == "cannot_connect" -async def test_discovery_with_ip_change( +async def test_integration_discovery_with_ip_change( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_discovery: AsyncMock, @@ -764,6 +780,36 @@ async def test_discovery_with_ip_change( mock_connect["connect"].assert_awaited_once_with(config=config) +async def test_dhcp_discovery_with_ip_change( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test dhcp discovery with an IP change.""" + mock_connect["connect"].side_effect = SmartDeviceException() + 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 == config_entries.ConfigEntryState.SETUP_RETRY + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 0 + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_LEGACY + assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.1" + + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="127.0.0.2", macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS + ), + ) + assert discovery_result["type"] is FlowResultType.ABORT + assert discovery_result["reason"] == "already_configured" + assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" + + async def test_reauth( hass: HomeAssistant, mock_added_config_entry: MockConfigEntry, @@ -1022,6 +1068,7 @@ async def test_pick_device_errors( }, ) assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["context"]["unique_id"] == MAC_ADDRESS async def test_discovery_timeout_connect( @@ -1046,6 +1093,7 @@ async def test_discovery_timeout_connect( ) await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["context"]["unique_id"] == MAC_ADDRESS assert mock_connect["connect"].call_count == 1 From 6f529a2c77dcbec4388d4022cedd87adc017337a Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 15 Feb 2024 18:22:23 +0100 Subject: [PATCH 330/334] Modbus, allow received int to be a float. (#110648) --- homeassistant/components/modbus/base_platform.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index cdc1e7a6986..ac11bab303d 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -199,6 +199,8 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): self._precision = config.get(CONF_PRECISION, 2) else: self._precision = config.get(CONF_PRECISION, 0) + if self._precision > 0 or self._scale != int(self._scale): + self._value_is_int = False def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]: """Do swap as needed.""" From bf002ac0b023797cd875ec8c219384c4f9c3840d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Feb 2024 05:27:13 -0600 Subject: [PATCH 331/334] Fix elkm1 service calls running in the executor (#110655) fixes ``` File "/usr/src/homeassistant/homeassistant/components/elkm1/__init__.py", line 416, in _set_time_service _getelk(service).panel.set_time(dt_util.now()) File "/usr/local/lib/python3.11/site-packages/elkm1_lib/panel.py", line 55, in set_time self._connection.send(rw_encode(datetime)) File "/usr/local/lib/python3.11/site-packages/elkm1_lib/connection.py", line 152, in send self._send(QueuedWrite(msg.message, msg.response_command), priority_send) File "/usr/local/lib/python3.11/site-packages/elkm1_lib/connection.py", line 148, in _send self._check_write_queue.set() File "/usr/local/lib/python3.11/asyncio/locks.py", line 192, in set fut.set_result(True) File "/usr/local/lib/python3.11/asyncio/base_events.py", line 763, in call_soon self._check_thread() File "/usr/local/lib/python3.11/asyncio/base_events.py", line 800, in _check_thread raise RuntimeError( RuntimeError: Non-thread-safe operation invoked on an event loop other than the current one ``` --- homeassistant/components/elkm1/__init__.py | 28 ++++++++++++++-------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index b633e1ae620..113fe2ac84e 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -10,7 +10,7 @@ from types import MappingProxyType from typing import Any from elkm1_lib.elements import Element -from elkm1_lib.elk import Elk +from elkm1_lib.elk import Elk, Panel from elkm1_lib.util import parse_url import voluptuous as vol @@ -398,22 +398,30 @@ async def async_wait_for_elk_to_sync( return success +@callback +def _async_get_elk_panel(hass: HomeAssistant, service: ServiceCall) -> Panel: + """Get the ElkM1 panel from a service call.""" + prefix = service.data["prefix"] + elk = _find_elk_by_prefix(hass, prefix) + if elk is None: + raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found") + return elk.panel + + def _create_elk_services(hass: HomeAssistant) -> None: - def _getelk(service: ServiceCall) -> Elk: - prefix = service.data["prefix"] - elk = _find_elk_by_prefix(hass, prefix) - if elk is None: - raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found") - return elk + """Create ElkM1 services.""" + @callback def _speak_word_service(service: ServiceCall) -> None: - _getelk(service).panel.speak_word(service.data["number"]) + _async_get_elk_panel(hass, service).speak_word(service.data["number"]) + @callback def _speak_phrase_service(service: ServiceCall) -> None: - _getelk(service).panel.speak_phrase(service.data["number"]) + _async_get_elk_panel(hass, service).speak_phrase(service.data["number"]) + @callback def _set_time_service(service: ServiceCall) -> None: - _getelk(service).panel.set_time(dt_util.now()) + _async_get_elk_panel(hass, service).set_time(dt_util.now()) hass.services.async_register( DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA From 34a3e88e0dea101e597ed6faaeef4ff770bb01bd Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 15 Feb 2024 19:17:06 +0100 Subject: [PATCH 332/334] Bump aiounifi to v71 (#110658) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index f69dffc2d57..f3092811227 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==70"], + "requirements": ["aiounifi==71"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 0c91bfb9e7a..e19a9b50c5f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -383,7 +383,7 @@ aiotankerkoenig==0.3.0 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==70 +aiounifi==71 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ada2190a484..2e6c0825674 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,7 +356,7 @@ aiotankerkoenig==0.3.0 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==70 +aiounifi==71 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 8c05ebd031d16d47a1efe11cb1537a246fa6d86f Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 16 Feb 2024 11:47:36 +0100 Subject: [PATCH 333/334] Bump deebot-client to 5.2.1 (#110683) * Bump deebot-client to 5.2.0 * Bumb again * Fix tests --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ecovacs/conftest.py | 12 ++++++++---- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 819f1db2f69..61bd425b139 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.9", "deebot-client==5.1.1"] + "requirements": ["py-sucks==0.9.9", "deebot-client==5.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index e19a9b50c5f..70c74a10dd4 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.1 +deebot-client==5.2.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e6c0825674..89ec09e301c 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.1 +deebot-client==5.2.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/tests/components/ecovacs/conftest.py b/tests/components/ecovacs/conftest.py index d0f0668cc8c..31d7246e6bc 100644 --- a/tests/components/ecovacs/conftest.py +++ b/tests/components/ecovacs/conftest.py @@ -3,7 +3,7 @@ from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch -from deebot_client.const import PATH_API_APPSVR_APP +from deebot_client import const from deebot_client.device import Device from deebot_client.exceptions import ApiError from deebot_client.models import Credentials @@ -75,9 +75,13 @@ def mock_authenticator(device_fixture: str) -> Generator[Mock, None, None]: query_params: dict[str, Any] | None = None, headers: dict[str, Any] | None = None, ) -> dict[str, Any]: - if path == PATH_API_APPSVR_APP: - return {"code": 0, "devices": devices, "errno": "0"} - raise ApiError("Path not mocked: {path}") + match path: + case const.PATH_API_APPSVR_APP: + return {"code": 0, "devices": devices, "errno": "0"} + case const.PATH_API_USERS_USER: + return {"todo": "result", "result": "ok", "devices": devices} + case _: + raise ApiError("Path not mocked: {path}") authenticator.post_authenticated.side_effect = post_authenticated yield authenticator From b55b2c8da305e57242274e28212ab521f4616c61 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Feb 2024 14:13:26 +0100 Subject: [PATCH 334/334] Bump version to 2024.2.2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a19ff18d8f3..84730bbf6d2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index b5d788cc39e..0f9f9187e9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.2.1" +version = "2024.2.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"