diff --git a/homeassistant/components/demo/notify.py b/homeassistant/components/demo/notify.py index 94999d26d10..9aab2572957 100644 --- a/homeassistant/components/demo/notify.py +++ b/homeassistant/components/demo/notify.py @@ -2,7 +2,7 @@ from __future__ import annotations -from homeassistant.components.notify import DOMAIN, NotifyEntity +from homeassistant.components.notify import DOMAIN, NotifyEntity, NotifyEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo @@ -33,12 +33,15 @@ class DemoNotifyEntity(NotifyEntity): ) -> None: """Initialize the Demo button entity.""" self._attr_unique_id = unique_id + self._attr_supported_features = NotifyEntityFeature.TITLE self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id)}, name=device_name, ) - async def async_send_message(self, message: str) -> None: + async def async_send_message(self, message: str, title: str | None = None) -> None: """Send a message to a user.""" - event_notitifcation = {"message": message} - self.hass.bus.async_fire(EVENT_NOTIFY, event_notitifcation) + event_notification = {"message": message} + if title is not None: + event_notification["title"] = title + self.hass.bus.async_fire(EVENT_NOTIFY, event_notification) diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py index 787130c403f..f7e2f1549d1 100644 --- a/homeassistant/components/ecobee/notify.py +++ b/homeassistant/components/ecobee/notify.py @@ -85,6 +85,6 @@ class EcobeeNotifyEntity(EcobeeBaseEntity, NotifyEntity): f"{self.thermostat["identifier"]}_notify_{thermostat_index}" ) - def send_message(self, message: str) -> None: + def send_message(self, message: str, title: str | None = None) -> None: """Send a message.""" self.data.ecobee.send_message(self.thermostat_index, message) diff --git a/homeassistant/components/kitchen_sink/notify.py b/homeassistant/components/kitchen_sink/notify.py index b0418411145..fb34a36f0b7 100644 --- a/homeassistant/components/kitchen_sink/notify.py +++ b/homeassistant/components/kitchen_sink/notify.py @@ -3,7 +3,7 @@ from __future__ import annotations from homeassistant.components import persistent_notification -from homeassistant.components.notify import NotifyEntity +from homeassistant.components.notify import NotifyEntity, NotifyEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo @@ -25,6 +25,12 @@ async def async_setup_entry( device_name="MyBox", entity_name="Personal notifier", ), + DemoNotify( + unique_id="just_notify_me_title", + device_name="MyBox", + entity_name="Personal notifier with title", + supported_features=NotifyEntityFeature.TITLE, + ), ] ) @@ -40,15 +46,19 @@ class DemoNotify(NotifyEntity): unique_id: str, device_name: str, entity_name: str | None, + supported_features: NotifyEntityFeature = NotifyEntityFeature(0), ) -> None: """Initialize the Demo button entity.""" self._attr_unique_id = unique_id + self._attr_supported_features = supported_features 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: + async def async_send_message(self, message: str, title: str | None = None) -> None: """Send out a persistent notification.""" - persistent_notification.async_create(self.hass, message, "Demo notification") + persistent_notification.async_create( + self.hass, message, title or "Demo notification" + ) diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index f206ee62ece..9390acb2c85 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -108,6 +108,6 @@ class KNXNotify(KnxEntity, NotifyEntity): self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) - async def async_send_message(self, message: str) -> None: + async def async_send_message(self, message: str, title: str | None = None) -> None: """Send a notification to knx bus.""" await self._device.set(message) diff --git a/homeassistant/components/mqtt/notify.py b/homeassistant/components/mqtt/notify.py index b7a17f07f7f..07ab0050b45 100644 --- a/homeassistant/components/mqtt/notify.py +++ b/homeassistant/components/mqtt/notify.py @@ -83,7 +83,7 @@ class MqttNotify(MqttEntity, NotifyEntity): async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" - async def async_send_message(self, message: str) -> None: + async def async_send_message(self, message: str, title: str | None = None) -> None: """Send a message.""" payload = self._command_template(message) await self.async_publish( diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 81b7d300acc..ce4f778993c 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta +from enum import IntFlag from functools import cached_property, partial import logging from typing import Any, final, override @@ -58,6 +59,12 @@ PLATFORM_SCHEMA = vol.Schema( ) +class NotifyEntityFeature(IntFlag): + """Supported features of a notify entity.""" + + TITLE = 1 + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the notify services.""" @@ -73,7 +80,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component = hass.data[DOMAIN] = EntityComponent[NotifyEntity](_LOGGER, DOMAIN, hass) component.async_register_entity_service( SERVICE_SEND_MESSAGE, - {vol.Required(ATTR_MESSAGE): cv.string}, + { + vol.Required(ATTR_MESSAGE): cv.string, + vol.Optional(ATTR_TITLE): cv.string, + }, "_async_send_message", ) @@ -128,6 +138,7 @@ class NotifyEntity(RestoreEntity): """Representation of a notify entity.""" entity_description: NotifyEntityDescription + _attr_supported_features: NotifyEntityFeature = NotifyEntityFeature(0) _attr_should_poll = False _attr_device_class: None _attr_state: None = None @@ -162,10 +173,19 @@ class NotifyEntity(RestoreEntity): self.async_write_ha_state() await self.async_send_message(**kwargs) - def send_message(self, message: str) -> None: + def send_message(self, message: str, title: str | None = None) -> None: """Send a message.""" raise NotImplementedError - async def async_send_message(self, message: str) -> None: + async def async_send_message(self, message: str, title: str | None = None) -> None: """Send a message.""" - await self.hass.async_add_executor_job(partial(self.send_message, message)) + kwargs: dict[str, Any] = {} + if ( + title is not None + and self.supported_features + and self.supported_features & NotifyEntityFeature.TITLE + ): + kwargs[ATTR_TITLE] = title + await self.hass.async_add_executor_job( + partial(self.send_message, message, **kwargs) + ) diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index ae2a0254761..c4778b10618 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -29,6 +29,13 @@ send_message: required: true selector: text: + title: + required: false + selector: + text: + filter: + supported_features: + - notify.NotifyEntityFeature.TITLE persistent_notification: fields: diff --git a/homeassistant/components/notify/strings.json b/homeassistant/components/notify/strings.json index b0dca501509..f6ac8c848f1 100644 --- a/homeassistant/components/notify/strings.json +++ b/homeassistant/components/notify/strings.json @@ -35,6 +35,10 @@ "message": { "name": "Message", "description": "Your notification message." + }, + "title": { + "name": "Title", + "description": "Title for your notification message." } } }, diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index c4db601fac6..a45ba2d1129 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -98,6 +98,7 @@ def _entity_features() -> dict[str, type[IntFlag]]: from homeassistant.components.light import LightEntityFeature from homeassistant.components.lock import LockEntityFeature from homeassistant.components.media_player import MediaPlayerEntityFeature + from homeassistant.components.notify import NotifyEntityFeature from homeassistant.components.remote import RemoteEntityFeature from homeassistant.components.siren import SirenEntityFeature from homeassistant.components.todo import TodoListEntityFeature @@ -119,6 +120,7 @@ def _entity_features() -> dict[str, type[IntFlag]]: "LightEntityFeature": LightEntityFeature, "LockEntityFeature": LockEntityFeature, "MediaPlayerEntityFeature": MediaPlayerEntityFeature, + "NotifyEntityFeature": NotifyEntityFeature, "RemoteEntityFeature": RemoteEntityFeature, "SirenEntityFeature": SirenEntityFeature, "TodoListEntityFeature": TodoListEntityFeature, diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index b0536873d66..50730fb6c1e 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -69,7 +69,17 @@ async def test_sending_message(hass: HomeAssistant, events: list[Event]) -> None await hass.services.async_call(notify.DOMAIN, notify.SERVICE_SEND_MESSAGE, data) await hass.async_block_till_done() last_event = events[-1] - assert last_event.data[notify.ATTR_MESSAGE] == "Test message" + assert last_event.data == {notify.ATTR_MESSAGE: "Test message"} + + data[notify.ATTR_TITLE] = "My title" + # Test with Title + await hass.services.async_call(notify.DOMAIN, notify.SERVICE_SEND_MESSAGE, data) + await hass.async_block_till_done() + last_event = events[-1] + assert last_event.data == { + notify.ATTR_MESSAGE: "Test message", + notify.ATTR_TITLE: "My title", + } async def test_calling_notify_from_script_loaded_from_yaml( diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index 1ecfc0d9ecf..cfafae28b6e 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -12,6 +12,7 @@ from homeassistant.components.notify import ( SERVICE_SEND_MESSAGE, NotifyEntity, NotifyEntityDescription, + NotifyEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform @@ -27,7 +28,8 @@ from tests.common import ( setup_test_component_platform, ) -TEST_KWARGS = {"message": "Test message"} +TEST_KWARGS = {notify.ATTR_MESSAGE: "Test message"} +TEST_KWARGS_TITLE = {notify.ATTR_MESSAGE: "Test message", notify.ATTR_TITLE: "My title"} class MockNotifyEntity(MockEntity, NotifyEntity): @@ -35,9 +37,9 @@ class MockNotifyEntity(MockEntity, NotifyEntity): send_message_mock_calls = MagicMock() - async def async_send_message(self, message: str) -> None: + async def async_send_message(self, message: str, title: str | None = None) -> None: """Send a notification message.""" - self.send_message_mock_calls(message=message) + self.send_message_mock_calls(message, title=title) class MockNotifyEntityNonAsync(MockEntity, NotifyEntity): @@ -45,9 +47,9 @@ class MockNotifyEntityNonAsync(MockEntity, NotifyEntity): send_message_mock_calls = MagicMock() - def send_message(self, message: str) -> None: + def send_message(self, message: str, title: str | None = None) -> None: """Send a notification message.""" - self.send_message_mock_calls(message=message) + self.send_message_mock_calls(message, title=title) async def help_async_setup_entry_init( @@ -132,6 +134,58 @@ async def test_send_message_service( assert await hass.config_entries.async_unload(config_entry.entry_id) +@pytest.mark.parametrize( + "entity", + [ + MockNotifyEntityNonAsync( + name="test", + entity_id="notify.test", + supported_features=NotifyEntityFeature.TITLE, + ), + MockNotifyEntity( + name="test", + entity_id="notify.test", + supported_features=NotifyEntityFeature.TITLE, + ), + ], + ids=["non_async", "async"], +) +async def test_send_message_service_with_title( + 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_TITLE) | {"entity_id": "notify.test"}, + blocking=True, + ) + await hass.async_block_till_done() + + entity.send_message_mock_calls.assert_called_once_with( + TEST_KWARGS_TITLE[notify.ATTR_MESSAGE], + title=TEST_KWARGS_TITLE[notify.ATTR_TITLE], + ) + + @pytest.mark.parametrize( ("state", "init_state"), [ @@ -202,12 +256,12 @@ async def test_name(hass: HomeAssistant, config_flow_fixture: None) -> None: state = hass.states.get(entity1.entity_id) assert state - assert state.attributes == {} + assert state.attributes == {"supported_features": NotifyEntityFeature(0)} state = hass.states.get(entity2.entity_id) assert state - assert state.attributes == {} + assert state.attributes == {"supported_features": NotifyEntityFeature(0)} state = hass.states.get(entity3.entity_id) assert state - assert state.attributes == {} + assert state.attributes == {"supported_features": NotifyEntityFeature(0)}