Skip to content

Commit

Permalink
Add title feature to notify entity platform (#116426)
Browse files Browse the repository at this point in the history
* Add title feature to notify entity platform

* Add overload variants

* Remove overloads, update signatures

* Improve test coverage

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <[email protected]>

* Do not use const

* fix typo

---------

Co-authored-by: Martin Hjelmare <[email protected]>
  • Loading branch information
jbouwh and MartinHjelmare committed May 3, 2024
1 parent ecdad19 commit 84308c9
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 23 deletions.
11 changes: 7 additions & 4 deletions homeassistant/components/demo/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -33,12 +33,15 @@ def __init__(
) -> 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)
2 changes: 1 addition & 1 deletion homeassistant/components/ecobee/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,6 @@ def __init__(self, data: EcobeeData, thermostat_index: int) -> None:
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)
16 changes: 13 additions & 3 deletions homeassistant/components/kitchen_sink/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
),
]
)

Expand All @@ -40,15 +46,19 @@ def __init__(
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"
)
2 changes: 1 addition & 1 deletion homeassistant/components/knx/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,6 @@ def __init__(self, xknx: XKNX, config: ConfigType) -> None:
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)
2 changes: 1 addition & 1 deletion homeassistant/components/mqtt/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def _prepare_subscribe_topics(self) -> None:
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(
Expand Down
28 changes: 24 additions & 4 deletions homeassistant/components/notify/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -58,6 +59,12 @@
)


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."""

Expand All @@ -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",
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -162,10 +173,19 @@ async def _async_send_message(self, **kwargs: Any) -> None:
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)
)
7 changes: 7 additions & 0 deletions homeassistant/components/notify/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ send_message:
required: true
selector:
text:
title:
required: false
selector:
text:
filter:
supported_features:
- notify.NotifyEntityFeature.TITLE

persistent_notification:
fields:
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/notify/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
"message": {
"name": "Message",
"description": "Your notification message."
},
"title": {
"name": "Title",
"description": "Title for your notification message."
}
}
},
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/helpers/selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
12 changes: 11 additions & 1 deletion tests/components/demo/test_notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
70 changes: 62 additions & 8 deletions tests/components/notify/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
SERVICE_SEND_MESSAGE,
NotifyEntity,
NotifyEntityDescription,
NotifyEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
Expand All @@ -27,27 +28,28 @@
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):
"""Mock Email notitier entity to use in tests."""

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):
"""Mock Email notitier entity to use in tests."""

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(
Expand Down Expand Up @@ -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"),
[
Expand Down Expand Up @@ -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)}

0 comments on commit 84308c9

Please sign in to comment.