Add notify entities to Notifications for Android TV / Fire TV (#169087)

This commit is contained in:
Manu
2026-06-22 23:36:45 +02:00
committed by GitHub
parent 312f4c8c35
commit 7e07f2ab87
8 changed files with 319 additions and 7 deletions
@@ -1,16 +1,23 @@
"""The NFAndroidTV integration."""
import logging
from notifications_android_tv.notifications import ConnectError, Notifications
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, Platform
from homeassistant.const import CONF_HOST, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.typing import ConfigType
from .const import DATA_HASS_CONFIG, DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.NOTIFY]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
type NFAndroidTVConfigEntry = ConfigEntry[Notifications]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@@ -20,9 +27,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: NFAndroidTVConfigEntry) -> bool:
"""Set up NFAndroidTV from a config entry."""
try:
client = await hass.async_add_executor_job(Notifications, entry.data[CONF_HOST])
except ConnectError as e:
_LOGGER.debug("Full exception:", exc_info=True)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="setup_connection_error",
translation_placeholders={CONF_NAME: entry.title},
) from e
entry.runtime_data = client
hass.async_create_task(
discovery.async_load_platform(
hass,
@@ -33,9 +52,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: NFAndroidTVConfigEntry
) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -0,0 +1,9 @@
{
"entity": {
"notify": {
"notify": {
"default": "mdi:television"
}
}
}
}
+48 -1
View File
@@ -14,13 +14,18 @@ from homeassistant.components.notify import (
ATTR_TITLE,
ATTR_TITLE_DEFAULT,
BaseNotificationService,
NotifyEntity,
NotifyEntityFeature,
)
from homeassistant.const import ATTR_ICON, CONF_HOST
from homeassistant.const import ATTR_ICON, CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import NFAndroidTVConfigEntry
from .const import (
ATTR_COLOR,
ATTR_DURATION,
@@ -48,6 +53,48 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: NFAndroidTVConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the notify platform."""
async_add_entities([NFAndroidTVNotifyEntity(config_entry)])
class NFAndroidTVNotifyEntity(NotifyEntity):
"""Representation of a notify entity."""
_attr_supported_features = NotifyEntityFeature.TITLE
_attr_translation_key = "notify"
_attr_has_entity_name = True
_attr_name = None
def __init__(self, entry: NFAndroidTVConfigEntry) -> None:
"""Initialize the entity."""
self._attr_unique_id = entry.entry_id
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
name=entry.title,
model="Notifications",
manufacturer="dream apps",
identifiers={(DOMAIN, entry.entry_id)},
)
self.entry = entry
self.client = entry.runtime_data
def send_message(self, message: str, title: str | None = None) -> None:
"""Send a message via notify.send_message action."""
try:
self.client.send(message=message, title=title)
except ConnectError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="notify_connection_error",
translation_placeholders={CONF_NAME: self.entry.title},
) from e
async def async_get_service(
hass: HomeAssistant,
config: ConfigType,
@@ -39,6 +39,12 @@
},
"invalid_notification_image": {
"message": "Invalid image data provided. Got {type}"
},
"notify_connection_error": {
"message": "Failed to send notification to {name} due to a connection error"
},
"setup_connection_error": {
"message": "Failed to connect to {name}"
}
}
}
+10 -3
View File
@@ -17,9 +17,16 @@ from tests.common import MockConfigEntry
def mock_notifications_android_tv() -> Generator[MagicMock]:
"""Mock notifications_android_tv."""
with patch(
"homeassistant.components.nfandroidtv.config_flow.Notifications", autospec=True
) as mock_client:
with (
patch(
"homeassistant.components.nfandroidtv.Notifications",
autospec=True,
) as mock_client,
patch(
"homeassistant.components.nfandroidtv.config_flow.Notifications",
new=mock_client,
),
):
client = mock_client.return_value
client.cls = mock_client
@@ -0,0 +1,52 @@
# serializer version: 1
# name: test_notify_platform[notify.android_tv_fire_tv_1_2_3_4-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.android_tv_fire_tv_1_2_3_4',
'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': 'nfandroidtv',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <NotifyEntityFeature: 1>,
'translation_key': 'notify',
'unique_id': '123456789',
'unit_of_measurement': None,
})
# ---
# name: test_notify_platform[notify.android_tv_fire_tv_1_2_3_4-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Android TV / Fire TV (1.2.3.4)',
'supported_features': <NotifyEntityFeature: 1>,
}),
'context': <ANY>,
'entity_id': 'notify.android_tv_fire_tv_1_2_3_4',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
+43
View File
@@ -0,0 +1,43 @@
"""Tests for the Notifications for Android TV / Fire TV integration."""
from unittest.mock import AsyncMock
from notifications_android_tv.notifications import ConnectError
import pytest
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("mock_notifications_android_tv")
async def test_entry_setup_unload(
hass: HomeAssistant, config_entry: MockConfigEntry
) -> None:
"""Test integration setup and unload."""
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.NOT_LOADED
async def test_config_entry_not_ready(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_notifications_android_tv: AsyncMock,
) -> None:
"""Test config entry not ready."""
mock_notifications_android_tv.cls.side_effect = ConnectError
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.SETUP_RETRY
+125
View File
@@ -0,0 +1,125 @@
"""Tests for the Notifications for Android TV / Fire TV notify platform."""
from collections.abc import AsyncGenerator
from unittest.mock import patch
from notifications_android_tv.notifications import ConnectError
import pytest
from syrupy.assertion import SnapshotAssertion
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, CONF_NAME, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import NAME
from tests.common import AsyncMock, MockConfigEntry, snapshot_platform
@pytest.fixture(autouse=True)
async def notify_only() -> AsyncGenerator[None]:
"""Enable only the notify platform."""
with patch(
"homeassistant.components.nfandroidtv.PLATFORMS",
[Platform.NOTIFY],
):
yield
@pytest.mark.usefixtures("mock_notifications_android_tv")
async def test_notify_platform(
hass: HomeAssistant,
config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Test setup of the notify platform."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
@pytest.mark.freeze_time("1970-01-01T00:00:00+00:00")
async def test_send_message(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_notifications_android_tv: AsyncMock,
) -> None:
"""Test sending a message."""
entity_id = "notify.android_tv_fire_tv_1_2_3_4"
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_UNKNOWN
await hass.services.async_call(
NOTIFY_DOMAIN,
SERVICE_SEND_MESSAGE,
{
ATTR_ENTITY_ID: entity_id,
ATTR_MESSAGE: "Hello",
ATTR_TITLE: "World",
},
blocking=True,
)
state = hass.states.get(entity_id)
assert state
assert state.state == "1970-01-01T00:00:00+00:00"
mock_notifications_android_tv.send.assert_called_once_with(
message="Hello", title="World"
)
async def test_send_message_exception(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_notifications_android_tv: AsyncMock,
) -> None:
"""Test sending a message exception."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
mock_notifications_android_tv.send.side_effect = ConnectError
with pytest.raises(HomeAssistantError) as err:
await hass.services.async_call(
NOTIFY_DOMAIN,
SERVICE_SEND_MESSAGE,
{
ATTR_ENTITY_ID: "notify.android_tv_fire_tv_1_2_3_4",
ATTR_MESSAGE: "Hello",
ATTR_TITLE: "World",
},
blocking=True,
)
assert err.value.translation_key == "notify_connection_error"
assert err.value.translation_placeholders == {CONF_NAME: NAME}
mock_notifications_android_tv.send.assert_called_once_with(
message="Hello", title="World"
)