Skip to content

Commit 20cdd93

Browse files
authored
Modernize template sensor (home-assistant#157251)
1 parent 1be2e4f commit 20cdd93

File tree

2 files changed

+66
-90
lines changed

2 files changed

+66
-90
lines changed

homeassistant/components/template/sensor.py

Lines changed: 65 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from datetime import date, datetime
5+
from datetime import datetime
66
import logging
77
from typing import Any
88

@@ -18,7 +18,6 @@
1818
STATE_CLASSES_SCHEMA,
1919
RestoreSensor,
2020
SensorDeviceClass,
21-
SensorEntity,
2221
SensorStateClass,
2322
)
2423
from homeassistant.components.sensor.helpers import ( # pylint: disable=hass-component-root-import
@@ -35,8 +34,6 @@
3534
CONF_NAME,
3635
CONF_SENSORS,
3736
CONF_STATE,
38-
CONF_TRIGGER,
39-
CONF_TRIGGERS,
4037
CONF_UNIQUE_ID,
4138
CONF_UNIT_OF_MEASUREMENT,
4239
CONF_VALUE_TEMPLATE,
@@ -54,6 +51,7 @@
5451
from homeassistant.util import dt as dt_util
5552

5653
from . import TriggerUpdateCoordinator
54+
from .entity import AbstractTemplateEntity
5755
from .helpers import (
5856
async_setup_template_entry,
5957
async_setup_template_platform,
@@ -136,33 +134,8 @@ def validate_last_reset(val):
136134
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY.schema),
137135
)
138136

139-
140-
def extra_validation_checks(val):
141-
"""Run extra validation checks."""
142-
if CONF_TRIGGERS in val or CONF_TRIGGER in val:
143-
raise vol.Invalid(
144-
"You can only add triggers to template entities if they are defined under"
145-
" `template:`. See the template documentation for more information:"
146-
" https://www.home-assistant.io/integrations/template/"
147-
)
148-
149-
if CONF_SENSORS not in val and SENSOR_DOMAIN not in val:
150-
raise vol.Invalid(f"Required key {SENSOR_DOMAIN} not defined")
151-
152-
return val
153-
154-
155-
PLATFORM_SCHEMA = vol.All(
156-
SENSOR_PLATFORM_SCHEMA.extend(
157-
{
158-
vol.Optional(CONF_TRIGGER): cv.match_all, # to raise custom warning
159-
vol.Optional(CONF_TRIGGERS): cv.match_all, # to raise custom warning
160-
vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(
161-
SENSOR_LEGACY_YAML_SCHEMA
162-
),
163-
}
164-
),
165-
extra_validation_checks,
137+
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
138+
{vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_LEGACY_YAML_SCHEMA)}
166139
)
167140

168141
_LOGGER = logging.getLogger(__name__)
@@ -213,7 +186,53 @@ def async_create_preview_sensor(
213186
)
214187

215188

216-
class StateSensorEntity(TemplateEntity, SensorEntity):
189+
class AbstractTemplateSensor(AbstractTemplateEntity, RestoreSensor):
190+
"""Representation of a template sensor features."""
191+
192+
_entity_id_format = ENTITY_ID_FORMAT
193+
194+
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
195+
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
196+
def __init__(self, config: ConfigType) -> None: # pylint: disable=super-init-not-called
197+
"""Initialize the features."""
198+
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
199+
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
200+
self._attr_state_class = config.get(CONF_STATE_CLASS)
201+
self._template: template.Template = config[CONF_STATE]
202+
self._attr_last_reset_template: template.Template | None = config.get(
203+
ATTR_LAST_RESET
204+
)
205+
206+
@callback
207+
def _update_last_reset(self, result: Any) -> None:
208+
if isinstance(result, datetime):
209+
self._attr_last_reset = result
210+
return
211+
212+
parsed_timestamp = dt_util.parse_datetime(result)
213+
if parsed_timestamp is None:
214+
_LOGGER.warning(
215+
"%s rendered invalid timestamp for last_reset attribute: %s",
216+
self.entity_id,
217+
result,
218+
)
219+
else:
220+
self._attr_last_reset = parsed_timestamp
221+
222+
def _handle_state(self, result: Any) -> None:
223+
if result is None or self.device_class not in (
224+
SensorDeviceClass.DATE,
225+
SensorDeviceClass.TIMESTAMP,
226+
):
227+
self._attr_native_value = result
228+
return
229+
230+
self._attr_native_value = async_parse_date_datetime(
231+
result, self.entity_id, self.device_class
232+
)
233+
234+
235+
class StateSensorEntity(TemplateEntity, AbstractTemplateSensor):
217236
"""Representation of a Template Sensor."""
218237

219238
_attr_should_poll = False
@@ -226,14 +245,8 @@ def __init__(
226245
unique_id: str | None,
227246
) -> None:
228247
"""Initialize the sensor."""
229-
super().__init__(hass, config, unique_id)
230-
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
231-
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
232-
self._attr_state_class = config.get(CONF_STATE_CLASS)
233-
self._template: template.Template = config[CONF_STATE]
234-
self._attr_last_reset_template: template.Template | None = config.get(
235-
ATTR_LAST_RESET
236-
)
248+
TemplateEntity.__init__(self, hass, config, unique_id)
249+
AbstractTemplateSensor.__init__(self, config)
237250

238251
@callback
239252
def _async_setup_templates(self) -> None:
@@ -251,35 +264,20 @@ def _async_setup_templates(self) -> None:
251264

252265
super()._async_setup_templates()
253266

254-
@callback
255-
def _update_last_reset(self, result):
256-
self._attr_last_reset = result
257-
258267
@callback
259268
def _update_state(self, result):
260269
super()._update_state(result)
261270
if isinstance(result, TemplateError):
262271
self._attr_native_value = None
263272
return
264273

265-
if result is None or self.device_class not in (
266-
SensorDeviceClass.DATE,
267-
SensorDeviceClass.TIMESTAMP,
268-
):
269-
self._attr_native_value = result
270-
return
271-
272-
self._attr_native_value = async_parse_date_datetime(
273-
result, self.entity_id, self.device_class
274-
)
274+
self._handle_state(result)
275275

276276

277-
class TriggerSensorEntity(TriggerEntity, RestoreSensor):
277+
class TriggerSensorEntity(TriggerEntity, AbstractTemplateSensor):
278278
"""Sensor entity based on trigger data."""
279279

280-
_entity_id_format = ENTITY_ID_FORMAT
281280
domain = SENSOR_DOMAIN
282-
extra_template_keys = (CONF_STATE,)
283281

284282
def __init__(
285283
self,
@@ -288,18 +286,18 @@ def __init__(
288286
config: ConfigType,
289287
) -> None:
290288
"""Initialize."""
291-
super().__init__(hass, coordinator, config)
289+
TriggerEntity.__init__(self, hass, coordinator, config)
290+
AbstractTemplateSensor.__init__(self, config)
292291

292+
self._to_render_simple.append(CONF_STATE)
293293
self._parse_result.add(CONF_STATE)
294-
if (last_reset_template := config.get(ATTR_LAST_RESET)) is not None:
294+
295+
if last_reset_template := self._attr_last_reset_template:
295296
if last_reset_template.is_static:
296297
self._static_rendered[ATTR_LAST_RESET] = last_reset_template.template
297298
else:
298299
self._to_render_simple.append(ATTR_LAST_RESET)
299300

300-
self._attr_state_class = config.get(CONF_STATE_CLASS)
301-
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
302-
303301
async def async_added_to_hass(self) -> None:
304302
"""Restore last state."""
305303
await super().async_added_to_hass()
@@ -311,39 +309,17 @@ async def async_added_to_hass(self) -> None:
311309
# then we should not restore state
312310
and CONF_STATE not in self._rendered
313311
):
314-
self._rendered[CONF_STATE] = extra_data.native_value
312+
self._attr_native_value = extra_data.native_value
315313
self.restore_attributes(last_state)
316314

317-
@property
318-
def native_value(self) -> str | datetime | date | None:
319-
"""Return state of the sensor."""
320-
return self._rendered.get(CONF_STATE)
321-
322315
@callback
323316
def _process_data(self) -> None:
324317
"""Process new data."""
325318
super()._process_data()
326319

327320
# Update last_reset
328-
if ATTR_LAST_RESET in self._rendered:
329-
parsed_timestamp = dt_util.parse_datetime(self._rendered[ATTR_LAST_RESET])
330-
if parsed_timestamp is None:
331-
_LOGGER.warning(
332-
"%s rendered invalid timestamp for last_reset attribute: %s",
333-
self.entity_id,
334-
self._rendered.get(ATTR_LAST_RESET),
335-
)
336-
else:
337-
self._attr_last_reset = parsed_timestamp
321+
if (last_reset := self._rendered.get(ATTR_LAST_RESET)) is not None:
322+
self._update_last_reset(last_reset)
338323

339-
if (
340-
state := self._rendered.get(CONF_STATE)
341-
) is None or self.device_class not in (
342-
SensorDeviceClass.DATE,
343-
SensorDeviceClass.TIMESTAMP,
344-
):
345-
return
346-
347-
self._rendered[CONF_STATE] = async_parse_date_datetime(
348-
state, self.entity_id, self.device_class
349-
)
324+
rendered = self._rendered.get(CONF_STATE)
325+
self._handle_state(rendered)

tests/components/template/test_sensor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1456,7 +1456,7 @@ async def test_trigger_not_allowed_platform_config(
14561456
state = hass.states.get(TEST_NAME)
14571457
assert state is None
14581458
assert (
1459-
"You can only add triggers to template entities if they are defined under `template:`."
1459+
"Invalid config for 'sensor' from integration 'template': 'trigger' is an invalid option for"
14601460
in caplog_setup_text
14611461
)
14621462

0 commit comments

Comments
 (0)