Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add title feature to notify entity platform #116426

Merged
merged 8 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 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}
jbouwh marked this conversation as resolved.
Show resolved Hide resolved
if title is not None:
event_notitifcation["title"] = title
jbouwh marked this conversation as resolved.
Show resolved Hide resolved
self.hass.bus.async_fire(EVENT_NOTIFY, event_notitifcation)
jbouwh marked this conversation as resolved.
Show resolved Hide resolved
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
30 changes: 26 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 @@ -57,6 +58,17 @@
extra=vol.ALLOW_EXTRA,
)

NOTIFY_SEND_MESSAGE_SERVICE_SCHEMA = {
jbouwh marked this conversation as resolved.
Show resolved Hide resolved
vol.Required(ATTR_MESSAGE): cv.string,
vol.Optional(ATTR_TITLE): cv.string,
}


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 +85,7 @@ 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},
NOTIFY_SEND_MESSAGE_SERVICE_SCHEMA,
"_async_send_message",
)

Expand Down Expand Up @@ -128,6 +140,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 +175,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)}