mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Merge branch 'dev' into reorder-group-member
This commit is contained in:
commit
643c2cb7b7
@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioamazondevices==3.2.10"]
|
||||
"requirements": ["aioamazondevices==3.5.0"]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/blue_current",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["bluecurrent_api"],
|
||||
"requirements": ["bluecurrent-api==1.2.3"]
|
||||
"requirements": ["bluecurrent-api==1.2.4"]
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ _CLOUD_ERRORS: dict[
|
||||
] = {
|
||||
TimeoutError: (
|
||||
HTTPStatus.BAD_GATEWAY,
|
||||
"Unable to reach the Home Assistant cloud.",
|
||||
"Unable to reach the Home Assistant Cloud.",
|
||||
),
|
||||
aiohttp.ClientError: (
|
||||
HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
|
@ -10,8 +10,6 @@ from typing import Any
|
||||
|
||||
from jsonpath import jsonpath
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.components.sensor.helpers import async_parse_date_datetime
|
||||
from homeassistant.const import (
|
||||
CONF_COMMAND,
|
||||
CONF_NAME,
|
||||
@ -188,16 +186,7 @@ class CommandSensor(ManualTriggerSensorEntity):
|
||||
self.entity_id, variables, None
|
||||
)
|
||||
|
||||
if self.device_class not in {
|
||||
SensorDeviceClass.DATE,
|
||||
SensorDeviceClass.TIMESTAMP,
|
||||
}:
|
||||
self._attr_native_value = value
|
||||
elif value is not None:
|
||||
self._attr_native_value = async_parse_date_datetime(
|
||||
value, self.entity_id, self.device_class
|
||||
)
|
||||
|
||||
self._set_native_value_with_possible_timestamp(value)
|
||||
self._process_manual_data(variables)
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
@ -320,7 +320,12 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
|
||||
# changed state, then we know it will still be zero.
|
||||
return
|
||||
schedule_max_sub_interval_exceeded(new_state)
|
||||
calc_derivative(new_state, new_state.state, event.data["old_last_reported"])
|
||||
calc_derivative(
|
||||
new_state,
|
||||
new_state.state,
|
||||
event.data["last_reported"],
|
||||
event.data["old_last_reported"],
|
||||
)
|
||||
|
||||
@callback
|
||||
def on_state_changed(event: Event[EventStateChangedData]) -> None:
|
||||
@ -334,19 +339,27 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
|
||||
schedule_max_sub_interval_exceeded(new_state)
|
||||
old_state = event.data["old_state"]
|
||||
if old_state is not None:
|
||||
calc_derivative(new_state, old_state.state, old_state.last_reported)
|
||||
calc_derivative(
|
||||
new_state,
|
||||
old_state.state,
|
||||
new_state.last_updated,
|
||||
old_state.last_reported,
|
||||
)
|
||||
else:
|
||||
# On first state change from none, update availability
|
||||
self.async_write_ha_state()
|
||||
|
||||
def calc_derivative(
|
||||
new_state: State, old_value: str, old_last_reported: datetime
|
||||
new_state: State,
|
||||
old_value: str,
|
||||
new_timestamp: datetime,
|
||||
old_timestamp: datetime,
|
||||
) -> None:
|
||||
"""Handle the sensor state changes."""
|
||||
if not _is_decimal_state(old_value):
|
||||
if self._last_valid_state_time:
|
||||
old_value = self._last_valid_state_time[0]
|
||||
old_last_reported = self._last_valid_state_time[1]
|
||||
old_timestamp = self._last_valid_state_time[1]
|
||||
else:
|
||||
# Sensor becomes valid for the first time, just keep the restored value
|
||||
self.async_write_ha_state()
|
||||
@ -358,12 +371,10 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
|
||||
"" if unit is None else unit
|
||||
)
|
||||
|
||||
self._prune_state_list(new_state.last_reported)
|
||||
self._prune_state_list(new_timestamp)
|
||||
|
||||
try:
|
||||
elapsed_time = (
|
||||
new_state.last_reported - old_last_reported
|
||||
).total_seconds()
|
||||
elapsed_time = (new_timestamp - old_timestamp).total_seconds()
|
||||
delta_value = Decimal(new_state.state) - Decimal(old_value)
|
||||
new_derivative = (
|
||||
delta_value
|
||||
@ -392,12 +403,10 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
|
||||
return
|
||||
|
||||
# add latest derivative to the window list
|
||||
self._state_list.append(
|
||||
(old_last_reported, new_state.last_reported, new_derivative)
|
||||
)
|
||||
self._state_list.append((old_timestamp, new_timestamp, new_derivative))
|
||||
self._last_valid_state_time = (
|
||||
new_state.state,
|
||||
new_state.last_reported,
|
||||
new_timestamp,
|
||||
)
|
||||
|
||||
# If outside of time window just report derivative (is the same as modeling it in the window),
|
||||
@ -405,9 +414,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
|
||||
if elapsed_time > self._time_window:
|
||||
derivative = new_derivative
|
||||
else:
|
||||
derivative = self._calc_derivative_from_state_list(
|
||||
new_state.last_reported
|
||||
)
|
||||
derivative = self._calc_derivative_from_state_list(new_timestamp)
|
||||
self._write_native_value(derivative)
|
||||
|
||||
source_state = self.hass.states.get(self._sensor_source_id)
|
||||
|
@ -295,23 +295,7 @@ class RuntimeEntryData:
|
||||
needed_platforms.add(Platform.BINARY_SENSOR)
|
||||
needed_platforms.add(Platform.SELECT)
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
registry_get_entity = ent_reg.async_get_entity_id
|
||||
for info in infos:
|
||||
platform = INFO_TYPE_TO_PLATFORM[type(info)]
|
||||
needed_platforms.add(platform)
|
||||
# If the unique id is in the old format, migrate it
|
||||
# except if they downgraded and upgraded, there might be a duplicate
|
||||
# so we want to keep the one that was already there.
|
||||
if (
|
||||
(old_unique_id := info.unique_id)
|
||||
and (old_entry := registry_get_entity(platform, DOMAIN, old_unique_id))
|
||||
and (new_unique_id := build_device_unique_id(mac, info))
|
||||
!= old_unique_id
|
||||
and not registry_get_entity(platform, DOMAIN, new_unique_id)
|
||||
):
|
||||
ent_reg.async_update_entity(old_entry, new_unique_id=new_unique_id)
|
||||
|
||||
needed_platforms.update(INFO_TYPE_TO_PLATFORM[type(info)] for info in infos)
|
||||
await self._ensure_platforms_loaded(hass, entry, needed_platforms)
|
||||
|
||||
# Make a dict of the EntityInfo by type and send
|
||||
|
@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==35.0.0",
|
||||
"aioesphomeapi==36.0.1",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==3.1.0"
|
||||
],
|
||||
|
@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250702.2"]
|
||||
"requirements": ["home-assistant-frontend==20250702.3"]
|
||||
}
|
||||
|
@ -541,6 +541,11 @@ class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity):
|
||||
"""Return the state attributes."""
|
||||
return self.entity_description.extra_state_attributes_fn(self.mower_attributes)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return the available attribute of the entity."""
|
||||
return super().available and self.native_value is not None
|
||||
|
||||
|
||||
class WorkAreaSensorEntity(WorkAreaAvailableEntity, SensorEntity):
|
||||
"""Defining the Work area sensors with WorkAreaSensorEntityDescription."""
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["imgw_pib==1.4.1"]
|
||||
"requirements": ["imgw_pib==1.4.2"]
|
||||
}
|
||||
|
@ -463,7 +463,7 @@ class IntegrationSensor(RestoreSensor):
|
||||
) -> None:
|
||||
"""Handle sensor state update when sub interval is configured."""
|
||||
self._integrate_on_state_update_with_max_sub_interval(
|
||||
None, event.data["old_state"], event.data["new_state"]
|
||||
None, None, event.data["old_state"], event.data["new_state"]
|
||||
)
|
||||
|
||||
@callback
|
||||
@ -472,13 +472,17 @@ class IntegrationSensor(RestoreSensor):
|
||||
) -> None:
|
||||
"""Handle sensor state report when sub interval is configured."""
|
||||
self._integrate_on_state_update_with_max_sub_interval(
|
||||
event.data["old_last_reported"], None, event.data["new_state"]
|
||||
event.data["old_last_reported"],
|
||||
event.data["last_reported"],
|
||||
None,
|
||||
event.data["new_state"],
|
||||
)
|
||||
|
||||
@callback
|
||||
def _integrate_on_state_update_with_max_sub_interval(
|
||||
self,
|
||||
old_last_reported: datetime | None,
|
||||
old_timestamp: datetime | None,
|
||||
new_timestamp: datetime | None,
|
||||
old_state: State | None,
|
||||
new_state: State | None,
|
||||
) -> None:
|
||||
@ -489,7 +493,9 @@ class IntegrationSensor(RestoreSensor):
|
||||
"""
|
||||
self._cancel_max_sub_interval_exceeded_callback()
|
||||
try:
|
||||
self._integrate_on_state_change(old_last_reported, old_state, new_state)
|
||||
self._integrate_on_state_change(
|
||||
old_timestamp, new_timestamp, old_state, new_state
|
||||
)
|
||||
self._last_integration_trigger = _IntegrationTrigger.StateEvent
|
||||
self._last_integration_time = datetime.now(tz=UTC)
|
||||
finally:
|
||||
@ -503,7 +509,7 @@ class IntegrationSensor(RestoreSensor):
|
||||
) -> None:
|
||||
"""Handle sensor state change."""
|
||||
return self._integrate_on_state_change(
|
||||
None, event.data["old_state"], event.data["new_state"]
|
||||
None, None, event.data["old_state"], event.data["new_state"]
|
||||
)
|
||||
|
||||
@callback
|
||||
@ -512,12 +518,16 @@ class IntegrationSensor(RestoreSensor):
|
||||
) -> None:
|
||||
"""Handle sensor state report."""
|
||||
return self._integrate_on_state_change(
|
||||
event.data["old_last_reported"], None, event.data["new_state"]
|
||||
event.data["old_last_reported"],
|
||||
event.data["last_reported"],
|
||||
None,
|
||||
event.data["new_state"],
|
||||
)
|
||||
|
||||
def _integrate_on_state_change(
|
||||
self,
|
||||
old_last_reported: datetime | None,
|
||||
old_timestamp: datetime | None,
|
||||
new_timestamp: datetime | None,
|
||||
old_state: State | None,
|
||||
new_state: State | None,
|
||||
) -> None:
|
||||
@ -531,16 +541,17 @@ class IntegrationSensor(RestoreSensor):
|
||||
|
||||
if old_state:
|
||||
# state has changed, we recover old_state from the event
|
||||
new_timestamp = new_state.last_updated
|
||||
old_state_state = old_state.state
|
||||
old_last_reported = old_state.last_reported
|
||||
old_timestamp = old_state.last_reported
|
||||
else:
|
||||
# event state reported without any state change
|
||||
# first state or event state reported without any state change
|
||||
old_state_state = new_state.state
|
||||
|
||||
self._attr_available = True
|
||||
self._derive_and_set_attributes_from_state(new_state)
|
||||
|
||||
if old_last_reported is None and old_state is None:
|
||||
if old_timestamp is None and old_state is None:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
@ -551,11 +562,12 @@ class IntegrationSensor(RestoreSensor):
|
||||
return
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert old_last_reported is not None
|
||||
assert new_timestamp is not None
|
||||
assert old_timestamp is not None
|
||||
elapsed_seconds = Decimal(
|
||||
(new_state.last_reported - old_last_reported).total_seconds()
|
||||
(new_timestamp - old_timestamp).total_seconds()
|
||||
if self._last_integration_trigger == _IntegrationTrigger.StateEvent
|
||||
else (new_state.last_reported - self._last_integration_time).total_seconds()
|
||||
else (new_timestamp - self._last_integration_time).total_seconds()
|
||||
)
|
||||
|
||||
area = self._method.calculate_area_with_two_states(elapsed_seconds, *states)
|
||||
|
@ -98,6 +98,12 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
|
||||
f"together with state class `{state_class}`"
|
||||
)
|
||||
|
||||
unit_of_measurement: str | None
|
||||
if (
|
||||
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
) is not None and not unit_of_measurement.strip():
|
||||
config.pop(CONF_UNIT_OF_MEASUREMENT)
|
||||
|
||||
# Only allow `options` to be set for `enum` sensors
|
||||
# to limit the possible sensor values
|
||||
if (options := config.get(CONF_OPTIONS)) is not None:
|
||||
|
@ -39,7 +39,10 @@ class OllamaTaskEntity(
|
||||
):
|
||||
"""Ollama AI Task entity."""
|
||||
|
||||
_attr_supported_features = ai_task.AITaskEntityFeature.GENERATE_DATA
|
||||
_attr_supported_features = (
|
||||
ai_task.AITaskEntityFeature.GENERATE_DATA
|
||||
| ai_task.AITaskEntityFeature.SUPPORT_ATTACHMENTS
|
||||
)
|
||||
|
||||
async def _async_generate_data(
|
||||
self,
|
||||
|
@ -106,9 +106,18 @@ def _convert_content(
|
||||
],
|
||||
)
|
||||
if isinstance(chat_content, conversation.UserContent):
|
||||
images: list[ollama.Image] = []
|
||||
for attachment in chat_content.attachments or ():
|
||||
if not attachment.mime_type.startswith("image/"):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unsupported_attachment_type",
|
||||
)
|
||||
images.append(ollama.Image(value=attachment.path))
|
||||
return ollama.Message(
|
||||
role=MessageRole.USER.value,
|
||||
content=chat_content.content,
|
||||
images=images or None,
|
||||
)
|
||||
if isinstance(chat_content, conversation.SystemContent):
|
||||
return ollama.Message(
|
||||
|
@ -94,5 +94,10 @@
|
||||
"download": "[%key:component::ollama::config_subentries::conversation::progress::download%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"unsupported_attachment_type": {
|
||||
"message": "Ollama only supports image attachments in user content, but received non-image attachment."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,7 @@ from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS,
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.sensor.helpers import async_parse_date_datetime
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_FORCE_UPDATE,
|
||||
@ -181,18 +179,6 @@ class RestSensor(ManualTriggerSensorEntity, RestEntity):
|
||||
self.entity_id, variables, None
|
||||
)
|
||||
|
||||
if value is None or self.device_class not in (
|
||||
SensorDeviceClass.DATE,
|
||||
SensorDeviceClass.TIMESTAMP,
|
||||
):
|
||||
self._attr_native_value = value
|
||||
self._process_manual_data(variables)
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
self._attr_native_value = async_parse_date_datetime(
|
||||
value, self.entity_id, self.device_class
|
||||
)
|
||||
|
||||
self._set_native_value_with_possible_timestamp(value)
|
||||
self._process_manual_data(variables)
|
||||
self.async_write_ha_state()
|
||||
|
@ -7,8 +7,7 @@ from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import CONF_STATE_CLASS, SensorDeviceClass
|
||||
from homeassistant.components.sensor.helpers import async_parse_date_datetime
|
||||
from homeassistant.components.sensor import CONF_STATE_CLASS
|
||||
from homeassistant.const import (
|
||||
CONF_ATTRIBUTE,
|
||||
CONF_DEVICE_CLASS,
|
||||
@ -218,17 +217,7 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], ManualTriggerSensorEnti
|
||||
self.entity_id, variables, None
|
||||
)
|
||||
|
||||
if self.device_class not in {
|
||||
SensorDeviceClass.DATE,
|
||||
SensorDeviceClass.TIMESTAMP,
|
||||
}:
|
||||
self._attr_native_value = value
|
||||
self._process_manual_data(variables)
|
||||
return
|
||||
|
||||
self._attr_native_value = async_parse_date_datetime(
|
||||
value, self.entity_id, self.device_class
|
||||
)
|
||||
self._set_native_value_with_possible_timestamp(value)
|
||||
self._process_manual_data(variables)
|
||||
|
||||
@property
|
||||
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmarlaapi", "pysignalr"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmarlaapi==0.9.0"]
|
||||
"requirements": ["pysmarlaapi==0.9.1"]
|
||||
}
|
||||
|
@ -217,7 +217,7 @@ class SnmpSensor(ManualTriggerSensorEntity):
|
||||
self.entity_id, variables, STATE_UNKNOWN
|
||||
)
|
||||
|
||||
self._attr_native_value = value
|
||||
self._set_native_value_with_possible_timestamp(value)
|
||||
self._process_manual_data(variables)
|
||||
|
||||
|
||||
|
@ -401,9 +401,10 @@ class SQLSensor(ManualTriggerSensorEntity):
|
||||
if data is not None and self._template is not None:
|
||||
variables = self._template_variables_with_value(data)
|
||||
if self._render_availability_template(variables):
|
||||
self._attr_native_value = self._template.async_render_as_value_template(
|
||||
_value = self._template.async_render_as_value_template(
|
||||
self.entity_id, variables, None
|
||||
)
|
||||
self._set_native_value_with_possible_timestamp(_value)
|
||||
self._process_manual_data(variables)
|
||||
else:
|
||||
self._attr_native_value = data
|
||||
|
@ -727,12 +727,11 @@ class StatisticsSensor(SensorEntity):
|
||||
|
||||
def _async_handle_new_state(
|
||||
self,
|
||||
reported_state: State | None,
|
||||
reported_state: State,
|
||||
timestamp: float,
|
||||
) -> None:
|
||||
"""Handle the sensor state changes."""
|
||||
if (new_state := reported_state) is None:
|
||||
return
|
||||
self._add_state_to_queue(new_state)
|
||||
self._add_state_to_queue(reported_state, timestamp)
|
||||
self._async_purge_update_and_schedule()
|
||||
|
||||
if self._preview_callback:
|
||||
@ -747,14 +746,18 @@ class StatisticsSensor(SensorEntity):
|
||||
self,
|
||||
event: Event[EventStateChangedData],
|
||||
) -> None:
|
||||
self._async_handle_new_state(event.data["new_state"])
|
||||
if (new_state := event.data["new_state"]) is None:
|
||||
return
|
||||
self._async_handle_new_state(new_state, new_state.last_updated_timestamp)
|
||||
|
||||
@callback
|
||||
def _async_stats_sensor_state_report_listener(
|
||||
self,
|
||||
event: Event[EventStateReportedData],
|
||||
) -> None:
|
||||
self._async_handle_new_state(event.data["new_state"])
|
||||
self._async_handle_new_state(
|
||||
event.data["new_state"], event.data["last_reported"].timestamp()
|
||||
)
|
||||
|
||||
async def _async_stats_sensor_startup(self) -> None:
|
||||
"""Add listener and get recorded state.
|
||||
@ -785,7 +788,9 @@ class StatisticsSensor(SensorEntity):
|
||||
"""Register callbacks."""
|
||||
await self._async_stats_sensor_startup()
|
||||
|
||||
def _add_state_to_queue(self, new_state: State) -> None:
|
||||
def _add_state_to_queue(
|
||||
self, new_state: State, last_reported_timestamp: float
|
||||
) -> None:
|
||||
"""Add the state to the queue."""
|
||||
|
||||
# Attention: it is not safe to store the new_state object,
|
||||
@ -805,7 +810,7 @@ class StatisticsSensor(SensorEntity):
|
||||
self.states.append(new_state.state == "on")
|
||||
else:
|
||||
self.states.append(float(new_state.state))
|
||||
self.ages.append(new_state.last_reported_timestamp)
|
||||
self.ages.append(last_reported_timestamp)
|
||||
self._attr_extra_state_attributes[STAT_SOURCE_VALUE_VALID] = True
|
||||
except ValueError:
|
||||
self._attr_extra_state_attributes[STAT_SOURCE_VALUE_VALID] = False
|
||||
@ -1062,7 +1067,7 @@ class StatisticsSensor(SensorEntity):
|
||||
self._fetch_states_from_database
|
||||
):
|
||||
for state in reversed(states):
|
||||
self._add_state_to_queue(state)
|
||||
self._add_state_to_queue(state, state.last_reported_timestamp)
|
||||
self._calculate_state_attributes(state)
|
||||
self._async_purge_update_and_schedule()
|
||||
|
||||
|
@ -41,5 +41,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["switchbot"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["PySwitchbot==0.68.1"]
|
||||
"requirements": ["PySwitchbot==0.68.2"]
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ from homeassistant.components.alarm_control_panel import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_NAME,
|
||||
CONF_STATE,
|
||||
CONF_UNIQUE_ID,
|
||||
@ -31,7 +30,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import config_validation as cv, selector, template
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
@ -43,8 +42,16 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TriggerUpdateCoordinator
|
||||
from .entity import AbstractTemplateEntity
|
||||
from .helpers import async_setup_template_platform
|
||||
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema
|
||||
from .helpers import (
|
||||
async_setup_template_entry,
|
||||
async_setup_template_platform,
|
||||
async_setup_template_preview,
|
||||
)
|
||||
from .template_entity import (
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_schema,
|
||||
)
|
||||
from .trigger_entity import TriggerEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -88,27 +95,28 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Alarm Control Panel"
|
||||
|
||||
ALARM_CONTROL_PANEL_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
|
||||
vol.Optional(
|
||||
CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name
|
||||
): cv.enum(TemplateCodeFormat),
|
||||
vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
ALARM_CONTROL_PANEL_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
|
||||
vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum(
|
||||
TemplateCodeFormat
|
||||
),
|
||||
vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
ALARM_CONTROL_PANEL_YAML_SCHEMA = ALARM_CONTROL_PANEL_COMMON_SCHEMA.extend(
|
||||
make_template_entity_common_modern_schema(DEFAULT_NAME).schema
|
||||
)
|
||||
|
||||
LEGACY_ALARM_CONTROL_PANEL_SCHEMA = vol.Schema(
|
||||
ALARM_CONTROL_PANEL_LEGACY_YAML_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA,
|
||||
@ -130,59 +138,29 @@ LEGACY_ALARM_CONTROL_PANEL_SCHEMA = vol.Schema(
|
||||
PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_ALARM_CONTROL_PANELS): cv.schema_with_slug_keys(
|
||||
LEGACY_ALARM_CONTROL_PANEL_SCHEMA
|
||||
ALARM_CONTROL_PANEL_LEGACY_YAML_SCHEMA
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
ALARM_CONTROL_PANEL_CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
|
||||
vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum(
|
||||
TemplateCodeFormat
|
||||
),
|
||||
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
|
||||
vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_NAME): cv.template,
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
ALARM_CONTROL_PANEL_CONFIG_ENTRY_SCHEMA = ALARM_CONTROL_PANEL_COMMON_SCHEMA.extend(
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
|
||||
)
|
||||
|
||||
|
||||
def rewrite_options_to_modern_conf(option_config: dict[str, dict]) -> dict[str, dict]:
|
||||
"""Rewrite option configuration to modern configuration."""
|
||||
option_config = {**option_config}
|
||||
|
||||
if CONF_VALUE_TEMPLATE in option_config:
|
||||
option_config[CONF_STATE] = option_config.pop(CONF_VALUE_TEMPLATE)
|
||||
|
||||
return option_config
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize config entry."""
|
||||
_options = dict(config_entry.options)
|
||||
_options.pop("template_type")
|
||||
_options = rewrite_options_to_modern_conf(_options)
|
||||
validated_config = ALARM_CONTROL_PANEL_CONFIG_SCHEMA(_options)
|
||||
async_add_entities(
|
||||
[
|
||||
StateAlarmControlPanelEntity(
|
||||
hass,
|
||||
validated_config,
|
||||
config_entry.entry_id,
|
||||
)
|
||||
]
|
||||
await async_setup_template_entry(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
StateAlarmControlPanelEntity,
|
||||
ALARM_CONTROL_PANEL_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
@ -211,11 +189,14 @@ def async_create_preview_alarm_control_panel(
|
||||
hass: HomeAssistant, name: str, config: dict[str, Any]
|
||||
) -> StateAlarmControlPanelEntity:
|
||||
"""Create a preview alarm control panel."""
|
||||
updated_config = rewrite_options_to_modern_conf(config)
|
||||
validated_config = ALARM_CONTROL_PANEL_CONFIG_SCHEMA(
|
||||
updated_config | {CONF_NAME: name}
|
||||
return async_setup_template_preview(
|
||||
hass,
|
||||
name,
|
||||
config,
|
||||
StateAlarmControlPanelEntity,
|
||||
ALARM_CONTROL_PANEL_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
)
|
||||
return StateAlarmControlPanelEntity(hass, validated_config, None)
|
||||
|
||||
|
||||
class AbstractTemplateAlarmControlPanel(
|
||||
|
@ -22,7 +22,6 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_ENTITY_PICTURE_TEMPLATE,
|
||||
CONF_FRIENDLY_NAME_TEMPLATE,
|
||||
CONF_ICON_TEMPLATE,
|
||||
@ -38,7 +37,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import config_validation as cv, selector, template
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
@ -50,8 +49,16 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import CONF_AVAILABILITY_TEMPLATE
|
||||
from .helpers import async_setup_template_platform
|
||||
from .template_entity import TEMPLATE_ENTITY_COMMON_SCHEMA, TemplateEntity
|
||||
from .helpers import (
|
||||
async_setup_template_entry,
|
||||
async_setup_template_platform,
|
||||
async_setup_template_preview,
|
||||
)
|
||||
from .template_entity import (
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
|
||||
TEMPLATE_ENTITY_COMMON_SCHEMA,
|
||||
TemplateEntity,
|
||||
)
|
||||
from .trigger_entity import TriggerEntity
|
||||
|
||||
CONF_DELAY_ON = "delay_on"
|
||||
@ -64,7 +71,7 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
BINARY_SENSOR_SCHEMA = vol.Schema(
|
||||
BINARY_SENSOR_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_AUTO_OFF): vol.Any(cv.positive_time_period, cv.template),
|
||||
vol.Optional(CONF_DELAY_OFF): vol.Any(cv.positive_time_period, cv.template),
|
||||
@ -73,15 +80,17 @@ BINARY_SENSOR_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
}
|
||||
).extend(TEMPLATE_ENTITY_COMMON_SCHEMA.schema)
|
||||
|
||||
BINARY_SENSOR_CONFIG_SCHEMA = BINARY_SENSOR_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
|
||||
}
|
||||
)
|
||||
|
||||
LEGACY_BINARY_SENSOR_SCHEMA = vol.All(
|
||||
BINARY_SENSOR_YAML_SCHEMA = BINARY_SENSOR_COMMON_SCHEMA.extend(
|
||||
TEMPLATE_ENTITY_COMMON_SCHEMA.schema
|
||||
)
|
||||
|
||||
BINARY_SENSOR_CONFIG_ENTRY_SCHEMA = BINARY_SENSOR_COMMON_SCHEMA.extend(
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
|
||||
)
|
||||
|
||||
BINARY_SENSOR_LEGACY_YAML_SCHEMA = vol.All(
|
||||
cv.deprecated(ATTR_ENTITY_ID),
|
||||
vol.Schema(
|
||||
{
|
||||
@ -106,7 +115,7 @@ LEGACY_BINARY_SENSOR_SCHEMA = vol.All(
|
||||
PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(
|
||||
LEGACY_BINARY_SENSOR_SCHEMA
|
||||
BINARY_SENSOR_LEGACY_YAML_SCHEMA
|
||||
),
|
||||
}
|
||||
)
|
||||
@ -138,11 +147,12 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize config entry."""
|
||||
_options = dict(config_entry.options)
|
||||
_options.pop("template_type")
|
||||
validated_config = BINARY_SENSOR_CONFIG_SCHEMA(_options)
|
||||
async_add_entities(
|
||||
[StateBinarySensorEntity(hass, validated_config, config_entry.entry_id)]
|
||||
await async_setup_template_entry(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
StateBinarySensorEntity,
|
||||
BINARY_SENSOR_CONFIG_ENTRY_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
@ -151,8 +161,9 @@ def async_create_preview_binary_sensor(
|
||||
hass: HomeAssistant, name: str, config: dict[str, Any]
|
||||
) -> StateBinarySensorEntity:
|
||||
"""Create a preview sensor."""
|
||||
validated_config = BINARY_SENSOR_CONFIG_SCHEMA(config | {CONF_NAME: name})
|
||||
return StateBinarySensorEntity(hass, validated_config, None)
|
||||
return async_setup_template_preview(
|
||||
hass, name, config, StateBinarySensorEntity, BINARY_SENSOR_CONFIG_ENTRY_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
class StateBinarySensorEntity(TemplateEntity, BinarySensorEntity, RestoreEntity):
|
||||
|
@ -14,9 +14,9 @@ from homeassistant.components.button import (
|
||||
ButtonEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_DEVICE_ID, CONF_NAME
|
||||
from homeassistant.const import CONF_DEVICE_CLASS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, selector
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
@ -24,29 +24,31 @@ from homeassistant.helpers.entity_platform import (
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import CONF_PRESS, DOMAIN
|
||||
from .helpers import async_setup_template_platform
|
||||
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema
|
||||
from .helpers import async_setup_template_entry, async_setup_template_platform
|
||||
from .template_entity import (
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_schema,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "Template Button"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
BUTTON_SCHEMA = vol.Schema(
|
||||
BUTTON_YAML_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PRESS): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
}
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
|
||||
CONFIG_BUTTON_SCHEMA = vol.Schema(
|
||||
BUTTON_CONFIG_ENTRY_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME): cv.template,
|
||||
vol.Optional(CONF_PRESS): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
|
||||
}
|
||||
)
|
||||
).extend(TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
@ -73,11 +75,12 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize config entry."""
|
||||
_options = dict(config_entry.options)
|
||||
_options.pop("template_type")
|
||||
validated_config = CONFIG_BUTTON_SCHEMA(_options)
|
||||
async_add_entities(
|
||||
[StateButtonEntity(hass, validated_config, config_entry.entry_id)]
|
||||
await async_setup_template_entry(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
StateButtonEntity,
|
||||
BUTTON_CONFIG_ENTRY_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
|
@ -102,57 +102,57 @@ CONFIG_SECTION_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Optional(CONF_ACTIONS): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_BINARY_SENSORS): cv.schema_with_slug_keys(
|
||||
binary_sensor_platform.LEGACY_BINARY_SENSOR_SCHEMA
|
||||
binary_sensor_platform.BINARY_SENSOR_LEGACY_YAML_SCHEMA
|
||||
),
|
||||
vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA,
|
||||
vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys(
|
||||
sensor_platform.LEGACY_SENSOR_SCHEMA
|
||||
sensor_platform.SENSOR_LEGACY_YAML_SCHEMA
|
||||
),
|
||||
vol.Optional(CONF_TRIGGERS): cv.TRIGGER_SCHEMA,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
|
||||
vol.Optional(DOMAIN_ALARM_CONTROL_PANEL): vol.All(
|
||||
cv.ensure_list,
|
||||
[alarm_control_panel_platform.ALARM_CONTROL_PANEL_SCHEMA],
|
||||
[alarm_control_panel_platform.ALARM_CONTROL_PANEL_YAML_SCHEMA],
|
||||
),
|
||||
vol.Optional(DOMAIN_BINARY_SENSOR): vol.All(
|
||||
cv.ensure_list, [binary_sensor_platform.BINARY_SENSOR_SCHEMA]
|
||||
cv.ensure_list, [binary_sensor_platform.BINARY_SENSOR_YAML_SCHEMA]
|
||||
),
|
||||
vol.Optional(DOMAIN_BUTTON): vol.All(
|
||||
cv.ensure_list, [button_platform.BUTTON_SCHEMA]
|
||||
cv.ensure_list, [button_platform.BUTTON_YAML_SCHEMA]
|
||||
),
|
||||
vol.Optional(DOMAIN_COVER): vol.All(
|
||||
cv.ensure_list, [cover_platform.COVER_SCHEMA]
|
||||
cv.ensure_list, [cover_platform.COVER_YAML_SCHEMA]
|
||||
),
|
||||
vol.Optional(DOMAIN_FAN): vol.All(
|
||||
cv.ensure_list, [fan_platform.FAN_SCHEMA]
|
||||
cv.ensure_list, [fan_platform.FAN_YAML_SCHEMA]
|
||||
),
|
||||
vol.Optional(DOMAIN_IMAGE): vol.All(
|
||||
cv.ensure_list, [image_platform.IMAGE_SCHEMA]
|
||||
cv.ensure_list, [image_platform.IMAGE_YAML_SCHEMA]
|
||||
),
|
||||
vol.Optional(DOMAIN_LIGHT): vol.All(
|
||||
cv.ensure_list, [light_platform.LIGHT_SCHEMA]
|
||||
cv.ensure_list, [light_platform.LIGHT_YAML_SCHEMA]
|
||||
),
|
||||
vol.Optional(DOMAIN_LOCK): vol.All(
|
||||
cv.ensure_list, [lock_platform.LOCK_SCHEMA]
|
||||
cv.ensure_list, [lock_platform.LOCK_YAML_SCHEMA]
|
||||
),
|
||||
vol.Optional(DOMAIN_NUMBER): vol.All(
|
||||
cv.ensure_list, [number_platform.NUMBER_SCHEMA]
|
||||
cv.ensure_list, [number_platform.NUMBER_YAML_SCHEMA]
|
||||
),
|
||||
vol.Optional(DOMAIN_SELECT): vol.All(
|
||||
cv.ensure_list, [select_platform.SELECT_SCHEMA]
|
||||
cv.ensure_list, [select_platform.SELECT_YAML_SCHEMA]
|
||||
),
|
||||
vol.Optional(DOMAIN_SENSOR): vol.All(
|
||||
cv.ensure_list, [sensor_platform.SENSOR_SCHEMA]
|
||||
cv.ensure_list, [sensor_platform.SENSOR_YAML_SCHEMA]
|
||||
),
|
||||
vol.Optional(DOMAIN_SWITCH): vol.All(
|
||||
cv.ensure_list, [switch_platform.SWITCH_SCHEMA]
|
||||
cv.ensure_list, [switch_platform.SWITCH_YAML_SCHEMA]
|
||||
),
|
||||
vol.Optional(DOMAIN_VACUUM): vol.All(
|
||||
cv.ensure_list, [vacuum_platform.VACUUM_SCHEMA]
|
||||
cv.ensure_list, [vacuum_platform.VACUUM_YAML_SCHEMA]
|
||||
),
|
||||
vol.Optional(DOMAIN_WEATHER): vol.All(
|
||||
cv.ensure_list, [weather_platform.WEATHER_SCHEMA]
|
||||
cv.ensure_list, [weather_platform.WEATHER_YAML_SCHEMA]
|
||||
),
|
||||
},
|
||||
),
|
||||
|
@ -1,6 +1,9 @@
|
||||
"""Constants for the Template Platform Components."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_ICON, CONF_NAME, CONF_UNIQUE_ID, Platform
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
CONF_ATTRIBUTE_TEMPLATES = "attribute_templates"
|
||||
@ -16,6 +19,15 @@ CONF_STEP = "step"
|
||||
CONF_TURN_OFF = "turn_off"
|
||||
CONF_TURN_ON = "turn_on"
|
||||
|
||||
TEMPLATE_ENTITY_BASE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ICON): cv.template,
|
||||
vol.Optional(CONF_NAME): cv.template,
|
||||
vol.Optional(CONF_PICTURE): cv.template,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
DOMAIN = "template"
|
||||
|
||||
PLATFORM_STORAGE_KEY = "template_platforms"
|
||||
|
@ -91,7 +91,7 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Cover"
|
||||
|
||||
COVER_SCHEMA = vol.All(
|
||||
COVER_YAML_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Inclusive(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA,
|
||||
@ -110,7 +110,7 @@ COVER_SCHEMA = vol.All(
|
||||
cv.has_at_least_one_key(OPEN_ACTION, POSITION_ACTION),
|
||||
)
|
||||
|
||||
LEGACY_COVER_SCHEMA = vol.All(
|
||||
COVER_LEGACY_YAML_SCHEMA = vol.All(
|
||||
cv.deprecated(CONF_ENTITY_ID),
|
||||
vol.Schema(
|
||||
{
|
||||
@ -134,7 +134,7 @@ LEGACY_COVER_SCHEMA = vol.All(
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend(
|
||||
{vol.Required(CONF_COVERS): cv.schema_with_slug_keys(LEGACY_COVER_SCHEMA)}
|
||||
{vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_LEGACY_YAML_SCHEMA)}
|
||||
)
|
||||
|
||||
|
||||
|
@ -81,7 +81,7 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Fan"
|
||||
|
||||
FAN_SCHEMA = vol.All(
|
||||
FAN_YAML_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_DIRECTION): cv.template,
|
||||
@ -101,7 +101,7 @@ FAN_SCHEMA = vol.All(
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
)
|
||||
|
||||
LEGACY_FAN_SCHEMA = vol.All(
|
||||
FAN_LEGACY_YAML_SCHEMA = vol.All(
|
||||
cv.deprecated(CONF_ENTITY_ID),
|
||||
vol.Schema(
|
||||
{
|
||||
@ -126,7 +126,7 @@ LEGACY_FAN_SCHEMA = vol.All(
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
|
||||
{vol.Required(CONF_FANS): cv.schema_with_slug_keys(LEGACY_FAN_SCHEMA)}
|
||||
{vol.Required(CONF_FANS): cv.schema_with_slug_keys(FAN_LEGACY_YAML_SCHEMA)}
|
||||
)
|
||||
|
||||
|
||||
|
@ -5,14 +5,19 @@ import itertools
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import blueprint
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_PICTURE_TEMPLATE,
|
||||
CONF_FRIENDLY_NAME,
|
||||
CONF_ICON,
|
||||
CONF_ICON_TEMPLATE,
|
||||
CONF_NAME,
|
||||
CONF_STATE,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
SERVICE_RELOAD,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@ -20,6 +25,7 @@ from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
async_get_platforms,
|
||||
)
|
||||
@ -228,3 +234,41 @@ async def async_setup_template_platform(
|
||||
discovery_info["entities"],
|
||||
discovery_info["unique_id"],
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_template_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
state_entity_cls: type[TemplateEntity],
|
||||
config_schema: vol.Schema,
|
||||
replace_value_template: bool = False,
|
||||
) -> None:
|
||||
"""Setup the Template from a config entry."""
|
||||
options = dict(config_entry.options)
|
||||
options.pop("template_type")
|
||||
|
||||
if replace_value_template and CONF_VALUE_TEMPLATE in options:
|
||||
options[CONF_STATE] = options.pop(CONF_VALUE_TEMPLATE)
|
||||
|
||||
validated_config = config_schema(options)
|
||||
|
||||
async_add_entities(
|
||||
[state_entity_cls(hass, validated_config, config_entry.entry_id)]
|
||||
)
|
||||
|
||||
|
||||
def async_setup_template_preview[T: TemplateEntity](
|
||||
hass: HomeAssistant,
|
||||
name: str,
|
||||
config: ConfigType,
|
||||
state_entity_cls: type[T],
|
||||
schema: vol.Schema,
|
||||
replace_value_template: bool = False,
|
||||
) -> T:
|
||||
"""Setup the Template preview."""
|
||||
if replace_value_template and CONF_VALUE_TEMPLATE in config:
|
||||
config[CONF_STATE] = config.pop(CONF_VALUE_TEMPLATE)
|
||||
|
||||
validated_config = schema(config | {CONF_NAME: name})
|
||||
return state_entity_cls(hass, validated_config, None)
|
||||
|
@ -13,10 +13,10 @@ from homeassistant.components.image import (
|
||||
ImageEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, CONF_URL, CONF_VERIFY_SSL
|
||||
from homeassistant.const import CONF_URL, CONF_VERIFY_SSL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import config_validation as cv, selector
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
@ -26,8 +26,9 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import CONF_PICTURE
|
||||
from .helpers import async_setup_template_platform
|
||||
from .helpers import async_setup_template_entry, async_setup_template_platform
|
||||
from .template_entity import (
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_attributes_schema,
|
||||
)
|
||||
@ -39,7 +40,7 @@ DEFAULT_NAME = "Template Image"
|
||||
|
||||
GET_IMAGE_TIMEOUT = 10
|
||||
|
||||
IMAGE_SCHEMA = vol.Schema(
|
||||
IMAGE_YAML_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_URL): cv.template,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
|
||||
@ -47,14 +48,12 @@ IMAGE_SCHEMA = vol.Schema(
|
||||
).extend(make_template_entity_common_modern_attributes_schema(DEFAULT_NAME).schema)
|
||||
|
||||
|
||||
IMAGE_CONFIG_SCHEMA = vol.Schema(
|
||||
IMAGE_CONFIG_ENTRY_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME): cv.template,
|
||||
vol.Required(CONF_URL): cv.template,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
|
||||
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
|
||||
}
|
||||
)
|
||||
).extend(TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
@ -81,11 +80,12 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize config entry."""
|
||||
_options = dict(config_entry.options)
|
||||
_options.pop("template_type")
|
||||
validated_config = IMAGE_CONFIG_SCHEMA(_options)
|
||||
async_add_entities(
|
||||
[StateImageEntity(hass, validated_config, config_entry.entry_id)]
|
||||
await async_setup_template_entry(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
StateImageEntity,
|
||||
IMAGE_CONFIG_ENTRY_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
|
@ -121,7 +121,7 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Light"
|
||||
|
||||
LIGHT_SCHEMA = vol.Schema(
|
||||
LIGHT_YAML_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA,
|
||||
vol.Inclusive(CONF_EFFECT_LIST, "effect"): cv.template,
|
||||
@ -147,7 +147,7 @@ LIGHT_SCHEMA = vol.Schema(
|
||||
}
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
|
||||
LEGACY_LIGHT_SCHEMA = vol.All(
|
||||
LIGHT_LEGACY_YAML_SCHEMA = vol.All(
|
||||
cv.deprecated(CONF_ENTITY_ID),
|
||||
vol.Schema(
|
||||
{
|
||||
@ -186,7 +186,7 @@ PLATFORM_SCHEMA = vol.All(
|
||||
cv.removed(CONF_WHITE_VALUE_ACTION),
|
||||
cv.removed(CONF_WHITE_VALUE_TEMPLATE),
|
||||
LIGHT_PLATFORM_SCHEMA.extend(
|
||||
{vol.Required(CONF_LIGHTS): cv.schema_with_slug_keys(LEGACY_LIGHT_SCHEMA)}
|
||||
{vol.Required(CONF_LIGHTS): cv.schema_with_slug_keys(LIGHT_LEGACY_YAML_SCHEMA)}
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -54,7 +54,7 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
LOCK_SCHEMA = vol.All(
|
||||
LOCK_YAML_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CODE_FORMAT): cv.template,
|
||||
@ -68,7 +68,6 @@ LOCK_SCHEMA = vol.All(
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
)
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_CODE_FORMAT_TEMPLATE): cv.template,
|
||||
|
@ -18,14 +18,13 @@ from homeassistant.components.number import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
CONF_NAME,
|
||||
CONF_OPTIMISTIC,
|
||||
CONF_STATE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, selector
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
@ -34,8 +33,16 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import CONF_MAX, CONF_MIN, CONF_STEP, DOMAIN
|
||||
from .helpers import async_setup_template_platform
|
||||
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema
|
||||
from .helpers import (
|
||||
async_setup_template_entry,
|
||||
async_setup_template_platform,
|
||||
async_setup_template_preview,
|
||||
)
|
||||
from .template_entity import (
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_schema,
|
||||
)
|
||||
from .trigger_entity import TriggerEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -45,30 +52,31 @@ CONF_SET_VALUE = "set_value"
|
||||
DEFAULT_NAME = "Template Number"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
NUMBER_SCHEMA = vol.Schema(
|
||||
NUMBER_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_STATE): cv.template,
|
||||
vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_STEP): cv.template,
|
||||
vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): cv.template,
|
||||
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
}
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
NUMBER_CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.template,
|
||||
vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): cv.template,
|
||||
vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_STATE): cv.template,
|
||||
vol.Required(CONF_STEP): cv.template,
|
||||
vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_MIN): cv.template,
|
||||
vol.Optional(CONF_MAX): cv.template,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
|
||||
}
|
||||
)
|
||||
|
||||
NUMBER_YAML_SCHEMA = (
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
.extend(NUMBER_COMMON_SCHEMA.schema)
|
||||
)
|
||||
|
||||
NUMBER_CONFIG_ENTRY_SCHEMA = NUMBER_COMMON_SCHEMA.extend(
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
@ -94,11 +102,12 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize config entry."""
|
||||
_options = dict(config_entry.options)
|
||||
_options.pop("template_type")
|
||||
validated_config = NUMBER_CONFIG_SCHEMA(_options)
|
||||
async_add_entities(
|
||||
[StateNumberEntity(hass, validated_config, config_entry.entry_id)]
|
||||
await async_setup_template_entry(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
StateNumberEntity,
|
||||
NUMBER_CONFIG_ENTRY_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
@ -107,8 +116,9 @@ def async_create_preview_number(
|
||||
hass: HomeAssistant, name: str, config: dict[str, Any]
|
||||
) -> StateNumberEntity:
|
||||
"""Create a preview number."""
|
||||
validated_config = NUMBER_CONFIG_SCHEMA(config | {CONF_NAME: name})
|
||||
return StateNumberEntity(hass, validated_config, None)
|
||||
return async_setup_template_preview(
|
||||
hass, name, config, StateNumberEntity, NUMBER_CONFIG_ENTRY_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
class StateNumberEntity(TemplateEntity, NumberEntity):
|
||||
|
@ -15,9 +15,9 @@ from homeassistant.components.select import (
|
||||
SelectEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, CONF_OPTIMISTIC, CONF_STATE
|
||||
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, selector
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
@ -27,8 +27,16 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .entity import AbstractTemplateEntity
|
||||
from .helpers import async_setup_template_platform
|
||||
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema
|
||||
from .helpers import (
|
||||
async_setup_template_entry,
|
||||
async_setup_template_platform,
|
||||
async_setup_template_preview,
|
||||
)
|
||||
from .template_entity import (
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_schema,
|
||||
)
|
||||
from .trigger_entity import TriggerEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -39,26 +47,28 @@ CONF_SELECT_OPTION = "select_option"
|
||||
DEFAULT_NAME = "Template Select"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
SELECT_SCHEMA = vol.Schema(
|
||||
SELECT_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Required(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(ATTR_OPTIONS): cv.template,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
}
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
|
||||
|
||||
SELECT_CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.template,
|
||||
vol.Required(CONF_STATE): cv.template,
|
||||
vol.Required(CONF_OPTIONS): cv.template,
|
||||
vol.Optional(ATTR_OPTIONS): cv.template,
|
||||
vol.Optional(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
}
|
||||
)
|
||||
|
||||
SELECT_YAML_SCHEMA = (
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
.extend(SELECT_COMMON_SCHEMA.schema)
|
||||
)
|
||||
|
||||
SELECT_CONFIG_ENTRY_SCHEMA = SELECT_COMMON_SCHEMA.extend(
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
@ -84,10 +94,13 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize config entry."""
|
||||
_options = dict(config_entry.options)
|
||||
_options.pop("template_type")
|
||||
validated_config = SELECT_CONFIG_SCHEMA(_options)
|
||||
async_add_entities([TemplateSelect(hass, validated_config, config_entry.entry_id)])
|
||||
await async_setup_template_entry(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
TemplateSelect,
|
||||
SELECT_CONFIG_ENTRY_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
@ -95,8 +108,9 @@ def async_create_preview_select(
|
||||
hass: HomeAssistant, name: str, config: dict[str, Any]
|
||||
) -> TemplateSelect:
|
||||
"""Create a preview select."""
|
||||
validated_config = SELECT_CONFIG_SCHEMA(config | {CONF_NAME: name})
|
||||
return TemplateSelect(hass, validated_config, None)
|
||||
return async_setup_template_preview(
|
||||
hass, name, config, TemplateSelect, SELECT_CONFIG_ENTRY_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
class AbstractTemplateSelect(AbstractTemplateEntity, SelectEntity):
|
||||
|
@ -15,6 +15,7 @@ from homeassistant.components.sensor import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
STATE_CLASSES_SCHEMA,
|
||||
RestoreSensor,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
@ -25,7 +26,6 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_ENTITY_PICTURE_TEMPLATE,
|
||||
CONF_FRIENDLY_NAME,
|
||||
CONF_FRIENDLY_NAME_TEMPLATE,
|
||||
@ -43,19 +43,26 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import config_validation as cv, selector, template
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.trigger_template_entity import TEMPLATE_SENSOR_BASE_SCHEMA
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import CONF_ATTRIBUTE_TEMPLATES, CONF_AVAILABILITY_TEMPLATE
|
||||
from .helpers import async_setup_template_platform
|
||||
from .template_entity import TEMPLATE_ENTITY_COMMON_SCHEMA, TemplateEntity
|
||||
from .helpers import (
|
||||
async_setup_template_entry,
|
||||
async_setup_template_platform,
|
||||
async_setup_template_preview,
|
||||
)
|
||||
from .template_entity import (
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
|
||||
TEMPLATE_ENTITY_COMMON_SCHEMA,
|
||||
TemplateEntity,
|
||||
)
|
||||
from .trigger_entity import TriggerEntity
|
||||
|
||||
LEGACY_FIELDS = {
|
||||
@ -77,29 +84,31 @@ def validate_last_reset(val):
|
||||
return val
|
||||
|
||||
|
||||
SENSOR_SCHEMA = vol.All(
|
||||
SENSOR_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
SENSOR_YAML_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_STATE): cv.template,
|
||||
vol.Optional(ATTR_LAST_RESET): cv.template,
|
||||
}
|
||||
)
|
||||
.extend(TEMPLATE_SENSOR_BASE_SCHEMA.schema)
|
||||
.extend(SENSOR_COMMON_SCHEMA.schema)
|
||||
.extend(TEMPLATE_ENTITY_COMMON_SCHEMA.schema),
|
||||
validate_last_reset,
|
||||
)
|
||||
|
||||
|
||||
SENSOR_CONFIG_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
|
||||
}
|
||||
).extend(TEMPLATE_SENSOR_BASE_SCHEMA.schema),
|
||||
SENSOR_CONFIG_ENTRY_SCHEMA = SENSOR_COMMON_SCHEMA.extend(
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
|
||||
)
|
||||
|
||||
LEGACY_SENSOR_SCHEMA = vol.All(
|
||||
SENSOR_LEGACY_YAML_SCHEMA = vol.All(
|
||||
cv.deprecated(ATTR_ENTITY_ID),
|
||||
vol.Schema(
|
||||
{
|
||||
@ -141,7 +150,9 @@ PLATFORM_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Optional(CONF_TRIGGER): cv.match_all, # to raise custom warning
|
||||
vol.Optional(CONF_TRIGGERS): cv.match_all, # to raise custom warning
|
||||
vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(LEGACY_SENSOR_SCHEMA),
|
||||
vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(
|
||||
SENSOR_LEGACY_YAML_SCHEMA
|
||||
),
|
||||
}
|
||||
),
|
||||
extra_validation_checks,
|
||||
@ -176,11 +187,12 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize config entry."""
|
||||
_options = dict(config_entry.options)
|
||||
_options.pop("template_type")
|
||||
validated_config = SENSOR_CONFIG_SCHEMA(_options)
|
||||
async_add_entities(
|
||||
[StateSensorEntity(hass, validated_config, config_entry.entry_id)]
|
||||
await async_setup_template_entry(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
StateSensorEntity,
|
||||
SENSOR_CONFIG_ENTRY_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
@ -189,8 +201,9 @@ def async_create_preview_sensor(
|
||||
hass: HomeAssistant, name: str, config: dict[str, Any]
|
||||
) -> StateSensorEntity:
|
||||
"""Create a preview sensor."""
|
||||
validated_config = SENSOR_CONFIG_SCHEMA(config | {CONF_NAME: name})
|
||||
return StateSensorEntity(hass, validated_config, None)
|
||||
return async_setup_template_preview(
|
||||
hass, name, config, StateSensorEntity, SENSOR_CONFIG_ENTRY_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
class StateSensorEntity(TemplateEntity, SensorEntity):
|
||||
|
@ -16,7 +16,6 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_NAME,
|
||||
CONF_STATE,
|
||||
CONF_SWITCHES,
|
||||
@ -29,7 +28,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import config_validation as cv, selector, template
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
@ -39,8 +38,13 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import CONF_TURN_OFF, CONF_TURN_ON, DOMAIN
|
||||
from .helpers import async_setup_template_platform
|
||||
from .helpers import (
|
||||
async_setup_template_entry,
|
||||
async_setup_template_platform,
|
||||
async_setup_template_preview,
|
||||
)
|
||||
from .template_entity import (
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
|
||||
TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY,
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_schema,
|
||||
@ -55,16 +59,19 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Switch"
|
||||
|
||||
|
||||
SWITCH_SCHEMA = vol.Schema(
|
||||
SWITCH_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Required(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
)
|
||||
|
||||
LEGACY_SWITCH_SCHEMA = vol.All(
|
||||
SWITCH_YAML_SCHEMA = SWITCH_COMMON_SCHEMA.extend(
|
||||
make_template_entity_common_modern_schema(DEFAULT_NAME).schema
|
||||
)
|
||||
|
||||
SWITCH_LEGACY_YAML_SCHEMA = vol.All(
|
||||
cv.deprecated(ATTR_ENTITY_ID),
|
||||
vol.Schema(
|
||||
{
|
||||
@ -79,17 +86,11 @@ LEGACY_SWITCH_SCHEMA = vol.All(
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
|
||||
{vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(LEGACY_SWITCH_SCHEMA)}
|
||||
{vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_LEGACY_YAML_SCHEMA)}
|
||||
)
|
||||
|
||||
SWITCH_CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.template,
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
|
||||
}
|
||||
SWITCH_CONFIG_ENTRY_SCHEMA = SWITCH_COMMON_SCHEMA.extend(
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
|
||||
)
|
||||
|
||||
|
||||
@ -129,12 +130,13 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize config entry."""
|
||||
_options = dict(config_entry.options)
|
||||
_options.pop("template_type")
|
||||
_options = rewrite_options_to_modern_conf(_options)
|
||||
validated_config = SWITCH_CONFIG_SCHEMA(_options)
|
||||
async_add_entities(
|
||||
[StateSwitchEntity(hass, validated_config, config_entry.entry_id)]
|
||||
await async_setup_template_entry(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
StateSwitchEntity,
|
||||
SWITCH_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
@ -143,9 +145,14 @@ def async_create_preview_switch(
|
||||
hass: HomeAssistant, name: str, config: dict[str, Any]
|
||||
) -> StateSwitchEntity:
|
||||
"""Create a preview switch."""
|
||||
updated_config = rewrite_options_to_modern_conf(config)
|
||||
validated_config = SWITCH_CONFIG_SCHEMA(updated_config | {CONF_NAME: name})
|
||||
return StateSwitchEntity(hass, validated_config, None)
|
||||
return async_setup_template_preview(
|
||||
hass,
|
||||
name,
|
||||
config,
|
||||
StateSwitchEntity,
|
||||
SWITCH_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class StateSwitchEntity(TemplateEntity, SwitchEntity, RestoreEntity):
|
||||
|
@ -12,6 +12,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
CONF_ENTITY_PICTURE_TEMPLATE,
|
||||
CONF_ICON,
|
||||
CONF_ICON_TEMPLATE,
|
||||
@ -30,7 +31,7 @@ from homeassistant.core import (
|
||||
validate_state,
|
||||
)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv, selector
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import (
|
||||
TrackTemplate,
|
||||
@ -46,7 +47,6 @@ from homeassistant.helpers.template import (
|
||||
result_as_boolean,
|
||||
)
|
||||
from homeassistant.helpers.trigger_template_entity import (
|
||||
TEMPLATE_ENTITY_BASE_SCHEMA,
|
||||
make_template_entity_base_schema,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@ -57,6 +57,7 @@ from .const import (
|
||||
CONF_AVAILABILITY,
|
||||
CONF_AVAILABILITY_TEMPLATE,
|
||||
CONF_PICTURE,
|
||||
TEMPLATE_ENTITY_BASE_SCHEMA,
|
||||
)
|
||||
from .entity import AbstractTemplateEntity
|
||||
|
||||
@ -91,6 +92,13 @@ TEMPLATE_ENTITY_COMMON_SCHEMA = (
|
||||
.extend(TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA.schema)
|
||||
)
|
||||
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.template,
|
||||
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
|
||||
}
|
||||
).extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
def make_template_entity_common_modern_schema(
|
||||
default_name: str,
|
||||
|
@ -76,7 +76,7 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
VACUUM_SCHEMA = vol.All(
|
||||
VACUUM_YAML_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_BATTERY_LEVEL): cv.template,
|
||||
@ -94,7 +94,7 @@ VACUUM_SCHEMA = vol.All(
|
||||
).extend(make_template_entity_common_modern_attributes_schema(DEFAULT_NAME).schema)
|
||||
)
|
||||
|
||||
LEGACY_VACUUM_SCHEMA = vol.All(
|
||||
VACUUM_LEGACY_YAML_SCHEMA = vol.All(
|
||||
cv.deprecated(CONF_ENTITY_ID),
|
||||
vol.Schema(
|
||||
{
|
||||
@ -119,7 +119,7 @@ LEGACY_VACUUM_SCHEMA = vol.All(
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
|
||||
{vol.Required(CONF_VACUUMS): cv.schema_with_slug_keys(LEGACY_VACUUM_SCHEMA)}
|
||||
{vol.Required(CONF_VACUUMS): cv.schema_with_slug_keys(VACUUM_LEGACY_YAML_SCHEMA)}
|
||||
)
|
||||
|
||||
|
||||
|
@ -31,7 +31,12 @@ from homeassistant.components.weather import (
|
||||
WeatherEntity,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.const import CONF_TEMPERATURE_UNIT, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_TEMPERATURE_UNIT,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
@ -100,7 +105,7 @@ CONF_APPARENT_TEMPERATURE_TEMPLATE = "apparent_temperature_template"
|
||||
|
||||
DEFAULT_NAME = "Template Weather"
|
||||
|
||||
WEATHER_SCHEMA = vol.Schema(
|
||||
WEATHER_YAML_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_APPARENT_TEMPERATURE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_ATTRIBUTION_TEMPLATE): cv.template,
|
||||
@ -126,7 +131,32 @@ WEATHER_SCHEMA = vol.Schema(
|
||||
}
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
|
||||
PLATFORM_SCHEMA = WEATHER_PLATFORM_SCHEMA.extend(WEATHER_SCHEMA.schema)
|
||||
PLATFORM_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_APPARENT_TEMPERATURE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_ATTRIBUTION_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_CLOUD_COVERAGE_TEMPLATE): cv.template,
|
||||
vol.Required(CONF_CONDITION_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_DEW_POINT_TEMPLATE): cv.template,
|
||||
vol.Required(CONF_HUMIDITY_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FORECAST_DAILY_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FORECAST_HOURLY_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FORECAST_TWICE_DAILY_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_OZONE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template,
|
||||
vol.Optional(CONF_PRECIPITATION_UNIT): vol.In(DistanceConverter.VALID_UNITS),
|
||||
vol.Optional(CONF_PRESSURE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.VALID_UNITS),
|
||||
vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(TemperatureConverter.VALID_UNITS),
|
||||
vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS),
|
||||
vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_WIND_GUST_SPEED_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_WIND_SPEED_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_WIND_SPEED_UNIT): vol.In(SpeedConverter.VALID_UNITS),
|
||||
}
|
||||
).extend(WEATHER_PLATFORM_SCHEMA.schema)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
|
@ -157,7 +157,6 @@ class EventStateEventData(TypedDict):
|
||||
"""Base class for EVENT_STATE_CHANGED and EVENT_STATE_REPORTED data."""
|
||||
|
||||
entity_id: str
|
||||
new_state: State | None
|
||||
|
||||
|
||||
class EventStateChangedData(EventStateEventData):
|
||||
@ -166,6 +165,7 @@ class EventStateChangedData(EventStateEventData):
|
||||
A state changed event is fired when on state write the state is changed.
|
||||
"""
|
||||
|
||||
new_state: State | None
|
||||
old_state: State | None
|
||||
|
||||
|
||||
@ -175,6 +175,8 @@ class EventStateReportedData(EventStateEventData):
|
||||
A state reported event is fired when on state write the state is unchanged.
|
||||
"""
|
||||
|
||||
last_reported: datetime.datetime
|
||||
new_state: State
|
||||
old_last_reported: datetime.datetime
|
||||
|
||||
|
||||
@ -1749,18 +1751,38 @@ class CompressedState(TypedDict):
|
||||
|
||||
|
||||
class State:
|
||||
"""Object to represent a state within the state machine.
|
||||
"""Object to represent a state within the state machine."""
|
||||
|
||||
entity_id: the entity that is represented.
|
||||
state: the state of the entity
|
||||
attributes: extra information on entity and state
|
||||
last_changed: last time the state was changed.
|
||||
last_reported: last time the state was reported.
|
||||
last_updated: last time the state or attributes were changed.
|
||||
context: Context in which it was created
|
||||
domain: Domain of this state.
|
||||
object_id: Object id of this state.
|
||||
entity_id: str
|
||||
"""The entity that is represented by the state."""
|
||||
domain: str
|
||||
"""Domain of the entity that is represented by the state."""
|
||||
object_id: str
|
||||
"""object_id: Object id of this state."""
|
||||
state: str
|
||||
"""The state of the entity."""
|
||||
attributes: ReadOnlyDict[str, Any]
|
||||
"""Extra information on entity and state"""
|
||||
last_changed: datetime.datetime
|
||||
"""Last time the state was changed."""
|
||||
last_reported: datetime.datetime
|
||||
"""Last time the state was reported.
|
||||
|
||||
Note: When the state is set and neither the state nor attributes are
|
||||
changed, the existing state will be mutated with an updated last_reported.
|
||||
|
||||
When handling a state change event, the last_reported attribute of the old
|
||||
state will not be modified and can safely be used. The last_reported attribute
|
||||
of the new state may be modified and the last_updated attribute should be used
|
||||
instead.
|
||||
|
||||
When handling a state report event, the last_reported attribute may be
|
||||
modified and last_reported from the event data should be used instead.
|
||||
"""
|
||||
last_updated: datetime.datetime
|
||||
"""Last time the state or attributes were changed."""
|
||||
context: Context
|
||||
"""Context in which the state was created."""
|
||||
|
||||
__slots__ = (
|
||||
"_cache",
|
||||
@ -1841,7 +1863,20 @@ class State:
|
||||
|
||||
@under_cached_property
|
||||
def last_reported_timestamp(self) -> float:
|
||||
"""Timestamp of last report."""
|
||||
"""Timestamp of last report.
|
||||
|
||||
Note: When the state is set and neither the state nor attributes are
|
||||
changed, the existing state will be mutated with an updated last_reported.
|
||||
|
||||
When handling a state change event, the last_reported_timestamp attribute
|
||||
of the old state will not be modified and can safely be used. The
|
||||
last_reported_timestamp attribute of the new state may be modified and the
|
||||
last_updated_timestamp attribute should be used instead.
|
||||
|
||||
When handling a state report event, the last_reported_timestamp attribute may
|
||||
be modified and last_reported from the event data should be used instead.
|
||||
"""
|
||||
|
||||
return self.last_reported.timestamp()
|
||||
|
||||
@under_cached_property
|
||||
@ -2340,6 +2375,7 @@ class StateMachine:
|
||||
EVENT_STATE_REPORTED,
|
||||
{
|
||||
"entity_id": entity_id,
|
||||
"last_reported": now,
|
||||
"old_last_reported": old_last_reported,
|
||||
"new_state": old_state,
|
||||
},
|
||||
|
@ -813,6 +813,7 @@ class EntitySelectorConfig(BaseSelectorConfig, EntityFilterSelectorConfig, total
|
||||
exclude_entities: list[str]
|
||||
include_entities: list[str]
|
||||
multiple: bool
|
||||
reorder: bool
|
||||
filter: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
|
||||
|
||||
|
||||
@ -829,6 +830,7 @@ class EntitySelector(Selector[EntitySelectorConfig]):
|
||||
vol.Optional("exclude_entities"): [str],
|
||||
vol.Optional("include_entities"): [str],
|
||||
vol.Optional("multiple", default=False): cv.boolean,
|
||||
vol.Optional("reorder", default=False): cv.boolean,
|
||||
vol.Optional("filter"): vol.All(
|
||||
cv.ensure_list,
|
||||
[ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA],
|
||||
|
@ -13,8 +13,10 @@ from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS,
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
STATE_CLASSES_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor.helpers import async_parse_date_datetime
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_PICTURE,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
@ -389,3 +391,20 @@ class ManualTriggerSensorEntity(ManualTriggerEntity, SensorEntity):
|
||||
ManualTriggerEntity.__init__(self, hass, config)
|
||||
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
self._attr_state_class = config.get(CONF_STATE_CLASS)
|
||||
|
||||
@callback
|
||||
def _set_native_value_with_possible_timestamp(self, value: Any) -> None:
|
||||
"""Set native value with possible timestamp.
|
||||
|
||||
If self.device_class is `date` or `timestamp`,
|
||||
it will try to parse the value to a date/datetime object.
|
||||
"""
|
||||
if self.device_class not in (
|
||||
SensorDeviceClass.DATE,
|
||||
SensorDeviceClass.TIMESTAMP,
|
||||
):
|
||||
self._attr_native_value = value
|
||||
elif value is not None:
|
||||
self._attr_native_value = async_parse_date_datetime(
|
||||
value, self.entity_id, self.device_class
|
||||
)
|
||||
|
@ -38,7 +38,7 @@ habluetooth==4.0.1
|
||||
hass-nabucasa==0.107.1
|
||||
hassil==2.2.3
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20250702.2
|
||||
home-assistant-frontend==20250702.3
|
||||
home-assistant-intents==2025.6.23
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
@ -589,10 +589,6 @@ filterwarnings = [
|
||||
# -- Websockets 14.1
|
||||
# https://websockets.readthedocs.io/en/stable/howto/upgrade.html
|
||||
"ignore:websockets.legacy is deprecated:DeprecationWarning:websockets.legacy",
|
||||
# https://github.com/bluecurrent/HomeAssistantAPI/pull/19 - >=1.2.4
|
||||
"ignore:websockets.client.connect is deprecated:DeprecationWarning:bluecurrent_api.websocket",
|
||||
"ignore:websockets.client.WebSocketClientProtocol is deprecated:DeprecationWarning:bluecurrent_api.websocket",
|
||||
"ignore:websockets.exceptions.InvalidStatusCode is deprecated:DeprecationWarning:bluecurrent_api.websocket",
|
||||
# https://github.com/graphql-python/gql/pull/543 - >=4.0.0a0
|
||||
"ignore:websockets.client.WebSocketClientProtocol is deprecated:DeprecationWarning:gql.transport.websockets_base",
|
||||
|
||||
|
14
requirements_all.txt
generated
14
requirements_all.txt
generated
@ -84,7 +84,7 @@ PyQRCode==1.2.1
|
||||
PyRMVtransport==0.3.3
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.68.1
|
||||
PySwitchbot==0.68.2
|
||||
|
||||
# homeassistant.components.switchmate
|
||||
PySwitchmate==0.5.1
|
||||
@ -185,7 +185,7 @@ aioairzone-cloud==0.6.15
|
||||
aioairzone==1.0.0
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==3.2.10
|
||||
aioamazondevices==3.5.0
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@ -247,7 +247,7 @@ aioelectricitymaps==0.4.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==35.0.0
|
||||
aioesphomeapi==36.0.1
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@ -634,7 +634,7 @@ blinkpy==0.23.0
|
||||
blockchain==1.4.4
|
||||
|
||||
# homeassistant.components.blue_current
|
||||
bluecurrent-api==1.2.3
|
||||
bluecurrent-api==1.2.4
|
||||
|
||||
# homeassistant.components.bluemaestro
|
||||
bluemaestro-ble==0.4.1
|
||||
@ -1168,7 +1168,7 @@ hole==0.9.0
|
||||
holidays==0.76
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250702.2
|
||||
home-assistant-frontend==20250702.3
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.6.23
|
||||
@ -1234,7 +1234,7 @@ ihcsdk==2.8.5
|
||||
imeon_inverter_api==0.3.12
|
||||
|
||||
# homeassistant.components.imgw_pib
|
||||
imgw_pib==1.4.1
|
||||
imgw_pib==1.4.2
|
||||
|
||||
# homeassistant.components.incomfort
|
||||
incomfort-client==0.6.9
|
||||
@ -2346,7 +2346,7 @@ pysma==0.7.5
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smarla
|
||||
pysmarlaapi==0.9.0
|
||||
pysmarlaapi==0.9.1
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.2.8
|
||||
|
14
requirements_test_all.txt
generated
14
requirements_test_all.txt
generated
@ -81,7 +81,7 @@ PyQRCode==1.2.1
|
||||
PyRMVtransport==0.3.3
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.68.1
|
||||
PySwitchbot==0.68.2
|
||||
|
||||
# homeassistant.components.syncthru
|
||||
PySyncThru==0.8.0
|
||||
@ -173,7 +173,7 @@ aioairzone-cloud==0.6.15
|
||||
aioairzone==1.0.0
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==3.2.10
|
||||
aioamazondevices==3.5.0
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@ -235,7 +235,7 @@ aioelectricitymaps==0.4.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==35.0.0
|
||||
aioesphomeapi==36.0.1
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@ -565,7 +565,7 @@ blebox-uniapi==2.5.0
|
||||
blinkpy==0.23.0
|
||||
|
||||
# homeassistant.components.blue_current
|
||||
bluecurrent-api==1.2.3
|
||||
bluecurrent-api==1.2.4
|
||||
|
||||
# homeassistant.components.bluemaestro
|
||||
bluemaestro-ble==0.4.1
|
||||
@ -1017,7 +1017,7 @@ hole==0.9.0
|
||||
holidays==0.76
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250702.2
|
||||
home-assistant-frontend==20250702.3
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.6.23
|
||||
@ -1068,7 +1068,7 @@ igloohome-api==0.1.1
|
||||
imeon_inverter_api==0.3.12
|
||||
|
||||
# homeassistant.components.imgw_pib
|
||||
imgw_pib==1.4.1
|
||||
imgw_pib==1.4.2
|
||||
|
||||
# homeassistant.components.incomfort
|
||||
incomfort-client==0.6.9
|
||||
@ -1949,7 +1949,7 @@ pysma==0.7.5
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smarla
|
||||
pysmarlaapi==0.9.0
|
||||
pysmarlaapi==0.9.1
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.2.8
|
||||
|
@ -38,6 +38,9 @@ FIELD_SCHEMA = vol.Schema(
|
||||
TRIGGER_SCHEMA = vol.Any(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional("target"): vol.Any(
|
||||
selector.TargetSelector.CONFIG_SCHEMA, None
|
||||
),
|
||||
vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}),
|
||||
}
|
||||
),
|
||||
|
@ -4,6 +4,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
@ -20,13 +21,15 @@ DOWNLOAD_DIR = Path("build/translations-download").absolute()
|
||||
def run_download_docker():
|
||||
"""Run the Docker image to download the translations."""
|
||||
print("Running Docker to download latest translations.")
|
||||
run = subprocess.run(
|
||||
result = subprocess.run(
|
||||
[
|
||||
"docker",
|
||||
"run",
|
||||
"-v",
|
||||
f"{DOWNLOAD_DIR}:/opt/dest/locale",
|
||||
"--rm",
|
||||
"--user",
|
||||
f"{os.getuid()}:{os.getgid()}",
|
||||
f"lokalise/lokalise-cli-2:{CLI_2_DOCKER_IMAGE}",
|
||||
# Lokalise command
|
||||
"lokalise2",
|
||||
@ -52,7 +55,7 @@ def run_download_docker():
|
||||
)
|
||||
print()
|
||||
|
||||
if run.returncode != 0:
|
||||
if result.returncode != 0:
|
||||
raise ExitApp("Failed to download translations")
|
||||
|
||||
|
||||
|
@ -203,6 +203,7 @@
|
||||
'light',
|
||||
]),
|
||||
'multiple': False,
|
||||
'reorder': False,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
@ -217,6 +218,7 @@
|
||||
'binary_sensor',
|
||||
]),
|
||||
'multiple': False,
|
||||
'reorder': False,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
@ -40,7 +40,6 @@ async def test_generic_alarm_control_panel_requires_code(
|
||||
object_id="myalarm_control_panel",
|
||||
key=1,
|
||||
name="my alarm_control_panel",
|
||||
unique_id="my_alarm_control_panel",
|
||||
supported_features=EspHomeACPFeatures.ARM_AWAY
|
||||
| EspHomeACPFeatures.ARM_CUSTOM_BYPASS
|
||||
| EspHomeACPFeatures.ARM_HOME
|
||||
@ -173,7 +172,6 @@ async def test_generic_alarm_control_panel_no_code(
|
||||
object_id="myalarm_control_panel",
|
||||
key=1,
|
||||
name="my alarm_control_panel",
|
||||
unique_id="my_alarm_control_panel",
|
||||
supported_features=EspHomeACPFeatures.ARM_AWAY
|
||||
| EspHomeACPFeatures.ARM_CUSTOM_BYPASS
|
||||
| EspHomeACPFeatures.ARM_HOME
|
||||
@ -219,7 +217,6 @@ async def test_generic_alarm_control_panel_missing_state(
|
||||
object_id="myalarm_control_panel",
|
||||
key=1,
|
||||
name="my alarm_control_panel",
|
||||
unique_id="my_alarm_control_panel",
|
||||
supported_features=EspHomeACPFeatures.ARM_AWAY
|
||||
| EspHomeACPFeatures.ARM_CUSTOM_BYPASS
|
||||
| EspHomeACPFeatures.ARM_HOME
|
||||
|
@ -953,7 +953,6 @@ async def test_tts_format_from_media_player(
|
||||
object_id="mymedia_player",
|
||||
key=1,
|
||||
name="my media_player",
|
||||
unique_id="my_media_player",
|
||||
supports_pause=True,
|
||||
supported_formats=[
|
||||
MediaPlayerSupportedFormat(
|
||||
@ -1020,7 +1019,6 @@ async def test_tts_minimal_format_from_media_player(
|
||||
object_id="mymedia_player",
|
||||
key=1,
|
||||
name="my media_player",
|
||||
unique_id="my_media_player",
|
||||
supports_pause=True,
|
||||
supported_formats=[
|
||||
MediaPlayerSupportedFormat(
|
||||
@ -1156,7 +1154,6 @@ async def test_announce_media_id(
|
||||
object_id="mymedia_player",
|
||||
key=1,
|
||||
name="my media_player",
|
||||
unique_id="my_media_player",
|
||||
supports_pause=True,
|
||||
supported_formats=[
|
||||
MediaPlayerSupportedFormat(
|
||||
@ -1437,7 +1434,6 @@ async def test_start_conversation_media_id(
|
||||
object_id="mymedia_player",
|
||||
key=1,
|
||||
name="my media_player",
|
||||
unique_id="my_media_player",
|
||||
supports_pause=True,
|
||||
supported_formats=[
|
||||
MediaPlayerSupportedFormat(
|
||||
|
@ -24,7 +24,6 @@ async def test_binary_sensor_generic_entity(
|
||||
object_id="mybinary_sensor",
|
||||
key=1,
|
||||
name="my binary_sensor",
|
||||
unique_id="my_binary_sensor",
|
||||
)
|
||||
]
|
||||
esphome_state, hass_state = binary_state
|
||||
@ -52,7 +51,6 @@ async def test_status_binary_sensor(
|
||||
object_id="mybinary_sensor",
|
||||
key=1,
|
||||
name="my binary_sensor",
|
||||
unique_id="my_binary_sensor",
|
||||
is_status_binary_sensor=True,
|
||||
)
|
||||
]
|
||||
@ -80,7 +78,6 @@ async def test_binary_sensor_missing_state(
|
||||
object_id="mybinary_sensor",
|
||||
key=1,
|
||||
name="my binary_sensor",
|
||||
unique_id="my_binary_sensor",
|
||||
)
|
||||
]
|
||||
states = [BinarySensorState(key=1, state=True, missing_state=True)]
|
||||
@ -107,7 +104,6 @@ async def test_binary_sensor_has_state_false(
|
||||
object_id="mybinary_sensor",
|
||||
key=1,
|
||||
name="my binary_sensor",
|
||||
unique_id="my_binary_sensor",
|
||||
)
|
||||
]
|
||||
states = []
|
||||
@ -152,14 +148,12 @@ async def test_binary_sensors_same_key_different_device_id(
|
||||
object_id="sensor",
|
||||
key=1,
|
||||
name="Motion",
|
||||
unique_id="motion_1",
|
||||
device_id=11111111,
|
||||
),
|
||||
BinarySensorInfo(
|
||||
object_id="sensor",
|
||||
key=1,
|
||||
name="Motion",
|
||||
unique_id="motion_2",
|
||||
device_id=22222222,
|
||||
),
|
||||
]
|
||||
@ -235,14 +229,12 @@ async def test_binary_sensor_main_and_sub_device_same_key(
|
||||
object_id="main_sensor",
|
||||
key=1,
|
||||
name="Main Sensor",
|
||||
unique_id="main_1",
|
||||
device_id=0, # Main device
|
||||
),
|
||||
BinarySensorInfo(
|
||||
object_id="sub_sensor",
|
||||
key=1,
|
||||
name="Sub Sensor",
|
||||
unique_id="sub_1",
|
||||
device_id=11111111,
|
||||
),
|
||||
]
|
||||
|
@ -18,7 +18,6 @@ async def test_button_generic_entity(
|
||||
object_id="mybutton",
|
||||
key=1,
|
||||
name="my button",
|
||||
unique_id="my_button",
|
||||
)
|
||||
]
|
||||
states = []
|
||||
|
@ -30,7 +30,6 @@ async def test_camera_single_image(
|
||||
object_id="mycamera",
|
||||
key=1,
|
||||
name="my camera",
|
||||
unique_id="my_camera",
|
||||
)
|
||||
]
|
||||
states = []
|
||||
@ -75,7 +74,6 @@ async def test_camera_single_image_unavailable_before_requested(
|
||||
object_id="mycamera",
|
||||
key=1,
|
||||
name="my camera",
|
||||
unique_id="my_camera",
|
||||
)
|
||||
]
|
||||
states = []
|
||||
@ -113,7 +111,6 @@ async def test_camera_single_image_unavailable_during_request(
|
||||
object_id="mycamera",
|
||||
key=1,
|
||||
name="my camera",
|
||||
unique_id="my_camera",
|
||||
)
|
||||
]
|
||||
states = []
|
||||
@ -155,7 +152,6 @@ async def test_camera_stream(
|
||||
object_id="mycamera",
|
||||
key=1,
|
||||
name="my camera",
|
||||
unique_id="my_camera",
|
||||
)
|
||||
]
|
||||
states = []
|
||||
@ -212,7 +208,6 @@ async def test_camera_stream_unavailable(
|
||||
object_id="mycamera",
|
||||
key=1,
|
||||
name="my camera",
|
||||
unique_id="my_camera",
|
||||
)
|
||||
]
|
||||
states = []
|
||||
@ -249,7 +244,6 @@ async def test_camera_stream_with_disconnection(
|
||||
object_id="mycamera",
|
||||
key=1,
|
||||
name="my camera",
|
||||
unique_id="my_camera",
|
||||
)
|
||||
]
|
||||
states = []
|
||||
|
@ -58,7 +58,6 @@ async def test_climate_entity(
|
||||
object_id="myclimate",
|
||||
key=1,
|
||||
name="my climate",
|
||||
unique_id="my_climate",
|
||||
supports_current_temperature=True,
|
||||
supports_action=True,
|
||||
visual_min_temperature=10.0,
|
||||
@ -110,7 +109,6 @@ async def test_climate_entity_with_step_and_two_point(
|
||||
object_id="myclimate",
|
||||
key=1,
|
||||
name="my climate",
|
||||
unique_id="my_climate",
|
||||
supports_current_temperature=True,
|
||||
supports_two_point_target_temperature=True,
|
||||
visual_target_temperature_step=2,
|
||||
@ -187,7 +185,6 @@ async def test_climate_entity_with_step_and_target_temp(
|
||||
object_id="myclimate",
|
||||
key=1,
|
||||
name="my climate",
|
||||
unique_id="my_climate",
|
||||
supports_current_temperature=True,
|
||||
visual_target_temperature_step=2,
|
||||
visual_current_temperature_step=2,
|
||||
@ -345,7 +342,6 @@ async def test_climate_entity_with_humidity(
|
||||
object_id="myclimate",
|
||||
key=1,
|
||||
name="my climate",
|
||||
unique_id="my_climate",
|
||||
supports_current_temperature=True,
|
||||
supports_two_point_target_temperature=True,
|
||||
supports_action=True,
|
||||
@ -409,7 +405,6 @@ async def test_climate_entity_with_inf_value(
|
||||
object_id="myclimate",
|
||||
key=1,
|
||||
name="my climate",
|
||||
unique_id="my_climate",
|
||||
supports_current_temperature=True,
|
||||
supports_two_point_target_temperature=True,
|
||||
supports_action=True,
|
||||
@ -465,7 +460,6 @@ async def test_climate_entity_attributes(
|
||||
object_id="myclimate",
|
||||
key=1,
|
||||
name="my climate",
|
||||
unique_id="my_climate",
|
||||
supports_current_temperature=True,
|
||||
visual_target_temperature_step=2,
|
||||
visual_current_temperature_step=2,
|
||||
@ -520,7 +514,6 @@ async def test_climate_entity_attribute_current_temperature_unsupported(
|
||||
object_id="myclimate",
|
||||
key=1,
|
||||
name="my climate",
|
||||
unique_id="my_climate",
|
||||
supports_current_temperature=False,
|
||||
)
|
||||
]
|
||||
|
@ -41,7 +41,6 @@ async def test_cover_entity(
|
||||
object_id="mycover",
|
||||
key=1,
|
||||
name="my cover",
|
||||
unique_id="my_cover",
|
||||
supports_position=True,
|
||||
supports_tilt=True,
|
||||
supports_stop=True,
|
||||
@ -169,7 +168,6 @@ async def test_cover_entity_without_position(
|
||||
object_id="mycover",
|
||||
key=1,
|
||||
name="my cover",
|
||||
unique_id="my_cover",
|
||||
supports_position=False,
|
||||
supports_tilt=False,
|
||||
supports_stop=False,
|
||||
|
@ -26,7 +26,6 @@ async def test_generic_date_entity(
|
||||
object_id="mydate",
|
||||
key=1,
|
||||
name="my date",
|
||||
unique_id="my_date",
|
||||
)
|
||||
]
|
||||
states = [DateState(key=1, year=2024, month=12, day=31)]
|
||||
@ -62,7 +61,6 @@ async def test_generic_date_missing_state(
|
||||
object_id="mydate",
|
||||
key=1,
|
||||
name="my date",
|
||||
unique_id="my_date",
|
||||
)
|
||||
]
|
||||
states = [DateState(key=1, missing_state=True)]
|
||||
|
@ -26,7 +26,6 @@ async def test_generic_datetime_entity(
|
||||
object_id="mydatetime",
|
||||
key=1,
|
||||
name="my datetime",
|
||||
unique_id="my_datetime",
|
||||
)
|
||||
]
|
||||
states = [DateTimeState(key=1, epoch_seconds=1713270896)]
|
||||
@ -65,7 +64,6 @@ async def test_generic_datetime_missing_state(
|
||||
object_id="mydatetime",
|
||||
key=1,
|
||||
name="my datetime",
|
||||
unique_id="my_datetime",
|
||||
)
|
||||
]
|
||||
states = [DateTimeState(key=1, missing_state=True)]
|
||||
|
@ -51,13 +51,11 @@ async def test_entities_removed(
|
||||
object_id="mybinary_sensor",
|
||||
key=1,
|
||||
name="my binary_sensor",
|
||||
unique_id="my_binary_sensor",
|
||||
),
|
||||
BinarySensorInfo(
|
||||
object_id="mybinary_sensor_to_be_removed",
|
||||
key=2,
|
||||
name="my binary_sensor to be removed",
|
||||
unique_id="mybinary_sensor_to_be_removed",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
@ -100,7 +98,6 @@ async def test_entities_removed(
|
||||
object_id="mybinary_sensor",
|
||||
key=1,
|
||||
name="my binary_sensor",
|
||||
unique_id="my_binary_sensor",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
@ -140,13 +137,11 @@ async def test_entities_removed_after_reload(
|
||||
object_id="mybinary_sensor",
|
||||
key=1,
|
||||
name="my binary_sensor",
|
||||
unique_id="my_binary_sensor",
|
||||
),
|
||||
BinarySensorInfo(
|
||||
object_id="mybinary_sensor_to_be_removed",
|
||||
key=2,
|
||||
name="my binary_sensor to be removed",
|
||||
unique_id="mybinary_sensor_to_be_removed",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
@ -214,7 +209,6 @@ async def test_entities_removed_after_reload(
|
||||
object_id="mybinary_sensor",
|
||||
key=1,
|
||||
name="my binary_sensor",
|
||||
unique_id="my_binary_sensor",
|
||||
),
|
||||
]
|
||||
mock_device.client.list_entities_services = AsyncMock(
|
||||
@ -267,7 +261,6 @@ async def test_entities_for_entire_platform_removed(
|
||||
object_id="mybinary_sensor_to_be_removed",
|
||||
key=1,
|
||||
name="my binary_sensor to be removed",
|
||||
unique_id="mybinary_sensor_to_be_removed",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
@ -325,7 +318,6 @@ async def test_entity_info_object_ids(
|
||||
object_id="object_id_is_used",
|
||||
key=1,
|
||||
name="my binary_sensor",
|
||||
unique_id="my_binary_sensor",
|
||||
)
|
||||
]
|
||||
states = []
|
||||
@ -350,13 +342,11 @@ async def test_deep_sleep_device(
|
||||
object_id="mybinary_sensor",
|
||||
key=1,
|
||||
name="my binary_sensor",
|
||||
unique_id="my_binary_sensor",
|
||||
),
|
||||
SensorInfo(
|
||||
object_id="my_sensor",
|
||||
key=3,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
@ -456,7 +446,6 @@ async def test_esphome_device_without_friendly_name(
|
||||
object_id="mybinary_sensor",
|
||||
key=1,
|
||||
name="my binary_sensor",
|
||||
unique_id="my_binary_sensor",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
@ -486,7 +475,6 @@ async def test_entity_without_name_device_with_friendly_name(
|
||||
object_id="mybinary_sensor",
|
||||
key=1,
|
||||
name="",
|
||||
unique_id="my_binary_sensor",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
@ -519,7 +507,6 @@ async def test_entity_id_preserved_on_upgrade(
|
||||
object_id="my",
|
||||
key=1,
|
||||
name="my",
|
||||
unique_id="binary_sensor_my",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
@ -560,7 +547,6 @@ async def test_entity_id_preserved_on_upgrade_old_format_entity_id(
|
||||
object_id="my",
|
||||
key=1,
|
||||
name="my",
|
||||
unique_id="binary_sensor_my",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
@ -601,7 +587,6 @@ async def test_entity_id_preserved_on_upgrade_when_in_storage(
|
||||
object_id="my",
|
||||
key=1,
|
||||
name="my",
|
||||
unique_id="binary_sensor_my",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
@ -660,7 +645,6 @@ async def test_deep_sleep_added_after_setup(
|
||||
object_id="test",
|
||||
key=1,
|
||||
name="test",
|
||||
unique_id="test",
|
||||
),
|
||||
],
|
||||
states=[
|
||||
@ -732,7 +716,6 @@ async def test_entity_assignment_to_sub_device(
|
||||
object_id="main_sensor",
|
||||
key=1,
|
||||
name="Main Sensor",
|
||||
unique_id="main_sensor",
|
||||
device_id=0,
|
||||
),
|
||||
# Entity for sub device 1
|
||||
@ -740,7 +723,6 @@ async def test_entity_assignment_to_sub_device(
|
||||
object_id="motion",
|
||||
key=2,
|
||||
name="Motion",
|
||||
unique_id="motion",
|
||||
device_id=11111111,
|
||||
),
|
||||
# Entity for sub device 2
|
||||
@ -748,7 +730,6 @@ async def test_entity_assignment_to_sub_device(
|
||||
object_id="door",
|
||||
key=3,
|
||||
name="Door",
|
||||
unique_id="door",
|
||||
device_id=22222222,
|
||||
),
|
||||
]
|
||||
@ -932,7 +913,6 @@ async def test_entity_switches_between_devices(
|
||||
object_id="sensor",
|
||||
key=1,
|
||||
name="Test Sensor",
|
||||
unique_id="sensor",
|
||||
# device_id omitted - entity belongs to main device
|
||||
),
|
||||
]
|
||||
@ -964,7 +944,6 @@ async def test_entity_switches_between_devices(
|
||||
object_id="sensor",
|
||||
key=1,
|
||||
name="Test Sensor",
|
||||
unique_id="sensor",
|
||||
device_id=11111111, # Now on sub device 1
|
||||
),
|
||||
]
|
||||
@ -993,7 +972,6 @@ async def test_entity_switches_between_devices(
|
||||
object_id="sensor",
|
||||
key=1,
|
||||
name="Test Sensor",
|
||||
unique_id="sensor",
|
||||
device_id=22222222, # Now on sub device 2
|
||||
),
|
||||
]
|
||||
@ -1020,7 +998,6 @@ async def test_entity_switches_between_devices(
|
||||
object_id="sensor",
|
||||
key=1,
|
||||
name="Test Sensor",
|
||||
unique_id="sensor",
|
||||
# device_id omitted - back to main device
|
||||
),
|
||||
]
|
||||
@ -1063,7 +1040,6 @@ async def test_entity_id_uses_sub_device_name(
|
||||
object_id="main_sensor",
|
||||
key=1,
|
||||
name="Main Sensor",
|
||||
unique_id="main_sensor",
|
||||
device_id=0,
|
||||
),
|
||||
# Entity for sub device 1
|
||||
@ -1071,7 +1047,6 @@ async def test_entity_id_uses_sub_device_name(
|
||||
object_id="motion",
|
||||
key=2,
|
||||
name="Motion",
|
||||
unique_id="motion",
|
||||
device_id=11111111,
|
||||
),
|
||||
# Entity for sub device 2
|
||||
@ -1079,7 +1054,6 @@ async def test_entity_id_uses_sub_device_name(
|
||||
object_id="door",
|
||||
key=3,
|
||||
name="Door",
|
||||
unique_id="door",
|
||||
device_id=22222222,
|
||||
),
|
||||
# Entity without name on sub device
|
||||
@ -1087,7 +1061,6 @@ async def test_entity_id_uses_sub_device_name(
|
||||
object_id="sensor_no_name",
|
||||
key=4,
|
||||
name="",
|
||||
unique_id="sensor_no_name",
|
||||
device_id=11111111,
|
||||
),
|
||||
]
|
||||
@ -1147,7 +1120,6 @@ async def test_entity_id_with_empty_sub_device_name(
|
||||
object_id="sensor",
|
||||
key=1,
|
||||
name="Sensor",
|
||||
unique_id="sensor",
|
||||
device_id=11111111,
|
||||
),
|
||||
]
|
||||
@ -1187,8 +1159,7 @@ async def test_unique_id_migration_when_entity_moves_between_devices(
|
||||
BinarySensorInfo(
|
||||
object_id="temperature",
|
||||
key=1,
|
||||
name="Temperature",
|
||||
unique_id="unused", # This field is not used by the integration
|
||||
name="Temperature", # This field is not used by the integration
|
||||
device_id=0, # Main device
|
||||
),
|
||||
]
|
||||
@ -1250,8 +1221,7 @@ async def test_unique_id_migration_when_entity_moves_between_devices(
|
||||
BinarySensorInfo(
|
||||
object_id="temperature", # Same object_id
|
||||
key=1, # Same key - this is what identifies the entity
|
||||
name="Temperature",
|
||||
unique_id="unused", # This field is not used
|
||||
name="Temperature", # This field is not used
|
||||
device_id=22222222, # Now on sub-device
|
||||
),
|
||||
]
|
||||
@ -1312,7 +1282,6 @@ async def test_unique_id_migration_sub_device_to_main_device(
|
||||
object_id="temperature",
|
||||
key=1,
|
||||
name="Temperature",
|
||||
unique_id="unused",
|
||||
device_id=22222222, # On sub-device
|
||||
),
|
||||
]
|
||||
@ -1347,7 +1316,6 @@ async def test_unique_id_migration_sub_device_to_main_device(
|
||||
object_id="temperature",
|
||||
key=1,
|
||||
name="Temperature",
|
||||
unique_id="unused",
|
||||
device_id=0, # Now on main device
|
||||
),
|
||||
]
|
||||
@ -1407,7 +1375,6 @@ async def test_unique_id_migration_between_sub_devices(
|
||||
object_id="temperature",
|
||||
key=1,
|
||||
name="Temperature",
|
||||
unique_id="unused",
|
||||
device_id=22222222, # On kitchen_controller
|
||||
),
|
||||
]
|
||||
@ -1442,7 +1409,6 @@ async def test_unique_id_migration_between_sub_devices(
|
||||
object_id="temperature",
|
||||
key=1,
|
||||
name="Temperature",
|
||||
unique_id="unused",
|
||||
device_id=33333333, # Now on bedroom_controller
|
||||
),
|
||||
]
|
||||
@ -1501,7 +1467,6 @@ async def test_entity_device_id_rename_in_yaml(
|
||||
object_id="sensor",
|
||||
key=1,
|
||||
name="Sensor",
|
||||
unique_id="unused",
|
||||
device_id=11111111,
|
||||
),
|
||||
]
|
||||
@ -1563,7 +1528,6 @@ async def test_entity_device_id_rename_in_yaml(
|
||||
object_id="sensor", # Same object_id
|
||||
key=1, # Same key
|
||||
name="Sensor",
|
||||
unique_id="unused",
|
||||
device_id=99999999, # New device_id after rename
|
||||
),
|
||||
]
|
||||
@ -1636,8 +1600,7 @@ async def test_entity_with_unicode_name(
|
||||
BinarySensorInfo(
|
||||
object_id=sanitized_object_id, # ESPHome sends the sanitized version
|
||||
key=1,
|
||||
name=unicode_name, # But also sends the original Unicode name
|
||||
unique_id="unicode_sensor",
|
||||
name=unicode_name, # But also sends the original Unicode name,
|
||||
)
|
||||
]
|
||||
states = [BinarySensorState(key=1, state=True)]
|
||||
@ -1677,8 +1640,7 @@ async def test_entity_without_name_uses_device_name_only(
|
||||
BinarySensorInfo(
|
||||
object_id="some_sanitized_id",
|
||||
key=1,
|
||||
name="", # Empty name
|
||||
unique_id="no_name_sensor",
|
||||
name="", # Empty name,
|
||||
)
|
||||
]
|
||||
states = [BinarySensorState(key=1, state=True)]
|
||||
|
@ -15,49 +15,6 @@ from homeassistant.helpers import entity_registry as er
|
||||
from .conftest import MockGenericDeviceEntryType
|
||||
|
||||
|
||||
async def test_migrate_entity_unique_id(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_client: APIClient,
|
||||
mock_generic_device_entry: MockGenericDeviceEntryType,
|
||||
) -> None:
|
||||
"""Test a generic sensor entity unique id migration."""
|
||||
entity_registry.async_get_or_create(
|
||||
"sensor",
|
||||
"esphome",
|
||||
"my_sensor",
|
||||
suggested_object_id="old_sensor",
|
||||
disabled_by=None,
|
||||
)
|
||||
entity_info = [
|
||||
SensorInfo(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
entity_category=ESPHomeEntityCategory.DIAGNOSTIC,
|
||||
icon="mdi:leaf",
|
||||
)
|
||||
]
|
||||
states = [SensorState(key=1, state=50)]
|
||||
user_service = []
|
||||
await mock_generic_device_entry(
|
||||
mock_client=mock_client,
|
||||
entity_info=entity_info,
|
||||
user_service=user_service,
|
||||
states=states,
|
||||
)
|
||||
state = hass.states.get("sensor.old_sensor")
|
||||
assert state is not None
|
||||
assert state.state == "50"
|
||||
entry = entity_registry.async_get("sensor.old_sensor")
|
||||
assert entry is not None
|
||||
assert entity_registry.async_get_entity_id("sensor", "esphome", "my_sensor") is None
|
||||
# Note that ESPHome includes the EntityInfo type in the unique id
|
||||
# as this is not a 1:1 mapping to the entity platform (ie. text_sensor)
|
||||
assert entry.unique_id == "11:22:33:44:55:AA-sensor-mysensor"
|
||||
|
||||
|
||||
async def test_migrate_entity_unique_id_downgrade_upgrade(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
@ -84,7 +41,6 @@ async def test_migrate_entity_unique_id_downgrade_upgrade(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
entity_category=ESPHomeEntityCategory.DIAGNOSTIC,
|
||||
icon="mdi:leaf",
|
||||
)
|
||||
|
@ -20,7 +20,6 @@ async def test_generic_event_entity(
|
||||
object_id="myevent",
|
||||
key=1,
|
||||
name="my event",
|
||||
unique_id="my_event",
|
||||
event_types=["type1", "type2"],
|
||||
device_class=EventDeviceClass.BUTTON,
|
||||
)
|
||||
|
@ -44,7 +44,6 @@ async def test_fan_entity_with_all_features_old_api(
|
||||
object_id="myfan",
|
||||
key=1,
|
||||
name="my fan",
|
||||
unique_id="my_fan",
|
||||
supports_direction=True,
|
||||
supports_speed=True,
|
||||
supports_oscillation=True,
|
||||
@ -147,7 +146,6 @@ async def test_fan_entity_with_all_features_new_api(
|
||||
object_id="myfan",
|
||||
key=1,
|
||||
name="my fan",
|
||||
unique_id="my_fan",
|
||||
supported_speed_count=4,
|
||||
supports_direction=True,
|
||||
supports_speed=True,
|
||||
@ -317,7 +315,6 @@ async def test_fan_entity_with_no_features_new_api(
|
||||
object_id="myfan",
|
||||
key=1,
|
||||
name="my fan",
|
||||
unique_id="my_fan",
|
||||
supports_direction=False,
|
||||
supports_speed=False,
|
||||
supports_oscillation=False,
|
||||
|
@ -56,7 +56,6 @@ async def test_light_on_off(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153,
|
||||
max_mireds=400,
|
||||
supported_color_modes=[ESPColorMode.ON_OFF],
|
||||
@ -98,7 +97,6 @@ async def test_light_brightness(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153,
|
||||
max_mireds=400,
|
||||
supported_color_modes=[LightColorCapability.BRIGHTNESS],
|
||||
@ -226,7 +224,6 @@ async def test_light_legacy_brightness(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153,
|
||||
max_mireds=400,
|
||||
supported_color_modes=[LightColorCapability.BRIGHTNESS, 2],
|
||||
@ -282,7 +279,6 @@ async def test_light_brightness_on_off(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153,
|
||||
max_mireds=400,
|
||||
supported_color_modes=[ESPColorMode.ON_OFF, ESPColorMode.BRIGHTNESS],
|
||||
@ -358,7 +354,6 @@ async def test_light_legacy_white_converted_to_brightness(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153,
|
||||
max_mireds=400,
|
||||
supported_color_modes=[
|
||||
@ -423,7 +418,6 @@ async def test_light_legacy_white_with_rgb(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153,
|
||||
max_mireds=400,
|
||||
supported_color_modes=[color_mode, color_mode_2],
|
||||
@ -478,7 +472,6 @@ async def test_light_brightness_on_off_with_unknown_color_mode(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153,
|
||||
max_mireds=400,
|
||||
supported_color_modes=[
|
||||
@ -555,7 +548,6 @@ async def test_light_on_and_brightness(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153,
|
||||
max_mireds=400,
|
||||
supported_color_modes=[
|
||||
@ -607,7 +599,6 @@ async def test_rgb_color_temp_light(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153,
|
||||
max_mireds=400,
|
||||
supported_color_modes=color_modes,
|
||||
@ -698,7 +689,6 @@ async def test_light_rgb(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
supported_color_modes=[
|
||||
LightColorCapability.RGB
|
||||
| LightColorCapability.ON_OFF
|
||||
@ -821,7 +811,6 @@ async def test_light_rgbw(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
supported_color_modes=[
|
||||
LightColorCapability.RGB
|
||||
| LightColorCapability.WHITE
|
||||
@ -991,7 +980,6 @@ async def test_light_rgbww_with_cold_warm_white_support(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153,
|
||||
max_mireds=400,
|
||||
supported_color_modes=[
|
||||
@ -1200,7 +1188,6 @@ async def test_light_rgbww_without_cold_warm_white_support(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153,
|
||||
max_mireds=400,
|
||||
supported_color_modes=[
|
||||
@ -1439,7 +1426,6 @@ async def test_light_color_temp(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153.846161,
|
||||
max_mireds=370.370361,
|
||||
supported_color_modes=[
|
||||
@ -1514,7 +1500,6 @@ async def test_light_color_temp_no_mireds_set(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=0,
|
||||
max_mireds=0,
|
||||
supported_color_modes=[
|
||||
@ -1610,7 +1595,6 @@ async def test_light_color_temp_legacy(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153.846161,
|
||||
max_mireds=370.370361,
|
||||
supported_color_modes=[
|
||||
@ -1695,7 +1679,6 @@ async def test_light_rgb_legacy(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153.846161,
|
||||
max_mireds=370.370361,
|
||||
supported_color_modes=[
|
||||
@ -1795,7 +1778,6 @@ async def test_light_effects(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153,
|
||||
max_mireds=400,
|
||||
effects=["effect1", "effect2"],
|
||||
@ -1859,7 +1841,6 @@ async def test_only_cold_warm_white_support(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153,
|
||||
max_mireds=400,
|
||||
supported_color_modes=[color_modes],
|
||||
@ -1955,7 +1936,6 @@ async def test_light_no_color_modes(
|
||||
object_id="mylight",
|
||||
key=1,
|
||||
name="my light",
|
||||
unique_id="my_light",
|
||||
min_mireds=153,
|
||||
max_mireds=400,
|
||||
supported_color_modes=[color_mode],
|
||||
|
@ -34,7 +34,6 @@ async def test_lock_entity_no_open(
|
||||
object_id="mylock",
|
||||
key=1,
|
||||
name="my lock",
|
||||
unique_id="my_lock",
|
||||
supports_open=False,
|
||||
requires_code=False,
|
||||
)
|
||||
@ -72,7 +71,6 @@ async def test_lock_entity_start_locked(
|
||||
object_id="mylock",
|
||||
key=1,
|
||||
name="my lock",
|
||||
unique_id="my_lock",
|
||||
)
|
||||
]
|
||||
states = [LockEntityState(key=1, state=ESPHomeLockState.LOCKED)]
|
||||
@ -99,7 +97,6 @@ async def test_lock_entity_supports_open(
|
||||
object_id="mylock",
|
||||
key=1,
|
||||
name="my lock",
|
||||
unique_id="my_lock",
|
||||
supports_open=True,
|
||||
requires_code=True,
|
||||
)
|
||||
|
@ -55,7 +55,6 @@ async def test_media_player_entity(
|
||||
object_id="mymedia_player",
|
||||
key=1,
|
||||
name="my media_player",
|
||||
unique_id="my_media_player",
|
||||
supports_pause=True,
|
||||
)
|
||||
]
|
||||
@ -202,7 +201,6 @@ async def test_media_player_entity_with_source(
|
||||
object_id="mymedia_player",
|
||||
key=1,
|
||||
name="my media_player",
|
||||
unique_id="my_media_player",
|
||||
supports_pause=True,
|
||||
)
|
||||
]
|
||||
@ -318,7 +316,6 @@ async def test_media_player_proxy(
|
||||
object_id="mymedia_player",
|
||||
key=1,
|
||||
name="my media_player",
|
||||
unique_id="my_media_player",
|
||||
supports_pause=True,
|
||||
supported_formats=[
|
||||
MediaPlayerSupportedFormat(
|
||||
@ -477,7 +474,6 @@ async def test_media_player_formats_reload_preserves_data(
|
||||
object_id="test_media_player",
|
||||
key=1,
|
||||
name="Test Media Player",
|
||||
unique_id="test_unique_id",
|
||||
supports_pause=True,
|
||||
supported_formats=supported_formats,
|
||||
)
|
||||
|
@ -35,7 +35,6 @@ async def test_generic_number_entity(
|
||||
object_id="mynumber",
|
||||
key=1,
|
||||
name="my number",
|
||||
unique_id="my_number",
|
||||
max_value=100,
|
||||
min_value=0,
|
||||
step=1,
|
||||
@ -75,7 +74,6 @@ async def test_generic_number_nan(
|
||||
object_id="mynumber",
|
||||
key=1,
|
||||
name="my number",
|
||||
unique_id="my_number",
|
||||
max_value=100,
|
||||
min_value=0,
|
||||
step=1,
|
||||
@ -107,7 +105,6 @@ async def test_generic_number_with_unit_of_measurement_as_empty_string(
|
||||
object_id="mynumber",
|
||||
key=1,
|
||||
name="my number",
|
||||
unique_id="my_number",
|
||||
max_value=100,
|
||||
min_value=0,
|
||||
step=1,
|
||||
@ -140,7 +137,6 @@ async def test_generic_number_entity_set_when_disconnected(
|
||||
object_id="mynumber",
|
||||
key=1,
|
||||
name="my number",
|
||||
unique_id="my_number",
|
||||
max_value=100,
|
||||
min_value=0,
|
||||
step=1,
|
||||
|
@ -133,7 +133,6 @@ async def test_device_conflict_migration(
|
||||
object_id="mybinary_sensor",
|
||||
key=1,
|
||||
name="my binary_sensor",
|
||||
unique_id="my_binary_sensor",
|
||||
is_status_binary_sensor=True,
|
||||
)
|
||||
]
|
||||
|
@ -67,7 +67,6 @@ async def test_select_generic_entity(
|
||||
object_id="myselect",
|
||||
key=1,
|
||||
name="my select",
|
||||
unique_id="my_select",
|
||||
options=["a", "b"],
|
||||
)
|
||||
]
|
||||
|
@ -54,7 +54,6 @@ async def test_generic_numeric_sensor(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
)
|
||||
]
|
||||
states = [SensorState(key=1, state=50)]
|
||||
@ -110,7 +109,6 @@ async def test_generic_numeric_sensor_with_entity_category_and_icon(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
entity_category=ESPHomeEntityCategory.DIAGNOSTIC,
|
||||
icon="mdi:leaf",
|
||||
)
|
||||
@ -147,7 +145,6 @@ async def test_generic_numeric_sensor_state_class_measurement(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
state_class=ESPHomeSensorStateClass.MEASUREMENT,
|
||||
device_class="power",
|
||||
unit_of_measurement="W",
|
||||
@ -184,7 +181,6 @@ async def test_generic_numeric_sensor_device_class_timestamp(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
device_class="timestamp",
|
||||
)
|
||||
]
|
||||
@ -212,7 +208,6 @@ async def test_generic_numeric_sensor_legacy_last_reset_convert(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
legacy_last_reset_type=LastResetType.AUTO,
|
||||
state_class=ESPHomeSensorStateClass.MEASUREMENT,
|
||||
)
|
||||
@ -242,7 +237,6 @@ async def test_generic_numeric_sensor_no_state(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
)
|
||||
]
|
||||
states = []
|
||||
@ -269,7 +263,6 @@ async def test_generic_numeric_sensor_nan_state(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
)
|
||||
]
|
||||
states = [SensorState(key=1, state=math.nan, missing_state=False)]
|
||||
@ -296,7 +289,6 @@ async def test_generic_numeric_sensor_missing_state(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
)
|
||||
]
|
||||
states = [SensorState(key=1, state=True, missing_state=True)]
|
||||
@ -323,7 +315,6 @@ async def test_generic_text_sensor(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
)
|
||||
]
|
||||
states = [TextSensorState(key=1, state="i am a teapot")]
|
||||
@ -350,7 +341,6 @@ async def test_generic_text_sensor_missing_state(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
)
|
||||
]
|
||||
states = [TextSensorState(key=1, state=True, missing_state=True)]
|
||||
@ -377,7 +367,6 @@ async def test_generic_text_sensor_device_class_timestamp(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
)
|
||||
]
|
||||
@ -406,7 +395,6 @@ async def test_generic_text_sensor_device_class_date(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
)
|
||||
]
|
||||
@ -435,7 +423,6 @@ async def test_generic_numeric_sensor_empty_string_uom(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
unit_of_measurement="",
|
||||
)
|
||||
]
|
||||
@ -493,7 +480,6 @@ async def test_suggested_display_precision_by_device_class(
|
||||
object_id="mysensor",
|
||||
key=1,
|
||||
name="my sensor",
|
||||
unique_id="my_sensor",
|
||||
accuracy_decimals=expected_precision,
|
||||
device_class=device_class.value,
|
||||
unit_of_measurement=unit_of_measurement,
|
||||
|
@ -26,7 +26,6 @@ async def test_switch_generic_entity(
|
||||
object_id="myswitch",
|
||||
key=1,
|
||||
name="my switch",
|
||||
unique_id="my_switch",
|
||||
)
|
||||
]
|
||||
states = [SwitchState(key=1, state=True)]
|
||||
@ -78,14 +77,12 @@ async def test_switch_sub_device_non_zero_device_id(
|
||||
object_id="main_switch",
|
||||
key=1,
|
||||
name="Main Switch",
|
||||
unique_id="main_switch_1",
|
||||
device_id=0, # Main device
|
||||
),
|
||||
SwitchInfo(
|
||||
object_id="sub_switch",
|
||||
key=2,
|
||||
name="Sub Switch",
|
||||
unique_id="sub_switch_1",
|
||||
device_id=11111111, # Sub-device
|
||||
),
|
||||
]
|
||||
|
@ -26,7 +26,6 @@ async def test_generic_text_entity(
|
||||
object_id="mytext",
|
||||
key=1,
|
||||
name="my text",
|
||||
unique_id="my_text",
|
||||
max_length=100,
|
||||
min_length=0,
|
||||
pattern=None,
|
||||
@ -66,7 +65,6 @@ async def test_generic_text_entity_no_state(
|
||||
object_id="mytext",
|
||||
key=1,
|
||||
name="my text",
|
||||
unique_id="my_text",
|
||||
max_length=100,
|
||||
min_length=0,
|
||||
pattern=None,
|
||||
@ -97,7 +95,6 @@ async def test_generic_text_entity_missing_state(
|
||||
object_id="mytext",
|
||||
key=1,
|
||||
name="my text",
|
||||
unique_id="my_text",
|
||||
max_length=100,
|
||||
min_length=0,
|
||||
pattern=None,
|
||||
|
@ -26,7 +26,6 @@ async def test_generic_time_entity(
|
||||
object_id="mytime",
|
||||
key=1,
|
||||
name="my time",
|
||||
unique_id="my_time",
|
||||
)
|
||||
]
|
||||
states = [TimeState(key=1, hour=12, minute=34, second=56)]
|
||||
@ -62,7 +61,6 @@ async def test_generic_time_missing_state(
|
||||
object_id="mytime",
|
||||
key=1,
|
||||
name="my time",
|
||||
unique_id="my_time",
|
||||
)
|
||||
]
|
||||
states = [TimeState(key=1, missing_state=True)]
|
||||
|
@ -436,7 +436,6 @@ async def test_generic_device_update_entity(
|
||||
object_id="myupdate",
|
||||
key=1,
|
||||
name="my update",
|
||||
unique_id="my_update",
|
||||
)
|
||||
]
|
||||
states = [
|
||||
@ -470,7 +469,6 @@ async def test_generic_device_update_entity_has_update(
|
||||
object_id="myupdate",
|
||||
key=1,
|
||||
name="my update",
|
||||
unique_id="my_update",
|
||||
)
|
||||
]
|
||||
states = [
|
||||
@ -561,7 +559,6 @@ async def test_update_entity_release_notes(
|
||||
object_id="myupdate",
|
||||
key=1,
|
||||
name="my update",
|
||||
unique_id="my_update",
|
||||
)
|
||||
]
|
||||
|
||||
|
@ -36,7 +36,6 @@ async def test_valve_entity(
|
||||
object_id="myvalve",
|
||||
key=1,
|
||||
name="my valve",
|
||||
unique_id="my_valve",
|
||||
supports_position=True,
|
||||
supports_stop=True,
|
||||
)
|
||||
@ -134,7 +133,6 @@ async def test_valve_entity_without_position(
|
||||
object_id="myvalve",
|
||||
key=1,
|
||||
name="my valve",
|
||||
unique_id="my_valve",
|
||||
supports_position=False,
|
||||
supports_stop=False,
|
||||
)
|
||||
|
@ -10,7 +10,7 @@ import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
@ -39,7 +39,7 @@ async def test_sensor_unknown_states(
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.test_mower_1_mode")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_cutting_blade_usage_time_sensor(
|
||||
@ -78,7 +78,7 @@ async def test_next_start_sensor(
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.test_mower_1_next_start")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_work_area_sensor(
|
||||
|
@ -924,6 +924,30 @@ async def test_invalid_unit_of_measurement(
|
||||
"device_class": None,
|
||||
"unit_of_measurement": None,
|
||||
},
|
||||
{
|
||||
"name": "Test 4",
|
||||
"state_topic": "test-topic",
|
||||
"device_class": "ph",
|
||||
"unit_of_measurement": "",
|
||||
},
|
||||
{
|
||||
"name": "Test 5",
|
||||
"state_topic": "test-topic",
|
||||
"device_class": "ph",
|
||||
"unit_of_measurement": " ",
|
||||
},
|
||||
{
|
||||
"name": "Test 6",
|
||||
"state_topic": "test-topic",
|
||||
"device_class": None,
|
||||
"unit_of_measurement": "",
|
||||
},
|
||||
{
|
||||
"name": "Test 7",
|
||||
"state_topic": "test-topic",
|
||||
"device_class": None,
|
||||
"unit_of_measurement": " ",
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -936,10 +960,25 @@ async def test_valid_device_class_and_uom(
|
||||
await mqtt_mock_entry()
|
||||
|
||||
state = hass.states.get("sensor.test_1")
|
||||
assert state is not None
|
||||
assert state.attributes["device_class"] == "temperature"
|
||||
state = hass.states.get("sensor.test_2")
|
||||
assert state is not None
|
||||
assert "device_class" not in state.attributes
|
||||
state = hass.states.get("sensor.test_3")
|
||||
assert state is not None
|
||||
assert "device_class" not in state.attributes
|
||||
state = hass.states.get("sensor.test_4")
|
||||
assert state is not None
|
||||
assert state.attributes["device_class"] == "ph"
|
||||
state = hass.states.get("sensor.test_5")
|
||||
assert state is not None
|
||||
assert state.attributes["device_class"] == "ph"
|
||||
state = hass.states.get("sensor.test_6")
|
||||
assert state is not None
|
||||
assert "device_class" not in state.attributes
|
||||
state = hass.states.get("sensor.test_7")
|
||||
assert state is not None
|
||||
assert "device_class" not in state.attributes
|
||||
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
"""Test AI Task platform of Ollama integration."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import ollama
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import ai_task
|
||||
from homeassistant.components import ai_task, media_source
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er, selector
|
||||
@ -243,3 +245,115 @@ async def test_generate_invalid_structured_data(
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_init_component")
|
||||
async def test_generate_data_with_attachment(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test AI Task data generation with image attachments."""
|
||||
entity_id = "ai_task.ollama_ai_task"
|
||||
|
||||
# Mock the Ollama chat response as an async iterator
|
||||
async def mock_chat_response():
|
||||
"""Mock streaming response."""
|
||||
yield {
|
||||
"message": {"role": "assistant", "content": "Generated test data"},
|
||||
"done": True,
|
||||
"done_reason": "stop",
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.media_source.async_resolve_media",
|
||||
side_effect=[
|
||||
media_source.PlayMedia(
|
||||
url="http://example.com/doorbell_snapshot.jpg",
|
||||
mime_type="image/jpeg",
|
||||
path=Path("doorbell_snapshot.jpg"),
|
||||
),
|
||||
],
|
||||
),
|
||||
patch(
|
||||
"ollama.AsyncClient.chat",
|
||||
return_value=mock_chat_response(),
|
||||
) as mock_chat,
|
||||
):
|
||||
result = await ai_task.async_generate_data(
|
||||
hass,
|
||||
task_name="Test Task",
|
||||
entity_id=entity_id,
|
||||
instructions="Generate test data",
|
||||
attachments=[
|
||||
{"media_content_id": "media-source://media/doorbell_snapshot.jpg"},
|
||||
],
|
||||
)
|
||||
|
||||
assert result.data == "Generated test data"
|
||||
|
||||
assert mock_chat.call_count == 1
|
||||
messages = mock_chat.call_args[1]["messages"]
|
||||
assert len(messages) == 2
|
||||
chat_message = messages[1]
|
||||
assert chat_message.role == "user"
|
||||
assert chat_message.content == "Generate test data"
|
||||
assert chat_message.images == [
|
||||
ollama.Image(value=Path("doorbell_snapshot.jpg")),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_init_component")
|
||||
async def test_generate_data_with_unsupported_file_format(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test AI Task data generation with image attachments."""
|
||||
entity_id = "ai_task.ollama_ai_task"
|
||||
|
||||
# Mock the Ollama chat response as an async iterator
|
||||
async def mock_chat_response():
|
||||
"""Mock streaming response."""
|
||||
yield {
|
||||
"message": {"role": "assistant", "content": "Generated test data"},
|
||||
"done": True,
|
||||
"done_reason": "stop",
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.media_source.async_resolve_media",
|
||||
side_effect=[
|
||||
media_source.PlayMedia(
|
||||
url="http://example.com/doorbell_snapshot.jpg",
|
||||
mime_type="image/jpeg",
|
||||
path=Path("doorbell_snapshot.jpg"),
|
||||
),
|
||||
media_source.PlayMedia(
|
||||
url="http://example.com/context.txt",
|
||||
mime_type="text/plain",
|
||||
path=Path("context.txt"),
|
||||
),
|
||||
],
|
||||
),
|
||||
patch(
|
||||
"ollama.AsyncClient.chat",
|
||||
return_value=mock_chat_response(),
|
||||
),
|
||||
pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Ollama only supports image attachments in user content",
|
||||
),
|
||||
):
|
||||
await ai_task.async_generate_data(
|
||||
hass,
|
||||
task_name="Test Task",
|
||||
entity_id=entity_id,
|
||||
instructions="Generate test data",
|
||||
attachments=[
|
||||
{"media_content_id": "media-source://media/doorbell_snapshot.jpg"},
|
||||
{"media_content_id": "media-source://media/context.txt"},
|
||||
],
|
||||
)
|
||||
|
@ -231,6 +231,11 @@ def test_device_selector_schema_error(schema) -> None:
|
||||
["sensor.abc123", "sensor.ghi789"],
|
||||
),
|
||||
),
|
||||
(
|
||||
{"multiple": True, "reorder": True},
|
||||
((["sensor.abc123", "sensor.def456"],)),
|
||||
(None, "abc123", ["sensor.abc123", None]),
|
||||
),
|
||||
(
|
||||
{"filter": {"domain": "light"}},
|
||||
("light.abc123", FAKE_UUID),
|
||||
|
@ -1091,6 +1091,7 @@ async def test_async_get_all_descriptions_filter(hass: HomeAssistant) -> None:
|
||||
}
|
||||
],
|
||||
"multiple": False,
|
||||
"reorder": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -1113,6 +1114,7 @@ async def test_async_get_all_descriptions_filter(hass: HomeAssistant) -> None:
|
||||
}
|
||||
],
|
||||
"multiple": False,
|
||||
"reorder": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -4,7 +4,10 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.components.sensor.helpers import async_parse_date_datetime
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ICON,
|
||||
CONF_NAME,
|
||||
CONF_STATE,
|
||||
@ -20,6 +23,7 @@ from homeassistant.helpers.trigger_template_entity import (
|
||||
CONF_AVAILABILITY,
|
||||
CONF_PICTURE,
|
||||
ManualTriggerEntity,
|
||||
ManualTriggerSensorEntity,
|
||||
ValueTemplate,
|
||||
)
|
||||
|
||||
@ -288,3 +292,38 @@ async def test_trigger_template_complex(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entity.some_other_key == {"test_key": "test_data"}
|
||||
|
||||
|
||||
async def test_manual_trigger_sensor_entity_with_date(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test manual trigger template entity when availability template isn't used."""
|
||||
config = {
|
||||
CONF_NAME: template.Template("test_entity", hass),
|
||||
CONF_STATE: template.Template("{{ as_datetime(value) }}", hass),
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP,
|
||||
}
|
||||
|
||||
class TestEntity(ManualTriggerSensorEntity):
|
||||
"""Test entity class."""
|
||||
|
||||
extra_template_keys = (CONF_STATE,)
|
||||
|
||||
@property
|
||||
def state(self) -> bool | None:
|
||||
"""Return extra attributes."""
|
||||
return "2025-01-01T00:00:00+00:00"
|
||||
|
||||
entity = TestEntity(hass, config)
|
||||
entity.entity_id = "test.entity"
|
||||
variables = entity._template_variables_with_value("2025-01-01T00:00:00+00:00")
|
||||
assert entity._render_availability_template(variables) is True
|
||||
assert entity.available is True
|
||||
entity._set_native_value_with_possible_timestamp(entity.state)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entity.native_value == async_parse_date_datetime(
|
||||
"2025-01-01T00:00:00+00:00", entity.entity_id, entity.device_class
|
||||
)
|
||||
assert entity.state == "2025-01-01T00:00:00+00:00"
|
||||
assert entity.device_class == SensorDeviceClass.TIMESTAMP
|
||||
|
Loading…
x
Reference in New Issue
Block a user