diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py
index d91cf080c2a..ea0c6079e5b 100644
--- a/homeassistant/components/tag/__init__.py
+++ b/homeassistant/components/tag/__init__.py
@@ -3,41 +3,55 @@
 from __future__ import annotations
 
 import logging
+from typing import TYPE_CHECKING, Any, final
 import uuid
 
 import voluptuous as vol
 
+from homeassistant.components import websocket_api
 from homeassistant.const import CONF_NAME
 from homeassistant.core import Context, HomeAssistant, callback
 from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers import collection
+from homeassistant.helpers import collection, entity_registry as er
 import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.dispatcher import (
+    async_dispatcher_connect,
+    async_dispatcher_send,
+)
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.entity_component import EntityComponent
 from homeassistant.helpers.storage import Store
 from homeassistant.helpers.typing import ConfigType
+from homeassistant.util import slugify
 import homeassistant.util.dt as dt_util
 from homeassistant.util.hass_dict import HassKey
 
-from .const import DEVICE_ID, DOMAIN, EVENT_TAG_SCANNED, TAG_ID
+from .const import DEFAULT_NAME, DEVICE_ID, DOMAIN, EVENT_TAG_SCANNED, LOGGER, TAG_ID
 
 _LOGGER = logging.getLogger(__name__)
 
 LAST_SCANNED = "last_scanned"
+LAST_SCANNED_BY_DEVICE_ID = "last_scanned_by_device_id"
 STORAGE_KEY = DOMAIN
 STORAGE_VERSION = 1
+STORAGE_VERSION_MINOR = 2
 
 TAG_DATA: HassKey[TagStorageCollection] = HassKey(DOMAIN)
+SIGNAL_TAG_CHANGED = "signal_tag_changed"
 
 CREATE_FIELDS = {
     vol.Optional(TAG_ID): cv.string,
     vol.Optional(CONF_NAME): vol.All(str, vol.Length(min=1)),
     vol.Optional("description"): cv.string,
     vol.Optional(LAST_SCANNED): cv.datetime,
+    vol.Optional(DEVICE_ID): cv.string,
 }
 
 UPDATE_FIELDS = {
     vol.Optional(CONF_NAME): vol.All(str, vol.Length(min=1)),
     vol.Optional("description"): cv.string,
     vol.Optional(LAST_SCANNED): cv.datetime,
+    vol.Optional(DEVICE_ID): cv.string,
 }
 
 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
@@ -63,12 +77,60 @@ class TagIDManager(collection.IDManager):
         return suggestion
 
 
+def _create_entry(
+    entity_registry: er.EntityRegistry, tag_id: str, name: str | None
+) -> er.RegistryEntry:
+    """Create an entity registry entry for a tag."""
+    entry = entity_registry.async_get_or_create(
+        DOMAIN,
+        DOMAIN,
+        tag_id,
+        original_name=f"{DEFAULT_NAME} {tag_id}",
+        suggested_object_id=slugify(name) if name else tag_id,
+    )
+    return entity_registry.async_update_entity(entry.entity_id, name=name)
+
+
+class TagStore(Store[collection.SerializedStorageCollection]):
+    """Store tag data."""
+
+    async def _async_migrate_func(
+        self,
+        old_major_version: int,
+        old_minor_version: int,
+        old_data: dict[str, list[dict[str, Any]]],
+    ) -> dict:
+        """Migrate to the new version."""
+        data = old_data
+        if old_major_version == 1 and old_minor_version < 2:
+            entity_registry = er.async_get(self.hass)
+            # Version 1.2 moves name to entity registry
+            for tag in data["items"]:
+                # Copy name in tag store to the entity registry
+                _create_entry(entity_registry, tag[TAG_ID], tag.get(CONF_NAME))
+                tag["migrated"] = True
+
+        if old_major_version > 1:
+            raise NotImplementedError
+
+        return data
+
+
 class TagStorageCollection(collection.DictStorageCollection):
     """Tag collection stored in storage."""
 
     CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
     UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
 
+    def __init__(
+        self,
+        store: TagStore,
+        id_manager: collection.IDManager | None = None,
+    ) -> None:
+        """Initialize the storage collection."""
+        super().__init__(store, id_manager)
+        self.entity_registry = er.async_get(self.hass)
+
     async def _process_create_data(self, data: dict) -> dict:
         """Validate the config is valid."""
         data = self.CREATE_SCHEMA(data)
@@ -77,6 +139,10 @@ class TagStorageCollection(collection.DictStorageCollection):
         # make last_scanned JSON serializeable
         if LAST_SCANNED in data:
             data[LAST_SCANNED] = data[LAST_SCANNED].isoformat()
+
+        # Create entity in entity_registry when creating the tag
+        # This is done early to store name only once in entity registry
+        _create_entry(self.entity_registry, data[TAG_ID], data.get(CONF_NAME))
         return data
 
     @callback
@@ -87,24 +153,163 @@ class TagStorageCollection(collection.DictStorageCollection):
     async def _update_data(self, item: dict, update_data: dict) -> dict:
         """Return a new updated data object."""
         data = {**item, **self.UPDATE_SCHEMA(update_data)}
+        tag_id = data[TAG_ID]
         # make last_scanned JSON serializeable
         if LAST_SCANNED in update_data:
             data[LAST_SCANNED] = data[LAST_SCANNED].isoformat()
+        if name := data.get(CONF_NAME):
+            if entity_id := self.entity_registry.async_get_entity_id(
+                DOMAIN, DOMAIN, tag_id
+            ):
+                self.entity_registry.async_update_entity(entity_id, name=name)
+            else:
+                raise collection.ItemNotFound(tag_id)
+
         return data
 
+    def _serialize_item(self, item_id: str, item: dict) -> dict:
+        """Return the serialized representation of an item for storing.
+
+        We don't store the name, it's stored in the entity registry.
+        """
+        # Preserve the name of migrated entries to allow downgrading to 2024.5
+        # without losing tag names. This can be removed in HA Core 2025.1.
+        migrated = item_id in self.data and "migrated" in self.data[item_id]
+        return {k: v for k, v in item.items() if k != CONF_NAME or migrated}
+
+
+class TagDictStorageCollectionWebsocket(
+    collection.StorageCollectionWebsocket[TagStorageCollection]
+):
+    """Class to expose tag storage collection management over websocket."""
+
+    def __init__(
+        self,
+        storage_collection: TagStorageCollection,
+        api_prefix: str,
+        model_name: str,
+        create_schema: ConfigType,
+        update_schema: ConfigType,
+    ) -> None:
+        """Initialize a websocket for tag."""
+        super().__init__(
+            storage_collection, api_prefix, model_name, create_schema, update_schema
+        )
+        self.entity_registry = er.async_get(storage_collection.hass)
+
+    @callback
+    def ws_list_item(
+        self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    ) -> None:
+        """List items specifically for tag.
+
+        Provides name from entity_registry instead of storage collection.
+        """
+        tag_items = []
+        for item in self.storage_collection.async_items():
+            # Make a copy to avoid adding name to the stored entry
+            item = {k: v for k, v in item.items() if k != "migrated"}
+            if (
+                entity_id := self.entity_registry.async_get_entity_id(
+                    DOMAIN, DOMAIN, item[TAG_ID]
+                )
+            ) and (entity := self.entity_registry.async_get(entity_id)):
+                item[CONF_NAME] = entity.name or entity.original_name
+            tag_items.append(item)
+        if _LOGGER.isEnabledFor(logging.DEBUG):
+            _LOGGER.debug("Listing tags %s", tag_items)
+        connection.send_result(msg["id"], tag_items)
+
 
 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
     """Set up the Tag component."""
+    component = EntityComponent[TagEntity](LOGGER, DOMAIN, hass)
     id_manager = TagIDManager()
     hass.data[TAG_DATA] = storage_collection = TagStorageCollection(
-        Store(hass, STORAGE_VERSION, STORAGE_KEY),
+        TagStore(
+            hass, STORAGE_VERSION, STORAGE_KEY, minor_version=STORAGE_VERSION_MINOR
+        ),
         id_manager,
     )
     await storage_collection.async_load()
-    collection.DictStorageCollectionWebsocket(
+    TagDictStorageCollectionWebsocket(
         storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
     ).async_setup(hass)
 
+    entity_registry = er.async_get(hass)
+
+    async def tag_change_listener(
+        change_type: str, item_id: str, updated_config: dict
+    ) -> None:
+        """Tag storage change listener."""
+
+        if _LOGGER.isEnabledFor(logging.DEBUG):
+            _LOGGER.debug(
+                "%s, item: %s, update: %s", change_type, item_id, updated_config
+            )
+        if change_type == collection.CHANGE_ADDED:
+            # When tags are added to storage
+            entity = _create_entry(entity_registry, updated_config[TAG_ID], None)
+            if TYPE_CHECKING:
+                assert entity.original_name
+            await component.async_add_entities(
+                [
+                    TagEntity(
+                        hass,
+                        entity.name or entity.original_name,
+                        updated_config[TAG_ID],
+                        updated_config.get(LAST_SCANNED),
+                        updated_config.get(DEVICE_ID),
+                    )
+                ]
+            )
+
+        elif change_type == collection.CHANGE_UPDATED:
+            # When tags are changed or updated in storage
+            async_dispatcher_send(
+                hass,
+                SIGNAL_TAG_CHANGED,
+                updated_config.get(DEVICE_ID),
+                updated_config.get(LAST_SCANNED),
+            )
+
+        # Deleted tags
+        elif change_type == collection.CHANGE_REMOVED:
+            # When tags are removed from storage
+            entity_id = entity_registry.async_get_entity_id(
+                DOMAIN, DOMAIN, updated_config[TAG_ID]
+            )
+            if entity_id:
+                entity_registry.async_remove(entity_id)
+
+    storage_collection.async_add_listener(tag_change_listener)
+
+    entities: list[TagEntity] = []
+    for tag in storage_collection.async_items():
+        if _LOGGER.isEnabledFor(logging.DEBUG):
+            _LOGGER.debug("Adding tag: %s", tag)
+        entity_id = entity_registry.async_get_entity_id(DOMAIN, DOMAIN, tag[TAG_ID])
+        if entity_id := entity_registry.async_get_entity_id(
+            DOMAIN, DOMAIN, tag[TAG_ID]
+        ):
+            entity = entity_registry.async_get(entity_id)
+        else:
+            entity = _create_entry(entity_registry, tag[TAG_ID], None)
+        if TYPE_CHECKING:
+            assert entity
+            assert entity.original_name
+        name = entity.name or entity.original_name
+        entities.append(
+            TagEntity(
+                hass,
+                name,
+                tag[TAG_ID],
+                tag.get(LAST_SCANNED),
+                tag.get(DEVICE_ID),
+            )
+        )
+    await component.async_add_entities(entities)
+
     return True
 
 
@@ -119,11 +324,13 @@ async def async_scan_tag(
         raise HomeAssistantError("tag component has not been set up.")
 
     storage_collection = hass.data[TAG_DATA]
+    entity_registry = er.async_get(hass)
+    entity_id = entity_registry.async_get_entity_id(DOMAIN, DOMAIN, tag_id)
 
-    # Get name from helper, default value None if not present in data
+    # Get name from entity registry, default value None if not present
     tag_name = None
-    if tag_data := storage_collection.data.get(tag_id):
-        tag_name = tag_data.get(CONF_NAME)
+    if entity_id and (entity := entity_registry.async_get(entity_id)):
+        tag_name = entity.name or entity.original_name
 
     hass.bus.async_fire(
         EVENT_TAG_SCANNED,
@@ -131,12 +338,87 @@ async def async_scan_tag(
         context=context,
     )
 
+    extra_kwargs = {}
+    if device_id:
+        extra_kwargs[DEVICE_ID] = device_id
     if tag_id in storage_collection.data:
+        if _LOGGER.isEnabledFor(logging.DEBUG):
+            _LOGGER.debug("Updating tag %s with extra %s", tag_id, extra_kwargs)
         await storage_collection.async_update_item(
-            tag_id, {LAST_SCANNED: dt_util.utcnow()}
+            tag_id, {LAST_SCANNED: dt_util.utcnow(), **extra_kwargs}
         )
     else:
+        if _LOGGER.isEnabledFor(logging.DEBUG):
+            _LOGGER.debug("Creating tag %s with extra %s", tag_id, extra_kwargs)
         await storage_collection.async_create_item(
-            {TAG_ID: tag_id, LAST_SCANNED: dt_util.utcnow()}
+            {TAG_ID: tag_id, LAST_SCANNED: dt_util.utcnow(), **extra_kwargs}
         )
     _LOGGER.debug("Tag: %s scanned by device: %s", tag_id, device_id)
+
+
+class TagEntity(Entity):
+    """Representation of a Tag entity."""
+
+    _unrecorded_attributes = frozenset({TAG_ID})
+    _attr_translation_key = DOMAIN
+    _attr_should_poll = False
+
+    def __init__(
+        self,
+        hass: HomeAssistant,
+        name: str,
+        tag_id: str,
+        last_scanned: str | None,
+        device_id: str | None,
+    ) -> None:
+        """Initialize the Tag event."""
+        self.hass = hass
+        self._attr_name = name
+        self._tag_id = tag_id
+        self._attr_unique_id = tag_id
+        self._last_device_id: str | None = device_id
+        self._last_scanned = last_scanned
+
+    @callback
+    def async_handle_event(
+        self, device_id: str | None, last_scanned: str | None
+    ) -> None:
+        """Handle the Tag scan event."""
+        if _LOGGER.isEnabledFor(logging.DEBUG):
+            _LOGGER.debug(
+                "Tag %s scanned by device %s at %s, last scanned at %s",
+                self._tag_id,
+                device_id,
+                last_scanned,
+                self._last_scanned,
+            )
+        self._last_device_id = device_id
+        self._last_scanned = last_scanned
+        self.async_write_ha_state()
+
+    @property
+    @final
+    def state(self) -> str | None:
+        """Return the entity state."""
+        if (
+            not self._last_scanned
+            or (last_scanned := dt_util.parse_datetime(self._last_scanned)) is None
+        ):
+            return None
+        return last_scanned.isoformat(timespec="milliseconds")
+
+    @property
+    def extra_state_attributes(self) -> dict[str, Any]:
+        """Return the state attributes of the sun."""
+        return {TAG_ID: self._tag_id, LAST_SCANNED_BY_DEVICE_ID: self._last_device_id}
+
+    async def async_added_to_hass(self) -> None:
+        """Handle entity which will be added."""
+        await super().async_added_to_hass()
+        self.async_on_remove(
+            async_dispatcher_connect(
+                self.hass,
+                SIGNAL_TAG_CHANGED,
+                self.async_handle_event,
+            )
+        )
diff --git a/homeassistant/components/tag/const.py b/homeassistant/components/tag/const.py
index ed74a1f0549..fd93e3ecac8 100644
--- a/homeassistant/components/tag/const.py
+++ b/homeassistant/components/tag/const.py
@@ -1,6 +1,10 @@
 """Constants for the Tag integration."""
 
+import logging
+
 DEVICE_ID = "device_id"
 DOMAIN = "tag"
 EVENT_TAG_SCANNED = "tag_scanned"
 TAG_ID = "tag_id"
+DEFAULT_NAME = "Tag"
+LOGGER = logging.getLogger(__package__)
diff --git a/homeassistant/components/tag/icons.json b/homeassistant/components/tag/icons.json
new file mode 100644
index 00000000000..d9532aadf73
--- /dev/null
+++ b/homeassistant/components/tag/icons.json
@@ -0,0 +1,9 @@
+{
+  "entity": {
+    "tag": {
+      "tag": {
+        "default": "mdi:tag-outline"
+      }
+    }
+  }
+}
diff --git a/homeassistant/components/tag/strings.json b/homeassistant/components/tag/strings.json
index ba680ba0d81..75cec1f9ef4 100644
--- a/homeassistant/components/tag/strings.json
+++ b/homeassistant/components/tag/strings.json
@@ -1,3 +1,17 @@
 {
-  "title": "Tag"
+  "title": "Tag",
+  "entity": {
+    "tag": {
+      "tag": {
+        "state_attributes": {
+          "tag_id": {
+            "name": "Tag ID"
+          },
+          "last_scanned_by_device_id": {
+            "name": "Last scanned by device ID"
+          }
+        }
+      }
+    }
+  }
 }
diff --git a/tests/components/tag/__init__.py b/tests/components/tag/__init__.py
index 5908bd04e59..66b23073d3e 100644
--- a/tests/components/tag/__init__.py
+++ b/tests/components/tag/__init__.py
@@ -1 +1,5 @@
 """Tests for the Tag integration."""
+
+TEST_TAG_ID = "test tag id"
+TEST_TAG_NAME = "test tag name"
+TEST_DEVICE_ID = "device id"
diff --git a/tests/components/tag/snapshots/test_init.ambr b/tests/components/tag/snapshots/test_init.ambr
new file mode 100644
index 00000000000..8a17079e16d
--- /dev/null
+++ b/tests/components/tag/snapshots/test_init.ambr
@@ -0,0 +1,28 @@
+# serializer version: 1
+# name: test_migration
+  dict({
+    'data': dict({
+      'items': list([
+        dict({
+          'id': 'test tag id',
+          'migrated': True,
+          'name': 'test tag name',
+          'tag_id': 'test tag id',
+        }),
+        dict({
+          'device_id': 'some_scanner',
+          'id': 'new tag',
+          'last_scanned': '2024-02-29T13:00:00+00:00',
+          'tag_id': 'new tag',
+        }),
+        dict({
+          'id': '1234567890',
+          'tag_id': '1234567890',
+        }),
+      ]),
+    }),
+    'key': 'tag',
+    'minor_version': 2,
+    'version': 1,
+  })
+# ---
diff --git a/tests/components/tag/test_event.py b/tests/components/tag/test_event.py
index ac24e837428..d3dc7f73058 100644
--- a/tests/components/tag/test_event.py
+++ b/tests/components/tag/test_event.py
@@ -4,18 +4,16 @@ from freezegun.api import FrozenDateTimeFactory
 import pytest
 
 from homeassistant.components.tag import DOMAIN, EVENT_TAG_SCANNED, async_scan_tag
-from homeassistant.const import CONF_NAME
 from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
 from homeassistant.setup import async_setup_component
 from homeassistant.util import dt as dt_util
 
+from . import TEST_DEVICE_ID, TEST_TAG_ID, TEST_TAG_NAME
+
 from tests.common import async_capture_events
 from tests.typing import WebSocketGenerator
 
-TEST_TAG_ID = "test tag id"
-TEST_TAG_NAME = "test tag name"
-TEST_DEVICE_ID = "device id"
-
 
 @pytest.fixture
 def storage_setup_named_tag(
@@ -29,10 +27,21 @@ def storage_setup_named_tag(
             hass_storage[DOMAIN] = {
                 "key": DOMAIN,
                 "version": 1,
-                "data": {"items": [{"id": TEST_TAG_ID, CONF_NAME: TEST_TAG_NAME}]},
+                "minor_version": 2,
+                "data": {
+                    "items": [
+                        {
+                            "id": TEST_TAG_ID,
+                            "tag_id": TEST_TAG_ID,
+                        }
+                    ]
+                },
             }
         else:
             hass_storage[DOMAIN] = items
+        entity_registry = er.async_get(hass)
+        entry = entity_registry.async_get_or_create(DOMAIN, DOMAIN, TEST_TAG_ID)
+        entity_registry.async_update_entity(entry.entity_id, name=TEST_TAG_NAME)
         config = {DOMAIN: {}}
         return await async_setup_component(hass, DOMAIN, config)
 
@@ -75,7 +84,8 @@ def storage_setup_unnamed_tag(hass: HomeAssistant, hass_storage):
             hass_storage[DOMAIN] = {
                 "key": DOMAIN,
                 "version": 1,
-                "data": {"items": [{"id": TEST_TAG_ID}]},
+                "minor_version": 2,
+                "data": {"items": [{"id": TEST_TAG_ID, "tag_id": TEST_TAG_ID}]},
             }
         else:
             hass_storage[DOMAIN] = items
@@ -107,6 +117,6 @@ async def test_unnamed_tag_scanned_event(
     event = events[0]
     event_data = event.data
 
-    assert event_data["name"] is None
+    assert event_data["name"] == "Tag test tag id"
     assert event_data["device_id"] == TEST_DEVICE_ID
     assert event_data["tag_id"] == TEST_TAG_ID
diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py
index 6d300b8ea6e..914719c8c1a 100644
--- a/tests/components/tag/test_init.py
+++ b/tests/components/tag/test_init.py
@@ -1,14 +1,21 @@
 """Tests for the tag component."""
 
+import logging
+
 from freezegun.api import FrozenDateTimeFactory
 import pytest
+from syrupy import SnapshotAssertion
 
-from homeassistant.components.tag import DOMAIN, async_scan_tag
+from homeassistant.components.tag import DOMAIN, _create_entry, async_scan_tag
+from homeassistant.const import CONF_NAME, STATE_UNKNOWN
 from homeassistant.core import HomeAssistant
-from homeassistant.helpers import collection
+from homeassistant.helpers import collection, entity_registry as er
 from homeassistant.setup import async_setup_component
 from homeassistant.util import dt as dt_util
 
+from . import TEST_DEVICE_ID, TEST_TAG_ID, TEST_TAG_NAME
+
+from tests.common import async_fire_time_changed
 from tests.typing import WebSocketGenerator
 
 
@@ -21,7 +28,45 @@ def storage_setup(hass: HomeAssistant, hass_storage):
             hass_storage[DOMAIN] = {
                 "key": DOMAIN,
                 "version": 1,
-                "data": {"items": [{"id": "test tag"}]},
+                "minor_version": 2,
+                "data": {
+                    "items": [
+                        {
+                            "id": TEST_TAG_ID,
+                            "tag_id": TEST_TAG_ID,
+                        }
+                    ]
+                },
+            }
+        else:
+            hass_storage[DOMAIN] = items
+        entity_registry = er.async_get(hass)
+        _create_entry(entity_registry, TEST_TAG_ID, TEST_TAG_NAME)
+        config = {DOMAIN: {}}
+        return await async_setup_component(hass, DOMAIN, config)
+
+    return _storage
+
+
+@pytest.fixture
+def storage_setup_1_1(hass: HomeAssistant, hass_storage):
+    """Storage version 1.1 setup."""
+
+    async def _storage(items=None):
+        if items is None:
+            hass_storage[DOMAIN] = {
+                "key": DOMAIN,
+                "version": 1,
+                "minor_version": 1,
+                "data": {
+                    "items": [
+                        {
+                            "id": TEST_TAG_ID,
+                            "tag_id": TEST_TAG_ID,
+                            CONF_NAME: TEST_TAG_NAME,
+                        }
+                    ]
+                },
             }
         else:
             hass_storage[DOMAIN] = items
@@ -31,6 +76,49 @@ def storage_setup(hass: HomeAssistant, hass_storage):
     return _storage
 
 
+async def test_migration(
+    hass: HomeAssistant,
+    hass_ws_client: WebSocketGenerator,
+    storage_setup_1_1,
+    freezer: FrozenDateTimeFactory,
+    hass_storage,
+    snapshot: SnapshotAssertion,
+) -> None:
+    """Test migrating tag store."""
+    assert await storage_setup_1_1()
+
+    client = await hass_ws_client(hass)
+
+    freezer.move_to("2024-02-29 13:00")
+
+    await client.send_json_auto_id({"type": f"{DOMAIN}/list"})
+    resp = await client.receive_json()
+    assert resp["success"]
+    assert resp["result"] == [
+        {"id": TEST_TAG_ID, "name": "test tag name", "tag_id": TEST_TAG_ID}
+    ]
+
+    # Scan a new tag
+    await async_scan_tag(hass, "new tag", "some_scanner")
+
+    # Add a new tag through WS
+    await client.send_json_auto_id(
+        {
+            "type": f"{DOMAIN}/create",
+            "tag_id": "1234567890",
+            "name": "Kitchen tag",
+        }
+    )
+    resp = await client.receive_json()
+    assert resp["success"]
+
+    # Trigger store
+    freezer.tick(11)
+    async_fire_time_changed(hass)
+    await hass.async_block_till_done()
+    assert hass_storage[DOMAIN] == snapshot
+
+
 async def test_ws_list(
     hass: HomeAssistant, hass_ws_client: WebSocketGenerator, storage_setup
 ) -> None:
@@ -39,14 +127,12 @@ async def test_ws_list(
 
     client = await hass_ws_client(hass)
 
-    await client.send_json({"id": 6, "type": f"{DOMAIN}/list"})
+    await client.send_json_auto_id({"type": f"{DOMAIN}/list"})
     resp = await client.receive_json()
     assert resp["success"]
-
-    result = {item["id"]: item for item in resp["result"]}
-
-    assert len(result) == 1
-    assert "test tag" in result
+    assert resp["result"] == [
+        {"id": TEST_TAG_ID, "name": "test tag name", "tag_id": TEST_TAG_ID}
+    ]
 
 
 async def test_ws_update(
@@ -58,21 +144,17 @@ async def test_ws_update(
 
     client = await hass_ws_client(hass)
 
-    await client.send_json(
+    await client.send_json_auto_id(
         {
-            "id": 6,
             "type": f"{DOMAIN}/update",
-            f"{DOMAIN}_id": "test tag",
+            f"{DOMAIN}_id": TEST_TAG_ID,
             "name": "New name",
         }
     )
     resp = await client.receive_json()
     assert resp["success"]
-
     item = resp["result"]
-
-    assert item["id"] == "test tag"
-    assert item["name"] == "New name"
+    assert item == {"id": TEST_TAG_ID, "name": "New name", "tag_id": TEST_TAG_ID}
 
 
 async def test_tag_scanned(
@@ -86,29 +168,37 @@ async def test_tag_scanned(
 
     client = await hass_ws_client(hass)
 
-    await client.send_json({"id": 6, "type": f"{DOMAIN}/list"})
+    await client.send_json_auto_id({"type": f"{DOMAIN}/list"})
     resp = await client.receive_json()
     assert resp["success"]
 
     result = {item["id"]: item for item in resp["result"]}
 
-    assert len(result) == 1
-    assert "test tag" in result
+    assert resp["result"] == [
+        {"id": TEST_TAG_ID, "name": "test tag name", "tag_id": TEST_TAG_ID}
+    ]
 
     now = dt_util.utcnow()
     freezer.move_to(now)
     await async_scan_tag(hass, "new tag", "some_scanner")
 
-    await client.send_json({"id": 7, "type": f"{DOMAIN}/list"})
+    await client.send_json_auto_id({"type": f"{DOMAIN}/list"})
     resp = await client.receive_json()
     assert resp["success"]
 
     result = {item["id"]: item for item in resp["result"]}
 
     assert len(result) == 2
-    assert "test tag" in result
-    assert "new tag" in result
-    assert result["new tag"]["last_scanned"] == now.isoformat()
+    assert resp["result"] == [
+        {"id": TEST_TAG_ID, "name": "test tag name", "tag_id": TEST_TAG_ID},
+        {
+            "device_id": "some_scanner",
+            "id": "new tag",
+            "last_scanned": now.isoformat(),
+            "name": "Tag new tag",
+            "tag_id": "new tag",
+        },
+    ]
 
 
 def track_changes(coll: collection.ObservableCollection):
@@ -131,8 +221,93 @@ async def test_tag_id_exists(
     changes = track_changes(hass.data[DOMAIN])
     client = await hass_ws_client(hass)
 
-    await client.send_json({"id": 2, "type": f"{DOMAIN}/create", "tag_id": "test tag"})
+    await client.send_json_auto_id({"type": f"{DOMAIN}/create", "tag_id": TEST_TAG_ID})
     response = await client.receive_json()
     assert not response["success"]
     assert response["error"]["code"] == "home_assistant_error"
     assert len(changes) == 0
+
+
+async def test_entity(
+    hass: HomeAssistant,
+    hass_ws_client: WebSocketGenerator,
+    freezer: FrozenDateTimeFactory,
+    storage_setup,
+) -> None:
+    """Test tag entity."""
+    assert await storage_setup()
+
+    await hass_ws_client(hass)
+
+    entity = hass.states.get("tag.test_tag_name")
+    assert entity
+    assert entity.state == STATE_UNKNOWN
+
+    now = dt_util.utcnow()
+    freezer.move_to(now)
+    await async_scan_tag(hass, TEST_TAG_ID, TEST_DEVICE_ID)
+
+    entity = hass.states.get("tag.test_tag_name")
+    assert entity
+    assert entity.state == now.isoformat(timespec="milliseconds")
+    assert entity.attributes == {
+        "tag_id": "test tag id",
+        "last_scanned_by_device_id": "device id",
+        "friendly_name": "test tag name",
+    }
+
+
+async def test_entity_created_and_removed(
+    caplog: pytest.LogCaptureFixture,
+    hass: HomeAssistant,
+    hass_ws_client: WebSocketGenerator,
+    freezer: FrozenDateTimeFactory,
+    storage_setup,
+    entity_registry: er.EntityRegistry,
+) -> None:
+    """Test tag entity created and removed."""
+    caplog.at_level(logging.DEBUG)
+    assert await storage_setup()
+
+    client = await hass_ws_client(hass)
+
+    await client.send_json_auto_id(
+        {
+            "type": f"{DOMAIN}/create",
+            "tag_id": "1234567890",
+            "name": "Kitchen tag",
+        }
+    )
+    resp = await client.receive_json()
+    assert resp["success"]
+    item = resp["result"]
+
+    assert item["id"] == "1234567890"
+    assert item["name"] == "Kitchen tag"
+
+    entity = hass.states.get("tag.kitchen_tag")
+    assert entity
+    assert entity.state == STATE_UNKNOWN
+    entity_id = entity.entity_id
+    assert entity_registry.async_get(entity_id)
+
+    now = dt_util.utcnow()
+    freezer.move_to(now)
+    await async_scan_tag(hass, "1234567890", TEST_DEVICE_ID)
+
+    entity = hass.states.get("tag.kitchen_tag")
+    assert entity
+    assert entity.state == now.isoformat(timespec="milliseconds")
+
+    await client.send_json_auto_id(
+        {
+            "type": f"{DOMAIN}/delete",
+            "tag_id": "1234567890",
+        }
+    )
+    resp = await client.receive_json()
+    assert resp["success"]
+
+    entity = hass.states.get("tag.kitchen_tag")
+    assert not entity
+    assert not entity_registry.async_get(entity_id)
diff --git a/tests/components/tag/test_trigger.py b/tests/components/tag/test_trigger.py
index baaa1ffa2ee..613b5585670 100644
--- a/tests/components/tag/test_trigger.py
+++ b/tests/components/tag/test_trigger.py
@@ -26,7 +26,8 @@ def tag_setup(hass: HomeAssistant, hass_storage):
             hass_storage[DOMAIN] = {
                 "key": DOMAIN,
                 "version": 1,
-                "data": {"items": [{"id": "test tag"}]},
+                "minor_version": 2,
+                "data": {"items": [{"id": "test tag", "tag_id": "test tag"}]},
             }
         else:
             hass_storage[DOMAIN] = items