From 10076e652353ac2959babb32fb790ac0630b218f Mon Sep 17 00:00:00 2001
From: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Date: Thu, 11 Apr 2024 12:04:08 +0200
Subject: [PATCH] Add notify entity component (#110950)

* Add notify entity component

* Device classes, restore state, icons

* Add icons file

* Add tests for kitchen_sink

* Remove notify from no_entity_platforms in hassfest icons, translation link

* ruff

* Remove `data` feature

* Only message support

* Complete initial device classes

* mypy pylint

* Remove device_class implementation

* format

* Follow up comments

* Remove _attr_supported_features

* Use setup_test_component_platform

* User helper at other places

* last comment

* Add entry unload test and non async test

* Avoid default mutable object in constructor
---
 .../components/kitchen_sink/__init__.py       |   3 +-
 .../components/kitchen_sink/notify.py         |  54 ++++
 homeassistant/components/notify/__init__.py   |  92 ++++++-
 homeassistant/components/notify/const.py      |   6 +-
 homeassistant/components/notify/icons.json    |   8 +-
 homeassistant/components/notify/services.yaml |  10 +
 homeassistant/components/notify/strings.json  |  15 ++
 homeassistant/helpers/service.py              |   2 +
 tests/components/kitchen_sink/test_notify.py  |  66 +++++
 tests/components/notify/conftest.py           |  23 ++
 tests/components/notify/test_init.py          | 241 ++++++++++++++++--
 11 files changed, 494 insertions(+), 26 deletions(-)
 create mode 100644 homeassistant/components/kitchen_sink/notify.py
 create mode 100644 tests/components/kitchen_sink/test_notify.py
 create mode 100644 tests/components/notify/conftest.py

diff --git a/homeassistant/components/kitchen_sink/__init__.py b/homeassistant/components/kitchen_sink/__init__.py
index 6b6694c920d..94dfca77410 100644
--- a/homeassistant/components/kitchen_sink/__init__.py
+++ b/homeassistant/components/kitchen_sink/__init__.py
@@ -32,6 +32,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
     Platform.IMAGE,
     Platform.LAWN_MOWER,
     Platform.LOCK,
+    Platform.NOTIFY,
     Platform.SENSOR,
     Platform.SWITCH,
     Platform.WEATHER,
@@ -70,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
     return True
 
 
-def _create_issues(hass):
+def _create_issues(hass: HomeAssistant) -> None:
     """Create some issue registry issues."""
     async_create_issue(
         hass,
diff --git a/homeassistant/components/kitchen_sink/notify.py b/homeassistant/components/kitchen_sink/notify.py
new file mode 100644
index 00000000000..b0418411145
--- /dev/null
+++ b/homeassistant/components/kitchen_sink/notify.py
@@ -0,0 +1,54 @@
+"""Demo platform that offers a fake notify entity."""
+
+from __future__ import annotations
+
+from homeassistant.components import persistent_notification
+from homeassistant.components.notify import NotifyEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.device_registry import DeviceInfo
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from . import DOMAIN
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    config_entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up the demo notify entity platform."""
+    async_add_entities(
+        [
+            DemoNotify(
+                unique_id="just_notify_me",
+                device_name="MyBox",
+                entity_name="Personal notifier",
+            ),
+        ]
+    )
+
+
+class DemoNotify(NotifyEntity):
+    """Representation of a demo notify entity."""
+
+    _attr_has_entity_name = True
+    _attr_should_poll = False
+
+    def __init__(
+        self,
+        unique_id: str,
+        device_name: str,
+        entity_name: str | None,
+    ) -> None:
+        """Initialize the Demo button entity."""
+        self._attr_unique_id = unique_id
+        self._attr_device_info = DeviceInfo(
+            identifiers={(DOMAIN, unique_id)},
+            name=device_name,
+        )
+        self._attr_name = entity_name
+
+    async def async_send_message(self, message: str) -> None:
+        """Send out a persistent notification."""
+        persistent_notification.async_create(self.hass, message, "Demo notification")
diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py
index e7390a49676..81b7d300acc 100644
--- a/homeassistant/components/notify/__init__.py
+++ b/homeassistant/components/notify/__init__.py
@@ -2,24 +2,36 @@
 
 from __future__ import annotations
 
+from datetime import timedelta
+from functools import cached_property, partial
+import logging
+from typing import Any, final, override
+
 import voluptuous as vol
 
 import homeassistant.components.persistent_notification as pn
-from homeassistant.const import CONF_NAME, CONF_PLATFORM
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_NAME, CONF_PLATFORM, STATE_UNAVAILABLE
 from homeassistant.core import HomeAssistant, ServiceCall
 import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import EntityDescription
+from homeassistant.helpers.entity_component import EntityComponent
+from homeassistant.helpers.restore_state import RestoreEntity
 from homeassistant.helpers.template import Template
 from homeassistant.helpers.typing import ConfigType
+from homeassistant.util import dt as dt_util
 
 from .const import (  # noqa: F401
     ATTR_DATA,
     ATTR_MESSAGE,
+    ATTR_RECIPIENTS,
     ATTR_TARGET,
     ATTR_TITLE,
     DOMAIN,
     NOTIFY_SERVICE_SCHEMA,
     SERVICE_NOTIFY,
     SERVICE_PERSISTENT_NOTIFICATION,
+    SERVICE_SEND_MESSAGE,
 )
 from .legacy import (  # noqa: F401
     BaseNotificationService,
@@ -29,9 +41,17 @@ from .legacy import (  # noqa: F401
     check_templates_warn,
 )
 
+# mypy: disallow-any-generics
+
 # Platform specific data
 ATTR_TITLE_DEFAULT = "Home Assistant"
 
+ENTITY_ID_FORMAT = DOMAIN + ".{}"
+
+MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
+
+_LOGGER = logging.getLogger(__name__)
+
 PLATFORM_SCHEMA = vol.Schema(
     {vol.Required(CONF_PLATFORM): cv.string, vol.Optional(CONF_NAME): cv.string},
     extra=vol.ALLOW_EXTRA,
@@ -50,6 +70,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
         # legacy platforms to finish setting up.
         hass.async_create_task(setup, eager_start=True)
 
+    component = hass.data[DOMAIN] = EntityComponent[NotifyEntity](_LOGGER, DOMAIN, hass)
+    component.async_register_entity_service(
+        SERVICE_SEND_MESSAGE,
+        {vol.Required(ATTR_MESSAGE): cv.string},
+        "_async_send_message",
+    )
+
     async def persistent_notification(service: ServiceCall) -> None:
         """Send notification via the built-in persistent_notify integration."""
         message: Template = service.data[ATTR_MESSAGE]
@@ -79,3 +106,66 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
     )
 
     return True
+
+
+class NotifyEntityDescription(EntityDescription, frozen_or_thawed=True):
+    """A class that describes button entities."""
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Set up a config entry."""
+    component: EntityComponent[NotifyEntity] = hass.data[DOMAIN]
+    return await component.async_setup_entry(entry)
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Unload a config entry."""
+    component: EntityComponent[NotifyEntity] = hass.data[DOMAIN]
+    return await component.async_unload_entry(entry)
+
+
+class NotifyEntity(RestoreEntity):
+    """Representation of a notify entity."""
+
+    entity_description: NotifyEntityDescription
+    _attr_should_poll = False
+    _attr_device_class: None
+    _attr_state: None = None
+    __last_notified_isoformat: str | None = None
+
+    @cached_property
+    @final
+    @override
+    def state(self) -> str | None:
+        """Return the entity state."""
+        return self.__last_notified_isoformat
+
+    def __set_state(self, state: str | None) -> None:
+        """Invalidate the cache of the cached property."""
+        self.__dict__.pop("state", None)
+        self.__last_notified_isoformat = state
+
+    async def async_internal_added_to_hass(self) -> None:
+        """Call when the notify entity is added to hass."""
+        await super().async_internal_added_to_hass()
+        state = await self.async_get_last_state()
+        if state is not None and state.state not in (STATE_UNAVAILABLE, None):
+            self.__set_state(state.state)
+
+    @final
+    async def _async_send_message(self, **kwargs: Any) -> None:
+        """Send a notification message (from e.g., service call).
+
+        Should not be overridden, handle setting last notification timestamp.
+        """
+        self.__set_state(dt_util.utcnow().isoformat())
+        self.async_write_ha_state()
+        await self.async_send_message(**kwargs)
+
+    def send_message(self, message: str) -> None:
+        """Send a message."""
+        raise NotImplementedError
+
+    async def async_send_message(self, message: str) -> None:
+        """Send a message."""
+        await self.hass.async_add_executor_job(partial(self.send_message, message))
diff --git a/homeassistant/components/notify/const.py b/homeassistant/components/notify/const.py
index b653b5d1cbf..6cd957e3afe 100644
--- a/homeassistant/components/notify/const.py
+++ b/homeassistant/components/notify/const.py
@@ -11,9 +11,12 @@ ATTR_DATA = "data"
 # Text to notify user of
 ATTR_MESSAGE = "message"
 
-# Target of the notification (user, device, etc)
+# Target of the (legacy) notification (user, device, etc)
 ATTR_TARGET = "target"
 
+# Recipients for a notification
+ATTR_RECIPIENTS = "recipients"
+
 # Title of notification
 ATTR_TITLE = "title"
 
@@ -22,6 +25,7 @@ DOMAIN = "notify"
 LOGGER = logging.getLogger(__package__)
 
 SERVICE_NOTIFY = "notify"
+SERVICE_SEND_MESSAGE = "send_message"
 SERVICE_PERSISTENT_NOTIFICATION = "persistent_notification"
 
 NOTIFY_SERVICE_SCHEMA = vol.Schema(
diff --git a/homeassistant/components/notify/icons.json b/homeassistant/components/notify/icons.json
index 88577bc2356..ace8ee0c96b 100644
--- a/homeassistant/components/notify/icons.json
+++ b/homeassistant/components/notify/icons.json
@@ -1,6 +1,12 @@
 {
+  "entity_component": {
+    "_": {
+      "default": "mdi:message"
+    }
+  },
   "services": {
     "notify": "mdi:bell-ring",
-    "persistent_notification": "mdi:bell-badge"
+    "persistent_notification": "mdi:bell-badge",
+    "send_message": "mdi:message-arrow-right"
   }
 }
diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml
index 8d053e3af58..ae2a0254761 100644
--- a/homeassistant/components/notify/services.yaml
+++ b/homeassistant/components/notify/services.yaml
@@ -20,6 +20,16 @@ notify:
       selector:
         object:
 
+send_message:
+  target:
+    entity:
+      domain: notify
+  fields:
+    message:
+      required: true
+      selector:
+        text:
+
 persistent_notification:
   fields:
     message:
diff --git a/homeassistant/components/notify/strings.json b/homeassistant/components/notify/strings.json
index cff7b265c37..b0dca501509 100644
--- a/homeassistant/components/notify/strings.json
+++ b/homeassistant/components/notify/strings.json
@@ -1,5 +1,10 @@
 {
   "title": "Notifications",
+  "entity_component": {
+    "_": {
+      "name": "[%key:component::notify::title%]"
+    }
+  },
   "services": {
     "notify": {
       "name": "Send a notification",
@@ -23,6 +28,16 @@
         }
       }
     },
+    "send_message": {
+      "name": "Send a notification message",
+      "description": "Sends a notification message.",
+      "fields": {
+        "message": {
+          "name": "Message",
+          "description": "Your notification message."
+        }
+      }
+    },
     "persistent_notification": {
       "name": "Send a persistent notification",
       "description": "Sends a notification that is visible in the **Notifications** panel.",
diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py
index 9af02402bc0..31e0d3648db 100644
--- a/homeassistant/helpers/service.py
+++ b/homeassistant/helpers/service.py
@@ -93,6 +93,7 @@ def _base_components() -> dict[str, ModuleType]:
         light,
         lock,
         media_player,
+        notify,
         remote,
         siren,
         todo,
@@ -112,6 +113,7 @@ def _base_components() -> dict[str, ModuleType]:
         "light": light,
         "lock": lock,
         "media_player": media_player,
+        "notify": notify,
         "remote": remote,
         "siren": siren,
         "todo": todo,
diff --git a/tests/components/kitchen_sink/test_notify.py b/tests/components/kitchen_sink/test_notify.py
new file mode 100644
index 00000000000..6d02bacb7be
--- /dev/null
+++ b/tests/components/kitchen_sink/test_notify.py
@@ -0,0 +1,66 @@
+"""The tests for the demo button component."""
+
+from collections.abc import AsyncGenerator
+from unittest.mock import patch
+
+from freezegun.api import FrozenDateTimeFactory
+import pytest
+
+from homeassistant.components.kitchen_sink import DOMAIN
+from homeassistant.components.notify import (
+    DOMAIN as NOTIFY_DOMAIN,
+    SERVICE_SEND_MESSAGE,
+)
+from homeassistant.components.notify.const import ATTR_MESSAGE
+from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
+from homeassistant.core import HomeAssistant
+from homeassistant.setup import async_setup_component
+from homeassistant.util import dt as dt_util
+
+ENTITY_DIRECT_MESSAGE = "notify.mybox_personal_notifier"
+
+
+@pytest.fixture
+async def notify_only() -> AsyncGenerator[None, None]:
+    """Enable only the button platform."""
+    with patch(
+        "homeassistant.components.kitchen_sink.COMPONENTS_WITH_DEMO_PLATFORM",
+        [Platform.NOTIFY],
+    ):
+        yield
+
+
+@pytest.fixture(autouse=True)
+async def setup_comp(hass: HomeAssistant, notify_only: None):
+    """Set up demo component."""
+    assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
+    await hass.async_block_till_done()
+
+
+def test_setup_params(hass: HomeAssistant) -> None:
+    """Test the initial parameters."""
+    state = hass.states.get(ENTITY_DIRECT_MESSAGE)
+    assert state
+    assert state.state == STATE_UNKNOWN
+
+
+async def test_send_message(
+    hass: HomeAssistant, freezer: FrozenDateTimeFactory
+) -> None:
+    """Test pressing the button."""
+    state = hass.states.get(ENTITY_DIRECT_MESSAGE)
+    assert state
+    assert state.state == STATE_UNKNOWN
+
+    now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
+    freezer.move_to(now)
+    await hass.services.async_call(
+        NOTIFY_DOMAIN,
+        SERVICE_SEND_MESSAGE,
+        {ATTR_ENTITY_ID: ENTITY_DIRECT_MESSAGE, ATTR_MESSAGE: "You have an update!"},
+        blocking=True,
+    )
+
+    state = hass.states.get(ENTITY_DIRECT_MESSAGE)
+    assert state
+    assert state.state == now.isoformat()
diff --git a/tests/components/notify/conftest.py b/tests/components/notify/conftest.py
new file mode 100644
index 00000000000..23930132f7b
--- /dev/null
+++ b/tests/components/notify/conftest.py
@@ -0,0 +1,23 @@
+"""Fixtures for Notify platform tests."""
+
+from collections.abc import Generator
+
+import pytest
+
+from homeassistant.config_entries import ConfigFlow
+from homeassistant.core import HomeAssistant
+
+from tests.common import mock_config_flow, mock_platform
+
+
+class MockFlow(ConfigFlow):
+    """Test flow."""
+
+
+@pytest.fixture
+def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]:
+    """Mock config flow."""
+    mock_platform(hass, "test.config_flow")
+
+    with mock_config_flow("test", MockFlow):
+        yield
diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py
index 0b75a3c4691..26ed2ddc250 100644
--- a/tests/components/notify/test_init.py
+++ b/tests/components/notify/test_init.py
@@ -1,28 +1,216 @@
 """The tests for notify services that change targets."""
 
 import asyncio
+import copy
 from pathlib import Path
-from unittest.mock import Mock, patch
+from typing import Any
+from unittest.mock import MagicMock, Mock, patch
 
 import pytest
 import yaml
 
 from homeassistant import config as hass_config
 from homeassistant.components import notify
-from homeassistant.const import SERVICE_RELOAD, Platform
-from homeassistant.core import HomeAssistant
+from homeassistant.components.notify import (
+    DOMAIN,
+    SERVICE_SEND_MESSAGE,
+    NotifyEntity,
+    NotifyEntityDescription,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+    SERVICE_RELOAD,
+    STATE_UNAVAILABLE,
+    STATE_UNKNOWN,
+    Platform,
+)
+from homeassistant.core import HomeAssistant, State
 from homeassistant.helpers.discovery import async_load_platform
 from homeassistant.helpers.reload import async_setup_reload_service
 from homeassistant.setup import async_setup_component
 
-from tests.common import MockPlatform, async_get_persistent_notifications, mock_platform
+from tests.common import (
+    MockConfigEntry,
+    MockEntity,
+    MockModule,
+    MockPlatform,
+    async_get_persistent_notifications,
+    mock_integration,
+    mock_platform,
+    mock_restore_cache,
+    setup_test_component_platform,
+)
+
+TEST_KWARGS = {"message": "Test message"}
+
+
+class MockNotifyEntity(MockEntity, NotifyEntity):
+    """Mock Email notitier entity to use in tests."""
+
+    send_message_mock_calls = MagicMock()
+
+    async def async_send_message(self, message: str) -> None:
+        """Send a notification message."""
+        self.send_message_mock_calls(message=message)
+
+
+class MockNotifyEntityNonAsync(MockEntity, NotifyEntity):
+    """Mock Email notitier entity to use in tests."""
+
+    send_message_mock_calls = MagicMock()
+
+    def send_message(self, message: str) -> None:
+        """Send a notification message."""
+        self.send_message_mock_calls(message=message)
+
+
+async def help_async_setup_entry_init(
+    hass: HomeAssistant, config_entry: ConfigEntry
+) -> bool:
+    """Set up test config entry."""
+    await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
+    return True
+
+
+async def help_async_unload_entry(
+    hass: HomeAssistant, config_entry: ConfigEntry
+) -> bool:
+    """Unload test config emntry."""
+    return await hass.config_entries.async_unload_platforms(
+        config_entry, [Platform.NOTIFY]
+    )
+
+
+@pytest.mark.parametrize(
+    "entity",
+    [
+        MockNotifyEntityNonAsync(name="test", entity_id="notify.test"),
+        MockNotifyEntity(name="test", entity_id="notify.test"),
+    ],
+    ids=["non_async", "async"],
+)
+async def test_send_message_service(
+    hass: HomeAssistant, config_flow_fixture: None, entity: NotifyEntity
+) -> None:
+    """Test send_message service."""
+
+    config_entry = MockConfigEntry(domain="test")
+    config_entry.add_to_hass(hass)
+
+    mock_integration(
+        hass,
+        MockModule(
+            "test",
+            async_setup_entry=help_async_setup_entry_init,
+            async_unload_entry=help_async_unload_entry,
+        ),
+    )
+    setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True)
+    assert await hass.config_entries.async_setup(config_entry.entry_id)
+
+    state = hass.states.get("notify.test")
+    assert state.state is STATE_UNKNOWN
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_SEND_MESSAGE,
+        copy.deepcopy(TEST_KWARGS) | {"entity_id": "notify.test"},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+
+    entity.send_message_mock_calls.assert_called_once()
+
+    # Test unloading the entry succeeds
+    assert await hass.config_entries.async_unload(config_entry.entry_id)
+
+
+@pytest.mark.parametrize(
+    ("state", "init_state"),
+    [
+        ("2021-01-01T23:59:59+00:00", "2021-01-01T23:59:59+00:00"),
+        (STATE_UNAVAILABLE, STATE_UNKNOWN),
+    ],
+)
+async def test_restore_state(
+    hass: HomeAssistant, config_flow_fixture: None, state: str, init_state: str
+) -> None:
+    """Test we restore state integration."""
+    mock_restore_cache(hass, (State("notify.test", state),))
+
+    mock_integration(
+        hass,
+        MockModule(
+            "test",
+            async_setup_entry=help_async_setup_entry_init,
+        ),
+    )
+
+    entity = MockNotifyEntity(name="test", entity_id="notify.test")
+    setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True)
+
+    config_entry = MockConfigEntry(domain="test")
+    config_entry.add_to_hass(hass)
+    assert await hass.config_entries.async_setup(config_entry.entry_id)
+
+    state = hass.states.get("notify.test")
+    assert state is not None
+    assert state.state is init_state
+
+
+async def test_name(hass: HomeAssistant, config_flow_fixture: None) -> None:
+    """Test notify name."""
+
+    mock_platform(hass, "test.config_flow")
+    mock_integration(
+        hass,
+        MockModule(
+            "test",
+            async_setup_entry=help_async_setup_entry_init,
+        ),
+    )
+
+    # Unnamed notify entity -> no name
+    entity1 = NotifyEntity()
+    entity1.entity_id = "notify.test1"
+
+    # Unnamed notify entity and has_entity_name True -> unnamed
+    entity2 = NotifyEntity()
+    entity2.entity_id = "notify.test3"
+    entity2._attr_has_entity_name = True
+
+    # Named notify entity and has_entity_name True -> named
+    entity3 = NotifyEntity()
+    entity3.entity_id = "notify.test4"
+    entity3.entity_description = NotifyEntityDescription("test", has_entity_name=True)
+
+    setup_test_component_platform(
+        hass, DOMAIN, [entity1, entity2, entity3], from_config_entry=True
+    )
+
+    config_entry = MockConfigEntry(domain="test")
+    config_entry.add_to_hass(hass)
+    assert await hass.config_entries.async_setup(config_entry.entry_id)
+    await hass.async_block_till_done()
+
+    state = hass.states.get(entity1.entity_id)
+    assert state
+    assert state.attributes == {}
+
+    state = hass.states.get(entity2.entity_id)
+    assert state
+    assert state.attributes == {}
+
+    state = hass.states.get(entity3.entity_id)
+    assert state
+    assert state.attributes == {}
 
 
 class MockNotifyPlatform(MockPlatform):
-    """Help to set up test notify service."""
+    """Help to set up a legacy test notify service."""
 
-    def __init__(self, async_get_service=None, get_service=None):
-        """Return the notify service."""
+    def __init__(self, async_get_service: Any = None, get_service: Any = None) -> None:
+        """Return a legacy notify service."""
         super().__init__()
         if get_service:
             self.get_service = get_service
@@ -31,9 +219,13 @@ class MockNotifyPlatform(MockPlatform):
 
 
 def mock_notify_platform(
-    hass, tmp_path, integration="notify", async_get_service=None, get_service=None
+    hass: HomeAssistant,
+    tmp_path: Path,
+    integration: str = "notify",
+    async_get_service: Any = None,
+    get_service: Any = None,
 ):
-    """Specialize the mock platform for notify."""
+    """Specialize the mock platform for legacy notify service."""
     loaded_platform = MockNotifyPlatform(async_get_service, get_service)
     mock_platform(hass, f"{integration}.notify", loaded_platform)
 
@@ -41,7 +233,7 @@ def mock_notify_platform(
 
 
 async def test_same_targets(hass: HomeAssistant) -> None:
-    """Test not changing the targets in a notify service."""
+    """Test not changing the targets in a legacy notify service."""
     test = NotificationService(hass)
     await test.async_setup(hass, "notify", "test")
     await test.async_register_services()
@@ -56,7 +248,7 @@ async def test_same_targets(hass: HomeAssistant) -> None:
 
 
 async def test_change_targets(hass: HomeAssistant) -> None:
-    """Test changing the targets in a notify service."""
+    """Test changing the targets in a legacy notify service."""
     test = NotificationService(hass)
     await test.async_setup(hass, "notify", "test")
     await test.async_register_services()
@@ -73,7 +265,7 @@ async def test_change_targets(hass: HomeAssistant) -> None:
 
 
 async def test_add_targets(hass: HomeAssistant) -> None:
-    """Test adding the targets in a notify service."""
+    """Test adding the targets in a legacy notify service."""
     test = NotificationService(hass)
     await test.async_setup(hass, "notify", "test")
     await test.async_register_services()
@@ -90,7 +282,7 @@ async def test_add_targets(hass: HomeAssistant) -> None:
 
 
 async def test_remove_targets(hass: HomeAssistant) -> None:
-    """Test removing targets from the targets in a notify service."""
+    """Test removing targets from the targets in a legacy notify service."""
     test = NotificationService(hass)
     await test.async_setup(hass, "notify", "test")
     await test.async_register_services()
@@ -107,17 +299,22 @@ async def test_remove_targets(hass: HomeAssistant) -> None:
 
 
 class NotificationService(notify.BaseNotificationService):
-    """A test class for notification services."""
-
-    def __init__(self, hass, target_list={"a": 1, "b": 2}, name="notify"):
+    """A test class for legacy notification services."""
+
+    def __init__(
+        self,
+        hass: HomeAssistant,
+        target_list: dict[str, Any] | None = None,
+        name="notify",
+    ) -> None:
         """Initialize the service."""
 
-        async def _async_make_reloadable(hass):
+        async def _async_make_reloadable(hass: HomeAssistant) -> None:
             """Initialize the reload service."""
             await async_setup_reload_service(hass, name, [notify.DOMAIN])
 
         self.hass = hass
-        self.target_list = target_list
+        self.target_list = target_list or {"a": 1, "b": 2}
         hass.async_create_task(_async_make_reloadable(hass))
 
     @property
@@ -229,7 +426,7 @@ async def test_platform_setup_with_error(
 async def test_reload_with_notify_builtin_platform_reload(
     hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path
 ) -> None:
-    """Test reload using the notify platform reload method."""
+    """Test reload using the legacy notify platform reload method."""
 
     async def async_get_service(hass, config, discovery_info=None):
         """Get notify service for mocked platform."""
@@ -271,7 +468,7 @@ async def test_setup_platform_and_reload(
         return NotificationService(hass, targetlist, "testnotify")
 
     async def async_get_service2(hass, config, discovery_info=None):
-        """Get notify service for mocked platform."""
+        """Get legacy notify service for mocked platform."""
         get_service_called(config, discovery_info)
         targetlist = {"c": 3, "d": 4}
         return NotificationService(hass, targetlist, "testnotify2")
@@ -351,7 +548,7 @@ async def test_setup_platform_and_reload(
 async def test_setup_platform_before_notify_setup(
     hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path
 ) -> None:
-    """Test trying to setup a platform before notify is setup."""
+    """Test trying to setup a platform before legacy notify service is setup."""
     get_service_called = Mock()
 
     async def async_get_service(hass, config, discovery_info=None):
@@ -401,7 +598,7 @@ async def test_setup_platform_before_notify_setup(
 async def test_setup_platform_after_notify_setup(
     hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path
 ) -> None:
-    """Test trying to setup a platform after notify is setup."""
+    """Test trying to setup a platform after legacy notify service is set up."""
     get_service_called = Mock()
 
     async def async_get_service(hass, config, discovery_info=None):
-- 
GitLab