Compare commits

...

8 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] afb90c91db Merge remote-tracking branch 'origin/dev' into repair_trigger_behavior
# Conflicts:
#	tests/helpers/test_trigger.py
2026-06-17 05:57:00 +00:00
epenet d9e2b49c0c Fix incorrect use of entity component constants in template (#172532) 2026-06-17 07:55:57 +02:00
renovate[bot] 4f9051464d Update cryptography to 48.0.1 (#174096) 2026-06-17 07:34:00 +02:00
Paulus Schoutsen 87894fd623 Activate venv before running python commands (#174093) 2026-06-17 07:32:22 +02:00
Franck Nijhof 34a70a9210 Clean up deprecated solar_rising entity from sun integration (#174079) 2026-06-17 06:44:16 +02:00
Paulus Schoutsen c9fb6a13fb Remove stale requirements_test_all.txt reference (#174095) 2026-06-17 05:08:20 +02:00
Erik 58f247afca Address review comments 2026-06-16 07:51:39 +02:00
Erik b9688b7fb2 Open repair issue when deprecated trigger behavior is used 2026-06-08 10:13:36 +02:00
15 changed files with 152 additions and 30 deletions
@@ -135,6 +135,10 @@
"description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove the {domain} entry from your configuration.yaml file and restart Home Assistant.",
"title": "The {integration_title} integration is being removed"
},
"deprecated_trigger_behavior": {
"description": "An automation, script or template entity uses the trigger behavior option `{deprecated_behavior}`, which has been renamed to `{new_behavior}`. The old value still works for now, but support for it will be removed in a future release.\n\nTo fix this issue, edit the affected automations and scripts and change the behavior option from `behavior: {deprecated_behavior}` to `behavior: {new_behavior}`, then restart Home Assistant.",
"title": "Deprecated trigger behavior option in use"
},
"deprecated_yaml": {
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
"title": "The {integration_title} YAML configuration is being removed"
+8 -1
View File
@@ -5,7 +5,7 @@ import logging
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
@@ -50,6 +50,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: SunConfigEntry) -> bool:
"""Set up from a config entry."""
# Remove deprecated solar_rising sensor entity (removed in 2026.1)
ent_reg = er.async_get(hass)
if entity_id := ent_reg.async_get_entity_id(
Platform.SENSOR, DOMAIN, f"{entry.entry_id}-solar_rising"
):
ent_reg.async_remove(entity_id)
sun = Sun(hass)
component = EntityComponent[Sun](_LOGGER, DOMAIN, hass)
await component.async_add_entities([sun])
+16 -10
View File
@@ -1,15 +1,12 @@
"""Support for Template fans."""
from enum import StrEnum
import logging
from typing import TYPE_CHECKING, Any
import voluptuous as vol
from homeassistant.components.fan import (
ATTR_DIRECTION,
ATTR_OSCILLATING,
ATTR_PERCENTAGE,
ATTR_PRESET_MODE,
DIRECTION_FORWARD,
DIRECTION_REVERSE,
DOMAIN as FAN_DOMAIN,
@@ -100,6 +97,15 @@ FAN_CONFIG_ENTRY_SCHEMA = FAN_COMMON_SCHEMA.extend(
)
class FanScriptVariable(StrEnum):
"""Variables for scripts."""
DIRECTION = "direction"
OSCILLATING = "oscillating"
PERCENTAGE = "percentage"
PRESET_MODE = "preset_mode"
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
@@ -235,8 +241,8 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
await self.async_run_script(
self._action_scripts[CONF_ON_ACTION],
run_variables={
ATTR_PERCENTAGE: percentage,
ATTR_PRESET_MODE: preset_mode,
FanScriptVariable.PERCENTAGE: percentage,
FanScriptVariable.PRESET_MODE: preset_mode,
},
context=self._context,
)
@@ -267,7 +273,7 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
if script := self._action_scripts.get(CONF_SET_PERCENTAGE_ACTION):
await self.async_run_script(
script,
run_variables={ATTR_PERCENTAGE: self._attr_percentage},
run_variables={FanScriptVariable.PERCENTAGE: self._attr_percentage},
context=self._context,
)
@@ -284,7 +290,7 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
if script := self._action_scripts.get(CONF_SET_PRESET_MODE_ACTION):
await self.async_run_script(
script,
run_variables={ATTR_PRESET_MODE: self._attr_preset_mode},
run_variables={FanScriptVariable.PRESET_MODE: self._attr_preset_mode},
context=self._context,
)
@@ -302,7 +308,7 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
) is not None:
await self.async_run_script(
script,
run_variables={ATTR_OSCILLATING: self.oscillating},
run_variables={FanScriptVariable.OSCILLATING: self.oscillating},
context=self._context,
)
@@ -318,7 +324,7 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
) is not None:
await self.async_run_script(
script,
run_variables={ATTR_DIRECTION: direction},
run_variables={FanScriptVariable.DIRECTION: direction},
context=self._context,
)
if CONF_DIRECTION not in self._templates:
+1 -2
View File
@@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Any
import voluptuous as vol
from homeassistant.components.number import (
ATTR_VALUE,
DEFAULT_MAX_VALUE,
DEFAULT_MIN_VALUE,
DEFAULT_STEP,
@@ -161,7 +160,7 @@ class AbstractTemplateNumber(AbstractTemplateEntity, NumberEntity):
if set_value := self._action_scripts.get(CONF_SET_VALUE):
await self.async_run_script(
set_value,
run_variables={ATTR_VALUE: value},
run_variables={"value": value},
context=self._context,
)
+3 -5
View File
@@ -6,8 +6,6 @@ from typing import TYPE_CHECKING, Any
import voluptuous as vol
from homeassistant.components.select import (
ATTR_OPTION,
ATTR_OPTIONS,
DOMAIN as SELECT_DOMAIN,
ENTITY_ID_FORMAT,
SelectEntity,
@@ -48,7 +46,7 @@ SCRIPT_FIELDS = (CONF_SELECT_OPTION,)
SELECT_COMMON_SCHEMA = vol.Schema(
{
vol.Required(ATTR_OPTIONS): cv.template,
vol.Required(CONF_OPTIONS): cv.template,
vol.Optional(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_STATE): cv.template,
}
@@ -147,7 +145,7 @@ class AbstractTemplateSelect(AbstractTemplateEntity, SelectEntity):
if select_option := self._action_scripts.get(CONF_SELECT_OPTION):
await self.async_run_script(
select_option,
run_variables={ATTR_OPTION: option},
run_variables={"option": option},
context=self._context,
)
@@ -175,7 +173,7 @@ class TriggerSelectEntity(TriggerEntity, AbstractTemplateSelect):
"""Select entity based on trigger data."""
domain = SELECT_DOMAIN
extra_template_keys_complex = (ATTR_OPTIONS,)
extra_template_keys_complex = (CONF_OPTIONS,)
def __init__(
self,
+5 -5
View File
@@ -9,7 +9,6 @@ from typing import Any
import voluptuous as vol
from homeassistant.components.sensor import (
ATTR_LAST_RESET,
CONF_STATE_CLASS,
DEVICE_CLASSES_SCHEMA,
DOMAIN as SENSOR_DOMAIN,
@@ -50,13 +49,14 @@ from .schemas import (
from .template_entity import TemplateEntity
from .trigger_entity import TriggerEntity
CONF_LAST_RESET = "last_reset"
DEFAULT_NAME = "Template Sensor"
def validate_last_reset(val):
"""Run extra validation checks."""
if (
val.get(ATTR_LAST_RESET) is not None
val.get(CONF_LAST_RESET) is not None
and val.get(CONF_STATE_CLASS) != SensorStateClass.TOTAL
):
raise vol.Invalid(
@@ -78,7 +78,7 @@ SENSOR_COMMON_SCHEMA = vol.Schema(
SENSOR_YAML_SCHEMA = vol.All(
vol.Schema(
{
vol.Optional(ATTR_LAST_RESET): cv.template,
vol.Optional(CONF_LAST_RESET): cv.template,
}
)
.extend(SENSOR_COMMON_SCHEMA.schema)
@@ -204,10 +204,10 @@ class AbstractTemplateSensor(AbstractTemplateEntity, RestoreSensor):
self._validate_state,
)
self.setup_template(
ATTR_LAST_RESET,
CONF_LAST_RESET,
"_attr_last_reset",
validate_datetime(
self, ATTR_LAST_RESET, SensorDeviceClass.TIMESTAMP, require_tzinfo=False
self, CONF_LAST_RESET, SensorDeviceClass.TIMESTAMP, require_tzinfo=False
),
)
+1 -2
View File
@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Any
import voluptuous as vol
from homeassistant.components.vacuum import (
ATTR_FAN_SPEED,
DOMAIN as VACUUM_DOMAIN,
SERVICE_CLEAN_SPOT,
SERVICE_LOCATE,
@@ -389,7 +388,7 @@ class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity):
if script := self._action_scripts.get(SERVICE_SET_FAN_SPEED):
await self.async_run_script(
script, run_variables={ATTR_FAN_SPEED: fan_speed}, context=self._context
script, run_variables={"fan_speed": fan_speed}, context=self._context
)
+28
View File
@@ -44,11 +44,13 @@ from homeassistant.const import (
)
from homeassistant.core import (
CALLBACK_TYPE,
DOMAIN as HOMEASSISTANT_DOMAIN,
Context,
HassJob,
HassJobType,
HomeAssistant,
State,
async_get_hass_or_none,
callback,
get_hassjob_callable_job_type,
is_callback,
@@ -331,11 +333,37 @@ BEHAVIOR_ALL: Final = "all"
BEHAVIOR_EACH: Final = "each"
def _create_deprecated_behavior_issue(deprecated: str, replacement: str) -> None:
"""Inform the user a renamed trigger behavior value is still in use."""
# Returns None when called from the wrong thread or before hass is set up
# (e.g. a `check_config` run), in which case there's nothing to report to.
if (hass := async_get_hass_or_none()) is None:
return
from .issue_registry import IssueSeverity, async_create_issue # noqa: PLC0415
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_trigger_behavior_{deprecated}",
breaks_in_ha_version="2027.1",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_trigger_behavior",
translation_placeholders={
"deprecated_behavior": deprecated,
"new_behavior": replacement,
},
)
def _backwards_compatible_behavior(value: Any) -> Any:
"""Convert legacy behavior values to new ones."""
if value == "any":
_create_deprecated_behavior_issue("any", BEHAVIOR_EACH)
return BEHAVIOR_EACH
if value == "last":
_create_deprecated_behavior_issue("last", BEHAVIOR_ALL)
return BEHAVIOR_ALL
return value
+1 -1
View File
@@ -29,7 +29,7 @@ cached-ipaddress==1.1.2
certifi>=2021.5.30
ciso8601==2.3.3
cronsim==2.7
cryptography==48.0.0
cryptography==48.0.1
dbus-fast==5.0.16
file-read-backwards==2.0.0
fnv-hash-fast==2.0.3
+1 -1
View File
@@ -57,7 +57,7 @@ dependencies = [
"lru-dict==1.4.1",
"PyJWT==2.12.1",
# PyJWT has loose dependency. We want the latest one.
"cryptography==48.0.0",
"cryptography==48.0.1",
"Pillow==12.2.0",
"propcache==0.5.2",
"pyOpenSSL==26.2.0",
+1 -1
View File
@@ -21,7 +21,7 @@ bcrypt==5.0.0
certifi>=2021.5.30
ciso8601==2.3.3
cronsim==2.7
cryptography==48.0.0
cryptography==48.0.1
fnv-hash-fast==2.0.3
ha-ffmpeg==3.2.2
hass-nabucasa==2.2.0
+4
View File
@@ -6,6 +6,10 @@ set -e
cd "$(realpath "$(dirname "$0")/..")"
if [ ! -n "$VIRTUAL_ENV" ]; then
source .venv/bin/activate
fi
echo "Installing development dependencies..."
uv pip install \
-e . \
+1 -1
View File
@@ -17,7 +17,7 @@ from script.hassfest.model import Config, Integration
# Requirements which can't be installed on all systems because they
# rely on additional system packages. Requirements listed in
# EXCLUDED_REQUIREMENTS_ALL will be commented-out in
# requirements_all.txt and requirements_test_all.txt.
# requirements_all.txt.
EXCLUDED_REQUIREMENTS_ALL = {
"atenpdu", # depends on pysnmp which is not maintained at this time
"avion",
+30 -1
View File
@@ -10,8 +10,9 @@ import pytest
from homeassistant.components import sun
from homeassistant.components.sun import entity
from homeassistant.const import EVENT_STATE_CHANGED
from homeassistant.const import EVENT_STATE_CHANGED, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
@@ -245,3 +246,31 @@ async def test_setup_and_remove_config_entry(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert hass.states.get(entity.ENTITY_ID) is None
async def test_cleanup_deprecated_solar_rising(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that the deprecated solar_rising entity is removed on setup."""
config_entry = MockConfigEntry(domain=sun.DOMAIN)
config_entry.add_to_hass(hass)
entity_registry.async_get_or_create(
Platform.SENSOR,
sun.DOMAIN,
unique_id=f"{config_entry.entry_id}-solar_rising",
config_entry=config_entry,
)
assert entity_registry.async_get_entity_id(
Platform.SENSOR, sun.DOMAIN, f"{config_entry.entry_id}-solar_rising"
)
now = datetime(2016, 6, 1, 8, 0, 0, tzinfo=dt_util.UTC)
with freeze_time(now):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert not entity_registry.async_get_entity_id(
Platform.SENSOR, sun.DOMAIN, f"{config_entry.entry_id}-solar_rising"
)
+48
View File
@@ -37,6 +37,7 @@ from homeassistant.const import (
)
from homeassistant.core import (
CALLBACK_TYPE,
DOMAIN as HOMEASSISTANT_DOMAIN,
Context,
Event,
EventStateChangedData,
@@ -49,6 +50,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
config_validation as cv,
entity_registry as er,
issue_registry as ir,
label_registry as lr,
trigger,
)
@@ -5699,6 +5701,52 @@ def test_entity_state_trigger_schema_behavior_backwards_compatible(
assert validated[CONF_OPTIONS][ATTR_BEHAVIOR] == expected
@pytest.mark.parametrize(
("behavior", "expected"),
[
("any", BEHAVIOR_EACH),
("last", BEHAVIOR_ALL),
],
)
async def test_entity_state_trigger_legacy_behavior_creates_repair_issue(
issue_registry: ir.IssueRegistry,
behavior: str,
expected: str,
) -> None:
"""Test a repair issue is raised when a legacy behavior value is used."""
config = {
CONF_TARGET: {CONF_ENTITY_ID: "test.entity"},
CONF_OPTIONS: {ATTR_BEHAVIOR: behavior},
}
validated = ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR(config)
assert validated[CONF_OPTIONS][ATTR_BEHAVIOR] == expected
issue = issue_registry.async_get_issue(
HOMEASSISTANT_DOMAIN, f"deprecated_trigger_behavior_{behavior}"
)
assert issue is not None
assert issue.translation_key == "deprecated_trigger_behavior"
assert issue.translation_placeholders == {
"deprecated_behavior": behavior,
"new_behavior": expected,
}
@pytest.mark.parametrize("behavior", [BEHAVIOR_EACH, BEHAVIOR_FIRST, BEHAVIOR_ALL])
async def test_entity_state_trigger_new_behavior_no_repair_issue(
issue_registry: ir.IssueRegistry,
behavior: str,
) -> None:
"""Test no repair issue is raised when a current behavior value is used."""
config = {
CONF_TARGET: {CONF_ENTITY_ID: "test.entity"},
CONF_OPTIONS: {ATTR_BEHAVIOR: behavior},
}
ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR(config)
assert not issue_registry.issues
def test_entity_state_trigger_schema_behavior_default() -> None:
"""Test the behavior defaults to 'each' when omitted."""
config = {