Add notify entity to System Bridge integration (#171736)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Manu
2026-05-21 17:49:25 +02:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent a1a76874fd
commit ad139b259b
6 changed files with 231 additions and 8 deletions
@@ -215,12 +215,9 @@ async def async_setup_entry(
entry.runtime_data = coordinator
# Set up all platforms except notify
await hass.config_entries.async_forward_entry_setups(
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# Set up notify platform
# Set up legacy notify platform
hass.async_create_task(
discovery.async_load_platform(
hass,
@@ -17,13 +17,13 @@ class SystemBridgeEntity(CoordinatorEntity[SystemBridgeDataUpdateCoordinator]):
self,
coordinator: SystemBridgeDataUpdateCoordinator,
api_port: int,
key: str,
key: str | None = None,
) -> None:
"""Initialize the System Bridge entity."""
super().__init__(coordinator)
self._hostname = coordinator.data.system.hostname
self._key = f"{self._hostname}_{key}"
self._key = f"{self._hostname}_{key}" if key is not None else self._hostname
self._configuration_url = (
f"http://{self._hostname}:{api_port}/app/settings.html"
)
@@ -3,6 +3,7 @@
import logging
from typing import Any
from systembridgeconnector.exceptions import ConnectionClosedException
from systembridgeconnector.models.notification import Notification
from homeassistant.components.notify import (
@@ -10,12 +11,18 @@ from homeassistant.components.notify import (
ATTR_TITLE,
ATTR_TITLE_DEFAULT,
BaseNotificationService,
NotifyEntity,
NotifyEntityFeature,
)
from homeassistant.const import ATTR_ICON, CONF_ENTITY_ID
from homeassistant.const import ATTR_ICON, CONF_ENTITY_ID, CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
from .entity import SystemBridgeEntity
_LOGGER = logging.getLogger(__name__)
@@ -25,6 +32,44 @@ ATTR_IMAGE = "image"
ATTR_TIMEOUT = "timeout"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: SystemBridgeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the notification entity platform."""
coordinator = config_entry.runtime_data
async_add_entities(
[SystemBridgeNotifyEntity(coordinator, config_entry.data[CONF_PORT])]
)
class SystemBridgeNotifyEntity(SystemBridgeEntity, NotifyEntity):
"""Representation of a notification entity."""
_attr_supported_features = NotifyEntityFeature.TITLE
_attr_name = None
async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a message via notify.send_message action."""
notification = Notification(
message=message, title=ATTR_TITLE_DEFAULT if title is None else title
)
try:
await self.coordinator.websocket_client.send_notification(notification)
except ConnectionClosedException as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="send_message_failed",
translation_placeholders={
"title": self.coordinator.config_entry.title,
"host": self.coordinator.config_entry.data[CONF_HOST],
},
) from e
async def async_get_service(
hass: HomeAssistant,
config: ConfigType,
@@ -114,6 +114,9 @@
"process_not_found": {
"message": "Could not find process with ID {id}."
},
"send_message_failed": {
"message": "Failed to send message to {title} ({host}) due to a connection error"
},
"timeout": {
"message": "A timeout occurred for {title} ({host})"
},
@@ -0,0 +1,52 @@
# serializer version: 1
# name: test_notify_platform[notify.hostname-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'notify',
'entity_category': None,
'entity_id': 'notify.hostname',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'system_bridge',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <NotifyEntityFeature: 1>,
'translation_key': None,
'unique_id': 'hostname',
'unit_of_measurement': None,
})
# ---
# name: test_notify_platform[notify.hostname-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'hostname',
'supported_features': <NotifyEntityFeature: 1>,
}),
'context': <ANY>,
'entity_id': 'notify.hostname',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
@@ -0,0 +1,126 @@
"""Tests for the System Bridge notify platform."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from systembridgeconnector.exceptions import ConnectionClosedException
from systembridgeconnector.models.notification import Notification
from homeassistant.components.notify import (
ATTR_MESSAGE,
ATTR_TITLE,
DOMAIN as NOTIFY_DOMAIN,
SERVICE_SEND_MESSAGE,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture(autouse=True)
def notify_only() -> Generator[None]:
"""Enable only the notify platform."""
with patch(
"homeassistant.components.system_bridge.PLATFORMS",
[Platform.NOTIFY],
):
yield
@pytest.mark.usefixtures("mock_version", "mock_websocket_client")
async def test_notify_platform(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Test setup of the notify platform."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.usefixtures("mock_version")
@pytest.mark.freeze_time("2009-02-13T23:31:30.000Z")
async def test_send_message(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_websocket_client: AsyncMock,
) -> None:
"""Test sending a message."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
state = hass.states.get("notify.hostname")
assert state
assert state.state == STATE_UNKNOWN
await hass.services.async_call(
NOTIFY_DOMAIN,
SERVICE_SEND_MESSAGE,
{
ATTR_ENTITY_ID: "notify.hostname",
ATTR_MESSAGE: "World",
ATTR_TITLE: "Hello",
},
blocking=True,
)
state = hass.states.get("notify.hostname")
assert state
assert state.state == "2009-02-13T23:31:30+00:00"
mock_websocket_client.send_notification.assert_awaited_once_with(
Notification(title="Hello", message="World")
)
@pytest.mark.usefixtures("mock_version")
@pytest.mark.freeze_time("2009-02-13T23:31:30.000Z")
async def test_send_message_exception(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_websocket_client: AsyncMock,
) -> None:
"""Test sending a message with exception."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
mock_websocket_client.send_notification.side_effect = ConnectionClosedException
with pytest.raises(HomeAssistantError) as e:
await hass.services.async_call(
NOTIFY_DOMAIN,
SERVICE_SEND_MESSAGE,
{
ATTR_ENTITY_ID: "notify.hostname",
ATTR_MESSAGE: "World",
ATTR_TITLE: "Hello",
},
blocking=True,
)
mock_websocket_client.send_notification.assert_awaited_once_with(
Notification(title="Hello", message="World")
)
assert e.value.translation_key == "send_message_failed"
assert e.value.translation_placeholders == {
"title": "TestSystem",
"host": "127.0.0.1",
}