From 39e9ffff29120600f673101289eb4601f6ad0c53 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Mon, 8 Sep 2025 20:45:42 +1000 Subject: [PATCH 1/8] Bump aiolifx-themes to 1.0.2 to support newer LIFX devices (#151898) Signed-off-by: Avi Miller --- homeassistant/components/lifx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 3c755779846bea..d7f50ca493bcd7 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -54,6 +54,6 @@ "requirements": [ "aiolifx==1.2.1", "aiolifx-effects==0.3.2", - "aiolifx-themes==0.6.4" + "aiolifx-themes==1.0.2" ] } diff --git a/requirements_all.txt b/requirements_all.txt index ca72e573a34033..111cb2832a709b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -298,7 +298,7 @@ aiokem==1.0.1 aiolifx-effects==0.3.2 # homeassistant.components.lifx -aiolifx-themes==0.6.4 +aiolifx-themes==1.0.2 # homeassistant.components.lifx aiolifx==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3fa2c91299e0b8..200f0a901b6026 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -280,7 +280,7 @@ aiokem==1.0.1 aiolifx-effects==0.3.2 # homeassistant.components.lifx -aiolifx-themes==0.6.4 +aiolifx-themes==1.0.2 # homeassistant.components.lifx aiolifx==1.2.1 From 1536375e82ef6aad723b6b4b18b876c6d92221d6 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 8 Sep 2025 12:48:23 +0200 Subject: [PATCH 2/8] Fix update of the entity ID does not clean up an old restored state (#151696) Co-authored-by: Erik Montnemery --- homeassistant/helpers/entity_registry.py | 16 +++++++- tests/helpers/test_entity_registry.py | 51 +++++++++++++++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index e8f1dea06397c9..3b0cb67f6a260e 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -1899,11 +1899,25 @@ def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) - @callback def cleanup_restored_states_filter(event_data: Mapping[str, Any]) -> bool: """Clean up restored states filter.""" - return bool(event_data["action"] == "remove") + return (event_data["action"] == "remove") or ( + event_data["action"] == "update" + and "old_entity_id" in event_data + and event_data["entity_id"] != event_data["old_entity_id"] + ) @callback def cleanup_restored_states(event: Event[EventEntityRegistryUpdatedData]) -> None: """Clean up restored states.""" + if event.data["action"] == "update": + old_entity_id = event.data["old_entity_id"] + old_state = hass.states.get(old_entity_id) + if old_state is None or not old_state.attributes.get(ATTR_RESTORED): + return + hass.states.async_remove(old_entity_id, context=event.context) + if entry := registry.async_get(event.data["entity_id"]): + entry.write_unavailable_state(hass) + return + state = hass.states.get(event.data["entity_id"]) if state is None or not state.attributes.get(ATTR_RESTORED): diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 421f52bca73769..593e1ea97032a6 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1462,9 +1462,56 @@ async def test_update_entity_unique_id_conflict( ) -async def test_update_entity_entity_id(entity_registry: er.EntityRegistry) -> None: - """Test entity's entity_id is updated.""" +async def test_update_entity_entity_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test entity's entity_id is updated for entity with a restored state.""" + hass.set_state(CoreState.not_running) + + mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + mock_config.add_to_hass(hass) + entry = entity_registry.async_get_or_create( + "light", "hue", "5678", config_entry=mock_config + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + assert ( + entity_registry.async_get_entity_id("light", "hue", "5678") == entry.entity_id + ) + state = hass.states.get(entry.entity_id) + assert state is not None + assert state.state == "unavailable" + assert state.attributes == {"restored": True, "supported_features": 0} + + new_entity_id = "light.blah" + assert new_entity_id != entry.entity_id + with patch.object(entity_registry, "async_schedule_save") as mock_schedule_save: + updated_entry = entity_registry.async_update_entity( + entry.entity_id, new_entity_id=new_entity_id + ) + assert updated_entry != entry + assert updated_entry.entity_id == new_entity_id + assert mock_schedule_save.call_count == 1 + + assert entity_registry.async_get(entry.entity_id) is None + assert entity_registry.async_get(new_entity_id) is not None + + # The restored state should be removed + old_state = hass.states.get(entry.entity_id) + assert old_state is None + + # The new entity should have an unavailable initial state + new_state = hass.states.get(new_entity_id) + assert new_state is not None + assert new_state.state == "unavailable" + + +async def test_update_entity_entity_id_without_state( + entity_registry: er.EntityRegistry, +) -> None: + """Test entity's entity_id is updated for entity without a state.""" entry = entity_registry.async_get_or_create("light", "hue", "5678") + assert ( entity_registry.async_get_entity_id("light", "hue", "5678") == entry.entity_id ) From 064d43480d75ae3c4a89a7a007047b614acd1091 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 8 Sep 2025 12:58:26 +0200 Subject: [PATCH 3/8] Bump aiovodafone to 1.2.1 (#151901) --- homeassistant/components/vodafone_station/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vodafone_station/manifest.json b/homeassistant/components/vodafone_station/manifest.json index 4c33cf1a4a5a31..a9ee2f49b4c659 100644 --- a/homeassistant/components/vodafone_station/manifest.json +++ b/homeassistant/components/vodafone_station/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "loggers": ["aiovodafone"], "quality_scale": "platinum", - "requirements": ["aiovodafone==0.10.0"] + "requirements": ["aiovodafone==1.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 111cb2832a709b..4f8e9fcc7b7da2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -426,7 +426,7 @@ aiousbwatcher==1.1.1 aiovlc==0.5.1 # homeassistant.components.vodafone_station -aiovodafone==0.10.0 +aiovodafone==1.2.1 # homeassistant.components.waqi aiowaqi==3.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 200f0a901b6026..0ca7f98889957f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -408,7 +408,7 @@ aiousbwatcher==1.1.1 aiovlc==0.5.1 # homeassistant.components.vodafone_station -aiovodafone==0.10.0 +aiovodafone==1.2.1 # homeassistant.components.waqi aiowaqi==3.1.0 From 98df5f5f0c227dffb2fd66f26de157b8e2f46a27 Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:20:11 +0200 Subject: [PATCH 4/8] Validate selectors in the condition helper (#151884) --- homeassistant/helpers/condition.py | 9 +++- tests/helpers/test_condition.py | 75 +++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index d9f16217c2e2a0..67c99eb70b478f 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -30,6 +30,7 @@ CONF_FOR, CONF_ID, CONF_MATCH, + CONF_SELECTOR, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WEEKDAY, @@ -59,9 +60,10 @@ from homeassistant.util.hass_dict import HassKey from homeassistant.util.yaml import load_yaml_dict -from . import config_validation as cv, entity_registry as er +from . import config_validation as cv, entity_registry as er, selector from .automation import get_absolute_description_key, get_relative_description_key from .integration_platform import async_process_integration_platforms +from .selector import TargetSelector from .template import Template, render_complex from .trace import ( TraceElement, @@ -110,12 +112,15 @@ # Basic schemas to sanity check the condition descriptions, # full validation is done by hassfest.conditions _FIELD_SCHEMA = vol.Schema( - {}, + { + vol.Optional(CONF_SELECTOR): selector.validate_selector, + }, extra=vol.ALLOW_EXTRA, ) _CONDITION_SCHEMA = vol.Schema( { + vol.Optional("target"): TargetSelector.CONFIG_SCHEMA, vol.Optional("fields"): vol.Schema({str: _FIELD_SCHEMA}), }, extra=vol.ALLOW_EXTRA, diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index b037d6a450ee87..fef476556dc895 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -2385,7 +2385,15 @@ async def test_async_get_all_descriptions( ) -> None: """Test async_get_all_descriptions.""" device_automation_condition_descriptions = """ - _device: {} + _device: + fields: + entity: + selector: + entity: + filter: + domain: alarm_control_panel + supported_features: + - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME """ assert await async_setup_component(hass, DOMAIN_SUN, {}) @@ -2427,14 +2435,28 @@ def _load_yaml(fname, secrets=None): "fields": { "after": { "example": "sunrise", - "selector": {"select": {"options": ["sunrise", "sunset"]}}, + "selector": { + "select": { + "custom_value": False, + "multiple": False, + "options": ["sunrise", "sunset"], + "sort": False, + } + }, }, - "after_offset": {"selector": {"time": None}}, + "after_offset": {"selector": {"time": {}}}, "before": { "example": "sunrise", - "selector": {"select": {"options": ["sunrise", "sunset"]}}, + "selector": { + "select": { + "custom_value": False, + "multiple": False, + "options": ["sunrise", "sunset"], + "sort": False, + } + }, }, - "before_offset": {"selector": {"time": None}}, + "before_offset": {"selector": {"time": {}}}, } } } @@ -2456,21 +2478,50 @@ def _load_yaml(fname, secrets=None): new_descriptions = await condition.async_get_all_descriptions(hass) assert new_descriptions is not descriptions assert new_descriptions == { - "device": { - "fields": {}, - }, "sun": { "fields": { "after": { "example": "sunrise", - "selector": {"select": {"options": ["sunrise", "sunset"]}}, + "selector": { + "select": { + "custom_value": False, + "multiple": False, + "options": ["sunrise", "sunset"], + "sort": False, + } + }, }, - "after_offset": {"selector": {"time": None}}, + "after_offset": {"selector": {"time": {}}}, "before": { "example": "sunrise", - "selector": {"select": {"options": ["sunrise", "sunset"]}}, + "selector": { + "select": { + "custom_value": False, + "multiple": False, + "options": ["sunrise", "sunset"], + "sort": False, + } + }, + }, + "before_offset": {"selector": {"time": {}}}, + } + }, + "device": { + "fields": { + "entity": { + "selector": { + "entity": { + "filter": [ + { + "domain": ["alarm_control_panel"], + "supported_features": [1], + } + ], + "multiple": False, + "reorder": False, + }, + }, }, - "before_offset": {"selector": {"time": None}}, } }, } From f2204e97ab48fdca0667622a50929d3549bc24de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:49:05 +0200 Subject: [PATCH 5/8] Bump github/codeql-action from 3.30.0 to 3.30.1 (#151890) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8d9c71eb1242a3..405b276224b85b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v5.0.0 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.30.0 + uses: github/codeql-action/init@v3.30.1 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.30.0 + uses: github/codeql-action/analyze@v3.30.1 with: category: "/language:python" From b7360dfad8b139aa4fe4654e4f524b7d8ac0ee1f Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Mon, 8 Sep 2025 07:01:08 -0700 Subject: [PATCH 6/8] Allow deleting kitchen_sink devices (#151826) --- homeassistant/components/kitchen_sink/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/homeassistant/components/kitchen_sink/__init__.py b/homeassistant/components/kitchen_sink/__init__.py index 8b81cd492798fd..e6a2e98bcafdef 100644 --- a/homeassistant/components/kitchen_sink/__init__.py +++ b/homeassistant/components/kitchen_sink/__init__.py @@ -32,6 +32,7 @@ ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util @@ -117,6 +118,20 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove a config entry from a device.""" + + # Allow deleting any device except statistics_issues, just to give + # something to test the negative case. + for identifier in device_entry.identifiers: + if identifier[0] == DOMAIN and identifier[1] == "statistics_issues": + return False + + return True + + async def _notify_backup_listeners(hass: HomeAssistant) -> None: for listener in hass.data.get(DATA_BACKUP_AGENT_LISTENERS, []): listener() From 56c865dcfe086782c4c169cb10a7a521d84be583 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:35:33 +0200 Subject: [PATCH 7/8] Fix _is_valid_suggested_unit in sensor platform (#151912) --- homeassistant/components/sensor/__init__.py | 2 +- tests/components/sensor/test_init.py | 8 + .../tuya/snapshots/test_sensor.ambr | 224 ++++++++++++++++++ 3 files changed, 233 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 5617170733871e..0268bd8b207185 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -365,7 +365,7 @@ def _is_valid_suggested_unit(self, suggested_unit_of_measurement: str) -> bool: unit converter supports both the native and the suggested units of measurement. """ # Make sure we can convert the units - if ( + if self.native_unit_of_measurement != suggested_unit_of_measurement and ( (unit_converter := UNIT_CONVERTERS.get(self.device_class)) is None or self.__native_unit_of_measurement_compat not in unit_converter.VALID_UNITS diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index ce78edfe481ff4..c31abe62826a97 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -32,6 +32,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, STATE_UNKNOWN, EntityCategory, @@ -2938,6 +2939,13 @@ async def test_suggested_unit_guard_invalid_unit( UnitOfDataRate.BITS_PER_SECOND, 10000, ), + ( + SensorDeviceClass.CO2, + CONCENTRATION_PARTS_PER_MILLION, + 10, + CONCENTRATION_PARTS_PER_MILLION, + 10, + ), ], ) async def test_suggested_unit_guard_valid_unit( diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index ffb7e8f4bada84..82b7c43c96fce0 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -507,6 +507,62 @@ 'state': '100.0', }) # --- +# name: test_platform_setup_and_discovery[sensor.aqi_carbon_dioxide-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aqi_carbon_dioxide', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'ppm', + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Carbon dioxide', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'carbon_dioxide', + 'unique_id': 'tuya.iks13mcaiyie3rryjb2occo2_value', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.aqi_carbon_dioxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'AQI Carbon dioxide', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.aqi_carbon_dioxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '541.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.aqi_formaldehyde-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -612,6 +668,62 @@ 'state': '53.0', }) # --- +# name: test_platform_setup_and_discovery[sensor.aqi_pm2_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aqi_pm2_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'μg/m³', + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM2.5', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'pm25', + 'unique_id': 'tuya.iks13mcaiyie3rryjb2ocpm25_value', + 'unit_of_measurement': 'μg/m³', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.aqi_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'AQI PM2.5', + 'state_class': , + 'unit_of_measurement': 'μg/m³', + }), + 'context': , + 'entity_id': 'sensor.aqi_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '17.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.aqi_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -8146,6 +8258,62 @@ 'state': '42.0', }) # --- +# name: test_platform_setup_and_discovery[sensor.kalado_air_purifier_pm2_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.kalado_air_purifier_pm2_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'μg/m³', + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM2.5', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'pm25', + 'unique_id': 'tuya.yo2karkjuhzztxsfjkpm25', + 'unit_of_measurement': 'μg/m³', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.kalado_air_purifier_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'Kalado Air Purifier PM2.5', + 'state_class': , + 'unit_of_measurement': 'μg/m³', + }), + 'context': , + 'entity_id': 'sensor.kalado_air_purifier_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.keller_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -13310,6 +13478,62 @@ 'state': '97.0', }) # --- +# name: test_platform_setup_and_discovery[sensor.smogo_carbon_monoxide-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smogo_carbon_monoxide', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'ppm', + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Carbon monoxide', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'carbon_monoxide', + 'unique_id': 'tuya.swhtzki3qrz5ydchjbocco_value', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smogo_carbon_monoxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_monoxide', + 'friendly_name': 'Smogo Carbon monoxide', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.smogo_carbon_monoxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.smoke_alarm_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 3cbf3bdf4c6b9b01881f5d3e4303e8c20fd02889 Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:16:23 +0200 Subject: [PATCH 8/8] Remove Kodi media player platform yaml support (#151786) --- homeassistant/components/kodi/config_flow.py | 22 ---- homeassistant/components/kodi/media_player.py | 104 +----------------- tests/components/kodi/test_config_flow.py | 97 ---------------- 3 files changed, 3 insertions(+), 220 deletions(-) diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index 0bd51f27ab67e1..30cffded66022e 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -233,28 +233,6 @@ async def async_step_ws_port( return self._show_ws_port_form(errors) - async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: - """Handle import from YAML.""" - reason = None - try: - await validate_http(self.hass, import_data) - await validate_ws(self.hass, import_data) - except InvalidAuth: - _LOGGER.exception("Invalid Kodi credentials") - reason = "invalid_auth" - except CannotConnect: - _LOGGER.exception("Cannot connect to Kodi") - reason = "cannot_connect" - except Exception: - _LOGGER.exception("Unexpected exception") - reason = "unknown" - else: - return self.async_create_entry( - title=import_data[CONF_NAME], data=import_data - ) - - return self.async_abort(reason=reason) - @callback def _show_credentials_form( self, errors: dict[str, str] | None = None diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 2e32d969fcea80..1efa6bec296922 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -15,7 +15,6 @@ from homeassistant.components import media_source from homeassistant.components.media_player import ( - PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, BrowseError, BrowseMedia, MediaPlayerEntity, @@ -24,19 +23,11 @@ MediaType, async_process_play_media_url, ) -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_ID, - CONF_HOST, CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_PROXY_SSL, - CONF_SSL, - CONF_TIMEOUT, CONF_TYPE, - CONF_USERNAME, EVENT_HOMEASSISTANT_STARTED, ) from homeassistant.core import CoreState, HomeAssistant, callback @@ -46,13 +37,10 @@ entity_platform, ) from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity_platform import ( - AddConfigEntryEntitiesCallback, - AddEntitiesCallback, -) +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.network import is_internal_request -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolDictType +from homeassistant.helpers.typing import VolDictType from homeassistant.util import dt as dt_util from . import KodiConfigEntry @@ -62,35 +50,12 @@ library_payload, media_source_content_filter, ) -from .const import ( - CONF_WS_PORT, - DEFAULT_PORT, - DEFAULT_SSL, - DEFAULT_TIMEOUT, - DEFAULT_WS_PORT, - DOMAIN, - EVENT_TURN_OFF, - EVENT_TURN_ON, -) +from .const import DOMAIN, EVENT_TURN_OFF, EVENT_TURN_ON _LOGGER = logging.getLogger(__name__) EVENT_KODI_CALL_METHOD_RESULT = "kodi_call_method_result" -CONF_TCP_PORT = "tcp_port" -CONF_TURN_ON_ACTION = "turn_on_action" -CONF_TURN_OFF_ACTION = "turn_off_action" -CONF_ENABLE_WEBSOCKET = "enable_websocket" - -DEPRECATED_TURN_OFF_ACTIONS = { - None: None, - "quit": "Application.Quit", - "hibernate": "System.Hibernate", - "suspend": "System.Suspend", - "reboot": "System.Reboot", - "shutdown": "System.Shutdown", -} - WEBSOCKET_WATCHDOG_INTERVAL = timedelta(seconds=10) # https://github.com/xbmc/xbmc/blob/master/xbmc/media/MediaType.h @@ -120,25 +85,6 @@ } -PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_TCP_PORT, default=DEFAULT_WS_PORT): cv.port, - vol.Optional(CONF_PROXY_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_TURN_ON_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_TURN_OFF_ACTION): vol.Any( - cv.SCRIPT_SCHEMA, vol.In(DEPRECATED_TURN_OFF_ACTIONS) - ), - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Inclusive(CONF_USERNAME, "auth"): cv.string, - vol.Inclusive(CONF_PASSWORD, "auth"): cv.string, - vol.Optional(CONF_ENABLE_WEBSOCKET, default=True): cv.boolean, - } -) - - SERVICE_ADD_MEDIA = "add_to_playlist" SERVICE_CALL_METHOD = "call_method" @@ -161,50 +107,6 @@ ) -def find_matching_config_entries_for_host(hass, host): - """Search existing config entries for one matching the host.""" - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_HOST] == host: - return entry - return None - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Kodi platform.""" - if discovery_info: - # Now handled by zeroconf in the config flow - return - - host = config[CONF_HOST] - if find_matching_config_entries_for_host(hass, host): - return - - websocket = config.get(CONF_ENABLE_WEBSOCKET) - ws_port = config.get(CONF_TCP_PORT) if websocket else None - - entry_data = { - CONF_NAME: config.get(CONF_NAME, host), - CONF_HOST: host, - CONF_PORT: config.get(CONF_PORT), - CONF_WS_PORT: ws_port, - CONF_USERNAME: config.get(CONF_USERNAME), - CONF_PASSWORD: config.get(CONF_PASSWORD), - CONF_SSL: config.get(CONF_PROXY_SSL), - CONF_TIMEOUT: config.get(CONF_TIMEOUT), - } - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_data - ) - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: KodiConfigEntry, diff --git a/tests/components/kodi/test_config_flow.py b/tests/components/kodi/test_config_flow.py index ad99067ac7abf8..d8968ef1449909 100644 --- a/tests/components/kodi/test_config_flow.py +++ b/tests/components/kodi/test_config_flow.py @@ -18,7 +18,6 @@ TEST_DISCOVERY, TEST_DISCOVERY_WO_UUID, TEST_HOST, - TEST_IMPORT, TEST_WS_PORT, UUID, MockConnection, @@ -666,99 +665,3 @@ async def test_discovery_without_unique_id(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_uuid" - - -async def test_form_import(hass: HomeAssistant) -> None: - """Test we get the form with import source.""" - with ( - patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - return_value=True, - ), - patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), - ), - patch( - "homeassistant.components.kodi.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=TEST_IMPORT, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == TEST_IMPORT["name"] - assert result["data"] == TEST_IMPORT - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_import_invalid_auth(hass: HomeAssistant) -> None: - """Test we handle invalid auth on import.""" - with ( - patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=InvalidAuthError, - ), - patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), - ), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=TEST_IMPORT, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "invalid_auth" - - -async def test_form_import_cannot_connect(hass: HomeAssistant) -> None: - """Test we handle cannot connect on import.""" - with ( - patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=CannotConnectError, - ), - patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), - ), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=TEST_IMPORT, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "cannot_connect" - - -async def test_form_import_exception(hass: HomeAssistant) -> None: - """Test we handle unknown exception on import.""" - with ( - patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=Exception, - ), - patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), - ), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=TEST_IMPORT, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "unknown"