From 48485fc2bf64bd7f850aab7eb6593ac95c67f5e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 May 2023 22:09:13 -0500 Subject: [PATCH] Complete persistent notifications migration (#92828) * Complete migration of persistent notifications Persistent notifications are no longer stored in the state machine and no longer fire events * Complete migration of persistent notifications Persistent notifications are no longer stored in the state machine and no longer fire events * fixes * fixes * fixes * ws test * update tests * update tests * fix more tests * fix more tests * more fixes * fix * fix person * fix person * keep whitelist * use singleton --- .../persistent_notification/__init__.py | 188 ++++++++++-------- homeassistant/components/person/__init__.py | 6 + tests/common.py | 10 +- tests/components/govee_ble/test_sensor.py | 2 +- tests/components/http/test_ban.py | 8 +- tests/components/hue/test_init.py | 8 +- tests/components/humidifier/test_recorder.py | 2 +- tests/components/netatmo/test_init.py | 14 +- tests/components/notify/test_init.py | 5 +- .../notify/test_persistent_notification.py | 12 +- .../persistent_notification/test_init.py | 108 ++++++---- tests/components/person/conftest.py | 45 +++++ tests/components/person/test_init.py | 41 +--- tests/components/person/test_recorder.py | 4 +- tests/components/safe_mode/test_init.py | 5 +- tests/test_config_entries.py | 42 ++-- tests/test_loader.py | 5 +- 17 files changed, 310 insertions(+), 195 deletions(-) create mode 100644 tests/components/person/conftest.py diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 36b496ddde2..042a6acfabb 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -2,44 +2,67 @@ from __future__ import annotations from collections.abc import Mapping +from datetime import datetime import logging -from typing import Any +from typing import Any, Final, TypedDict import voluptuous as vol +from homeassistant.backports.enum import StrEnum from homeassistant.components import websocket_api -from homeassistant.const import ATTR_FRIENDLY_NAME -from homeassistant.core import Context, HomeAssistant, ServiceCall, callback -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers import config_validation as cv, singleton +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from homeassistant.util import slugify import homeassistant.util.dt as dt_util - -ATTR_CREATED_AT = "created_at" -ATTR_MESSAGE = "message" -ATTR_NOTIFICATION_ID = "notification_id" -ATTR_TITLE = "title" -ATTR_STATUS = "status" +from homeassistant.util.uuid import random_uuid_hex DOMAIN = "persistent_notification" -ENTITY_ID_FORMAT = DOMAIN + ".{}" +ATTR_CREATED_AT: Final = "created_at" +ATTR_MESSAGE: Final = "message" +ATTR_NOTIFICATION_ID: Final = "notification_id" +ATTR_TITLE: Final = "title" +ATTR_STATUS: Final = "status" +STATUS_UNREAD = "unread" +STATUS_READ = "read" + +# Remove EVENT_PERSISTENT_NOTIFICATIONS_UPDATED in Home Assistant 2023.9 EVENT_PERSISTENT_NOTIFICATIONS_UPDATED = "persistent_notifications_updated" + +class Notification(TypedDict): + """Persistent notification.""" + + created_at: datetime + message: str + notification_id: str + title: str | None + status: str + + +class UpdateType(StrEnum): + """Persistent notification update type.""" + + CURRENT = "current" + ADDED = "added" + REMOVED = "removed" + UPDATED = "updated" + + +SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED = "persistent_notifications_updated" + SCHEMA_SERVICE_NOTIFICATION = vol.Schema( {vol.Required(ATTR_NOTIFICATION_ID): cv.string} ) -DEFAULT_OBJECT_ID = "notification" _LOGGER = logging.getLogger(__name__) -STATE = "notifying" -STATUS_UNREAD = "unread" -STATUS_READ = "read" - @bind_hass def create( @@ -65,31 +88,12 @@ def async_create( message: str, title: str | None = None, notification_id: str | None = None, - *, - context: Context | None = None, ) -> None: """Generate a notification.""" - if (notifications := hass.data.get(DOMAIN)) is None: - notifications = hass.data[DOMAIN] = {} - - if notification_id is not None: - entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) - else: - entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, DEFAULT_OBJECT_ID, hass=hass - ) - notification_id = entity_id.split(".")[1] - - attr: dict[str, str] = {ATTR_MESSAGE: message} - if title is not None: - attr[ATTR_TITLE] = title - attr[ATTR_FRIENDLY_NAME] = title - - hass.states.async_set(entity_id, STATE, attr, context=context) - - # Store notification and fire event - # This will eventually replace state machine storage - notifications[entity_id] = { + notifications = _async_get_or_create_notifications(hass) + if notification_id is None: + notification_id = random_uuid_hex() + notifications[notification_id] = { ATTR_MESSAGE: message, ATTR_NOTIFICATION_ID: notification_id, ATTR_STATUS: STATUS_UNREAD, @@ -97,32 +101,39 @@ def async_create( ATTR_CREATED_AT: dt_util.utcnow(), } - hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, context=context) + async_dispatcher_send( + hass, + SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED, + UpdateType.ADDED, + {notification_id: notifications[notification_id]}, + ) + + +@callback +@singleton.singleton(DOMAIN) +def _async_get_or_create_notifications(hass: HomeAssistant) -> dict[str, Notification]: + """Get or create notifications data.""" + return {} @callback @bind_hass -def async_dismiss( - hass: HomeAssistant, notification_id: str, *, context: Context | None = None -) -> None: +def async_dismiss(hass: HomeAssistant, notification_id: str) -> None: """Remove a notification.""" - if (notifications := hass.data.get(DOMAIN)) is None: - notifications = hass.data[DOMAIN] = {} - - entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) - - if entity_id not in notifications: + notifications = _async_get_or_create_notifications(hass) + if not (notification := notifications.pop(notification_id, None)): return - - hass.states.async_remove(entity_id, context) - - del notifications[entity_id] - hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) + async_dispatcher_send( + hass, + SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED, + UpdateType.REMOVED, + {notification_id: notification}, + ) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the persistent notification component.""" - notifications = hass.data.setdefault(DOMAIN, {}) + notifications = _async_get_or_create_notifications(hass) @callback def create_service(call: ServiceCall) -> None: @@ -132,21 +143,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: call.data[ATTR_MESSAGE], call.data.get(ATTR_TITLE), call.data.get(ATTR_NOTIFICATION_ID), - context=call.context, ) @callback def dismiss_service(call: ServiceCall) -> None: """Handle the dismiss notification service call.""" - async_dismiss(hass, call.data[ATTR_NOTIFICATION_ID], context=call.context) + async_dismiss(hass, call.data[ATTR_NOTIFICATION_ID]) @callback def mark_read_service(call: ServiceCall) -> None: """Handle the mark_read notification service call.""" notification_id = call.data.get(ATTR_NOTIFICATION_ID) - entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) - - if entity_id not in notifications: + if notification_id not in notifications: _LOGGER.error( ( "Marking persistent_notification read failed: " @@ -156,9 +164,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) return - notifications[entity_id][ATTR_STATUS] = STATUS_READ - hass.bus.async_fire( - EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, context=call.context + notification = notifications[notification_id] + notification[ATTR_STATUS] = STATUS_READ + async_dispatcher_send( + hass, + SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED, + UpdateType.UPDATED, + {notification_id: notification}, ) hass.services.async_register( @@ -183,6 +195,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) websocket_api.async_register_command(hass, websocket_get_notifications) + websocket_api.async_register_command(hass, websocket_subscribe_notifications) return True @@ -197,19 +210,36 @@ def websocket_get_notifications( """Return a list of persistent_notifications.""" connection.send_message( websocket_api.result_message( - msg["id"], - [ - { - key: data[key] - for key in ( - ATTR_NOTIFICATION_ID, - ATTR_MESSAGE, - ATTR_STATUS, - ATTR_TITLE, - ATTR_CREATED_AT, - ) - } - for data in hass.data[DOMAIN].values() - ], + msg["id"], list(_async_get_or_create_notifications(hass).values()) ) ) + + +@callback +@websocket_api.websocket_command( + {vol.Required("type"): "persistent_notification/subscribe"} +) +def websocket_subscribe_notifications( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: Mapping[str, Any], +) -> None: + """Return a list of persistent_notifications.""" + notifications = _async_get_or_create_notifications(hass) + msg_id = msg["id"] + + @callback + def _async_send_notification_update( + update_type: UpdateType, notifications: dict[str, Notification] + ) -> None: + connection.send_message( + websocket_api.event_message( + msg["id"], {"type": update_type, "notifications": notifications} + ) + ) + + connection.subscriptions[msg_id] = async_dispatcher_connect( + hass, SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED, _async_send_notification_update + ) + connection.send_result(msg_id) + _async_send_notification_update(UpdateType.CURRENT, notifications) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index fe6925b4847..ea325380e11 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -47,6 +47,9 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.integration_platform import ( + async_process_integration_platform_for_component, +) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType @@ -330,6 +333,9 @@ The following persons point at invalid users: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the person component.""" + # Process integration platforms right away since + # we will create entities before firing EVENT_COMPONENT_LOADED + await async_process_integration_platform_for_component(hass, DOMAIN) entity_component = EntityComponent[Person](_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() yaml_collection = collection.YamlCollection( diff --git a/tests/common.py b/tests/common.py index 632294a50fb..65e119c8c49 100644 --- a/tests/common.py +++ b/tests/common.py @@ -29,7 +29,7 @@ from homeassistant.auth import ( providers as auth_providers, ) from homeassistant.auth.permissions import system_policies -from homeassistant.components import device_automation +from homeassistant.components import device_automation, persistent_notification as pn from homeassistant.components.device_automation import ( # noqa: F401 _async_get_device_automation_capabilities as async_get_device_automation_capabilities, ) @@ -1396,3 +1396,11 @@ def raise_contains_mocks(val: Any) -> None: if isinstance(val, list): for dict_value in val: raise_contains_mocks(dict_value) + + +@callback +def async_get_persistent_notifications( + hass: HomeAssistant, +) -> dict[str, pn.Notification]: + """Get the current persistent notifications.""" + return pn._async_get_or_create_notifications(hass) diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index e9e66ba73e8..1408a35142a 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -55,7 +55,7 @@ async def test_gvh5178_error(hass: HomeAssistant) -> None: assert len(hass.states.async_all()) == 0 inject_bluetooth_service_info(hass, GVH5178_SERVICE_INFO_ERROR) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 3 temp_sensor = hass.states.get("sensor.b51782bc8_remote_temperature") assert temp_sensor.state == STATE_UNAVAILABLE diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index b34c8866ff1..e6e237a7b67 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -25,6 +25,7 @@ from homeassistant.setup import async_setup_component from . import mock_real_ip +from tests.common import async_get_persistent_notifications from tests.typing import ClientSessionGenerator SUPERVISOR_IP = "1.2.3.4" @@ -307,11 +308,10 @@ async def test_ip_bans_file_creation( assert resp.status == HTTPStatus.FORBIDDEN assert m_open.call_count == 1 + notifications = async_get_persistent_notifications(hass) + assert len(notifications) == 2 assert ( - len(notifications := hass.states.async_all("persistent_notification")) == 2 - ) - assert ( - notifications[0].attributes["message"] + notifications["http-login"]["message"] == "Login attempt or request with invalid authentication from example.com (200.201.202.204). See the log for details." ) diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 8a635497237..bdca6ee135c 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -9,7 +9,7 @@ from homeassistant.components import hue from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_get_persistent_notifications @pytest.fixture @@ -162,6 +162,6 @@ async def test_security_vuln_check(hass: HomeAssistant) -> None: await hass.async_block_till_done() - state = hass.states.get("persistent_notification.hue_hub_firmware") - assert state is not None - assert "CVE-2020-6007" in state.attributes["message"] + notifications = async_get_persistent_notifications(hass) + assert "hue_hub_firmware" in notifications + assert "CVE-2020-6007" in notifications["hue_hub_firmware"]["message"] diff --git a/tests/components/humidifier/test_recorder.py b/tests/components/humidifier/test_recorder.py index 4ac765d7f50..0a38ff05080 100644 --- a/tests/components/humidifier/test_recorder.py +++ b/tests/components/humidifier/test_recorder.py @@ -34,7 +34,7 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) states = await hass.async_add_executor_job( get_significant_states, hass, now, None, hass.states.async_entity_ids() ) - assert len(states) > 1 + assert len(states) >= 1 for entity_states in states.values(): for state in entity_states: assert ATTR_MIN_HUMIDITY not in state.attributes diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 2af9aece9e3..1914380521b 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -21,7 +21,11 @@ from .common import ( simulate_webhook, ) -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + async_get_persistent_notifications, +) from tests.components.cloud import mock_cloud # Fake webhook thermostat mode change to "Max" @@ -391,7 +395,10 @@ async def test_setup_component_invalid_token_scope(hass: HomeAssistant) -> None: assert config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR assert hass.config_entries.async_entries(DOMAIN) - assert len(hass.states.async_all()) > 0 + + notifications = async_get_persistent_notifications(hass) + + assert len(notifications) > 0 for config_entry in hass.config_entries.async_entries("netatmo"): await hass.config_entries.async_remove(config_entry.entry_id) @@ -437,7 +444,8 @@ async def test_setup_component_invalid_token(hass: HomeAssistant, config_entry) assert config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR assert hass.config_entries.async_entries(DOMAIN) - assert len(hass.states.async_all()) > 0 + notifications = async_get_persistent_notifications(hass) + assert len(notifications) > 0 for config_entry in hass.config_entries.async_entries("netatmo"): await hass.config_entries.async_remove(config_entry.entry_id) diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index dcdfb961d0d..9fb5b9e531a 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -14,7 +14,7 @@ from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.setup import async_setup_component -from tests.common import MockPlatform, mock_platform +from tests.common import MockPlatform, async_get_persistent_notifications, mock_platform class MockNotifyPlatform(MockPlatform): @@ -139,7 +139,8 @@ async def test_warn_template( ) # We should only log it once assert caplog.text.count("Passing templates to notify service is deprecated") == 1 - assert hass.states.get("persistent_notification.notification") is not None + notifications = async_get_persistent_notifications(hass) + assert len(notifications) == 1 async def test_invalid_platform( diff --git a/tests/components/notify/test_persistent_notification.py b/tests/components/notify/test_persistent_notification.py index 32634e354e9..6f273ac3d9b 100644 --- a/tests/components/notify/test_persistent_notification.py +++ b/tests/components/notify/test_persistent_notification.py @@ -4,6 +4,8 @@ import homeassistant.components.persistent_notification as pn from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import async_get_persistent_notifications + async def test_async_send_message(hass: HomeAssistant) -> None: """Test sending a message to notify.persistent_notification service.""" @@ -17,9 +19,9 @@ async def test_async_send_message(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - entity_ids = hass.states.async_entity_ids(pn.DOMAIN) - assert len(entity_ids) == 1 + notifications = async_get_persistent_notifications(hass) + assert len(notifications) == 1 + notification = notifications[list(notifications)[0]] - state = hass.states.get(entity_ids[0]) - assert state.attributes.get("message") == "Hello" - assert state.attributes.get("title") == "Test notification" + assert notification["message"] == "Hello" + assert notification["title"] == "Test notification" diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py index de0ae25497a..7fd8b00d3e1 100644 --- a/tests/components/persistent_notification/test_init.py +++ b/tests/components/persistent_notification/test_init.py @@ -6,7 +6,6 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import async_capture_events from tests.typing import WebSocketGenerator @@ -18,22 +17,14 @@ async def setup_integration(hass): async def test_create(hass: HomeAssistant) -> None: """Test creating notification without title or notification id.""" - notifications = hass.data[pn.DOMAIN] + notifications = pn._async_get_or_create_notifications(hass) assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 0 assert len(notifications) == 0 pn.async_create(hass, "Hello World 2", title="2 beers") - - entity_ids = hass.states.async_entity_ids(pn.DOMAIN) - assert len(entity_ids) == 1 assert len(notifications) == 1 - state = hass.states.get(entity_ids[0]) - assert state.state == pn.STATE - assert state.attributes.get("message") == "Hello World 2" - assert state.attributes.get("title") == "2 beers" - - notification = notifications.get(entity_ids[0]) + notification = notifications[list(notifications)[0]] assert notification["status"] == pn.STATUS_UNREAD assert notification["message"] == "Hello World 2" assert notification["title"] == "2 beers" @@ -42,54 +33,42 @@ async def test_create(hass: HomeAssistant) -> None: async def test_create_notification_id(hass: HomeAssistant) -> None: """Ensure overwrites existing notification with same id.""" - notifications = hass.data[pn.DOMAIN] + notifications = pn._async_get_or_create_notifications(hass) assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 0 assert len(notifications) == 0 pn.async_create(hass, "test", notification_id="Beer 2") - assert len(hass.states.async_entity_ids()) == 1 assert len(notifications) == 1 + notification = notifications[list(notifications)[0]] - entity_id = "persistent_notification.beer_2" - state = hass.states.get(entity_id) - assert state.attributes.get("message") == "test" - - notification = notifications.get(entity_id) assert notification["message"] == "test" assert notification["title"] is None pn.async_create(hass, "test 2", notification_id="Beer 2") # We should have overwritten old one - assert len(hass.states.async_entity_ids()) == 1 - state = hass.states.get(entity_id) - assert state.attributes.get("message") == "test 2" + notification = notifications[list(notifications)[0]] - notification = notifications.get(entity_id) assert notification["message"] == "test 2" async def test_dismiss_notification(hass: HomeAssistant) -> None: """Ensure removal of specific notification.""" - notifications = hass.data[pn.DOMAIN] - assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 0 + notifications = pn._async_get_or_create_notifications(hass) assert len(notifications) == 0 pn.async_create(hass, "test", notification_id="Beer 2") - assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 1 assert len(notifications) == 1 pn.async_dismiss(hass, notification_id="Beer 2") - assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 0 assert len(notifications) == 0 async def test_mark_read(hass: HomeAssistant) -> None: """Ensure notification is marked as Read.""" - events = async_capture_events(hass, pn.EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) - notifications = hass.data[pn.DOMAIN] + notifications = pn._async_get_or_create_notifications(hass) assert len(notifications) == 0 await hass.services.async_call( @@ -99,20 +78,17 @@ async def test_mark_read(hass: HomeAssistant) -> None: blocking=True, ) - entity_id = "persistent_notification.beer_2" assert len(notifications) == 1 - notification = notifications.get(entity_id) + notification = notifications[list(notifications)[0]] assert notification["status"] == pn.STATUS_UNREAD - assert len(events) == 1 await hass.services.async_call( pn.DOMAIN, "mark_read", {"notification_id": "Beer 2"}, blocking=True ) assert len(notifications) == 1 - notification = notifications.get(entity_id) + notification = notifications[list(notifications)[0]] assert notification["status"] == pn.STATUS_READ - assert len(events) == 2 await hass.services.async_call( pn.DOMAIN, @@ -121,7 +97,6 @@ async def test_mark_read(hass: HomeAssistant) -> None: blocking=True, ) assert len(notifications) == 0 - assert len(events) == 3 async def test_ws_get_notifications( @@ -172,3 +147,68 @@ async def test_ws_get_notifications( msg = await client.receive_json() notifications = msg["result"] assert len(notifications) == 0 + + +async def test_ws_get_subscribe( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test websocket subscribe endpoint for retrieving persistent notifications.""" + await async_setup_component(hass, pn.DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 5, "type": "persistent_notification/subscribe"}) + msg = await client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + assert msg["event"] + event = msg["event"] + assert event["type"] == "current" + assert event["notifications"] == {} + + # Create + pn.async_create(hass, "test", notification_id="Beer 2") + + msg = await client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + assert msg["event"] + event = msg["event"] + assert event["type"] == "added" + notifications = event["notifications"] + assert len(notifications) == 1 + notification = notifications[list(notifications)[0]] + assert notification["notification_id"] == "Beer 2" + assert notification["message"] == "test" + assert notification["title"] is None + assert notification["status"] == pn.STATUS_UNREAD + assert notification["created_at"] is not None + + # Mark Read + await hass.services.async_call( + pn.DOMAIN, "mark_read", {"notification_id": "Beer 2"} + ) + msg = await client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + assert msg["event"] + event = msg["event"] + assert event["type"] == "updated" + notifications = event["notifications"] + assert len(notifications) == 1 + notification = notifications[list(notifications)[0]] + assert notification["status"] == pn.STATUS_READ + + # Dismiss + pn.async_dismiss(hass, "Beer 2") + msg = await client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + assert msg["event"] + event = msg["event"] + assert event["type"] == "removed" diff --git a/tests/components/person/conftest.py b/tests/components/person/conftest.py new file mode 100644 index 00000000000..4079ff24267 --- /dev/null +++ b/tests/components/person/conftest.py @@ -0,0 +1,45 @@ +"""The tests for the person component.""" +import logging + +import pytest + +from homeassistant.components import person +from homeassistant.components.person import DOMAIN +from homeassistant.helpers import collection +from homeassistant.setup import async_setup_component + +DEVICE_TRACKER = "device_tracker.test_tracker" +DEVICE_TRACKER_2 = "device_tracker.test_tracker_2" + + +@pytest.fixture +def storage_collection(hass): + """Return an empty storage collection.""" + id_manager = collection.IDManager() + return person.PersonStorageCollection( + person.PersonStore(hass, person.STORAGE_VERSION, person.STORAGE_KEY), + id_manager, + collection.YamlCollection( + logging.getLogger(f"{person.__name__}.yaml_collection"), id_manager + ), + ) + + +@pytest.fixture +def storage_setup(hass, hass_storage, hass_admin_user): + """Storage setup.""" + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": { + "persons": [ + { + "id": "1234", + "name": "tracked person", + "user_id": hass_admin_user.id, + "device_trackers": [DEVICE_TRACKER], + } + ] + }, + } + assert hass.loop.run_until_complete(async_setup_component(hass, DOMAIN, {})) diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 433c9529e78..71491ee3caf 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -1,5 +1,4 @@ """The tests for the person component.""" -import logging from typing import Any from unittest.mock import patch @@ -24,48 +23,14 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import Context, CoreState, HomeAssistant, State -from homeassistant.helpers import collection, entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from .conftest import DEVICE_TRACKER, DEVICE_TRACKER_2 + from tests.common import MockUser, mock_component, mock_restore_cache from tests.typing import WebSocketGenerator -DEVICE_TRACKER = "device_tracker.test_tracker" -DEVICE_TRACKER_2 = "device_tracker.test_tracker_2" - - -@pytest.fixture -def storage_collection(hass): - """Return an empty storage collection.""" - id_manager = collection.IDManager() - return person.PersonStorageCollection( - person.PersonStore(hass, person.STORAGE_VERSION, person.STORAGE_KEY), - id_manager, - collection.YamlCollection( - logging.getLogger(f"{person.__name__}.yaml_collection"), id_manager - ), - ) - - -@pytest.fixture -def storage_setup(hass, hass_storage, hass_admin_user): - """Storage setup.""" - hass_storage[DOMAIN] = { - "key": DOMAIN, - "version": 1, - "data": { - "persons": [ - { - "id": "1234", - "name": "tracked person", - "user_id": hass_admin_user.id, - "device_trackers": [DEVICE_TRACKER], - } - ] - }, - } - assert hass.loop.run_until_complete(async_setup_component(hass, DOMAIN, {})) - async def test_minimal_setup(hass: HomeAssistant) -> None: """Test minimal config with only name.""" diff --git a/tests/components/person/test_recorder.py b/tests/components/person/test_recorder.py index 879db2ec11f..51b5691bd8e 100644 --- a/tests/components/person/test_recorder.py +++ b/tests/components/person/test_recorder.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import MockUser, async_fire_time_changed from tests.components.recorder.common import async_wait_recording_done @@ -18,6 +18,8 @@ async def test_exclude_attributes( recorder_mock: Recorder, hass: HomeAssistant, enable_custom_integrations: None, + hass_admin_user: MockUser, + storage_setup, ) -> None: """Test update attributes to be excluded.""" now = dt_util.utcnow() diff --git a/tests/components/safe_mode/test_init.py b/tests/components/safe_mode/test_init.py index 043c65d7e43..82f5f5180da 100644 --- a/tests/components/safe_mode/test_init.py +++ b/tests/components/safe_mode/test_init.py @@ -2,9 +2,12 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import async_get_persistent_notifications + async def test_works(hass: HomeAssistant) -> None: """Test safe mode works.""" assert await async_setup_component(hass, "safe_mode", {}) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids()) == 1 + notifications = async_get_persistent_notifications(hass) + assert len(notifications) == 1 diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 53602ec28ff..774c85dbc0d 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -46,6 +46,8 @@ from .common import ( mock_integration, ) +from tests.common import async_get_persistent_notifications + @pytest.fixture(autouse=True) def mock_handlers() -> Generator[None, None, None]: @@ -733,14 +735,16 @@ async def test_discovery_notification(hass: HomeAssistant) -> None: title="Test Title", data={"token": "abcd"} ) + notifications = async_get_persistent_notifications(hass) + assert "config_entry_discovery" not in notifications + # Start first discovery flow to assert that reconfigure notification fires flow1 = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY} ) - await hass.async_block_till_done() - state = hass.states.get("persistent_notification.config_entry_discovery") - assert state is not None + notifications = async_get_persistent_notifications(hass) + assert "config_entry_discovery" in notifications # Start a second discovery flow so we can finish the first and assert that # the discovery notification persists until the second one is complete @@ -752,15 +756,15 @@ async def test_discovery_notification(hass: HomeAssistant) -> None: assert flow1["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() - state = hass.states.get("persistent_notification.config_entry_discovery") - assert state is not None + notifications = async_get_persistent_notifications(hass) + assert "config_entry_discovery" in notifications flow2 = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) assert flow2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() - state = hass.states.get("persistent_notification.config_entry_discovery") - assert state is None + notifications = async_get_persistent_notifications(hass) + assert "config_entry_discovery" not in notifications async def test_reauth_notification(hass: HomeAssistant) -> None: @@ -797,8 +801,8 @@ async def test_reauth_notification(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - state = hass.states.get("persistent_notification.config_entry_reconfigure") - assert state is None + notifications = async_get_persistent_notifications(hass) + assert "config_entry_reconfigure" not in notifications # Start first reauth flow to assert that reconfigure notification fires flow1 = await hass.config_entries.flow.async_init( @@ -806,8 +810,8 @@ async def test_reauth_notification(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - state = hass.states.get("persistent_notification.config_entry_reconfigure") - assert state is not None + notifications = async_get_persistent_notifications(hass) + assert "config_entry_reconfigure" in notifications # Start a second reauth flow so we can finish the first and assert that # the reconfigure notification persists until the second one is complete @@ -819,15 +823,15 @@ async def test_reauth_notification(hass: HomeAssistant) -> None: assert flow1["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() - state = hass.states.get("persistent_notification.config_entry_reconfigure") - assert state is not None + notifications = async_get_persistent_notifications(hass) + assert "config_entry_reconfigure" in notifications flow2 = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) assert flow2["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() - state = hass.states.get("persistent_notification.config_entry_reconfigure") - assert state is None + notifications = async_get_persistent_notifications(hass) + assert "config_entry_reconfigure" not in notifications async def test_discovery_notification_not_created(hass: HomeAssistant) -> None: @@ -2461,8 +2465,8 @@ async def test_partial_flows_hidden( assert len(hass.config_entries.flow.async_progress()) == 0 await hass.async_block_till_done() - state = hass.states.get("persistent_notification.config_entry_discovery") - assert state is None + notifications = async_get_persistent_notifications(hass) + assert "config_entry_discovery" not in notifications # Let the flow init complete pause_discovery.set() @@ -2474,8 +2478,8 @@ async def test_partial_flows_hidden( assert len(hass.config_entries.flow.async_progress()) == 1 await hass.async_block_till_done() - state = hass.states.get("persistent_notification.config_entry_discovery") - assert state is not None + notifications = async_get_persistent_notifications(hass) + assert "config_entry_discovery" in notifications async def test_async_setup_init_entry(hass: HomeAssistant) -> None: diff --git a/tests/test_loader.py b/tests/test_loader.py index a591ccbb204..6e62be08f66 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -8,7 +8,7 @@ from homeassistant.components import http, hue from homeassistant.components.hue import light as hue_light from homeassistant.core import HomeAssistant, callback -from .common import MockModule, mock_integration +from .common import MockModule, async_get_persistent_notifications, mock_integration async def test_component_dependencies(hass: HomeAssistant) -> None: @@ -61,7 +61,8 @@ async def test_component_wrapper(hass: HomeAssistant) -> None: components = loader.Components(hass) components.persistent_notification.async_create("message") - assert len(hass.states.async_entity_ids("persistent_notification")) == 1 + notifications = async_get_persistent_notifications(hass) + assert len(notifications) async def test_helpers_wrapper(hass: HomeAssistant) -> None: