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", "iot_class": "cloud_polling",
"loggers": ["aioamazondevices"], "loggers": ["aioamazondevices"],
"quality_scale": "silver", "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", "documentation": "https://www.home-assistant.io/integrations/blue_current",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["bluecurrent_api"], "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: ( TimeoutError: (
HTTPStatus.BAD_GATEWAY, HTTPStatus.BAD_GATEWAY,
"Unable to reach the Home Assistant cloud.", "Unable to reach the Home Assistant Cloud.",
), ),
aiohttp.ClientError: ( aiohttp.ClientError: (
HTTPStatus.INTERNAL_SERVER_ERROR, HTTPStatus.INTERNAL_SERVER_ERROR,

View File

@ -10,8 +10,6 @@ from typing import Any
from jsonpath import jsonpath from jsonpath import jsonpath
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import ( from homeassistant.const import (
CONF_COMMAND, CONF_COMMAND,
CONF_NAME, CONF_NAME,
@ -188,16 +186,7 @@ class CommandSensor(ManualTriggerSensorEntity):
self.entity_id, variables, None self.entity_id, variables, None
) )
if self.device_class not in { self._set_native_value_with_possible_timestamp(value)
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._process_manual_data(variables) self._process_manual_data(variables)
self.async_write_ha_state() 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. # changed state, then we know it will still be zero.
return return
schedule_max_sub_interval_exceeded(new_state) 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 @callback
def on_state_changed(event: Event[EventStateChangedData]) -> None: def on_state_changed(event: Event[EventStateChangedData]) -> None:
@ -334,19 +339,27 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
schedule_max_sub_interval_exceeded(new_state) schedule_max_sub_interval_exceeded(new_state)
old_state = event.data["old_state"] old_state = event.data["old_state"]
if old_state is not None: 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: else:
# On first state change from none, update availability # On first state change from none, update availability
self.async_write_ha_state() self.async_write_ha_state()
def calc_derivative( 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: ) -> None:
"""Handle the sensor state changes.""" """Handle the sensor state changes."""
if not _is_decimal_state(old_value): if not _is_decimal_state(old_value):
if self._last_valid_state_time: if self._last_valid_state_time:
old_value = self._last_valid_state_time[0] 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: else:
# Sensor becomes valid for the first time, just keep the restored value # Sensor becomes valid for the first time, just keep the restored value
self.async_write_ha_state() self.async_write_ha_state()
@ -358,12 +371,10 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
"" if unit is None else unit "" if unit is None else unit
) )
self._prune_state_list(new_state.last_reported) self._prune_state_list(new_timestamp)
try: try:
elapsed_time = ( elapsed_time = (new_timestamp - old_timestamp).total_seconds()
new_state.last_reported - old_last_reported
).total_seconds()
delta_value = Decimal(new_state.state) - Decimal(old_value) delta_value = Decimal(new_state.state) - Decimal(old_value)
new_derivative = ( new_derivative = (
delta_value delta_value
@ -392,12 +403,10 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
return return
# add latest derivative to the window list # add latest derivative to the window list
self._state_list.append( self._state_list.append((old_timestamp, new_timestamp, new_derivative))
(old_last_reported, new_state.last_reported, new_derivative)
)
self._last_valid_state_time = ( self._last_valid_state_time = (
new_state.state, 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), # 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: if elapsed_time > self._time_window:
derivative = new_derivative derivative = new_derivative
else: else:
derivative = self._calc_derivative_from_state_list( derivative = self._calc_derivative_from_state_list(new_timestamp)
new_state.last_reported
)
self._write_native_value(derivative) self._write_native_value(derivative)
source_state = self.hass.states.get(self._sensor_source_id) 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.BINARY_SENSOR)
needed_platforms.add(Platform.SELECT) needed_platforms.add(Platform.SELECT)
ent_reg = er.async_get(hass) needed_platforms.update(INFO_TYPE_TO_PLATFORM[type(info)] for info in infos)
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)
await self._ensure_platforms_loaded(hass, entry, needed_platforms) await self._ensure_platforms_loaded(hass, entry, needed_platforms)
# Make a dict of the EntityInfo by type and send # Make a dict of the EntityInfo by type and send

View File

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

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "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 the state attributes."""
return self.entity_description.extra_state_attributes_fn(self.mower_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): class WorkAreaSensorEntity(WorkAreaAvailableEntity, SensorEntity):
"""Defining the Work area sensors with WorkAreaSensorEntityDescription.""" """Defining the Work area sensors with WorkAreaSensorEntityDescription."""

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib", "documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "silver", "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: ) -> None:
"""Handle sensor state update when sub interval is configured.""" """Handle sensor state update when sub interval is configured."""
self._integrate_on_state_update_with_max_sub_interval( 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 @callback
@ -472,13 +472,17 @@ class IntegrationSensor(RestoreSensor):
) -> None: ) -> None:
"""Handle sensor state report when sub interval is configured.""" """Handle sensor state report when sub interval is configured."""
self._integrate_on_state_update_with_max_sub_interval( 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 @callback
def _integrate_on_state_update_with_max_sub_interval( def _integrate_on_state_update_with_max_sub_interval(
self, self,
old_last_reported: datetime | None, old_timestamp: datetime | None,
new_timestamp: datetime | None,
old_state: State | None, old_state: State | None,
new_state: State | None, new_state: State | None,
) -> None: ) -> None:
@ -489,7 +493,9 @@ class IntegrationSensor(RestoreSensor):
""" """
self._cancel_max_sub_interval_exceeded_callback() self._cancel_max_sub_interval_exceeded_callback()
try: 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_trigger = _IntegrationTrigger.StateEvent
self._last_integration_time = datetime.now(tz=UTC) self._last_integration_time = datetime.now(tz=UTC)
finally: finally:
@ -503,7 +509,7 @@ class IntegrationSensor(RestoreSensor):
) -> None: ) -> None:
"""Handle sensor state change.""" """Handle sensor state change."""
return self._integrate_on_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 @callback
@ -512,12 +518,16 @@ class IntegrationSensor(RestoreSensor):
) -> None: ) -> None:
"""Handle sensor state report.""" """Handle sensor state report."""
return self._integrate_on_state_change( 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( def _integrate_on_state_change(
self, self,
old_last_reported: datetime | None, old_timestamp: datetime | None,
new_timestamp: datetime | None,
old_state: State | None, old_state: State | None,
new_state: State | None, new_state: State | None,
) -> None: ) -> None:
@ -531,16 +541,17 @@ class IntegrationSensor(RestoreSensor):
if old_state: if old_state:
# state has changed, we recover old_state from the event # state has changed, we recover old_state from the event
new_timestamp = new_state.last_updated
old_state_state = old_state.state old_state_state = old_state.state
old_last_reported = old_state.last_reported old_timestamp = old_state.last_reported
else: else:
# event state reported without any state change # first state or event state reported without any state change
old_state_state = new_state.state old_state_state = new_state.state
self._attr_available = True self._attr_available = True
self._derive_and_set_attributes_from_state(new_state) 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() self.async_write_ha_state()
return return
@ -551,11 +562,12 @@ class IntegrationSensor(RestoreSensor):
return return
if TYPE_CHECKING: 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( 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 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) 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}`" 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 # Only allow `options` to be set for `enum` sensors
# to limit the possible sensor values # to limit the possible sensor values
if (options := config.get(CONF_OPTIONS)) is not None: if (options := config.get(CONF_OPTIONS)) is not None:

View File

@ -39,7 +39,10 @@ class OllamaTaskEntity(
): ):
"""Ollama AI Task entity.""" """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( async def _async_generate_data(
self, self,

View File

@ -106,9 +106,18 @@ def _convert_content(
], ],
) )
if isinstance(chat_content, conversation.UserContent): 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( return ollama.Message(
role=MessageRole.USER.value, role=MessageRole.USER.value,
content=chat_content.content, content=chat_content.content,
images=images or None,
) )
if isinstance(chat_content, conversation.SystemContent): if isinstance(chat_content, conversation.SystemContent):
return ollama.Message( return ollama.Message(

View File

@ -94,5 +94,10 @@
"download": "[%key:component::ollama::config_subentries::conversation::progress::download%]" "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, CONF_STATE_CLASS,
DOMAIN as SENSOR_DOMAIN, DOMAIN as SENSOR_DOMAIN,
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorDeviceClass,
) )
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE, CONF_FORCE_UPDATE,
@ -181,18 +179,6 @@ class RestSensor(ManualTriggerSensorEntity, RestEntity):
self.entity_id, variables, None self.entity_id, variables, None
) )
if value is None or self.device_class not in ( self._set_native_value_with_possible_timestamp(value)
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._process_manual_data(variables) self._process_manual_data(variables)
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -7,8 +7,7 @@ from typing import Any, cast
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import CONF_STATE_CLASS, SensorDeviceClass from homeassistant.components.sensor import CONF_STATE_CLASS
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import ( from homeassistant.const import (
CONF_ATTRIBUTE, CONF_ATTRIBUTE,
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
@ -218,17 +217,7 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], ManualTriggerSensorEnti
self.entity_id, variables, None self.entity_id, variables, None
) )
if self.device_class not in { self._set_native_value_with_possible_timestamp(value)
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._process_manual_data(variables) self._process_manual_data(variables)
@property @property

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pysmarlaapi", "pysignalr"], "loggers": ["pysmarlaapi", "pysignalr"],
"quality_scale": "bronze", "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.entity_id, variables, STATE_UNKNOWN
) )
self._attr_native_value = value self._set_native_value_with_possible_timestamp(value)
self._process_manual_data(variables) 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: if data is not None and self._template is not None:
variables = self._template_variables_with_value(data) variables = self._template_variables_with_value(data)
if self._render_availability_template(variables): 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.entity_id, variables, None
) )
self._set_native_value_with_possible_timestamp(_value)
self._process_manual_data(variables) self._process_manual_data(variables)
else: else:
self._attr_native_value = data self._attr_native_value = data

View File

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

View File

@ -41,5 +41,5 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["switchbot"], "loggers": ["switchbot"],
"quality_scale": "gold", "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.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, ATTR_CODE,
CONF_DEVICE_ID,
CONF_NAME, CONF_NAME,
CONF_STATE, CONF_STATE,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
@ -31,7 +30,7 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError 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 ( from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback, AddConfigEntryEntitiesCallback,
AddEntitiesCallback, AddEntitiesCallback,
@ -43,8 +42,16 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN from .const import DOMAIN
from .coordinator import TriggerUpdateCoordinator from .coordinator import TriggerUpdateCoordinator
from .entity import AbstractTemplateEntity from .entity import AbstractTemplateEntity
from .helpers import async_setup_template_platform from .helpers import (
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema 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 from .trigger_entity import TriggerEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -88,8 +95,7 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Alarm Control Panel" DEFAULT_NAME = "Template Alarm Control Panel"
ALARM_CONTROL_PANEL_SCHEMA = vol.All( ALARM_CONTROL_PANEL_COMMON_SCHEMA = vol.Schema(
vol.Schema(
{ {
vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA,
@ -97,18 +103,20 @@ ALARM_CONTROL_PANEL_SCHEMA = vol.All(
vol.Optional(CONF_ARM_NIGHT_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_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
vol.Optional( vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum(
CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name TemplateCodeFormat
): cv.enum(TemplateCodeFormat), ),
vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_STATE): cv.template, vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
} }
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).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_AWAY_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_CUSTOM_BYPASS_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( PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_ALARM_CONTROL_PANELS): cv.schema_with_slug_keys( 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( ALARM_CONTROL_PANEL_CONFIG_ENTRY_SCHEMA = ALARM_CONTROL_PANEL_COMMON_SCHEMA.extend(
{ TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.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,
}
) )
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( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Initialize config entry.""" """Initialize config entry."""
_options = dict(config_entry.options) await async_setup_template_entry(
_options.pop("template_type")
_options = rewrite_options_to_modern_conf(_options)
validated_config = ALARM_CONTROL_PANEL_CONFIG_SCHEMA(_options)
async_add_entities(
[
StateAlarmControlPanelEntity(
hass, hass,
validated_config, config_entry,
config_entry.entry_id, 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] hass: HomeAssistant, name: str, config: dict[str, Any]
) -> StateAlarmControlPanelEntity: ) -> StateAlarmControlPanelEntity:
"""Create a preview alarm control panel.""" """Create a preview alarm control panel."""
updated_config = rewrite_options_to_modern_conf(config) return async_setup_template_preview(
validated_config = ALARM_CONTROL_PANEL_CONFIG_SCHEMA( hass,
updated_config | {CONF_NAME: name} name,
config,
StateAlarmControlPanelEntity,
ALARM_CONTROL_PANEL_CONFIG_ENTRY_SCHEMA,
True,
) )
return StateAlarmControlPanelEntity(hass, validated_config, None)
class AbstractTemplateAlarmControlPanel( class AbstractTemplateAlarmControlPanel(

View File

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

View File

@ -14,9 +14,9 @@ from homeassistant.components.button import (
ButtonEntity, ButtonEntity,
) )
from homeassistant.config_entries import ConfigEntry 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.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 ( from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback, AddConfigEntryEntitiesCallback,
AddEntitiesCallback, AddEntitiesCallback,
@ -24,29 +24,31 @@ from homeassistant.helpers.entity_platform import (
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_PRESS, DOMAIN from .const import CONF_PRESS, DOMAIN
from .helpers import async_setup_template_platform from .helpers import async_setup_template_entry, async_setup_template_platform
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema from .template_entity import (
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TemplateEntity,
make_template_entity_common_modern_schema,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Template Button" DEFAULT_NAME = "Template Button"
DEFAULT_OPTIMISTIC = False DEFAULT_OPTIMISTIC = False
BUTTON_SCHEMA = vol.Schema( BUTTON_YAML_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_PRESS): cv.SCRIPT_SCHEMA, vol.Required(CONF_PRESS): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
} }
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).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_PRESS): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_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( async def async_setup_platform(
@ -73,11 +75,12 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Initialize config entry.""" """Initialize config entry."""
_options = dict(config_entry.options) await async_setup_template_entry(
_options.pop("template_type") hass,
validated_config = CONFIG_BUTTON_SCHEMA(_options) config_entry,
async_add_entities( async_add_entities,
[StateButtonEntity(hass, validated_config, config_entry.entry_id)] 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_ACTIONS): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_BINARY_SENSORS): cv.schema_with_slug_keys( 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_CONDITIONS): cv.CONDITIONS_SCHEMA,
vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys( 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_TRIGGERS): cv.TRIGGER_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
vol.Optional(DOMAIN_ALARM_CONTROL_PANEL): vol.All( vol.Optional(DOMAIN_ALARM_CONTROL_PANEL): vol.All(
cv.ensure_list, 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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( 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.""" """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 from homeassistant.helpers.typing import ConfigType
CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" CONF_ATTRIBUTE_TEMPLATES = "attribute_templates"
@ -16,6 +19,15 @@ CONF_STEP = "step"
CONF_TURN_OFF = "turn_off" CONF_TURN_OFF = "turn_off"
CONF_TURN_ON = "turn_on" 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" DOMAIN = "template"
PLATFORM_STORAGE_KEY = "template_platforms" PLATFORM_STORAGE_KEY = "template_platforms"

View File

@ -91,7 +91,7 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Cover" DEFAULT_NAME = "Template Cover"
COVER_SCHEMA = vol.All( COVER_YAML_SCHEMA = vol.All(
vol.Schema( vol.Schema(
{ {
vol.Inclusive(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_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), 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), cv.deprecated(CONF_ENTITY_ID),
vol.Schema( vol.Schema(
{ {
@ -134,7 +134,7 @@ LEGACY_COVER_SCHEMA = vol.All(
) )
PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( 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" DEFAULT_NAME = "Template Fan"
FAN_SCHEMA = vol.All( FAN_YAML_SCHEMA = vol.All(
vol.Schema( vol.Schema(
{ {
vol.Optional(CONF_DIRECTION): cv.template, vol.Optional(CONF_DIRECTION): cv.template,
@ -101,7 +101,7 @@ FAN_SCHEMA = vol.All(
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema) ).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), cv.deprecated(CONF_ENTITY_ID),
vol.Schema( vol.Schema(
{ {
@ -126,7 +126,7 @@ LEGACY_FAN_SCHEMA = vol.All(
) )
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( 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 import logging
from typing import Any from typing import Any
import voluptuous as vol
from homeassistant.components import blueprint from homeassistant.components import blueprint
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_ENTITY_PICTURE_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE,
CONF_FRIENDLY_NAME, CONF_FRIENDLY_NAME,
CONF_ICON, CONF_ICON,
CONF_ICON_TEMPLATE, CONF_ICON_TEMPLATE,
CONF_NAME, CONF_NAME,
CONF_STATE,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
SERVICE_RELOAD, SERVICE_RELOAD,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -20,6 +25,7 @@ from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import template from homeassistant.helpers import template
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import ( from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback, AddEntitiesCallback,
async_get_platforms, async_get_platforms,
) )
@ -228,3 +234,41 @@ async def async_setup_template_platform(
discovery_info["entities"], discovery_info["entities"],
discovery_info["unique_id"], 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, ImageEntity,
) )
from homeassistant.config_entries import ConfigEntry 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.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError 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 ( from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback, AddConfigEntryEntitiesCallback,
AddEntitiesCallback, AddEntitiesCallback,
@ -26,8 +26,9 @@ from homeassistant.util import dt as dt_util
from . import TriggerUpdateCoordinator from . import TriggerUpdateCoordinator
from .const import CONF_PICTURE 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 ( from .template_entity import (
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TemplateEntity, TemplateEntity,
make_template_entity_common_modern_attributes_schema, make_template_entity_common_modern_attributes_schema,
) )
@ -39,7 +40,7 @@ DEFAULT_NAME = "Template Image"
GET_IMAGE_TIMEOUT = 10 GET_IMAGE_TIMEOUT = 10
IMAGE_SCHEMA = vol.Schema( IMAGE_YAML_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_URL): cv.template, vol.Required(CONF_URL): cv.template,
vol.Optional(CONF_VERIFY_SSL, default=True): bool, 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) ).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.Required(CONF_URL): cv.template,
vol.Optional(CONF_VERIFY_SSL, default=True): bool, 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( async def async_setup_platform(
@ -81,11 +80,12 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Initialize config entry.""" """Initialize config entry."""
_options = dict(config_entry.options) await async_setup_template_entry(
_options.pop("template_type") hass,
validated_config = IMAGE_CONFIG_SCHEMA(_options) config_entry,
async_add_entities( async_add_entities,
[StateImageEntity(hass, validated_config, config_entry.entry_id)] StateImageEntity,
IMAGE_CONFIG_ENTRY_SCHEMA,
) )

View File

@ -121,7 +121,7 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Light" 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_ACTION, "effect"): cv.SCRIPT_SCHEMA,
vol.Inclusive(CONF_EFFECT_LIST, "effect"): cv.template, 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) ).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), cv.deprecated(CONF_ENTITY_ID),
vol.Schema( vol.Schema(
{ {
@ -186,7 +186,7 @@ PLATFORM_SCHEMA = vol.All(
cv.removed(CONF_WHITE_VALUE_ACTION), cv.removed(CONF_WHITE_VALUE_ACTION),
cv.removed(CONF_WHITE_VALUE_TEMPLATE), cv.removed(CONF_WHITE_VALUE_TEMPLATE),
LIGHT_PLATFORM_SCHEMA.extend( 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, CONF_VALUE_TEMPLATE: CONF_STATE,
} }
LOCK_SCHEMA = vol.All( LOCK_YAML_SCHEMA = vol.All(
vol.Schema( vol.Schema(
{ {
vol.Optional(CONF_CODE_FORMAT): cv.template, 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) ).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
) )
PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend(
{ {
vol.Optional(CONF_CODE_FORMAT_TEMPLATE): cv.template, 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.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_ID,
CONF_NAME, CONF_NAME,
CONF_OPTIMISTIC, CONF_OPTIMISTIC,
CONF_STATE, CONF_STATE,
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
) )
from homeassistant.core import HomeAssistant, callback 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 ( from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback, AddConfigEntryEntitiesCallback,
AddEntitiesCallback, AddEntitiesCallback,
@ -34,8 +33,16 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import TriggerUpdateCoordinator from . import TriggerUpdateCoordinator
from .const import CONF_MAX, CONF_MIN, CONF_STEP, DOMAIN from .const import CONF_MAX, CONF_MIN, CONF_STEP, DOMAIN
from .helpers import async_setup_template_platform from .helpers import (
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema 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 from .trigger_entity import TriggerEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -45,28 +52,29 @@ CONF_SET_VALUE = "set_value"
DEFAULT_NAME = "Template Number" DEFAULT_NAME = "Template Number"
DEFAULT_OPTIMISTIC = False 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_MAX, default=DEFAULT_MAX_VALUE): 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.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
}
)
NUMBER_YAML_SCHEMA = (
vol.Schema(
{
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
} }
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema) )
NUMBER_CONFIG_SCHEMA = vol.Schema( .extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
{ .extend(NUMBER_COMMON_SCHEMA.schema)
vol.Required(CONF_NAME): cv.template, )
vol.Required(CONF_STATE): cv.template,
vol.Required(CONF_STEP): cv.template, NUMBER_CONFIG_ENTRY_SCHEMA = NUMBER_COMMON_SCHEMA.extend(
vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA, TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.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(),
}
) )
@ -94,11 +102,12 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Initialize config entry.""" """Initialize config entry."""
_options = dict(config_entry.options) await async_setup_template_entry(
_options.pop("template_type") hass,
validated_config = NUMBER_CONFIG_SCHEMA(_options) config_entry,
async_add_entities( async_add_entities,
[StateNumberEntity(hass, validated_config, config_entry.entry_id)] StateNumberEntity,
NUMBER_CONFIG_ENTRY_SCHEMA,
) )
@ -107,8 +116,9 @@ def async_create_preview_number(
hass: HomeAssistant, name: str, config: dict[str, Any] hass: HomeAssistant, name: str, config: dict[str, Any]
) -> StateNumberEntity: ) -> StateNumberEntity:
"""Create a preview number.""" """Create a preview number."""
validated_config = NUMBER_CONFIG_SCHEMA(config | {CONF_NAME: name}) return async_setup_template_preview(
return StateNumberEntity(hass, validated_config, None) hass, name, config, StateNumberEntity, NUMBER_CONFIG_ENTRY_SCHEMA
)
class StateNumberEntity(TemplateEntity, NumberEntity): class StateNumberEntity(TemplateEntity, NumberEntity):

View File

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

View File

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

View File

@ -16,7 +16,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME, ATTR_FRIENDLY_NAME,
CONF_DEVICE_ID,
CONF_NAME, CONF_NAME,
CONF_STATE, CONF_STATE,
CONF_SWITCHES, CONF_SWITCHES,
@ -29,7 +28,7 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError 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 ( from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback, AddConfigEntryEntitiesCallback,
AddEntitiesCallback, AddEntitiesCallback,
@ -39,8 +38,13 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import TriggerUpdateCoordinator from . import TriggerUpdateCoordinator
from .const import CONF_TURN_OFF, CONF_TURN_ON, DOMAIN 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 ( from .template_entity import (
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY, TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY,
TemplateEntity, TemplateEntity,
make_template_entity_common_modern_schema, make_template_entity_common_modern_schema,
@ -55,16 +59,19 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Switch" DEFAULT_NAME = "Template Switch"
SWITCH_COMMON_SCHEMA = vol.Schema(
SWITCH_SCHEMA = vol.Schema(
{ {
vol.Optional(CONF_STATE): cv.template, vol.Optional(CONF_STATE): cv.template,
vol.Required(CONF_TURN_ON): cv.SCRIPT_SCHEMA, vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
vol.Required(CONF_TURN_OFF): 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), cv.deprecated(ATTR_ENTITY_ID),
vol.Schema( vol.Schema(
{ {
@ -79,17 +86,11 @@ LEGACY_SWITCH_SCHEMA = vol.All(
) )
PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( 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( SWITCH_CONFIG_ENTRY_SCHEMA = SWITCH_COMMON_SCHEMA.extend(
{ TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.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(),
}
) )
@ -129,12 +130,13 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Initialize config entry.""" """Initialize config entry."""
_options = dict(config_entry.options) await async_setup_template_entry(
_options.pop("template_type") hass,
_options = rewrite_options_to_modern_conf(_options) config_entry,
validated_config = SWITCH_CONFIG_SCHEMA(_options) async_add_entities,
async_add_entities( StateSwitchEntity,
[StateSwitchEntity(hass, validated_config, config_entry.entry_id)] SWITCH_CONFIG_ENTRY_SCHEMA,
True,
) )
@ -143,9 +145,14 @@ def async_create_preview_switch(
hass: HomeAssistant, name: str, config: dict[str, Any] hass: HomeAssistant, name: str, config: dict[str, Any]
) -> StateSwitchEntity: ) -> StateSwitchEntity:
"""Create a preview switch.""" """Create a preview switch."""
updated_config = rewrite_options_to_modern_conf(config) return async_setup_template_preview(
validated_config = SWITCH_CONFIG_SCHEMA(updated_config | {CONF_NAME: name}) hass,
return StateSwitchEntity(hass, validated_config, None) name,
config,
StateSwitchEntity,
SWITCH_CONFIG_ENTRY_SCHEMA,
True,
)
class StateSwitchEntity(TemplateEntity, SwitchEntity, RestoreEntity): 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.components.blueprint import CONF_USE_BLUEPRINT
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_ID,
CONF_ENTITY_PICTURE_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE,
CONF_ICON, CONF_ICON,
CONF_ICON_TEMPLATE, CONF_ICON_TEMPLATE,
@ -30,7 +31,7 @@ from homeassistant.core import (
validate_state, validate_state,
) )
from homeassistant.exceptions import TemplateError 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.entity import Entity
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
TrackTemplate, TrackTemplate,
@ -46,7 +47,6 @@ from homeassistant.helpers.template import (
result_as_boolean, result_as_boolean,
) )
from homeassistant.helpers.trigger_template_entity import ( from homeassistant.helpers.trigger_template_entity import (
TEMPLATE_ENTITY_BASE_SCHEMA,
make_template_entity_base_schema, make_template_entity_base_schema,
) )
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -57,6 +57,7 @@ from .const import (
CONF_AVAILABILITY, CONF_AVAILABILITY,
CONF_AVAILABILITY_TEMPLATE, CONF_AVAILABILITY_TEMPLATE,
CONF_PICTURE, CONF_PICTURE,
TEMPLATE_ENTITY_BASE_SCHEMA,
) )
from .entity import AbstractTemplateEntity from .entity import AbstractTemplateEntity
@ -91,6 +92,13 @@ TEMPLATE_ENTITY_COMMON_SCHEMA = (
.extend(TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA.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( def make_template_entity_common_modern_schema(
default_name: str, default_name: str,

View File

@ -76,7 +76,7 @@ LEGACY_FIELDS = {
CONF_VALUE_TEMPLATE: CONF_STATE, CONF_VALUE_TEMPLATE: CONF_STATE,
} }
VACUUM_SCHEMA = vol.All( VACUUM_YAML_SCHEMA = vol.All(
vol.Schema( vol.Schema(
{ {
vol.Optional(CONF_BATTERY_LEVEL): cv.template, 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) ).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), cv.deprecated(CONF_ENTITY_ID),
vol.Schema( vol.Schema(
{ {
@ -119,7 +119,7 @@ LEGACY_VACUUM_SCHEMA = vol.All(
) )
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( 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, WeatherEntity,
WeatherEntityFeature, 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.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers import config_validation as cv, template
@ -100,7 +105,7 @@ CONF_APPARENT_TEMPERATURE_TEMPLATE = "apparent_temperature_template"
DEFAULT_NAME = "Template Weather" DEFAULT_NAME = "Template Weather"
WEATHER_SCHEMA = vol.Schema( WEATHER_YAML_SCHEMA = vol.Schema(
{ {
vol.Optional(CONF_APPARENT_TEMPERATURE_TEMPLATE): cv.template, vol.Optional(CONF_APPARENT_TEMPERATURE_TEMPLATE): cv.template,
vol.Optional(CONF_ATTRIBUTION_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) ).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( 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.""" """Base class for EVENT_STATE_CHANGED and EVENT_STATE_REPORTED data."""
entity_id: str entity_id: str
new_state: State | None
class EventStateChangedData(EventStateEventData): class EventStateChangedData(EventStateEventData):
@ -166,6 +165,7 @@ class EventStateChangedData(EventStateEventData):
A state changed event is fired when on state write the state is changed. A state changed event is fired when on state write the state is changed.
""" """
new_state: State | None
old_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. 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 old_last_reported: datetime.datetime
@ -1749,18 +1751,38 @@ class CompressedState(TypedDict):
class State: 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. entity_id: str
state: the state of the entity """The entity that is represented by the state."""
attributes: extra information on entity and state domain: str
last_changed: last time the state was changed. """Domain of the entity that is represented by the state."""
last_reported: last time the state was reported. object_id: str
last_updated: last time the state or attributes were changed. """object_id: Object id of this state."""
context: Context in which it was created state: str
domain: Domain of this state. """The state of the entity."""
object_id: Object id of this state. 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__ = ( __slots__ = (
"_cache", "_cache",
@ -1841,7 +1863,20 @@ class State:
@under_cached_property @under_cached_property
def last_reported_timestamp(self) -> float: 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() return self.last_reported.timestamp()
@under_cached_property @under_cached_property
@ -2340,6 +2375,7 @@ class StateMachine:
EVENT_STATE_REPORTED, EVENT_STATE_REPORTED,
{ {
"entity_id": entity_id, "entity_id": entity_id,
"last_reported": now,
"old_last_reported": old_last_reported, "old_last_reported": old_last_reported,
"new_state": old_state, "new_state": old_state,
}, },

View File

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

View File

@ -13,8 +13,10 @@ from homeassistant.components.sensor import (
CONF_STATE_CLASS, CONF_STATE_CLASS,
DEVICE_CLASSES_SCHEMA, DEVICE_CLASSES_SCHEMA,
STATE_CLASSES_SCHEMA, STATE_CLASSES_SCHEMA,
SensorDeviceClass,
SensorEntity, SensorEntity,
) )
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_PICTURE, ATTR_ENTITY_PICTURE,
ATTR_FRIENDLY_NAME, ATTR_FRIENDLY_NAME,
@ -389,3 +391,20 @@ class ManualTriggerSensorEntity(ManualTriggerEntity, SensorEntity):
ManualTriggerEntity.__init__(self, hass, config) ManualTriggerEntity.__init__(self, hass, config)
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
self._attr_state_class = config.get(CONF_STATE_CLASS) 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 hass-nabucasa==0.107.1
hassil==2.2.3 hassil==2.2.3
home-assistant-bluetooth==1.13.1 home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250702.2 home-assistant-frontend==20250702.3
home-assistant-intents==2025.6.23 home-assistant-intents==2025.6.23
httpx==0.28.1 httpx==0.28.1
ifaddr==0.2.0 ifaddr==0.2.0

View File

@ -589,10 +589,6 @@ filterwarnings = [
# -- Websockets 14.1 # -- Websockets 14.1
# https://websockets.readthedocs.io/en/stable/howto/upgrade.html # https://websockets.readthedocs.io/en/stable/howto/upgrade.html
"ignore:websockets.legacy is deprecated:DeprecationWarning:websockets.legacy", "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 # https://github.com/graphql-python/gql/pull/543 - >=4.0.0a0
"ignore:websockets.client.WebSocketClientProtocol is deprecated:DeprecationWarning:gql.transport.websockets_base", "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 PyRMVtransport==0.3.3
# homeassistant.components.switchbot # homeassistant.components.switchbot
PySwitchbot==0.68.1 PySwitchbot==0.68.2
# homeassistant.components.switchmate # homeassistant.components.switchmate
PySwitchmate==0.5.1 PySwitchmate==0.5.1
@ -185,7 +185,7 @@ aioairzone-cloud==0.6.15
aioairzone==1.0.0 aioairzone==1.0.0
# homeassistant.components.alexa_devices # homeassistant.components.alexa_devices
aioamazondevices==3.2.10 aioamazondevices==3.5.0
# homeassistant.components.ambient_network # homeassistant.components.ambient_network
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
@ -247,7 +247,7 @@ aioelectricitymaps==0.4.0
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==35.0.0 aioesphomeapi==36.0.1
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@ -634,7 +634,7 @@ blinkpy==0.23.0
blockchain==1.4.4 blockchain==1.4.4
# homeassistant.components.blue_current # homeassistant.components.blue_current
bluecurrent-api==1.2.3 bluecurrent-api==1.2.4
# homeassistant.components.bluemaestro # homeassistant.components.bluemaestro
bluemaestro-ble==0.4.1 bluemaestro-ble==0.4.1
@ -1168,7 +1168,7 @@ hole==0.9.0
holidays==0.76 holidays==0.76
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20250702.2 home-assistant-frontend==20250702.3
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2025.6.23 home-assistant-intents==2025.6.23
@ -1234,7 +1234,7 @@ ihcsdk==2.8.5
imeon_inverter_api==0.3.12 imeon_inverter_api==0.3.12
# homeassistant.components.imgw_pib # homeassistant.components.imgw_pib
imgw_pib==1.4.1 imgw_pib==1.4.2
# homeassistant.components.incomfort # homeassistant.components.incomfort
incomfort-client==0.6.9 incomfort-client==0.6.9
@ -2346,7 +2346,7 @@ pysma==0.7.5
pysmappee==0.2.29 pysmappee==0.2.29
# homeassistant.components.smarla # homeassistant.components.smarla
pysmarlaapi==0.9.0 pysmarlaapi==0.9.1
# homeassistant.components.smartthings # homeassistant.components.smartthings
pysmartthings==3.2.8 pysmartthings==3.2.8

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,49 +15,6 @@ from homeassistant.helpers import entity_registry as er
from .conftest import MockGenericDeviceEntryType 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( async def test_migrate_entity_unique_id_downgrade_upgrade(
hass: HomeAssistant, hass: HomeAssistant,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
@ -84,7 +41,6 @@ async def test_migrate_entity_unique_id_downgrade_upgrade(
object_id="mysensor", object_id="mysensor",
key=1, key=1,
name="my sensor", name="my sensor",
unique_id="my_sensor",
entity_category=ESPHomeEntityCategory.DIAGNOSTIC, entity_category=ESPHomeEntityCategory.DIAGNOSTIC,
icon="mdi:leaf", icon="mdi:leaf",
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -924,6 +924,30 @@ async def test_invalid_unit_of_measurement(
"device_class": None, "device_class": None,
"unit_of_measurement": 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() await mqtt_mock_entry()
state = hass.states.get("sensor.test_1") state = hass.states.get("sensor.test_1")
assert state is not None
assert state.attributes["device_class"] == "temperature" assert state.attributes["device_class"] == "temperature"
state = hass.states.get("sensor.test_2") state = hass.states.get("sensor.test_2")
assert state is not None
assert "device_class" not in state.attributes assert "device_class" not in state.attributes
state = hass.states.get("sensor.test_3") 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 assert "device_class" not in state.attributes

View File

@ -1,11 +1,13 @@
"""Test AI Task platform of Ollama integration.""" """Test AI Task platform of Ollama integration."""
from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
import ollama
import pytest import pytest
import voluptuous as vol 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.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er, selector 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"], ["sensor.abc123", "sensor.ghi789"],
), ),
), ),
(
{"multiple": True, "reorder": True},
((["sensor.abc123", "sensor.def456"],)),
(None, "abc123", ["sensor.abc123", None]),
),
( (
{"filter": {"domain": "light"}}, {"filter": {"domain": "light"}},
("light.abc123", FAKE_UUID), ("light.abc123", FAKE_UUID),

View File

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

View File

@ -4,7 +4,10 @@ from typing import Any
import pytest import pytest
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_ICON, CONF_ICON,
CONF_NAME, CONF_NAME,
CONF_STATE, CONF_STATE,
@ -20,6 +23,7 @@ from homeassistant.helpers.trigger_template_entity import (
CONF_AVAILABILITY, CONF_AVAILABILITY,
CONF_PICTURE, CONF_PICTURE,
ManualTriggerEntity, ManualTriggerEntity,
ManualTriggerSensorEntity,
ValueTemplate, ValueTemplate,
) )
@ -288,3 +292,38 @@ async def test_trigger_template_complex(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
assert entity.some_other_key == {"test_key": "test_data"} 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