Merge branch 'dev' into reorder-group-member

This commit is contained in:
Paul Bottein 2025-07-19 09:56:42 +02:00 committed by GitHub
commit 643c2cb7b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
81 changed files with 792 additions and 580 deletions

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "silver",
"requirements": ["aioamazondevices==3.2.10"]
"requirements": ["aioamazondevices==3.5.0"]
}

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/blue_current",
"iot_class": "cloud_push",
"loggers": ["bluecurrent_api"],
"requirements": ["bluecurrent-api==1.2.3"]
"requirements": ["bluecurrent-api==1.2.4"]
}

View File

@ -71,7 +71,7 @@ _CLOUD_ERRORS: dict[
] = {
TimeoutError: (
HTTPStatus.BAD_GATEWAY,
"Unable to reach the Home Assistant cloud.",
"Unable to reach the Home Assistant Cloud.",
),
aiohttp.ClientError: (
HTTPStatus.INTERNAL_SERVER_ERROR,

View File

@ -10,8 +10,6 @@ from typing import Any
from jsonpath import jsonpath
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import (
CONF_COMMAND,
CONF_NAME,
@ -188,16 +186,7 @@ class CommandSensor(ManualTriggerSensorEntity):
self.entity_id, variables, None
)
if self.device_class not in {
SensorDeviceClass.DATE,
SensorDeviceClass.TIMESTAMP,
}:
self._attr_native_value = value
elif value is not None:
self._attr_native_value = async_parse_date_datetime(
value, self.entity_id, self.device_class
)
self._set_native_value_with_possible_timestamp(value)
self._process_manual_data(variables)
self.async_write_ha_state()

View File

@ -320,7 +320,12 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
# changed state, then we know it will still be zero.
return
schedule_max_sub_interval_exceeded(new_state)
calc_derivative(new_state, new_state.state, event.data["old_last_reported"])
calc_derivative(
new_state,
new_state.state,
event.data["last_reported"],
event.data["old_last_reported"],
)
@callback
def on_state_changed(event: Event[EventStateChangedData]) -> None:
@ -334,19 +339,27 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
schedule_max_sub_interval_exceeded(new_state)
old_state = event.data["old_state"]
if old_state is not None:
calc_derivative(new_state, old_state.state, old_state.last_reported)
calc_derivative(
new_state,
old_state.state,
new_state.last_updated,
old_state.last_reported,
)
else:
# On first state change from none, update availability
self.async_write_ha_state()
def calc_derivative(
new_state: State, old_value: str, old_last_reported: datetime
new_state: State,
old_value: str,
new_timestamp: datetime,
old_timestamp: datetime,
) -> None:
"""Handle the sensor state changes."""
if not _is_decimal_state(old_value):
if self._last_valid_state_time:
old_value = self._last_valid_state_time[0]
old_last_reported = self._last_valid_state_time[1]
old_timestamp = self._last_valid_state_time[1]
else:
# Sensor becomes valid for the first time, just keep the restored value
self.async_write_ha_state()
@ -358,12 +371,10 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
"" if unit is None else unit
)
self._prune_state_list(new_state.last_reported)
self._prune_state_list(new_timestamp)
try:
elapsed_time = (
new_state.last_reported - old_last_reported
).total_seconds()
elapsed_time = (new_timestamp - old_timestamp).total_seconds()
delta_value = Decimal(new_state.state) - Decimal(old_value)
new_derivative = (
delta_value
@ -392,12 +403,10 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
return
# add latest derivative to the window list
self._state_list.append(
(old_last_reported, new_state.last_reported, new_derivative)
)
self._state_list.append((old_timestamp, new_timestamp, new_derivative))
self._last_valid_state_time = (
new_state.state,
new_state.last_reported,
new_timestamp,
)
# If outside of time window just report derivative (is the same as modeling it in the window),
@ -405,9 +414,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
if elapsed_time > self._time_window:
derivative = new_derivative
else:
derivative = self._calc_derivative_from_state_list(
new_state.last_reported
)
derivative = self._calc_derivative_from_state_list(new_timestamp)
self._write_native_value(derivative)
source_state = self.hass.states.get(self._sensor_source_id)

View File

@ -295,23 +295,7 @@ class RuntimeEntryData:
needed_platforms.add(Platform.BINARY_SENSOR)
needed_platforms.add(Platform.SELECT)
ent_reg = er.async_get(hass)
registry_get_entity = ent_reg.async_get_entity_id
for info in infos:
platform = INFO_TYPE_TO_PLATFORM[type(info)]
needed_platforms.add(platform)
# If the unique id is in the old format, migrate it
# except if they downgraded and upgraded, there might be a duplicate
# so we want to keep the one that was already there.
if (
(old_unique_id := info.unique_id)
and (old_entry := registry_get_entity(platform, DOMAIN, old_unique_id))
and (new_unique_id := build_device_unique_id(mac, info))
!= old_unique_id
and not registry_get_entity(platform, DOMAIN, new_unique_id)
):
ent_reg.async_update_entity(old_entry, new_unique_id=new_unique_id)
needed_platforms.update(INFO_TYPE_TO_PLATFORM[type(info)] for info in infos)
await self._ensure_platforms_loaded(hass, entry, needed_platforms)
# Make a dict of the EntityInfo by type and send

View File

@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==35.0.0",
"aioesphomeapi==36.0.1",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.1.0"
],

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250702.2"]
"requirements": ["home-assistant-frontend==20250702.3"]
}

View File

@ -541,6 +541,11 @@ class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity):
"""Return the state attributes."""
return self.entity_description.extra_state_attributes_fn(self.mower_attributes)
@property
def available(self) -> bool:
"""Return the available attribute of the entity."""
return super().available and self.native_value is not None
class WorkAreaSensorEntity(WorkAreaAvailableEntity, SensorEntity):
"""Defining the Work area sensors with WorkAreaSensorEntityDescription."""

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["imgw_pib==1.4.1"]
"requirements": ["imgw_pib==1.4.2"]
}

View File

@ -463,7 +463,7 @@ class IntegrationSensor(RestoreSensor):
) -> None:
"""Handle sensor state update when sub interval is configured."""
self._integrate_on_state_update_with_max_sub_interval(
None, event.data["old_state"], event.data["new_state"]
None, None, event.data["old_state"], event.data["new_state"]
)
@callback
@ -472,13 +472,17 @@ class IntegrationSensor(RestoreSensor):
) -> None:
"""Handle sensor state report when sub interval is configured."""
self._integrate_on_state_update_with_max_sub_interval(
event.data["old_last_reported"], None, event.data["new_state"]
event.data["old_last_reported"],
event.data["last_reported"],
None,
event.data["new_state"],
)
@callback
def _integrate_on_state_update_with_max_sub_interval(
self,
old_last_reported: datetime | None,
old_timestamp: datetime | None,
new_timestamp: datetime | None,
old_state: State | None,
new_state: State | None,
) -> None:
@ -489,7 +493,9 @@ class IntegrationSensor(RestoreSensor):
"""
self._cancel_max_sub_interval_exceeded_callback()
try:
self._integrate_on_state_change(old_last_reported, old_state, new_state)
self._integrate_on_state_change(
old_timestamp, new_timestamp, old_state, new_state
)
self._last_integration_trigger = _IntegrationTrigger.StateEvent
self._last_integration_time = datetime.now(tz=UTC)
finally:
@ -503,7 +509,7 @@ class IntegrationSensor(RestoreSensor):
) -> None:
"""Handle sensor state change."""
return self._integrate_on_state_change(
None, event.data["old_state"], event.data["new_state"]
None, None, event.data["old_state"], event.data["new_state"]
)
@callback
@ -512,12 +518,16 @@ class IntegrationSensor(RestoreSensor):
) -> None:
"""Handle sensor state report."""
return self._integrate_on_state_change(
event.data["old_last_reported"], None, event.data["new_state"]
event.data["old_last_reported"],
event.data["last_reported"],
None,
event.data["new_state"],
)
def _integrate_on_state_change(
self,
old_last_reported: datetime | None,
old_timestamp: datetime | None,
new_timestamp: datetime | None,
old_state: State | None,
new_state: State | None,
) -> None:
@ -531,16 +541,17 @@ class IntegrationSensor(RestoreSensor):
if old_state:
# state has changed, we recover old_state from the event
new_timestamp = new_state.last_updated
old_state_state = old_state.state
old_last_reported = old_state.last_reported
old_timestamp = old_state.last_reported
else:
# event state reported without any state change
# first state or event state reported without any state change
old_state_state = new_state.state
self._attr_available = True
self._derive_and_set_attributes_from_state(new_state)
if old_last_reported is None and old_state is None:
if old_timestamp is None and old_state is None:
self.async_write_ha_state()
return
@ -551,11 +562,12 @@ class IntegrationSensor(RestoreSensor):
return
if TYPE_CHECKING:
assert old_last_reported is not None
assert new_timestamp is not None
assert old_timestamp is not None
elapsed_seconds = Decimal(
(new_state.last_reported - old_last_reported).total_seconds()
(new_timestamp - old_timestamp).total_seconds()
if self._last_integration_trigger == _IntegrationTrigger.StateEvent
else (new_state.last_reported - self._last_integration_time).total_seconds()
else (new_timestamp - self._last_integration_time).total_seconds()
)
area = self._method.calculate_area_with_two_states(elapsed_seconds, *states)

View File

@ -98,6 +98,12 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
f"together with state class `{state_class}`"
)
unit_of_measurement: str | None
if (
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
) is not None and not unit_of_measurement.strip():
config.pop(CONF_UNIT_OF_MEASUREMENT)
# Only allow `options` to be set for `enum` sensors
# to limit the possible sensor values
if (options := config.get(CONF_OPTIONS)) is not None:

View File

@ -39,7 +39,10 @@ class OllamaTaskEntity(
):
"""Ollama AI Task entity."""
_attr_supported_features = ai_task.AITaskEntityFeature.GENERATE_DATA
_attr_supported_features = (
ai_task.AITaskEntityFeature.GENERATE_DATA
| ai_task.AITaskEntityFeature.SUPPORT_ATTACHMENTS
)
async def _async_generate_data(
self,

View File

@ -106,9 +106,18 @@ def _convert_content(
],
)
if isinstance(chat_content, conversation.UserContent):
images: list[ollama.Image] = []
for attachment in chat_content.attachments or ():
if not attachment.mime_type.startswith("image/"):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="unsupported_attachment_type",
)
images.append(ollama.Image(value=attachment.path))
return ollama.Message(
role=MessageRole.USER.value,
content=chat_content.content,
images=images or None,
)
if isinstance(chat_content, conversation.SystemContent):
return ollama.Message(

View File

@ -94,5 +94,10 @@
"download": "[%key:component::ollama::config_subentries::conversation::progress::download%]"
}
}
},
"exceptions": {
"unsupported_attachment_type": {
"message": "Ollama only supports image attachments in user content, but received non-image attachment."
}
}
}

View File

@ -13,9 +13,7 @@ from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DOMAIN as SENSOR_DOMAIN,
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorDeviceClass,
)
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE,
@ -181,18 +179,6 @@ class RestSensor(ManualTriggerSensorEntity, RestEntity):
self.entity_id, variables, None
)
if value is None or self.device_class not in (
SensorDeviceClass.DATE,
SensorDeviceClass.TIMESTAMP,
):
self._attr_native_value = value
self._process_manual_data(variables)
self.async_write_ha_state()
return
self._attr_native_value = async_parse_date_datetime(
value, self.entity_id, self.device_class
)
self._set_native_value_with_possible_timestamp(value)
self._process_manual_data(variables)
self.async_write_ha_state()

View File

@ -7,8 +7,7 @@ from typing import Any, cast
import voluptuous as vol
from homeassistant.components.sensor import CONF_STATE_CLASS, SensorDeviceClass
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.components.sensor import CONF_STATE_CLASS
from homeassistant.const import (
CONF_ATTRIBUTE,
CONF_DEVICE_CLASS,
@ -218,17 +217,7 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], ManualTriggerSensorEnti
self.entity_id, variables, None
)
if self.device_class not in {
SensorDeviceClass.DATE,
SensorDeviceClass.TIMESTAMP,
}:
self._attr_native_value = value
self._process_manual_data(variables)
return
self._attr_native_value = async_parse_date_datetime(
value, self.entity_id, self.device_class
)
self._set_native_value_with_possible_timestamp(value)
self._process_manual_data(variables)
@property

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmarlaapi", "pysignalr"],
"quality_scale": "bronze",
"requirements": ["pysmarlaapi==0.9.0"]
"requirements": ["pysmarlaapi==0.9.1"]
}

View File

@ -217,7 +217,7 @@ class SnmpSensor(ManualTriggerSensorEntity):
self.entity_id, variables, STATE_UNKNOWN
)
self._attr_native_value = value
self._set_native_value_with_possible_timestamp(value)
self._process_manual_data(variables)

View File

@ -401,9 +401,10 @@ class SQLSensor(ManualTriggerSensorEntity):
if data is not None and self._template is not None:
variables = self._template_variables_with_value(data)
if self._render_availability_template(variables):
self._attr_native_value = self._template.async_render_as_value_template(
_value = self._template.async_render_as_value_template(
self.entity_id, variables, None
)
self._set_native_value_with_possible_timestamp(_value)
self._process_manual_data(variables)
else:
self._attr_native_value = data

View File

@ -727,12 +727,11 @@ class StatisticsSensor(SensorEntity):
def _async_handle_new_state(
self,
reported_state: State | None,
reported_state: State,
timestamp: float,
) -> None:
"""Handle the sensor state changes."""
if (new_state := reported_state) is None:
return
self._add_state_to_queue(new_state)
self._add_state_to_queue(reported_state, timestamp)
self._async_purge_update_and_schedule()
if self._preview_callback:
@ -747,14 +746,18 @@ class StatisticsSensor(SensorEntity):
self,
event: Event[EventStateChangedData],
) -> None:
self._async_handle_new_state(event.data["new_state"])
if (new_state := event.data["new_state"]) is None:
return
self._async_handle_new_state(new_state, new_state.last_updated_timestamp)
@callback
def _async_stats_sensor_state_report_listener(
self,
event: Event[EventStateReportedData],
) -> None:
self._async_handle_new_state(event.data["new_state"])
self._async_handle_new_state(
event.data["new_state"], event.data["last_reported"].timestamp()
)
async def _async_stats_sensor_startup(self) -> None:
"""Add listener and get recorded state.
@ -785,7 +788,9 @@ class StatisticsSensor(SensorEntity):
"""Register callbacks."""
await self._async_stats_sensor_startup()
def _add_state_to_queue(self, new_state: State) -> None:
def _add_state_to_queue(
self, new_state: State, last_reported_timestamp: float
) -> None:
"""Add the state to the queue."""
# Attention: it is not safe to store the new_state object,
@ -805,7 +810,7 @@ class StatisticsSensor(SensorEntity):
self.states.append(new_state.state == "on")
else:
self.states.append(float(new_state.state))
self.ages.append(new_state.last_reported_timestamp)
self.ages.append(last_reported_timestamp)
self._attr_extra_state_attributes[STAT_SOURCE_VALUE_VALID] = True
except ValueError:
self._attr_extra_state_attributes[STAT_SOURCE_VALUE_VALID] = False
@ -1062,7 +1067,7 @@ class StatisticsSensor(SensorEntity):
self._fetch_states_from_database
):
for state in reversed(states):
self._add_state_to_queue(state)
self._add_state_to_queue(state, state.last_reported_timestamp)
self._calculate_state_attributes(state)
self._async_purge_update_and_schedule()

View File

@ -41,5 +41,5 @@
"iot_class": "local_push",
"loggers": ["switchbot"],
"quality_scale": "gold",
"requirements": ["PySwitchbot==0.68.1"]
"requirements": ["PySwitchbot==0.68.2"]
}

View File

@ -21,7 +21,6 @@ from homeassistant.components.alarm_control_panel import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_CODE,
CONF_DEVICE_ID,
CONF_NAME,
CONF_STATE,
CONF_UNIQUE_ID,
@ -31,7 +30,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv, selector, template
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
@ -43,8 +42,16 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN
from .coordinator import TriggerUpdateCoordinator
from .entity import AbstractTemplateEntity
from .helpers import async_setup_template_platform
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema
from .helpers import (
async_setup_template_entry,
async_setup_template_platform,
async_setup_template_preview,
)
from .template_entity import (
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TemplateEntity,
make_template_entity_common_modern_schema,
)
from .trigger_entity import TriggerEntity
_LOGGER = logging.getLogger(__name__)
@ -88,27 +95,28 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Alarm Control Panel"
ALARM_CONTROL_PANEL_SCHEMA = vol.All(
vol.Schema(
{
vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
vol.Optional(
CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name
): cv.enum(TemplateCodeFormat),
vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
}
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
ALARM_CONTROL_PANEL_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum(
TemplateCodeFormat
),
vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
}
)
ALARM_CONTROL_PANEL_YAML_SCHEMA = ALARM_CONTROL_PANEL_COMMON_SCHEMA.extend(
make_template_entity_common_modern_schema(DEFAULT_NAME).schema
)
LEGACY_ALARM_CONTROL_PANEL_SCHEMA = vol.Schema(
ALARM_CONTROL_PANEL_LEGACY_YAML_SCHEMA = vol.Schema(
{
vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA,
@ -130,59 +138,29 @@ LEGACY_ALARM_CONTROL_PANEL_SCHEMA = vol.Schema(
PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ALARM_CONTROL_PANELS): cv.schema_with_slug_keys(
LEGACY_ALARM_CONTROL_PANEL_SCHEMA
ALARM_CONTROL_PANEL_LEGACY_YAML_SCHEMA
),
}
)
ALARM_CONTROL_PANEL_CONFIG_SCHEMA = vol.Schema(
{
vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum(
TemplateCodeFormat
),
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_NAME): cv.template,
vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
}
ALARM_CONTROL_PANEL_CONFIG_ENTRY_SCHEMA = ALARM_CONTROL_PANEL_COMMON_SCHEMA.extend(
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
)
def rewrite_options_to_modern_conf(option_config: dict[str, dict]) -> dict[str, dict]:
"""Rewrite option configuration to modern configuration."""
option_config = {**option_config}
if CONF_VALUE_TEMPLATE in option_config:
option_config[CONF_STATE] = option_config.pop(CONF_VALUE_TEMPLATE)
return option_config
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize config entry."""
_options = dict(config_entry.options)
_options.pop("template_type")
_options = rewrite_options_to_modern_conf(_options)
validated_config = ALARM_CONTROL_PANEL_CONFIG_SCHEMA(_options)
async_add_entities(
[
StateAlarmControlPanelEntity(
hass,
validated_config,
config_entry.entry_id,
)
]
await async_setup_template_entry(
hass,
config_entry,
async_add_entities,
StateAlarmControlPanelEntity,
ALARM_CONTROL_PANEL_CONFIG_ENTRY_SCHEMA,
True,
)
@ -211,11 +189,14 @@ def async_create_preview_alarm_control_panel(
hass: HomeAssistant, name: str, config: dict[str, Any]
) -> StateAlarmControlPanelEntity:
"""Create a preview alarm control panel."""
updated_config = rewrite_options_to_modern_conf(config)
validated_config = ALARM_CONTROL_PANEL_CONFIG_SCHEMA(
updated_config | {CONF_NAME: name}
return async_setup_template_preview(
hass,
name,
config,
StateAlarmControlPanelEntity,
ALARM_CONTROL_PANEL_CONFIG_ENTRY_SCHEMA,
True,
)
return StateAlarmControlPanelEntity(hass, validated_config, None)
class AbstractTemplateAlarmControlPanel(

View File

@ -22,7 +22,6 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
CONF_DEVICE_CLASS,
CONF_DEVICE_ID,
CONF_ENTITY_PICTURE_TEMPLATE,
CONF_FRIENDLY_NAME_TEMPLATE,
CONF_ICON_TEMPLATE,
@ -38,7 +37,7 @@ from homeassistant.const import (
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv, selector, template
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
@ -50,8 +49,16 @@ from homeassistant.util import dt as dt_util
from . import TriggerUpdateCoordinator
from .const import CONF_AVAILABILITY_TEMPLATE
from .helpers import async_setup_template_platform
from .template_entity import TEMPLATE_ENTITY_COMMON_SCHEMA, TemplateEntity
from .helpers import (
async_setup_template_entry,
async_setup_template_platform,
async_setup_template_preview,
)
from .template_entity import (
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TEMPLATE_ENTITY_COMMON_SCHEMA,
TemplateEntity,
)
from .trigger_entity import TriggerEntity
CONF_DELAY_ON = "delay_on"
@ -64,7 +71,7 @@ LEGACY_FIELDS = {
CONF_VALUE_TEMPLATE: CONF_STATE,
}
BINARY_SENSOR_SCHEMA = vol.Schema(
BINARY_SENSOR_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_AUTO_OFF): vol.Any(cv.positive_time_period, cv.template),
vol.Optional(CONF_DELAY_OFF): vol.Any(cv.positive_time_period, cv.template),
@ -73,15 +80,17 @@ BINARY_SENSOR_SCHEMA = vol.Schema(
vol.Required(CONF_STATE): cv.template,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
}
).extend(TEMPLATE_ENTITY_COMMON_SCHEMA.schema)
BINARY_SENSOR_CONFIG_SCHEMA = BINARY_SENSOR_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
}
)
LEGACY_BINARY_SENSOR_SCHEMA = vol.All(
BINARY_SENSOR_YAML_SCHEMA = BINARY_SENSOR_COMMON_SCHEMA.extend(
TEMPLATE_ENTITY_COMMON_SCHEMA.schema
)
BINARY_SENSOR_CONFIG_ENTRY_SCHEMA = BINARY_SENSOR_COMMON_SCHEMA.extend(
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
)
BINARY_SENSOR_LEGACY_YAML_SCHEMA = vol.All(
cv.deprecated(ATTR_ENTITY_ID),
vol.Schema(
{
@ -106,7 +115,7 @@ LEGACY_BINARY_SENSOR_SCHEMA = vol.All(
PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(
LEGACY_BINARY_SENSOR_SCHEMA
BINARY_SENSOR_LEGACY_YAML_SCHEMA
),
}
)
@ -138,11 +147,12 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize config entry."""
_options = dict(config_entry.options)
_options.pop("template_type")
validated_config = BINARY_SENSOR_CONFIG_SCHEMA(_options)
async_add_entities(
[StateBinarySensorEntity(hass, validated_config, config_entry.entry_id)]
await async_setup_template_entry(
hass,
config_entry,
async_add_entities,
StateBinarySensorEntity,
BINARY_SENSOR_CONFIG_ENTRY_SCHEMA,
)
@ -151,8 +161,9 @@ def async_create_preview_binary_sensor(
hass: HomeAssistant, name: str, config: dict[str, Any]
) -> StateBinarySensorEntity:
"""Create a preview sensor."""
validated_config = BINARY_SENSOR_CONFIG_SCHEMA(config | {CONF_NAME: name})
return StateBinarySensorEntity(hass, validated_config, None)
return async_setup_template_preview(
hass, name, config, StateBinarySensorEntity, BINARY_SENSOR_CONFIG_ENTRY_SCHEMA
)
class StateBinarySensorEntity(TemplateEntity, BinarySensorEntity, RestoreEntity):

View File

@ -14,9 +14,9 @@ from homeassistant.components.button import (
ButtonEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_CLASS, CONF_DEVICE_ID, CONF_NAME
from homeassistant.const import CONF_DEVICE_CLASS
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, selector
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
@ -24,29 +24,31 @@ from homeassistant.helpers.entity_platform import (
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_PRESS, DOMAIN
from .helpers import async_setup_template_platform
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema
from .helpers import async_setup_template_entry, async_setup_template_platform
from .template_entity import (
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TemplateEntity,
make_template_entity_common_modern_schema,
)
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Template Button"
DEFAULT_OPTIMISTIC = False
BUTTON_SCHEMA = vol.Schema(
BUTTON_YAML_SCHEMA = vol.Schema(
{
vol.Required(CONF_PRESS): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
CONFIG_BUTTON_SCHEMA = vol.Schema(
BUTTON_CONFIG_ENTRY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME): cv.template,
vol.Optional(CONF_PRESS): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
}
)
).extend(TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema)
async def async_setup_platform(
@ -73,11 +75,12 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize config entry."""
_options = dict(config_entry.options)
_options.pop("template_type")
validated_config = CONFIG_BUTTON_SCHEMA(_options)
async_add_entities(
[StateButtonEntity(hass, validated_config, config_entry.entry_id)]
await async_setup_template_entry(
hass,
config_entry,
async_add_entities,
StateButtonEntity,
BUTTON_CONFIG_ENTRY_SCHEMA,
)

View File

@ -102,57 +102,57 @@ CONFIG_SECTION_SCHEMA = vol.All(
{
vol.Optional(CONF_ACTIONS): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_BINARY_SENSORS): cv.schema_with_slug_keys(
binary_sensor_platform.LEGACY_BINARY_SENSOR_SCHEMA
binary_sensor_platform.BINARY_SENSOR_LEGACY_YAML_SCHEMA
),
vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA,
vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys(
sensor_platform.LEGACY_SENSOR_SCHEMA
sensor_platform.SENSOR_LEGACY_YAML_SCHEMA
),
vol.Optional(CONF_TRIGGERS): cv.TRIGGER_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
vol.Optional(DOMAIN_ALARM_CONTROL_PANEL): vol.All(
cv.ensure_list,
[alarm_control_panel_platform.ALARM_CONTROL_PANEL_SCHEMA],
[alarm_control_panel_platform.ALARM_CONTROL_PANEL_YAML_SCHEMA],
),
vol.Optional(DOMAIN_BINARY_SENSOR): vol.All(
cv.ensure_list, [binary_sensor_platform.BINARY_SENSOR_SCHEMA]
cv.ensure_list, [binary_sensor_platform.BINARY_SENSOR_YAML_SCHEMA]
),
vol.Optional(DOMAIN_BUTTON): vol.All(
cv.ensure_list, [button_platform.BUTTON_SCHEMA]
cv.ensure_list, [button_platform.BUTTON_YAML_SCHEMA]
),
vol.Optional(DOMAIN_COVER): vol.All(
cv.ensure_list, [cover_platform.COVER_SCHEMA]
cv.ensure_list, [cover_platform.COVER_YAML_SCHEMA]
),
vol.Optional(DOMAIN_FAN): vol.All(
cv.ensure_list, [fan_platform.FAN_SCHEMA]
cv.ensure_list, [fan_platform.FAN_YAML_SCHEMA]
),
vol.Optional(DOMAIN_IMAGE): vol.All(
cv.ensure_list, [image_platform.IMAGE_SCHEMA]
cv.ensure_list, [image_platform.IMAGE_YAML_SCHEMA]
),
vol.Optional(DOMAIN_LIGHT): vol.All(
cv.ensure_list, [light_platform.LIGHT_SCHEMA]
cv.ensure_list, [light_platform.LIGHT_YAML_SCHEMA]
),
vol.Optional(DOMAIN_LOCK): vol.All(
cv.ensure_list, [lock_platform.LOCK_SCHEMA]
cv.ensure_list, [lock_platform.LOCK_YAML_SCHEMA]
),
vol.Optional(DOMAIN_NUMBER): vol.All(
cv.ensure_list, [number_platform.NUMBER_SCHEMA]
cv.ensure_list, [number_platform.NUMBER_YAML_SCHEMA]
),
vol.Optional(DOMAIN_SELECT): vol.All(
cv.ensure_list, [select_platform.SELECT_SCHEMA]
cv.ensure_list, [select_platform.SELECT_YAML_SCHEMA]
),
vol.Optional(DOMAIN_SENSOR): vol.All(
cv.ensure_list, [sensor_platform.SENSOR_SCHEMA]
cv.ensure_list, [sensor_platform.SENSOR_YAML_SCHEMA]
),
vol.Optional(DOMAIN_SWITCH): vol.All(
cv.ensure_list, [switch_platform.SWITCH_SCHEMA]
cv.ensure_list, [switch_platform.SWITCH_YAML_SCHEMA]
),
vol.Optional(DOMAIN_VACUUM): vol.All(
cv.ensure_list, [vacuum_platform.VACUUM_SCHEMA]
cv.ensure_list, [vacuum_platform.VACUUM_YAML_SCHEMA]
),
vol.Optional(DOMAIN_WEATHER): vol.All(
cv.ensure_list, [weather_platform.WEATHER_SCHEMA]
cv.ensure_list, [weather_platform.WEATHER_YAML_SCHEMA]
),
},
),

View File

@ -1,6 +1,9 @@
"""Constants for the Template Platform Components."""
from homeassistant.const import Platform
import voluptuous as vol
from homeassistant.const import CONF_ICON, CONF_NAME, CONF_UNIQUE_ID, Platform
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
CONF_ATTRIBUTE_TEMPLATES = "attribute_templates"
@ -16,6 +19,15 @@ CONF_STEP = "step"
CONF_TURN_OFF = "turn_off"
CONF_TURN_ON = "turn_on"
TEMPLATE_ENTITY_BASE_SCHEMA = vol.Schema(
{
vol.Optional(CONF_ICON): cv.template,
vol.Optional(CONF_NAME): cv.template,
vol.Optional(CONF_PICTURE): cv.template,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
DOMAIN = "template"
PLATFORM_STORAGE_KEY = "template_platforms"

View File

@ -91,7 +91,7 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Cover"
COVER_SCHEMA = vol.All(
COVER_YAML_SCHEMA = vol.All(
vol.Schema(
{
vol.Inclusive(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA,
@ -110,7 +110,7 @@ COVER_SCHEMA = vol.All(
cv.has_at_least_one_key(OPEN_ACTION, POSITION_ACTION),
)
LEGACY_COVER_SCHEMA = vol.All(
COVER_LEGACY_YAML_SCHEMA = vol.All(
cv.deprecated(CONF_ENTITY_ID),
vol.Schema(
{
@ -134,7 +134,7 @@ LEGACY_COVER_SCHEMA = vol.All(
)
PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend(
{vol.Required(CONF_COVERS): cv.schema_with_slug_keys(LEGACY_COVER_SCHEMA)}
{vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_LEGACY_YAML_SCHEMA)}
)

View File

@ -81,7 +81,7 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Fan"
FAN_SCHEMA = vol.All(
FAN_YAML_SCHEMA = vol.All(
vol.Schema(
{
vol.Optional(CONF_DIRECTION): cv.template,
@ -101,7 +101,7 @@ FAN_SCHEMA = vol.All(
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
)
LEGACY_FAN_SCHEMA = vol.All(
FAN_LEGACY_YAML_SCHEMA = vol.All(
cv.deprecated(CONF_ENTITY_ID),
vol.Schema(
{
@ -126,7 +126,7 @@ LEGACY_FAN_SCHEMA = vol.All(
)
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
{vol.Required(CONF_FANS): cv.schema_with_slug_keys(LEGACY_FAN_SCHEMA)}
{vol.Required(CONF_FANS): cv.schema_with_slug_keys(FAN_LEGACY_YAML_SCHEMA)}
)

View File

@ -5,14 +5,19 @@ import itertools
import logging
from typing import Any
import voluptuous as vol
from homeassistant.components import blueprint
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_ENTITY_PICTURE_TEMPLATE,
CONF_FRIENDLY_NAME,
CONF_ICON,
CONF_ICON_TEMPLATE,
CONF_NAME,
CONF_STATE,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
SERVICE_RELOAD,
)
from homeassistant.core import HomeAssistant, callback
@ -20,6 +25,7 @@ from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import template
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
async_get_platforms,
)
@ -228,3 +234,41 @@ async def async_setup_template_platform(
discovery_info["entities"],
discovery_info["unique_id"],
)
async def async_setup_template_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
state_entity_cls: type[TemplateEntity],
config_schema: vol.Schema,
replace_value_template: bool = False,
) -> None:
"""Setup the Template from a config entry."""
options = dict(config_entry.options)
options.pop("template_type")
if replace_value_template and CONF_VALUE_TEMPLATE in options:
options[CONF_STATE] = options.pop(CONF_VALUE_TEMPLATE)
validated_config = config_schema(options)
async_add_entities(
[state_entity_cls(hass, validated_config, config_entry.entry_id)]
)
def async_setup_template_preview[T: TemplateEntity](
hass: HomeAssistant,
name: str,
config: ConfigType,
state_entity_cls: type[T],
schema: vol.Schema,
replace_value_template: bool = False,
) -> T:
"""Setup the Template preview."""
if replace_value_template and CONF_VALUE_TEMPLATE in config:
config[CONF_STATE] = config.pop(CONF_VALUE_TEMPLATE)
validated_config = schema(config | {CONF_NAME: name})
return state_entity_cls(hass, validated_config, None)

View File

@ -13,10 +13,10 @@ from homeassistant.components.image import (
ImageEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, CONF_URL, CONF_VERIFY_SSL
from homeassistant.const import CONF_URL, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv, selector
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
@ -26,8 +26,9 @@ from homeassistant.util import dt as dt_util
from . import TriggerUpdateCoordinator
from .const import CONF_PICTURE
from .helpers import async_setup_template_platform
from .helpers import async_setup_template_entry, async_setup_template_platform
from .template_entity import (
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TemplateEntity,
make_template_entity_common_modern_attributes_schema,
)
@ -39,7 +40,7 @@ DEFAULT_NAME = "Template Image"
GET_IMAGE_TIMEOUT = 10
IMAGE_SCHEMA = vol.Schema(
IMAGE_YAML_SCHEMA = vol.Schema(
{
vol.Required(CONF_URL): cv.template,
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
@ -47,14 +48,12 @@ IMAGE_SCHEMA = vol.Schema(
).extend(make_template_entity_common_modern_attributes_schema(DEFAULT_NAME).schema)
IMAGE_CONFIG_SCHEMA = vol.Schema(
IMAGE_CONFIG_ENTRY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME): cv.template,
vol.Required(CONF_URL): cv.template,
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
}
)
).extend(TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema)
async def async_setup_platform(
@ -81,11 +80,12 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize config entry."""
_options = dict(config_entry.options)
_options.pop("template_type")
validated_config = IMAGE_CONFIG_SCHEMA(_options)
async_add_entities(
[StateImageEntity(hass, validated_config, config_entry.entry_id)]
await async_setup_template_entry(
hass,
config_entry,
async_add_entities,
StateImageEntity,
IMAGE_CONFIG_ENTRY_SCHEMA,
)

View File

@ -121,7 +121,7 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Light"
LIGHT_SCHEMA = vol.Schema(
LIGHT_YAML_SCHEMA = vol.Schema(
{
vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA,
vol.Inclusive(CONF_EFFECT_LIST, "effect"): cv.template,
@ -147,7 +147,7 @@ LIGHT_SCHEMA = vol.Schema(
}
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
LEGACY_LIGHT_SCHEMA = vol.All(
LIGHT_LEGACY_YAML_SCHEMA = vol.All(
cv.deprecated(CONF_ENTITY_ID),
vol.Schema(
{
@ -186,7 +186,7 @@ PLATFORM_SCHEMA = vol.All(
cv.removed(CONF_WHITE_VALUE_ACTION),
cv.removed(CONF_WHITE_VALUE_TEMPLATE),
LIGHT_PLATFORM_SCHEMA.extend(
{vol.Required(CONF_LIGHTS): cv.schema_with_slug_keys(LEGACY_LIGHT_SCHEMA)}
{vol.Required(CONF_LIGHTS): cv.schema_with_slug_keys(LIGHT_LEGACY_YAML_SCHEMA)}
),
)

View File

@ -54,7 +54,7 @@ LEGACY_FIELDS = {
CONF_VALUE_TEMPLATE: CONF_STATE,
}
LOCK_SCHEMA = vol.All(
LOCK_YAML_SCHEMA = vol.All(
vol.Schema(
{
vol.Optional(CONF_CODE_FORMAT): cv.template,
@ -68,7 +68,6 @@ LOCK_SCHEMA = vol.All(
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
)
PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_CODE_FORMAT_TEMPLATE): cv.template,

View File

@ -18,14 +18,13 @@ from homeassistant.components.number import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_NAME,
CONF_OPTIMISTIC,
CONF_STATE,
CONF_UNIT_OF_MEASUREMENT,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, selector
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
@ -34,8 +33,16 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import TriggerUpdateCoordinator
from .const import CONF_MAX, CONF_MIN, CONF_STEP, DOMAIN
from .helpers import async_setup_template_platform
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema
from .helpers import (
async_setup_template_entry,
async_setup_template_platform,
async_setup_template_preview,
)
from .template_entity import (
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TemplateEntity,
make_template_entity_common_modern_schema,
)
from .trigger_entity import TriggerEntity
_LOGGER = logging.getLogger(__name__)
@ -45,30 +52,31 @@ CONF_SET_VALUE = "set_value"
DEFAULT_NAME = "Template Number"
DEFAULT_OPTIMISTIC = False
NUMBER_SCHEMA = vol.Schema(
NUMBER_COMMON_SCHEMA = vol.Schema(
{
vol.Required(CONF_STATE): cv.template,
vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA,
vol.Required(CONF_STEP): cv.template,
vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): cv.template,
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
}
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
NUMBER_CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.template,
vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): cv.template,
vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA,
vol.Required(CONF_STATE): cv.template,
vol.Required(CONF_STEP): cv.template,
vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_MIN): cv.template,
vol.Optional(CONF_MAX): cv.template,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
}
)
NUMBER_YAML_SCHEMA = (
vol.Schema(
{
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
}
)
.extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
.extend(NUMBER_COMMON_SCHEMA.schema)
)
NUMBER_CONFIG_ENTRY_SCHEMA = NUMBER_COMMON_SCHEMA.extend(
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
)
async def async_setup_platform(
hass: HomeAssistant,
@ -94,11 +102,12 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize config entry."""
_options = dict(config_entry.options)
_options.pop("template_type")
validated_config = NUMBER_CONFIG_SCHEMA(_options)
async_add_entities(
[StateNumberEntity(hass, validated_config, config_entry.entry_id)]
await async_setup_template_entry(
hass,
config_entry,
async_add_entities,
StateNumberEntity,
NUMBER_CONFIG_ENTRY_SCHEMA,
)
@ -107,8 +116,9 @@ def async_create_preview_number(
hass: HomeAssistant, name: str, config: dict[str, Any]
) -> StateNumberEntity:
"""Create a preview number."""
validated_config = NUMBER_CONFIG_SCHEMA(config | {CONF_NAME: name})
return StateNumberEntity(hass, validated_config, None)
return async_setup_template_preview(
hass, name, config, StateNumberEntity, NUMBER_CONFIG_ENTRY_SCHEMA
)
class StateNumberEntity(TemplateEntity, NumberEntity):

View File

@ -15,9 +15,9 @@ from homeassistant.components.select import (
SelectEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, CONF_OPTIMISTIC, CONF_STATE
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, selector
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
@ -27,8 +27,16 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import TriggerUpdateCoordinator
from .const import DOMAIN
from .entity import AbstractTemplateEntity
from .helpers import async_setup_template_platform
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema
from .helpers import (
async_setup_template_entry,
async_setup_template_platform,
async_setup_template_preview,
)
from .template_entity import (
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TemplateEntity,
make_template_entity_common_modern_schema,
)
from .trigger_entity import TriggerEntity
_LOGGER = logging.getLogger(__name__)
@ -39,26 +47,28 @@ CONF_SELECT_OPTION = "select_option"
DEFAULT_NAME = "Template Select"
DEFAULT_OPTIMISTIC = False
SELECT_SCHEMA = vol.Schema(
SELECT_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_STATE): cv.template,
vol.Required(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA,
vol.Required(ATTR_OPTIONS): cv.template,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
}
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
SELECT_CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.template,
vol.Required(CONF_STATE): cv.template,
vol.Required(CONF_OPTIONS): cv.template,
vol.Optional(ATTR_OPTIONS): cv.template,
vol.Optional(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
vol.Optional(CONF_STATE): cv.template,
}
)
SELECT_YAML_SCHEMA = (
vol.Schema(
{
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
}
)
.extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
.extend(SELECT_COMMON_SCHEMA.schema)
)
SELECT_CONFIG_ENTRY_SCHEMA = SELECT_COMMON_SCHEMA.extend(
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
)
async def async_setup_platform(
hass: HomeAssistant,
@ -84,10 +94,13 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize config entry."""
_options = dict(config_entry.options)
_options.pop("template_type")
validated_config = SELECT_CONFIG_SCHEMA(_options)
async_add_entities([TemplateSelect(hass, validated_config, config_entry.entry_id)])
await async_setup_template_entry(
hass,
config_entry,
async_add_entities,
TemplateSelect,
SELECT_CONFIG_ENTRY_SCHEMA,
)
@callback
@ -95,8 +108,9 @@ def async_create_preview_select(
hass: HomeAssistant, name: str, config: dict[str, Any]
) -> TemplateSelect:
"""Create a preview select."""
validated_config = SELECT_CONFIG_SCHEMA(config | {CONF_NAME: name})
return TemplateSelect(hass, validated_config, None)
return async_setup_template_preview(
hass, name, config, TemplateSelect, SELECT_CONFIG_ENTRY_SCHEMA
)
class AbstractTemplateSelect(AbstractTemplateEntity, SelectEntity):

View File

@ -15,6 +15,7 @@ from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
ENTITY_ID_FORMAT,
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
STATE_CLASSES_SCHEMA,
RestoreSensor,
SensorDeviceClass,
SensorEntity,
@ -25,7 +26,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_DEVICE_CLASS,
CONF_DEVICE_ID,
CONF_ENTITY_PICTURE_TEMPLATE,
CONF_FRIENDLY_NAME,
CONF_FRIENDLY_NAME_TEMPLATE,
@ -43,19 +43,26 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv, selector, template
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.trigger_template_entity import TEMPLATE_SENSOR_BASE_SCHEMA
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from . import TriggerUpdateCoordinator
from .const import CONF_ATTRIBUTE_TEMPLATES, CONF_AVAILABILITY_TEMPLATE
from .helpers import async_setup_template_platform
from .template_entity import TEMPLATE_ENTITY_COMMON_SCHEMA, TemplateEntity
from .helpers import (
async_setup_template_entry,
async_setup_template_platform,
async_setup_template_preview,
)
from .template_entity import (
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TEMPLATE_ENTITY_COMMON_SCHEMA,
TemplateEntity,
)
from .trigger_entity import TriggerEntity
LEGACY_FIELDS = {
@ -77,29 +84,31 @@ def validate_last_reset(val):
return val
SENSOR_SCHEMA = vol.All(
SENSOR_COMMON_SCHEMA = vol.Schema(
{
vol.Required(CONF_STATE): cv.template,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
}
)
SENSOR_YAML_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_STATE): cv.template,
vol.Optional(ATTR_LAST_RESET): cv.template,
}
)
.extend(TEMPLATE_SENSOR_BASE_SCHEMA.schema)
.extend(SENSOR_COMMON_SCHEMA.schema)
.extend(TEMPLATE_ENTITY_COMMON_SCHEMA.schema),
validate_last_reset,
)
SENSOR_CONFIG_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_STATE): cv.template,
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
}
).extend(TEMPLATE_SENSOR_BASE_SCHEMA.schema),
SENSOR_CONFIG_ENTRY_SCHEMA = SENSOR_COMMON_SCHEMA.extend(
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
)
LEGACY_SENSOR_SCHEMA = vol.All(
SENSOR_LEGACY_YAML_SCHEMA = vol.All(
cv.deprecated(ATTR_ENTITY_ID),
vol.Schema(
{
@ -141,7 +150,9 @@ PLATFORM_SCHEMA = vol.All(
{
vol.Optional(CONF_TRIGGER): cv.match_all, # to raise custom warning
vol.Optional(CONF_TRIGGERS): cv.match_all, # to raise custom warning
vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(LEGACY_SENSOR_SCHEMA),
vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(
SENSOR_LEGACY_YAML_SCHEMA
),
}
),
extra_validation_checks,
@ -176,11 +187,12 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize config entry."""
_options = dict(config_entry.options)
_options.pop("template_type")
validated_config = SENSOR_CONFIG_SCHEMA(_options)
async_add_entities(
[StateSensorEntity(hass, validated_config, config_entry.entry_id)]
await async_setup_template_entry(
hass,
config_entry,
async_add_entities,
StateSensorEntity,
SENSOR_CONFIG_ENTRY_SCHEMA,
)
@ -189,8 +201,9 @@ def async_create_preview_sensor(
hass: HomeAssistant, name: str, config: dict[str, Any]
) -> StateSensorEntity:
"""Create a preview sensor."""
validated_config = SENSOR_CONFIG_SCHEMA(config | {CONF_NAME: name})
return StateSensorEntity(hass, validated_config, None)
return async_setup_template_preview(
hass, name, config, StateSensorEntity, SENSOR_CONFIG_ENTRY_SCHEMA
)
class StateSensorEntity(TemplateEntity, SensorEntity):

View File

@ -16,7 +16,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
CONF_DEVICE_ID,
CONF_NAME,
CONF_STATE,
CONF_SWITCHES,
@ -29,7 +28,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv, selector, template
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
@ -39,8 +38,13 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import TriggerUpdateCoordinator
from .const import CONF_TURN_OFF, CONF_TURN_ON, DOMAIN
from .helpers import async_setup_template_platform
from .helpers import (
async_setup_template_entry,
async_setup_template_platform,
async_setup_template_preview,
)
from .template_entity import (
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY,
TemplateEntity,
make_template_entity_common_modern_schema,
@ -55,16 +59,19 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Switch"
SWITCH_SCHEMA = vol.Schema(
SWITCH_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_STATE): cv.template,
vol.Required(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
vol.Required(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
}
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
)
LEGACY_SWITCH_SCHEMA = vol.All(
SWITCH_YAML_SCHEMA = SWITCH_COMMON_SCHEMA.extend(
make_template_entity_common_modern_schema(DEFAULT_NAME).schema
)
SWITCH_LEGACY_YAML_SCHEMA = vol.All(
cv.deprecated(ATTR_ENTITY_ID),
vol.Schema(
{
@ -79,17 +86,11 @@ LEGACY_SWITCH_SCHEMA = vol.All(
)
PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
{vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(LEGACY_SWITCH_SCHEMA)}
{vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_LEGACY_YAML_SCHEMA)}
)
SWITCH_CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.template,
vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
}
SWITCH_CONFIG_ENTRY_SCHEMA = SWITCH_COMMON_SCHEMA.extend(
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
)
@ -129,12 +130,13 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize config entry."""
_options = dict(config_entry.options)
_options.pop("template_type")
_options = rewrite_options_to_modern_conf(_options)
validated_config = SWITCH_CONFIG_SCHEMA(_options)
async_add_entities(
[StateSwitchEntity(hass, validated_config, config_entry.entry_id)]
await async_setup_template_entry(
hass,
config_entry,
async_add_entities,
StateSwitchEntity,
SWITCH_CONFIG_ENTRY_SCHEMA,
True,
)
@ -143,9 +145,14 @@ def async_create_preview_switch(
hass: HomeAssistant, name: str, config: dict[str, Any]
) -> StateSwitchEntity:
"""Create a preview switch."""
updated_config = rewrite_options_to_modern_conf(config)
validated_config = SWITCH_CONFIG_SCHEMA(updated_config | {CONF_NAME: name})
return StateSwitchEntity(hass, validated_config, None)
return async_setup_template_preview(
hass,
name,
config,
StateSwitchEntity,
SWITCH_CONFIG_ENTRY_SCHEMA,
True,
)
class StateSwitchEntity(TemplateEntity, SwitchEntity, RestoreEntity):

View File

@ -12,6 +12,7 @@ import voluptuous as vol
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_ENTITY_PICTURE_TEMPLATE,
CONF_ICON,
CONF_ICON_TEMPLATE,
@ -30,7 +31,7 @@ from homeassistant.core import (
validate_state,
)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, selector
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import (
TrackTemplate,
@ -46,7 +47,6 @@ from homeassistant.helpers.template import (
result_as_boolean,
)
from homeassistant.helpers.trigger_template_entity import (
TEMPLATE_ENTITY_BASE_SCHEMA,
make_template_entity_base_schema,
)
from homeassistant.helpers.typing import ConfigType
@ -57,6 +57,7 @@ from .const import (
CONF_AVAILABILITY,
CONF_AVAILABILITY_TEMPLATE,
CONF_PICTURE,
TEMPLATE_ENTITY_BASE_SCHEMA,
)
from .entity import AbstractTemplateEntity
@ -91,6 +92,13 @@ TEMPLATE_ENTITY_COMMON_SCHEMA = (
.extend(TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA.schema)
)
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.template,
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
}
).extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
def make_template_entity_common_modern_schema(
default_name: str,

View File

@ -76,7 +76,7 @@ LEGACY_FIELDS = {
CONF_VALUE_TEMPLATE: CONF_STATE,
}
VACUUM_SCHEMA = vol.All(
VACUUM_YAML_SCHEMA = vol.All(
vol.Schema(
{
vol.Optional(CONF_BATTERY_LEVEL): cv.template,
@ -94,7 +94,7 @@ VACUUM_SCHEMA = vol.All(
).extend(make_template_entity_common_modern_attributes_schema(DEFAULT_NAME).schema)
)
LEGACY_VACUUM_SCHEMA = vol.All(
VACUUM_LEGACY_YAML_SCHEMA = vol.All(
cv.deprecated(CONF_ENTITY_ID),
vol.Schema(
{
@ -119,7 +119,7 @@ LEGACY_VACUUM_SCHEMA = vol.All(
)
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
{vol.Required(CONF_VACUUMS): cv.schema_with_slug_keys(LEGACY_VACUUM_SCHEMA)}
{vol.Required(CONF_VACUUMS): cv.schema_with_slug_keys(VACUUM_LEGACY_YAML_SCHEMA)}
)

View File

@ -31,7 +31,12 @@ from homeassistant.components.weather import (
WeatherEntity,
WeatherEntityFeature,
)
from homeassistant.const import CONF_TEMPERATURE_UNIT, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.const import (
CONF_NAME,
CONF_TEMPERATURE_UNIT,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv, template
@ -100,7 +105,7 @@ CONF_APPARENT_TEMPERATURE_TEMPLATE = "apparent_temperature_template"
DEFAULT_NAME = "Template Weather"
WEATHER_SCHEMA = vol.Schema(
WEATHER_YAML_SCHEMA = vol.Schema(
{
vol.Optional(CONF_APPARENT_TEMPERATURE_TEMPLATE): cv.template,
vol.Optional(CONF_ATTRIBUTION_TEMPLATE): cv.template,
@ -126,7 +131,32 @@ WEATHER_SCHEMA = vol.Schema(
}
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
PLATFORM_SCHEMA = WEATHER_PLATFORM_SCHEMA.extend(WEATHER_SCHEMA.schema)
PLATFORM_SCHEMA = vol.Schema(
{
vol.Optional(CONF_APPARENT_TEMPERATURE_TEMPLATE): cv.template,
vol.Optional(CONF_ATTRIBUTION_TEMPLATE): cv.template,
vol.Optional(CONF_CLOUD_COVERAGE_TEMPLATE): cv.template,
vol.Required(CONF_CONDITION_TEMPLATE): cv.template,
vol.Optional(CONF_DEW_POINT_TEMPLATE): cv.template,
vol.Required(CONF_HUMIDITY_TEMPLATE): cv.template,
vol.Optional(CONF_FORECAST_DAILY_TEMPLATE): cv.template,
vol.Optional(CONF_FORECAST_HOURLY_TEMPLATE): cv.template,
vol.Optional(CONF_FORECAST_TWICE_DAILY_TEMPLATE): cv.template,
vol.Optional(CONF_OZONE_TEMPLATE): cv.template,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template,
vol.Optional(CONF_PRECIPITATION_UNIT): vol.In(DistanceConverter.VALID_UNITS),
vol.Optional(CONF_PRESSURE_TEMPLATE): cv.template,
vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.VALID_UNITS),
vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template,
vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(TemperatureConverter.VALID_UNITS),
vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template,
vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS),
vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template,
vol.Optional(CONF_WIND_GUST_SPEED_TEMPLATE): cv.template,
vol.Optional(CONF_WIND_SPEED_TEMPLATE): cv.template,
vol.Optional(CONF_WIND_SPEED_UNIT): vol.In(SpeedConverter.VALID_UNITS),
}
).extend(WEATHER_PLATFORM_SCHEMA.schema)
async def async_setup_platform(

View File

@ -157,7 +157,6 @@ class EventStateEventData(TypedDict):
"""Base class for EVENT_STATE_CHANGED and EVENT_STATE_REPORTED data."""
entity_id: str
new_state: State | None
class EventStateChangedData(EventStateEventData):
@ -166,6 +165,7 @@ class EventStateChangedData(EventStateEventData):
A state changed event is fired when on state write the state is changed.
"""
new_state: State | None
old_state: State | None
@ -175,6 +175,8 @@ class EventStateReportedData(EventStateEventData):
A state reported event is fired when on state write the state is unchanged.
"""
last_reported: datetime.datetime
new_state: State
old_last_reported: datetime.datetime
@ -1749,18 +1751,38 @@ class CompressedState(TypedDict):
class State:
"""Object to represent a state within the state machine.
"""Object to represent a state within the state machine."""
entity_id: the entity that is represented.
state: the state of the entity
attributes: extra information on entity and state
last_changed: last time the state was changed.
last_reported: last time the state was reported.
last_updated: last time the state or attributes were changed.
context: Context in which it was created
domain: Domain of this state.
object_id: Object id of this state.
entity_id: str
"""The entity that is represented by the state."""
domain: str
"""Domain of the entity that is represented by the state."""
object_id: str
"""object_id: Object id of this state."""
state: str
"""The state of the entity."""
attributes: ReadOnlyDict[str, Any]
"""Extra information on entity and state"""
last_changed: datetime.datetime
"""Last time the state was changed."""
last_reported: datetime.datetime
"""Last time the state was reported.
Note: When the state is set and neither the state nor attributes are
changed, the existing state will be mutated with an updated last_reported.
When handling a state change event, the last_reported attribute of the old
state will not be modified and can safely be used. The last_reported attribute
of the new state may be modified and the last_updated attribute should be used
instead.
When handling a state report event, the last_reported attribute may be
modified and last_reported from the event data should be used instead.
"""
last_updated: datetime.datetime
"""Last time the state or attributes were changed."""
context: Context
"""Context in which the state was created."""
__slots__ = (
"_cache",
@ -1841,7 +1863,20 @@ class State:
@under_cached_property
def last_reported_timestamp(self) -> float:
"""Timestamp of last report."""
"""Timestamp of last report.
Note: When the state is set and neither the state nor attributes are
changed, the existing state will be mutated with an updated last_reported.
When handling a state change event, the last_reported_timestamp attribute
of the old state will not be modified and can safely be used. The
last_reported_timestamp attribute of the new state may be modified and the
last_updated_timestamp attribute should be used instead.
When handling a state report event, the last_reported_timestamp attribute may
be modified and last_reported from the event data should be used instead.
"""
return self.last_reported.timestamp()
@under_cached_property
@ -2340,6 +2375,7 @@ class StateMachine:
EVENT_STATE_REPORTED,
{
"entity_id": entity_id,
"last_reported": now,
"old_last_reported": old_last_reported,
"new_state": old_state,
},

View File

@ -813,6 +813,7 @@ class EntitySelectorConfig(BaseSelectorConfig, EntityFilterSelectorConfig, total
exclude_entities: list[str]
include_entities: list[str]
multiple: bool
reorder: bool
filter: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
@ -829,6 +830,7 @@ class EntitySelector(Selector[EntitySelectorConfig]):
vol.Optional("exclude_entities"): [str],
vol.Optional("include_entities"): [str],
vol.Optional("multiple", default=False): cv.boolean,
vol.Optional("reorder", default=False): cv.boolean,
vol.Optional("filter"): vol.All(
cv.ensure_list,
[ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA],

View File

@ -13,8 +13,10 @@ from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASSES_SCHEMA,
STATE_CLASSES_SCHEMA,
SensorDeviceClass,
SensorEntity,
)
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import (
ATTR_ENTITY_PICTURE,
ATTR_FRIENDLY_NAME,
@ -389,3 +391,20 @@ class ManualTriggerSensorEntity(ManualTriggerEntity, SensorEntity):
ManualTriggerEntity.__init__(self, hass, config)
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
self._attr_state_class = config.get(CONF_STATE_CLASS)
@callback
def _set_native_value_with_possible_timestamp(self, value: Any) -> None:
"""Set native value with possible timestamp.
If self.device_class is `date` or `timestamp`,
it will try to parse the value to a date/datetime object.
"""
if self.device_class not in (
SensorDeviceClass.DATE,
SensorDeviceClass.TIMESTAMP,
):
self._attr_native_value = value
elif value is not None:
self._attr_native_value = async_parse_date_datetime(
value, self.entity_id, self.device_class
)

View File

@ -38,7 +38,7 @@ habluetooth==4.0.1
hass-nabucasa==0.107.1
hassil==2.2.3
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250702.2
home-assistant-frontend==20250702.3
home-assistant-intents==2025.6.23
httpx==0.28.1
ifaddr==0.2.0

View File

@ -589,10 +589,6 @@ filterwarnings = [
# -- Websockets 14.1
# https://websockets.readthedocs.io/en/stable/howto/upgrade.html
"ignore:websockets.legacy is deprecated:DeprecationWarning:websockets.legacy",
# https://github.com/bluecurrent/HomeAssistantAPI/pull/19 - >=1.2.4
"ignore:websockets.client.connect is deprecated:DeprecationWarning:bluecurrent_api.websocket",
"ignore:websockets.client.WebSocketClientProtocol is deprecated:DeprecationWarning:bluecurrent_api.websocket",
"ignore:websockets.exceptions.InvalidStatusCode is deprecated:DeprecationWarning:bluecurrent_api.websocket",
# https://github.com/graphql-python/gql/pull/543 - >=4.0.0a0
"ignore:websockets.client.WebSocketClientProtocol is deprecated:DeprecationWarning:gql.transport.websockets_base",

14
requirements_all.txt generated
View File

@ -84,7 +84,7 @@ PyQRCode==1.2.1
PyRMVtransport==0.3.3
# homeassistant.components.switchbot
PySwitchbot==0.68.1
PySwitchbot==0.68.2
# homeassistant.components.switchmate
PySwitchmate==0.5.1
@ -185,7 +185,7 @@ aioairzone-cloud==0.6.15
aioairzone==1.0.0
# homeassistant.components.alexa_devices
aioamazondevices==3.2.10
aioamazondevices==3.5.0
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@ -247,7 +247,7 @@ aioelectricitymaps==0.4.0
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==35.0.0
aioesphomeapi==36.0.1
# homeassistant.components.flo
aioflo==2021.11.0
@ -634,7 +634,7 @@ blinkpy==0.23.0
blockchain==1.4.4
# homeassistant.components.blue_current
bluecurrent-api==1.2.3
bluecurrent-api==1.2.4
# homeassistant.components.bluemaestro
bluemaestro-ble==0.4.1
@ -1168,7 +1168,7 @@ hole==0.9.0
holidays==0.76
# homeassistant.components.frontend
home-assistant-frontend==20250702.2
home-assistant-frontend==20250702.3
# homeassistant.components.conversation
home-assistant-intents==2025.6.23
@ -1234,7 +1234,7 @@ ihcsdk==2.8.5
imeon_inverter_api==0.3.12
# homeassistant.components.imgw_pib
imgw_pib==1.4.1
imgw_pib==1.4.2
# homeassistant.components.incomfort
incomfort-client==0.6.9
@ -2346,7 +2346,7 @@ pysma==0.7.5
pysmappee==0.2.29
# homeassistant.components.smarla
pysmarlaapi==0.9.0
pysmarlaapi==0.9.1
# homeassistant.components.smartthings
pysmartthings==3.2.8

View File

@ -81,7 +81,7 @@ PyQRCode==1.2.1
PyRMVtransport==0.3.3
# homeassistant.components.switchbot
PySwitchbot==0.68.1
PySwitchbot==0.68.2
# homeassistant.components.syncthru
PySyncThru==0.8.0
@ -173,7 +173,7 @@ aioairzone-cloud==0.6.15
aioairzone==1.0.0
# homeassistant.components.alexa_devices
aioamazondevices==3.2.10
aioamazondevices==3.5.0
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@ -235,7 +235,7 @@ aioelectricitymaps==0.4.0
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==35.0.0
aioesphomeapi==36.0.1
# homeassistant.components.flo
aioflo==2021.11.0
@ -565,7 +565,7 @@ blebox-uniapi==2.5.0
blinkpy==0.23.0
# homeassistant.components.blue_current
bluecurrent-api==1.2.3
bluecurrent-api==1.2.4
# homeassistant.components.bluemaestro
bluemaestro-ble==0.4.1
@ -1017,7 +1017,7 @@ hole==0.9.0
holidays==0.76
# homeassistant.components.frontend
home-assistant-frontend==20250702.2
home-assistant-frontend==20250702.3
# homeassistant.components.conversation
home-assistant-intents==2025.6.23
@ -1068,7 +1068,7 @@ igloohome-api==0.1.1
imeon_inverter_api==0.3.12
# homeassistant.components.imgw_pib
imgw_pib==1.4.1
imgw_pib==1.4.2
# homeassistant.components.incomfort
incomfort-client==0.6.9
@ -1949,7 +1949,7 @@ pysma==0.7.5
pysmappee==0.2.29
# homeassistant.components.smarla
pysmarlaapi==0.9.0
pysmarlaapi==0.9.1
# homeassistant.components.smartthings
pysmartthings==3.2.8

View File

@ -38,6 +38,9 @@ FIELD_SCHEMA = vol.Schema(
TRIGGER_SCHEMA = vol.Any(
vol.Schema(
{
vol.Optional("target"): vol.Any(
selector.TargetSelector.CONFIG_SCHEMA, None
),
vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}),
}
),

View File

@ -4,6 +4,7 @@
from __future__ import annotations
import json
import os
from pathlib import Path
import re
import subprocess
@ -20,13 +21,15 @@ DOWNLOAD_DIR = Path("build/translations-download").absolute()
def run_download_docker():
"""Run the Docker image to download the translations."""
print("Running Docker to download latest translations.")
run = subprocess.run(
result = subprocess.run(
[
"docker",
"run",
"-v",
f"{DOWNLOAD_DIR}:/opt/dest/locale",
"--rm",
"--user",
f"{os.getuid()}:{os.getgid()}",
f"lokalise/lokalise-cli-2:{CLI_2_DOCKER_IMAGE}",
# Lokalise command
"lokalise2",
@ -52,7 +55,7 @@ def run_download_docker():
)
print()
if run.returncode != 0:
if result.returncode != 0:
raise ExitApp("Failed to download translations")

View File

@ -203,6 +203,7 @@
'light',
]),
'multiple': False,
'reorder': False,
}),
}),
}),
@ -217,6 +218,7 @@
'binary_sensor',
]),
'multiple': False,
'reorder': False,
}),
}),
}),

View File

@ -40,7 +40,6 @@ async def test_generic_alarm_control_panel_requires_code(
object_id="myalarm_control_panel",
key=1,
name="my alarm_control_panel",
unique_id="my_alarm_control_panel",
supported_features=EspHomeACPFeatures.ARM_AWAY
| EspHomeACPFeatures.ARM_CUSTOM_BYPASS
| EspHomeACPFeatures.ARM_HOME
@ -173,7 +172,6 @@ async def test_generic_alarm_control_panel_no_code(
object_id="myalarm_control_panel",
key=1,
name="my alarm_control_panel",
unique_id="my_alarm_control_panel",
supported_features=EspHomeACPFeatures.ARM_AWAY
| EspHomeACPFeatures.ARM_CUSTOM_BYPASS
| EspHomeACPFeatures.ARM_HOME
@ -219,7 +217,6 @@ async def test_generic_alarm_control_panel_missing_state(
object_id="myalarm_control_panel",
key=1,
name="my alarm_control_panel",
unique_id="my_alarm_control_panel",
supported_features=EspHomeACPFeatures.ARM_AWAY
| EspHomeACPFeatures.ARM_CUSTOM_BYPASS
| EspHomeACPFeatures.ARM_HOME

View File

@ -953,7 +953,6 @@ async def test_tts_format_from_media_player(
object_id="mymedia_player",
key=1,
name="my media_player",
unique_id="my_media_player",
supports_pause=True,
supported_formats=[
MediaPlayerSupportedFormat(
@ -1020,7 +1019,6 @@ async def test_tts_minimal_format_from_media_player(
object_id="mymedia_player",
key=1,
name="my media_player",
unique_id="my_media_player",
supports_pause=True,
supported_formats=[
MediaPlayerSupportedFormat(
@ -1156,7 +1154,6 @@ async def test_announce_media_id(
object_id="mymedia_player",
key=1,
name="my media_player",
unique_id="my_media_player",
supports_pause=True,
supported_formats=[
MediaPlayerSupportedFormat(
@ -1437,7 +1434,6 @@ async def test_start_conversation_media_id(
object_id="mymedia_player",
key=1,
name="my media_player",
unique_id="my_media_player",
supports_pause=True,
supported_formats=[
MediaPlayerSupportedFormat(

View File

@ -24,7 +24,6 @@ async def test_binary_sensor_generic_entity(
object_id="mybinary_sensor",
key=1,
name="my binary_sensor",
unique_id="my_binary_sensor",
)
]
esphome_state, hass_state = binary_state
@ -52,7 +51,6 @@ async def test_status_binary_sensor(
object_id="mybinary_sensor",
key=1,
name="my binary_sensor",
unique_id="my_binary_sensor",
is_status_binary_sensor=True,
)
]
@ -80,7 +78,6 @@ async def test_binary_sensor_missing_state(
object_id="mybinary_sensor",
key=1,
name="my binary_sensor",
unique_id="my_binary_sensor",
)
]
states = [BinarySensorState(key=1, state=True, missing_state=True)]
@ -107,7 +104,6 @@ async def test_binary_sensor_has_state_false(
object_id="mybinary_sensor",
key=1,
name="my binary_sensor",
unique_id="my_binary_sensor",
)
]
states = []
@ -152,14 +148,12 @@ async def test_binary_sensors_same_key_different_device_id(
object_id="sensor",
key=1,
name="Motion",
unique_id="motion_1",
device_id=11111111,
),
BinarySensorInfo(
object_id="sensor",
key=1,
name="Motion",
unique_id="motion_2",
device_id=22222222,
),
]
@ -235,14 +229,12 @@ async def test_binary_sensor_main_and_sub_device_same_key(
object_id="main_sensor",
key=1,
name="Main Sensor",
unique_id="main_1",
device_id=0, # Main device
),
BinarySensorInfo(
object_id="sub_sensor",
key=1,
name="Sub Sensor",
unique_id="sub_1",
device_id=11111111,
),
]

View File

@ -18,7 +18,6 @@ async def test_button_generic_entity(
object_id="mybutton",
key=1,
name="my button",
unique_id="my_button",
)
]
states = []

View File

@ -30,7 +30,6 @@ async def test_camera_single_image(
object_id="mycamera",
key=1,
name="my camera",
unique_id="my_camera",
)
]
states = []
@ -75,7 +74,6 @@ async def test_camera_single_image_unavailable_before_requested(
object_id="mycamera",
key=1,
name="my camera",
unique_id="my_camera",
)
]
states = []
@ -113,7 +111,6 @@ async def test_camera_single_image_unavailable_during_request(
object_id="mycamera",
key=1,
name="my camera",
unique_id="my_camera",
)
]
states = []
@ -155,7 +152,6 @@ async def test_camera_stream(
object_id="mycamera",
key=1,
name="my camera",
unique_id="my_camera",
)
]
states = []
@ -212,7 +208,6 @@ async def test_camera_stream_unavailable(
object_id="mycamera",
key=1,
name="my camera",
unique_id="my_camera",
)
]
states = []
@ -249,7 +244,6 @@ async def test_camera_stream_with_disconnection(
object_id="mycamera",
key=1,
name="my camera",
unique_id="my_camera",
)
]
states = []

View File

@ -58,7 +58,6 @@ async def test_climate_entity(
object_id="myclimate",
key=1,
name="my climate",
unique_id="my_climate",
supports_current_temperature=True,
supports_action=True,
visual_min_temperature=10.0,
@ -110,7 +109,6 @@ async def test_climate_entity_with_step_and_two_point(
object_id="myclimate",
key=1,
name="my climate",
unique_id="my_climate",
supports_current_temperature=True,
supports_two_point_target_temperature=True,
visual_target_temperature_step=2,
@ -187,7 +185,6 @@ async def test_climate_entity_with_step_and_target_temp(
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,
@ -345,7 +342,6 @@ async def test_climate_entity_with_humidity(
object_id="myclimate",
key=1,
name="my climate",
unique_id="my_climate",
supports_current_temperature=True,
supports_two_point_target_temperature=True,
supports_action=True,
@ -409,7 +405,6 @@ async def test_climate_entity_with_inf_value(
object_id="myclimate",
key=1,
name="my climate",
unique_id="my_climate",
supports_current_temperature=True,
supports_two_point_target_temperature=True,
supports_action=True,
@ -465,7 +460,6 @@ async def test_climate_entity_attributes(
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,
@ -520,7 +514,6 @@ async def test_climate_entity_attribute_current_temperature_unsupported(
object_id="myclimate",
key=1,
name="my climate",
unique_id="my_climate",
supports_current_temperature=False,
)
]

View File

@ -41,7 +41,6 @@ async def test_cover_entity(
object_id="mycover",
key=1,
name="my cover",
unique_id="my_cover",
supports_position=True,
supports_tilt=True,
supports_stop=True,
@ -169,7 +168,6 @@ async def test_cover_entity_without_position(
object_id="mycover",
key=1,
name="my cover",
unique_id="my_cover",
supports_position=False,
supports_tilt=False,
supports_stop=False,

View File

@ -26,7 +26,6 @@ async def test_generic_date_entity(
object_id="mydate",
key=1,
name="my date",
unique_id="my_date",
)
]
states = [DateState(key=1, year=2024, month=12, day=31)]
@ -62,7 +61,6 @@ async def test_generic_date_missing_state(
object_id="mydate",
key=1,
name="my date",
unique_id="my_date",
)
]
states = [DateState(key=1, missing_state=True)]

View File

@ -26,7 +26,6 @@ async def test_generic_datetime_entity(
object_id="mydatetime",
key=1,
name="my datetime",
unique_id="my_datetime",
)
]
states = [DateTimeState(key=1, epoch_seconds=1713270896)]
@ -65,7 +64,6 @@ async def test_generic_datetime_missing_state(
object_id="mydatetime",
key=1,
name="my datetime",
unique_id="my_datetime",
)
]
states = [DateTimeState(key=1, missing_state=True)]

View File

@ -51,13 +51,11 @@ async def test_entities_removed(
object_id="mybinary_sensor",
key=1,
name="my binary_sensor",
unique_id="my_binary_sensor",
),
BinarySensorInfo(
object_id="mybinary_sensor_to_be_removed",
key=2,
name="my binary_sensor to be removed",
unique_id="mybinary_sensor_to_be_removed",
),
]
states = [
@ -100,7 +98,6 @@ async def test_entities_removed(
object_id="mybinary_sensor",
key=1,
name="my binary_sensor",
unique_id="my_binary_sensor",
),
]
states = [
@ -140,13 +137,11 @@ async def test_entities_removed_after_reload(
object_id="mybinary_sensor",
key=1,
name="my binary_sensor",
unique_id="my_binary_sensor",
),
BinarySensorInfo(
object_id="mybinary_sensor_to_be_removed",
key=2,
name="my binary_sensor to be removed",
unique_id="mybinary_sensor_to_be_removed",
),
]
states = [
@ -214,7 +209,6 @@ async def test_entities_removed_after_reload(
object_id="mybinary_sensor",
key=1,
name="my binary_sensor",
unique_id="my_binary_sensor",
),
]
mock_device.client.list_entities_services = AsyncMock(
@ -267,7 +261,6 @@ async def test_entities_for_entire_platform_removed(
object_id="mybinary_sensor_to_be_removed",
key=1,
name="my binary_sensor to be removed",
unique_id="mybinary_sensor_to_be_removed",
),
]
states = [
@ -325,7 +318,6 @@ async def test_entity_info_object_ids(
object_id="object_id_is_used",
key=1,
name="my binary_sensor",
unique_id="my_binary_sensor",
)
]
states = []
@ -350,13 +342,11 @@ async def test_deep_sleep_device(
object_id="mybinary_sensor",
key=1,
name="my binary_sensor",
unique_id="my_binary_sensor",
),
SensorInfo(
object_id="my_sensor",
key=3,
name="my sensor",
unique_id="my_sensor",
),
]
states = [
@ -456,7 +446,6 @@ async def test_esphome_device_without_friendly_name(
object_id="mybinary_sensor",
key=1,
name="my binary_sensor",
unique_id="my_binary_sensor",
),
]
states = [
@ -486,7 +475,6 @@ async def test_entity_without_name_device_with_friendly_name(
object_id="mybinary_sensor",
key=1,
name="",
unique_id="my_binary_sensor",
),
]
states = [
@ -519,7 +507,6 @@ async def test_entity_id_preserved_on_upgrade(
object_id="my",
key=1,
name="my",
unique_id="binary_sensor_my",
),
]
states = [
@ -560,7 +547,6 @@ async def test_entity_id_preserved_on_upgrade_old_format_entity_id(
object_id="my",
key=1,
name="my",
unique_id="binary_sensor_my",
),
]
states = [
@ -601,7 +587,6 @@ async def test_entity_id_preserved_on_upgrade_when_in_storage(
object_id="my",
key=1,
name="my",
unique_id="binary_sensor_my",
),
]
states = [
@ -660,7 +645,6 @@ async def test_deep_sleep_added_after_setup(
object_id="test",
key=1,
name="test",
unique_id="test",
),
],
states=[
@ -732,7 +716,6 @@ async def test_entity_assignment_to_sub_device(
object_id="main_sensor",
key=1,
name="Main Sensor",
unique_id="main_sensor",
device_id=0,
),
# Entity for sub device 1
@ -740,7 +723,6 @@ async def test_entity_assignment_to_sub_device(
object_id="motion",
key=2,
name="Motion",
unique_id="motion",
device_id=11111111,
),
# Entity for sub device 2
@ -748,7 +730,6 @@ async def test_entity_assignment_to_sub_device(
object_id="door",
key=3,
name="Door",
unique_id="door",
device_id=22222222,
),
]
@ -932,7 +913,6 @@ async def test_entity_switches_between_devices(
object_id="sensor",
key=1,
name="Test Sensor",
unique_id="sensor",
# device_id omitted - entity belongs to main device
),
]
@ -964,7 +944,6 @@ async def test_entity_switches_between_devices(
object_id="sensor",
key=1,
name="Test Sensor",
unique_id="sensor",
device_id=11111111, # Now on sub device 1
),
]
@ -993,7 +972,6 @@ async def test_entity_switches_between_devices(
object_id="sensor",
key=1,
name="Test Sensor",
unique_id="sensor",
device_id=22222222, # Now on sub device 2
),
]
@ -1020,7 +998,6 @@ async def test_entity_switches_between_devices(
object_id="sensor",
key=1,
name="Test Sensor",
unique_id="sensor",
# device_id omitted - back to main device
),
]
@ -1063,7 +1040,6 @@ async def test_entity_id_uses_sub_device_name(
object_id="main_sensor",
key=1,
name="Main Sensor",
unique_id="main_sensor",
device_id=0,
),
# Entity for sub device 1
@ -1071,7 +1047,6 @@ async def test_entity_id_uses_sub_device_name(
object_id="motion",
key=2,
name="Motion",
unique_id="motion",
device_id=11111111,
),
# Entity for sub device 2
@ -1079,7 +1054,6 @@ async def test_entity_id_uses_sub_device_name(
object_id="door",
key=3,
name="Door",
unique_id="door",
device_id=22222222,
),
# Entity without name on sub device
@ -1087,7 +1061,6 @@ async def test_entity_id_uses_sub_device_name(
object_id="sensor_no_name",
key=4,
name="",
unique_id="sensor_no_name",
device_id=11111111,
),
]
@ -1147,7 +1120,6 @@ async def test_entity_id_with_empty_sub_device_name(
object_id="sensor",
key=1,
name="Sensor",
unique_id="sensor",
device_id=11111111,
),
]
@ -1187,8 +1159,7 @@ async def test_unique_id_migration_when_entity_moves_between_devices(
BinarySensorInfo(
object_id="temperature",
key=1,
name="Temperature",
unique_id="unused", # This field is not used by the integration
name="Temperature", # This field is not used by the integration
device_id=0, # Main device
),
]
@ -1250,8 +1221,7 @@ async def test_unique_id_migration_when_entity_moves_between_devices(
BinarySensorInfo(
object_id="temperature", # Same object_id
key=1, # Same key - this is what identifies the entity
name="Temperature",
unique_id="unused", # This field is not used
name="Temperature", # This field is not used
device_id=22222222, # Now on sub-device
),
]
@ -1312,7 +1282,6 @@ async def test_unique_id_migration_sub_device_to_main_device(
object_id="temperature",
key=1,
name="Temperature",
unique_id="unused",
device_id=22222222, # On sub-device
),
]
@ -1347,7 +1316,6 @@ async def test_unique_id_migration_sub_device_to_main_device(
object_id="temperature",
key=1,
name="Temperature",
unique_id="unused",
device_id=0, # Now on main device
),
]
@ -1407,7 +1375,6 @@ async def test_unique_id_migration_between_sub_devices(
object_id="temperature",
key=1,
name="Temperature",
unique_id="unused",
device_id=22222222, # On kitchen_controller
),
]
@ -1442,7 +1409,6 @@ async def test_unique_id_migration_between_sub_devices(
object_id="temperature",
key=1,
name="Temperature",
unique_id="unused",
device_id=33333333, # Now on bedroom_controller
),
]
@ -1501,7 +1467,6 @@ async def test_entity_device_id_rename_in_yaml(
object_id="sensor",
key=1,
name="Sensor",
unique_id="unused",
device_id=11111111,
),
]
@ -1563,7 +1528,6 @@ async def test_entity_device_id_rename_in_yaml(
object_id="sensor", # Same object_id
key=1, # Same key
name="Sensor",
unique_id="unused",
device_id=99999999, # New device_id after rename
),
]
@ -1636,8 +1600,7 @@ async def test_entity_with_unicode_name(
BinarySensorInfo(
object_id=sanitized_object_id, # ESPHome sends the sanitized version
key=1,
name=unicode_name, # But also sends the original Unicode name
unique_id="unicode_sensor",
name=unicode_name, # But also sends the original Unicode name,
)
]
states = [BinarySensorState(key=1, state=True)]
@ -1677,8 +1640,7 @@ async def test_entity_without_name_uses_device_name_only(
BinarySensorInfo(
object_id="some_sanitized_id",
key=1,
name="", # Empty name
unique_id="no_name_sensor",
name="", # Empty name,
)
]
states = [BinarySensorState(key=1, state=True)]

View File

@ -15,49 +15,6 @@ from homeassistant.helpers import entity_registry as er
from .conftest import MockGenericDeviceEntryType
async def test_migrate_entity_unique_id(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test a generic sensor entity unique id migration."""
entity_registry.async_get_or_create(
"sensor",
"esphome",
"my_sensor",
suggested_object_id="old_sensor",
disabled_by=None,
)
entity_info = [
SensorInfo(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
entity_category=ESPHomeEntityCategory.DIAGNOSTIC,
icon="mdi:leaf",
)
]
states = [SensorState(key=1, state=50)]
user_service = []
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
user_service=user_service,
states=states,
)
state = hass.states.get("sensor.old_sensor")
assert state is not None
assert state.state == "50"
entry = entity_registry.async_get("sensor.old_sensor")
assert entry is not None
assert entity_registry.async_get_entity_id("sensor", "esphome", "my_sensor") is None
# Note that ESPHome includes the EntityInfo type in the unique id
# as this is not a 1:1 mapping to the entity platform (ie. text_sensor)
assert entry.unique_id == "11:22:33:44:55:AA-sensor-mysensor"
async def test_migrate_entity_unique_id_downgrade_upgrade(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
@ -84,7 +41,6 @@ async def test_migrate_entity_unique_id_downgrade_upgrade(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
entity_category=ESPHomeEntityCategory.DIAGNOSTIC,
icon="mdi:leaf",
)

View File

@ -20,7 +20,6 @@ async def test_generic_event_entity(
object_id="myevent",
key=1,
name="my event",
unique_id="my_event",
event_types=["type1", "type2"],
device_class=EventDeviceClass.BUTTON,
)

View File

@ -44,7 +44,6 @@ async def test_fan_entity_with_all_features_old_api(
object_id="myfan",
key=1,
name="my fan",
unique_id="my_fan",
supports_direction=True,
supports_speed=True,
supports_oscillation=True,
@ -147,7 +146,6 @@ async def test_fan_entity_with_all_features_new_api(
object_id="myfan",
key=1,
name="my fan",
unique_id="my_fan",
supported_speed_count=4,
supports_direction=True,
supports_speed=True,
@ -317,7 +315,6 @@ async def test_fan_entity_with_no_features_new_api(
object_id="myfan",
key=1,
name="my fan",
unique_id="my_fan",
supports_direction=False,
supports_speed=False,
supports_oscillation=False,

View File

@ -56,7 +56,6 @@ async def test_light_on_off(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=[ESPColorMode.ON_OFF],
@ -98,7 +97,6 @@ async def test_light_brightness(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=[LightColorCapability.BRIGHTNESS],
@ -226,7 +224,6 @@ async def test_light_legacy_brightness(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=[LightColorCapability.BRIGHTNESS, 2],
@ -282,7 +279,6 @@ async def test_light_brightness_on_off(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=[ESPColorMode.ON_OFF, ESPColorMode.BRIGHTNESS],
@ -358,7 +354,6 @@ async def test_light_legacy_white_converted_to_brightness(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=[
@ -423,7 +418,6 @@ async def test_light_legacy_white_with_rgb(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=[color_mode, color_mode_2],
@ -478,7 +472,6 @@ async def test_light_brightness_on_off_with_unknown_color_mode(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=[
@ -555,7 +548,6 @@ async def test_light_on_and_brightness(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=[
@ -607,7 +599,6 @@ async def test_rgb_color_temp_light(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=color_modes,
@ -698,7 +689,6 @@ async def test_light_rgb(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
supported_color_modes=[
LightColorCapability.RGB
| LightColorCapability.ON_OFF
@ -821,7 +811,6 @@ async def test_light_rgbw(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
supported_color_modes=[
LightColorCapability.RGB
| LightColorCapability.WHITE
@ -991,7 +980,6 @@ async def test_light_rgbww_with_cold_warm_white_support(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=[
@ -1200,7 +1188,6 @@ async def test_light_rgbww_without_cold_warm_white_support(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=[
@ -1439,7 +1426,6 @@ async def test_light_color_temp(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153.846161,
max_mireds=370.370361,
supported_color_modes=[
@ -1514,7 +1500,6 @@ async def test_light_color_temp_no_mireds_set(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=0,
max_mireds=0,
supported_color_modes=[
@ -1610,7 +1595,6 @@ async def test_light_color_temp_legacy(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153.846161,
max_mireds=370.370361,
supported_color_modes=[
@ -1695,7 +1679,6 @@ async def test_light_rgb_legacy(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153.846161,
max_mireds=370.370361,
supported_color_modes=[
@ -1795,7 +1778,6 @@ async def test_light_effects(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
effects=["effect1", "effect2"],
@ -1859,7 +1841,6 @@ async def test_only_cold_warm_white_support(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=[color_modes],
@ -1955,7 +1936,6 @@ async def test_light_no_color_modes(
object_id="mylight",
key=1,
name="my light",
unique_id="my_light",
min_mireds=153,
max_mireds=400,
supported_color_modes=[color_mode],

View File

@ -34,7 +34,6 @@ async def test_lock_entity_no_open(
object_id="mylock",
key=1,
name="my lock",
unique_id="my_lock",
supports_open=False,
requires_code=False,
)
@ -72,7 +71,6 @@ async def test_lock_entity_start_locked(
object_id="mylock",
key=1,
name="my lock",
unique_id="my_lock",
)
]
states = [LockEntityState(key=1, state=ESPHomeLockState.LOCKED)]
@ -99,7 +97,6 @@ async def test_lock_entity_supports_open(
object_id="mylock",
key=1,
name="my lock",
unique_id="my_lock",
supports_open=True,
requires_code=True,
)

View File

@ -55,7 +55,6 @@ async def test_media_player_entity(
object_id="mymedia_player",
key=1,
name="my media_player",
unique_id="my_media_player",
supports_pause=True,
)
]
@ -202,7 +201,6 @@ async def test_media_player_entity_with_source(
object_id="mymedia_player",
key=1,
name="my media_player",
unique_id="my_media_player",
supports_pause=True,
)
]
@ -318,7 +316,6 @@ async def test_media_player_proxy(
object_id="mymedia_player",
key=1,
name="my media_player",
unique_id="my_media_player",
supports_pause=True,
supported_formats=[
MediaPlayerSupportedFormat(
@ -477,7 +474,6 @@ async def test_media_player_formats_reload_preserves_data(
object_id="test_media_player",
key=1,
name="Test Media Player",
unique_id="test_unique_id",
supports_pause=True,
supported_formats=supported_formats,
)

View File

@ -35,7 +35,6 @@ async def test_generic_number_entity(
object_id="mynumber",
key=1,
name="my number",
unique_id="my_number",
max_value=100,
min_value=0,
step=1,
@ -75,7 +74,6 @@ async def test_generic_number_nan(
object_id="mynumber",
key=1,
name="my number",
unique_id="my_number",
max_value=100,
min_value=0,
step=1,
@ -107,7 +105,6 @@ async def test_generic_number_with_unit_of_measurement_as_empty_string(
object_id="mynumber",
key=1,
name="my number",
unique_id="my_number",
max_value=100,
min_value=0,
step=1,
@ -140,7 +137,6 @@ async def test_generic_number_entity_set_when_disconnected(
object_id="mynumber",
key=1,
name="my number",
unique_id="my_number",
max_value=100,
min_value=0,
step=1,

View File

@ -133,7 +133,6 @@ async def test_device_conflict_migration(
object_id="mybinary_sensor",
key=1,
name="my binary_sensor",
unique_id="my_binary_sensor",
is_status_binary_sensor=True,
)
]

View File

@ -67,7 +67,6 @@ async def test_select_generic_entity(
object_id="myselect",
key=1,
name="my select",
unique_id="my_select",
options=["a", "b"],
)
]

View File

@ -54,7 +54,6 @@ async def test_generic_numeric_sensor(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
)
]
states = [SensorState(key=1, state=50)]
@ -110,7 +109,6 @@ async def test_generic_numeric_sensor_with_entity_category_and_icon(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
entity_category=ESPHomeEntityCategory.DIAGNOSTIC,
icon="mdi:leaf",
)
@ -147,7 +145,6 @@ async def test_generic_numeric_sensor_state_class_measurement(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
state_class=ESPHomeSensorStateClass.MEASUREMENT,
device_class="power",
unit_of_measurement="W",
@ -184,7 +181,6 @@ async def test_generic_numeric_sensor_device_class_timestamp(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
device_class="timestamp",
)
]
@ -212,7 +208,6 @@ async def test_generic_numeric_sensor_legacy_last_reset_convert(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
legacy_last_reset_type=LastResetType.AUTO,
state_class=ESPHomeSensorStateClass.MEASUREMENT,
)
@ -242,7 +237,6 @@ async def test_generic_numeric_sensor_no_state(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
)
]
states = []
@ -269,7 +263,6 @@ async def test_generic_numeric_sensor_nan_state(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
)
]
states = [SensorState(key=1, state=math.nan, missing_state=False)]
@ -296,7 +289,6 @@ async def test_generic_numeric_sensor_missing_state(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
)
]
states = [SensorState(key=1, state=True, missing_state=True)]
@ -323,7 +315,6 @@ async def test_generic_text_sensor(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
)
]
states = [TextSensorState(key=1, state="i am a teapot")]
@ -350,7 +341,6 @@ async def test_generic_text_sensor_missing_state(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
)
]
states = [TextSensorState(key=1, state=True, missing_state=True)]
@ -377,7 +367,6 @@ async def test_generic_text_sensor_device_class_timestamp(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
device_class=SensorDeviceClass.TIMESTAMP,
)
]
@ -406,7 +395,6 @@ async def test_generic_text_sensor_device_class_date(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
device_class=SensorDeviceClass.DATE,
)
]
@ -435,7 +423,6 @@ async def test_generic_numeric_sensor_empty_string_uom(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
unit_of_measurement="",
)
]
@ -493,7 +480,6 @@ async def test_suggested_display_precision_by_device_class(
object_id="mysensor",
key=1,
name="my sensor",
unique_id="my_sensor",
accuracy_decimals=expected_precision,
device_class=device_class.value,
unit_of_measurement=unit_of_measurement,

View File

@ -26,7 +26,6 @@ async def test_switch_generic_entity(
object_id="myswitch",
key=1,
name="my switch",
unique_id="my_switch",
)
]
states = [SwitchState(key=1, state=True)]
@ -78,14 +77,12 @@ async def test_switch_sub_device_non_zero_device_id(
object_id="main_switch",
key=1,
name="Main Switch",
unique_id="main_switch_1",
device_id=0, # Main device
),
SwitchInfo(
object_id="sub_switch",
key=2,
name="Sub Switch",
unique_id="sub_switch_1",
device_id=11111111, # Sub-device
),
]

View File

@ -26,7 +26,6 @@ async def test_generic_text_entity(
object_id="mytext",
key=1,
name="my text",
unique_id="my_text",
max_length=100,
min_length=0,
pattern=None,
@ -66,7 +65,6 @@ async def test_generic_text_entity_no_state(
object_id="mytext",
key=1,
name="my text",
unique_id="my_text",
max_length=100,
min_length=0,
pattern=None,
@ -97,7 +95,6 @@ async def test_generic_text_entity_missing_state(
object_id="mytext",
key=1,
name="my text",
unique_id="my_text",
max_length=100,
min_length=0,
pattern=None,

View File

@ -26,7 +26,6 @@ async def test_generic_time_entity(
object_id="mytime",
key=1,
name="my time",
unique_id="my_time",
)
]
states = [TimeState(key=1, hour=12, minute=34, second=56)]
@ -62,7 +61,6 @@ async def test_generic_time_missing_state(
object_id="mytime",
key=1,
name="my time",
unique_id="my_time",
)
]
states = [TimeState(key=1, missing_state=True)]

View File

@ -436,7 +436,6 @@ async def test_generic_device_update_entity(
object_id="myupdate",
key=1,
name="my update",
unique_id="my_update",
)
]
states = [
@ -470,7 +469,6 @@ async def test_generic_device_update_entity_has_update(
object_id="myupdate",
key=1,
name="my update",
unique_id="my_update",
)
]
states = [
@ -561,7 +559,6 @@ async def test_update_entity_release_notes(
object_id="myupdate",
key=1,
name="my update",
unique_id="my_update",
)
]

View File

@ -36,7 +36,6 @@ async def test_valve_entity(
object_id="myvalve",
key=1,
name="my valve",
unique_id="my_valve",
supports_position=True,
supports_stop=True,
)
@ -134,7 +133,6 @@ async def test_valve_entity_without_position(
object_id="myvalve",
key=1,
name="my valve",
unique_id="my_valve",
supports_position=False,
supports_stop=False,
)

View File

@ -10,7 +10,7 @@ import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL
from homeassistant.const import STATE_UNKNOWN, Platform
from homeassistant.const import STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@ -39,7 +39,7 @@ async def test_sensor_unknown_states(
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_mower_1_mode")
assert state.state == STATE_UNKNOWN
assert state.state == STATE_UNAVAILABLE
async def test_cutting_blade_usage_time_sensor(
@ -78,7 +78,7 @@ async def test_next_start_sensor(
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_mower_1_next_start")
assert state.state == STATE_UNKNOWN
assert state.state == STATE_UNAVAILABLE
async def test_work_area_sensor(

View File

@ -924,6 +924,30 @@ async def test_invalid_unit_of_measurement(
"device_class": None,
"unit_of_measurement": None,
},
{
"name": "Test 4",
"state_topic": "test-topic",
"device_class": "ph",
"unit_of_measurement": "",
},
{
"name": "Test 5",
"state_topic": "test-topic",
"device_class": "ph",
"unit_of_measurement": " ",
},
{
"name": "Test 6",
"state_topic": "test-topic",
"device_class": None,
"unit_of_measurement": "",
},
{
"name": "Test 7",
"state_topic": "test-topic",
"device_class": None,
"unit_of_measurement": " ",
},
]
}
}
@ -936,10 +960,25 @@ async def test_valid_device_class_and_uom(
await mqtt_mock_entry()
state = hass.states.get("sensor.test_1")
assert state is not None
assert state.attributes["device_class"] == "temperature"
state = hass.states.get("sensor.test_2")
assert state is not None
assert "device_class" not in state.attributes
state = hass.states.get("sensor.test_3")
assert state is not None
assert "device_class" not in state.attributes
state = hass.states.get("sensor.test_4")
assert state is not None
assert state.attributes["device_class"] == "ph"
state = hass.states.get("sensor.test_5")
assert state is not None
assert state.attributes["device_class"] == "ph"
state = hass.states.get("sensor.test_6")
assert state is not None
assert "device_class" not in state.attributes
state = hass.states.get("sensor.test_7")
assert state is not None
assert "device_class" not in state.attributes

View File

@ -1,11 +1,13 @@
"""Test AI Task platform of Ollama integration."""
from pathlib import Path
from unittest.mock import patch
import ollama
import pytest
import voluptuous as vol
from homeassistant.components import ai_task
from homeassistant.components import ai_task, media_source
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er, selector
@ -243,3 +245,115 @@ async def test_generate_invalid_structured_data(
},
),
)
@pytest.mark.usefixtures("mock_init_component")
async def test_generate_data_with_attachment(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test AI Task data generation with image attachments."""
entity_id = "ai_task.ollama_ai_task"
# Mock the Ollama chat response as an async iterator
async def mock_chat_response():
"""Mock streaming response."""
yield {
"message": {"role": "assistant", "content": "Generated test data"},
"done": True,
"done_reason": "stop",
}
with (
patch(
"homeassistant.components.media_source.async_resolve_media",
side_effect=[
media_source.PlayMedia(
url="http://example.com/doorbell_snapshot.jpg",
mime_type="image/jpeg",
path=Path("doorbell_snapshot.jpg"),
),
],
),
patch(
"ollama.AsyncClient.chat",
return_value=mock_chat_response(),
) as mock_chat,
):
result = await ai_task.async_generate_data(
hass,
task_name="Test Task",
entity_id=entity_id,
instructions="Generate test data",
attachments=[
{"media_content_id": "media-source://media/doorbell_snapshot.jpg"},
],
)
assert result.data == "Generated test data"
assert mock_chat.call_count == 1
messages = mock_chat.call_args[1]["messages"]
assert len(messages) == 2
chat_message = messages[1]
assert chat_message.role == "user"
assert chat_message.content == "Generate test data"
assert chat_message.images == [
ollama.Image(value=Path("doorbell_snapshot.jpg")),
]
@pytest.mark.usefixtures("mock_init_component")
async def test_generate_data_with_unsupported_file_format(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test AI Task data generation with image attachments."""
entity_id = "ai_task.ollama_ai_task"
# Mock the Ollama chat response as an async iterator
async def mock_chat_response():
"""Mock streaming response."""
yield {
"message": {"role": "assistant", "content": "Generated test data"},
"done": True,
"done_reason": "stop",
}
with (
patch(
"homeassistant.components.media_source.async_resolve_media",
side_effect=[
media_source.PlayMedia(
url="http://example.com/doorbell_snapshot.jpg",
mime_type="image/jpeg",
path=Path("doorbell_snapshot.jpg"),
),
media_source.PlayMedia(
url="http://example.com/context.txt",
mime_type="text/plain",
path=Path("context.txt"),
),
],
),
patch(
"ollama.AsyncClient.chat",
return_value=mock_chat_response(),
),
pytest.raises(
HomeAssistantError,
match="Ollama only supports image attachments in user content",
),
):
await ai_task.async_generate_data(
hass,
task_name="Test Task",
entity_id=entity_id,
instructions="Generate test data",
attachments=[
{"media_content_id": "media-source://media/doorbell_snapshot.jpg"},
{"media_content_id": "media-source://media/context.txt"},
],
)

View File

@ -231,6 +231,11 @@ def test_device_selector_schema_error(schema) -> None:
["sensor.abc123", "sensor.ghi789"],
),
),
(
{"multiple": True, "reorder": True},
((["sensor.abc123", "sensor.def456"],)),
(None, "abc123", ["sensor.abc123", None]),
),
(
{"filter": {"domain": "light"}},
("light.abc123", FAKE_UUID),

View File

@ -1091,6 +1091,7 @@ async def test_async_get_all_descriptions_filter(hass: HomeAssistant) -> None:
}
],
"multiple": False,
"reorder": False,
},
},
},
@ -1113,6 +1114,7 @@ async def test_async_get_all_descriptions_filter(hass: HomeAssistant) -> None:
}
],
"multiple": False,
"reorder": False,
},
},
},

View File

@ -4,7 +4,10 @@ from typing import Any
import pytest
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_ICON,
CONF_NAME,
CONF_STATE,
@ -20,6 +23,7 @@ from homeassistant.helpers.trigger_template_entity import (
CONF_AVAILABILITY,
CONF_PICTURE,
ManualTriggerEntity,
ManualTriggerSensorEntity,
ValueTemplate,
)
@ -288,3 +292,38 @@ async def test_trigger_template_complex(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert entity.some_other_key == {"test_key": "test_data"}
async def test_manual_trigger_sensor_entity_with_date(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test manual trigger template entity when availability template isn't used."""
config = {
CONF_NAME: template.Template("test_entity", hass),
CONF_STATE: template.Template("{{ as_datetime(value) }}", hass),
CONF_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP,
}
class TestEntity(ManualTriggerSensorEntity):
"""Test entity class."""
extra_template_keys = (CONF_STATE,)
@property
def state(self) -> bool | None:
"""Return extra attributes."""
return "2025-01-01T00:00:00+00:00"
entity = TestEntity(hass, config)
entity.entity_id = "test.entity"
variables = entity._template_variables_with_value("2025-01-01T00:00:00+00:00")
assert entity._render_availability_template(variables) is True
assert entity.available is True
entity._set_native_value_with_possible_timestamp(entity.state)
await hass.async_block_till_done()
assert entity.native_value == async_parse_date_datetime(
"2025-01-01T00:00:00+00:00", entity.entity_id, entity.device_class
)
assert entity.state == "2025-01-01T00:00:00+00:00"
assert entity.device_class == SensorDeviceClass.TIMESTAMP