diff --git a/homeassistant/components/nfandroidtv/__init__.py b/homeassistant/components/nfandroidtv/__init__.py index aae4b9d43c32..38688c211526 100644 --- a/homeassistant/components/nfandroidtv/__init__.py +++ b/homeassistant/components/nfandroidtv/__init__.py @@ -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) diff --git a/homeassistant/components/nfandroidtv/icons.json b/homeassistant/components/nfandroidtv/icons.json new file mode 100644 index 000000000000..e2856ffa9f8e --- /dev/null +++ b/homeassistant/components/nfandroidtv/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "notify": { + "notify": { + "default": "mdi:television" + } + } + } +} diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index 64a95db66344..763335ac6ff9 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -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, diff --git a/homeassistant/components/nfandroidtv/strings.json b/homeassistant/components/nfandroidtv/strings.json index 5cf31fa863ab..61a1620dfa2e 100644 --- a/homeassistant/components/nfandroidtv/strings.json +++ b/homeassistant/components/nfandroidtv/strings.json @@ -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}" } } } diff --git a/tests/components/nfandroidtv/conftest.py b/tests/components/nfandroidtv/conftest.py index 6129b36ce93b..5a8140bedf00 100644 --- a/tests/components/nfandroidtv/conftest.py +++ b/tests/components/nfandroidtv/conftest.py @@ -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 diff --git a/tests/components/nfandroidtv/snapshots/test_notify.ambr b/tests/components/nfandroidtv/snapshots/test_notify.ambr new file mode 100644 index 000000000000..e0c6fcf12aaa --- /dev/null +++ b/tests/components/nfandroidtv/snapshots/test_notify.ambr @@ -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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + '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': , + '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': , + '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': , + }), + 'context': , + 'entity_id': 'notify.android_tv_fire_tv_1_2_3_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/nfandroidtv/test_init.py b/tests/components/nfandroidtv/test_init.py new file mode 100644 index 000000000000..01f2809a5b7a --- /dev/null +++ b/tests/components/nfandroidtv/test_init.py @@ -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 diff --git a/tests/components/nfandroidtv/test_notify.py b/tests/components/nfandroidtv/test_notify.py new file mode 100644 index 000000000000..fd0d82b7e1b3 --- /dev/null +++ b/tests/components/nfandroidtv/test_notify.py @@ -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" + )