diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py
index 53736050ed3..9c317b27647 100644
--- a/homeassistant/components/template/sensor.py
+++ b/homeassistant/components/template/sensor.py
@@ -20,17 +20,15 @@ from homeassistant.const import (
     CONF_SENSORS,
     CONF_UNIQUE_ID,
     CONF_VALUE_TEMPLATE,
-    EVENT_HOMEASSISTANT_START,
-    MATCH_ALL,
 )
 from homeassistant.core import callback
 from homeassistant.exceptions import TemplateError
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity import Entity, async_generate_entity_id
-from homeassistant.helpers.event import async_track_state_change_event
+from homeassistant.helpers.template import result_as_boolean
 
-from . import extract_entities, initialise_templates
 from .const import CONF_AVAILABILITY_TEMPLATE
+from .template_entity import TemplateEntity
 
 CONF_ATTRIBUTE_TEMPLATES = "attribute_templates"
 
@@ -75,23 +73,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
         attribute_templates = device_config[CONF_ATTRIBUTE_TEMPLATES]
         unique_id = device_config.get(CONF_UNIQUE_ID)
 
-        templates = {
-            CONF_VALUE_TEMPLATE: state_template,
-            CONF_ICON_TEMPLATE: icon_template,
-            CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template,
-            CONF_FRIENDLY_NAME_TEMPLATE: friendly_name_template,
-            CONF_AVAILABILITY_TEMPLATE: availability_template,
-        }
-
-        initialise_templates(hass, templates, attribute_templates)
-        entity_ids = extract_entities(
-            device,
-            "sensor",
-            device_config.get(ATTR_ENTITY_ID),
-            templates,
-            attribute_templates,
-        )
-
         sensors.append(
             SensorTemplate(
                 hass,
@@ -103,7 +84,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
                 icon_template,
                 entity_picture_template,
                 availability_template,
-                entity_ids,
                 device_class,
                 attribute_templates,
                 unique_id,
@@ -115,7 +95,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     return True
 
 
-class SensorTemplate(Entity):
+class SensorTemplate(TemplateEntity, Entity):
     """Representation of a Template Sensor."""
 
     def __init__(
@@ -129,7 +109,6 @@ class SensorTemplate(Entity):
         icon_template,
         entity_picture_template,
         availability_template,
-        entity_ids,
         device_class,
         attribute_templates,
         unique_id,
@@ -149,35 +128,66 @@ class SensorTemplate(Entity):
         self._availability_template = availability_template
         self._icon = None
         self._entity_picture = None
-        self._entities = entity_ids
         self._device_class = device_class
         self._available = True
         self._attribute_templates = attribute_templates
         self._attributes = {}
         self._unique_id = unique_id
+        super().__init__()
 
     async def async_added_to_hass(self):
         """Register callbacks."""
 
-        @callback
-        def template_sensor_state_listener(event):
-            """Handle device state changes."""
-            self.async_schedule_update_ha_state(True)
+        self.add_template_attribute("_state", self._template, None, self._update_state)
+        if self._icon_template is not None:
+            self.add_template_attribute(
+                "_icon", self._icon_template, vol.Or(cv.whitespace, cv.icon)
+            )
+        if self._entity_picture_template is not None:
+            self.add_template_attribute(
+                "_entity_picture", self._entity_picture_template
+            )
+        if self._friendly_name_template is not None:
+            self.add_template_attribute("_name", self._friendly_name_template)
+        if self._availability_template is not None:
+            self.add_template_attribute(
+                "_available", self._availability_template, None, self._update_available
+            )
 
-        @callback
-        def template_sensor_startup(event):
-            """Update template on startup."""
-            if self._entities != MATCH_ALL:
-                # Track state change only for valid templates
-                async_track_state_change_event(
-                    self.hass, self._entities, template_sensor_state_listener
-                )
+        for key, value in self._attribute_templates.items():
+            self._add_attribute_template(key, value)
 
-            self.async_schedule_update_ha_state(True)
+        await super().async_added_to_hass()
 
-        self.hass.bus.async_listen_once(
-            EVENT_HOMEASSISTANT_START, template_sensor_startup
-        )
+    @callback
+    def _add_attribute_template(self, attribute_key, attribute_template):
+        """Create a template tracker for the attribute."""
+
+        def _update_attribute(result):
+            attr_result = None if isinstance(result, TemplateError) else result
+            self._attributes[attribute_key] = attr_result
+
+        self.add_template_attribute(None, attribute_template, None, _update_attribute)
+
+    @callback
+    def _update_state(self, result):
+        if isinstance(result, TemplateError):
+            if not self._availability_template:
+                self._available = False
+            self._state = None
+            return
+
+        if not self._availability_template:
+            self._available = True
+        self._state = result
+
+    @callback
+    def _update_available(self, result):
+        if isinstance(result, TemplateError):
+            self._available = True
+            return
+
+        self._available = result_as_boolean(result)
 
     @property
     def name(self):
@@ -228,69 +238,3 @@ class SensorTemplate(Entity):
     def should_poll(self):
         """No polling needed."""
         return False
-
-    async def async_update(self):
-        """Update the state from the template."""
-        try:
-            self._state = self._template.async_render()
-            self._available = True
-        except TemplateError as ex:
-            self._available = False
-            if ex.args and ex.args[0].startswith(
-                "UndefinedError: 'None' has no attribute"
-            ):
-                # Common during HA startup - so just a warning
-                _LOGGER.warning(
-                    "Could not render template %s, the state is unknown", self._name
-                )
-            else:
-                self._state = None
-                _LOGGER.error("Could not render template %s: %s", self._name, ex)
-
-        attrs = {}
-        for key, value in self._attribute_templates.items():
-            try:
-                attrs[key] = value.async_render()
-            except TemplateError as err:
-                _LOGGER.error("Error rendering attribute %s: %s", key, err)
-
-        self._attributes = attrs
-
-        templates = {
-            "_icon": self._icon_template,
-            "_entity_picture": self._entity_picture_template,
-            "_name": self._friendly_name_template,
-            "_available": self._availability_template,
-        }
-
-        for property_name, template in templates.items():
-            if template is None:
-                continue
-
-            try:
-                value = template.async_render()
-                if property_name == "_available":
-                    value = value.lower() == "true"
-                setattr(self, property_name, value)
-            except TemplateError as ex:
-                friendly_property_name = property_name[1:].replace("_", " ")
-                if ex.args and ex.args[0].startswith(
-                    "UndefinedError: 'None' has no attribute"
-                ):
-                    # Common during HA startup - so just a warning
-                    _LOGGER.warning(
-                        "Could not render %s template %s, the state is unknown",
-                        friendly_property_name,
-                        self._name,
-                    )
-                    continue
-
-                try:
-                    setattr(self, property_name, getattr(super(), property_name))
-                except AttributeError:
-                    _LOGGER.error(
-                        "Could not render %s template %s: %s",
-                        friendly_property_name,
-                        self._name,
-                        ex,
-                    )
diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py
new file mode 100644
index 00000000000..810b0277a58
--- /dev/null
+++ b/homeassistant/components/template/template_entity.py
@@ -0,0 +1,179 @@
+"""TemplateEntity utility class."""
+
+import logging
+from typing import Any, Callable, Optional, Union
+
+import voluptuous as vol
+
+from homeassistant.core import EVENT_HOMEASSISTANT_START, callback
+from homeassistant.exceptions import TemplateError
+from homeassistant.helpers.config_validation import match_all
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.event import Event, async_track_template_result
+from homeassistant.helpers.template import Template
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class _TemplateAttribute:
+    """Attribute value linked to template result."""
+
+    def __init__(
+        self,
+        entity: Entity,
+        attribute: str,
+        template: Template,
+        validator: Callable[[Any], Any] = match_all,
+        on_update: Optional[Callable[[Any], None]] = None,
+    ):
+        """Template attribute."""
+        self._entity = entity
+        self._attribute = attribute
+        self.template = template
+        self.validator = validator
+        self.on_update = on_update
+        self.async_update = None
+        self.add_complete = False
+
+    @callback
+    def async_setup(self):
+        """Config update path for the attribute."""
+        if self.on_update:
+            return
+
+        if not hasattr(self._entity, self._attribute):
+            raise AttributeError(f"Attribute '{self._attribute}' does not exist.")
+
+        self.on_update = self._default_update
+
+    @callback
+    def _default_update(self, result):
+        attr_result = None if isinstance(result, TemplateError) else result
+        setattr(self._entity, self._attribute, attr_result)
+
+    @callback
+    def _write_update_if_added(self):
+        if self.add_complete:
+            self._entity.async_write_ha_state()
+
+    @callback
+    def _handle_result(
+        self,
+        event: Optional[Event],
+        template: Template,
+        last_result: Optional[str],
+        result: Union[str, TemplateError],
+    ) -> None:
+        if isinstance(result, TemplateError):
+            _LOGGER.error(
+                "TemplateError('%s') "
+                "while processing template '%s' "
+                "for attribute '%s' in entity '%s'",
+                result,
+                self.template,
+                self._attribute,
+                self._entity.entity_id,
+            )
+            self.on_update(result)
+            self._write_update_if_added()
+
+            return
+
+        if not self.validator:
+            self.on_update(result)
+            self._write_update_if_added()
+            return
+
+        try:
+            validated = self.validator(result)
+        except vol.Invalid as ex:
+            _LOGGER.error(
+                "Error validating template result '%s' "
+                "from template '%s' "
+                "for attribute '%s' in entity %s "
+                "validation message '%s'",
+                result,
+                self.template,
+                self._attribute,
+                self._entity.entity_id,
+                ex.msg,
+            )
+            self.on_update(None)
+            self._write_update_if_added()
+            return
+
+        self.on_update(validated)
+        self._write_update_if_added()
+
+    @callback
+    def async_template_startup(self) -> None:
+        """Call from containing entity when added to hass."""
+        result_info = async_track_template_result(
+            self._entity.hass, self.template, self._handle_result
+        )
+        self.async_update = result_info.async_refresh
+
+        @callback
+        def _remove_from_hass():
+            result_info.async_remove()
+
+        return _remove_from_hass
+
+
+class TemplateEntity(Entity):
+    """Entity that uses templates to calculate attributes."""
+
+    def __init__(self):
+        """Template Entity."""
+        self._template_attrs = []
+
+    def add_template_attribute(
+        self,
+        attribute: str,
+        template: Template,
+        validator: Callable[[Any], Any] = match_all,
+        on_update: Optional[Callable[[Any], None]] = None,
+    ) -> None:
+        """
+        Call in the constructor to add a template linked to a attribute.
+
+        Parameters
+        ----------
+        attribute
+            The name of the attribute to link to. This attribute must exist
+            unless a custom on_update method is supplied.
+        template
+            The template to calculate.
+        validator
+            Validator function to parse the result and ensure it's valid.
+        on_update
+            Called to store the template result rather than storing it
+            the supplied attribute. Passed the result of the validator, or None
+            if the template or validator resulted in an error.
+
+        """
+        attribute = _TemplateAttribute(self, attribute, template, validator, on_update)
+        attribute.async_setup()
+        self._template_attrs.append(attribute)
+
+    async def _async_template_startup(self, _) -> None:
+        # async_update will not write state
+        # until "add_complete" is set on the attribute
+        for attribute in self._template_attrs:
+            self.async_on_remove(attribute.async_template_startup())
+        await self.async_update()
+        for attribute in self._template_attrs:
+            attribute.add_complete = True
+        self.async_write_ha_state()
+
+    async def async_added_to_hass(self) -> None:
+        """Run when entity about to be added to hass."""
+        self.hass.bus.async_listen_once(
+            EVENT_HOMEASSISTANT_START, self._async_template_startup
+        )
+
+    async def async_update(self) -> None:
+        """Call for forced update."""
+        for attribute in self._template_attrs:
+            if attribute.async_update:
+                attribute.async_update()
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index 7144aad6bbc..f3327e23222 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -156,6 +156,17 @@ def boolean(value: Any) -> bool:
     raise vol.Invalid(f"invalid boolean value {value}")
 
 
+_WS = re.compile("\\s*")
+
+
+def whitespace(value: Any) -> str:
+    """Validate result contains only whitespace."""
+    if isinstance(value, str) and _WS.fullmatch(value):
+        return value
+
+    raise vol.Invalid(f"contains non-whitespace: {value}")
+
+
 def isdevice(value: Any) -> str:
     """Validate that value is a real device."""
     try:
diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py
index dd1d39a0a9b..0dc4764732f 100644
--- a/homeassistant/helpers/event.py
+++ b/homeassistant/helpers/event.py
@@ -405,7 +405,11 @@ def async_track_template(
     ) -> None:
         """Check if condition is correct and run action."""
         if isinstance(result, TemplateError):
-            _LOGGER.exception(result)
+            _LOGGER.error(
+                "Error while processing template: %s",
+                template.template,
+                exc_info=result,
+            )
             return
 
         if result_as_boolean(last_result) or not result_as_boolean(result):
@@ -444,10 +448,10 @@ class _TrackTemplateResultInfo:
         """Handle removal / refresh of tracker init."""
         self.hass = hass
         self._template = template
+        self._template.hass = hass
         self._action = action
         self._variables = variables
-        self._last_result: Optional[str] = None
-        self._last_exception = False
+        self._last_result: Optional[Union[str, TemplateError]] = None
         self._all_listener: Optional[Callable] = None
         self._domains_listener: Optional[Callable] = None
         self._entities_listener: Optional[Callable] = None
@@ -458,8 +462,11 @@ class _TrackTemplateResultInfo:
         """Activation of template tracking."""
         self._info = self._template.async_render_to_info(self._variables)
         if self._info.exception:
-            self._last_exception = True
-            _LOGGER.exception(self._info.exception)
+            _LOGGER.error(
+                "Error while processing template: %s",
+                self._template.template,
+                exc_info=self._info.exception,
+            )
         self._create_listeners()
         self._last_info = self._info
 
@@ -593,26 +600,26 @@ class _TrackTemplateResultInfo:
             self._variables = variables
         self._refresh(None)
 
+    @callback
     def _refresh(self, event: Optional[Event]) -> None:
         self._info = self._template.async_render_to_info(self._variables)
         self._update_listeners()
         self._last_info = self._info
 
         try:
-            result = self._info.result
+            result: Union[str, TemplateError] = self._info.result
         except TemplateError as ex:
-            if not self._last_exception:
-                self.hass.async_run_job(
-                    self._action, event, self._template, self._last_result, ex
-                )
-            self._last_exception = True
-            return
-        self._last_exception = False
+            result = ex
 
         # Check to see if the result has changed
         if result == self._last_result:
             return
 
+        if isinstance(result, TemplateError) and isinstance(
+            self._last_result, TemplateError
+        ):
+            return
+
         self.hass.async_run_job(
             self._action, event, self._template, self._last_result, result
         )
diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py
index aeee08dc757..039a4cd6e5c 100644
--- a/tests/components/template/test_binary_sensor.py
+++ b/tests/components/template/test_binary_sensor.py
@@ -3,6 +3,8 @@ from datetime import timedelta
 import unittest
 from unittest import mock
 
+import jinja2
+
 from homeassistant import setup
 from homeassistant.components.template import binary_sensor as template
 from homeassistant.const import (
@@ -318,10 +320,10 @@ class TestBinarySensorTemplate(unittest.TestCase):
             None,
             None,
         ).result()
-        mock_render.side_effect = TemplateError("foo")
+        mock_render.side_effect = TemplateError(jinja2.TemplateError("foo"))
         run_callback_threadsafe(self.hass.loop, vs.async_check_state).result()
         mock_render.side_effect = TemplateError(
-            "UndefinedError: 'None' has no attribute"
+            jinja2.TemplateError("UndefinedError: 'None' has no attribute")
         )
         run_callback_threadsafe(self.hass.loop, vs.async_check_state).result()
 
diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py
index 3899a7b3afe..08bf4650bba 100644
--- a/tests/components/template/test_sensor.py
+++ b/tests/components/template/test_sensor.py
@@ -544,9 +544,13 @@ async def test_invalid_attribute_template(hass, caplog):
     )
     await hass.async_block_till_done()
     assert len(hass.states.async_all()) == 2
+
+    hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+    await hass.async_block_till_done()
     await hass.helpers.entity_component.async_update_entity("sensor.invalid_template")
 
-    assert ("Error rendering attribute test_attribute") in caplog.text
+    assert "TemplateError" in caplog.text
+    assert "test_attribute" in caplog.text
 
 
 async def test_invalid_availability_template_keeps_component_available(hass, caplog):
@@ -577,7 +581,7 @@ async def test_invalid_availability_template_keeps_component_available(hass, cap
 
 
 async def test_no_template_match_all(hass, caplog):
-    """Test that we do not allow sensors that match on all."""
+    """Test that we allow static templates."""
     hass.states.async_set("sensor.test_sensor", "startup")
 
     await async_setup_component(
@@ -617,31 +621,6 @@ async def test_no_template_match_all(hass, caplog):
 
     await hass.async_block_till_done()
     assert len(hass.states.async_all()) == 6
-    assert (
-        "Template sensor 'invalid_state' has no entity ids "
-        "configured to track nor were we able to extract the entities to "
-        "track from the value template"
-    ) in caplog.text
-    assert (
-        "Template sensor 'invalid_icon' has no entity ids "
-        "configured to track nor were we able to extract the entities to "
-        "track from the icon template"
-    ) in caplog.text
-    assert (
-        "Template sensor 'invalid_entity_picture' has no entity ids "
-        "configured to track nor were we able to extract the entities to "
-        "track from the entity_picture template"
-    ) in caplog.text
-    assert (
-        "Template sensor 'invalid_friendly_name' has no entity ids "
-        "configured to track nor were we able to extract the entities to "
-        "track from the friendly_name template"
-    ) in caplog.text
-    assert (
-        "Template sensor 'invalid_attribute' has no entity ids "
-        "configured to track nor were we able to extract the entities to "
-        "track from the test_attribute template"
-    ) in caplog.text
 
     assert hass.states.get("sensor.invalid_state").state == "unknown"
     assert hass.states.get("sensor.invalid_icon").state == "unknown"
diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py
index 7da0557c9eb..5898457d363 100644
--- a/tests/helpers/test_config_validation.py
+++ b/tests/helpers/test_config_validation.py
@@ -1092,3 +1092,24 @@ def test_script(caplog):
             cv.script_action(data)
 
         assert msg in str(excinfo.value)
+
+
+def test_whitespace():
+    """Test whitespace validation."""
+    schema = vol.Schema(cv.whitespace)
+
+    for value in (
+        None,
+        "" "T",
+        "negative",
+        "lock",
+        "tr  ue",
+        [],
+        [1, 2],
+        {"one": "two"},
+    ):
+        with pytest.raises(vol.MultipleInvalid):
+            schema(value)
+
+    for value in ("  ", "   "):
+        assert schema(value)
diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py
index 00daf6b9574..a9a4277bae0 100644
--- a/tests/helpers/test_event.py
+++ b/tests/helpers/test_event.py
@@ -4,6 +4,7 @@ import asyncio
 from datetime import datetime, timedelta
 
 from astral import Astral
+import jinja2
 import pytest
 
 from homeassistant.components import sun
@@ -942,7 +943,7 @@ async def test_track_template_result_errors(hass, caplog):
     hass.states.async_set("switch.not_exist", "on")
     await hass.async_block_till_done()
 
-    assert len(syntax_error_runs) == 0
+    assert len(syntax_error_runs) == 1
     assert len(not_exist_runs) == 2
     assert not_exist_runs[1][0].data.get("entity_id") == "switch.not_exist"
     assert not_exist_runs[1][1] == template_not_exist
@@ -950,7 +951,7 @@ async def test_track_template_result_errors(hass, caplog):
     assert not_exist_runs[1][3] == "on"
 
     with patch.object(Template, "async_render") as render:
-        render.side_effect = TemplateError("Test")
+        render.side_effect = TemplateError(jinja2.TemplateError())
 
         hass.states.async_set("switch.not_exist", "off")
         await hass.async_block_till_done()