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
This commit is contained in:
J. Nick Koston 2023-05-25 22:09:13 -05:00 committed by GitHub
parent e2b69fc470
commit 48485fc2bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 310 additions and 195 deletions

View File

@ -2,44 +2,67 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
from datetime import datetime
import logging import logging
from typing import Any from typing import Any, Final, TypedDict
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.enum import StrEnum
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.core import Context, HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv, singleton
from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import (
from homeassistant.helpers.entity import async_generate_entity_id async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.util import slugify
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.util.uuid import random_uuid_hex
ATTR_CREATED_AT = "created_at"
ATTR_MESSAGE = "message"
ATTR_NOTIFICATION_ID = "notification_id"
ATTR_TITLE = "title"
ATTR_STATUS = "status"
DOMAIN = "persistent_notification" 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" 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( SCHEMA_SERVICE_NOTIFICATION = vol.Schema(
{vol.Required(ATTR_NOTIFICATION_ID): cv.string} {vol.Required(ATTR_NOTIFICATION_ID): cv.string}
) )
DEFAULT_OBJECT_ID = "notification"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
STATE = "notifying"
STATUS_UNREAD = "unread"
STATUS_READ = "read"
@bind_hass @bind_hass
def create( def create(
@ -65,31 +88,12 @@ def async_create(
message: str, message: str,
title: str | None = None, title: str | None = None,
notification_id: str | None = None, notification_id: str | None = None,
*,
context: Context | None = None,
) -> None: ) -> None:
"""Generate a notification.""" """Generate a notification."""
if (notifications := hass.data.get(DOMAIN)) is None: notifications = _async_get_or_create_notifications(hass)
notifications = hass.data[DOMAIN] = {} if notification_id is None:
notification_id = random_uuid_hex()
if notification_id is not None: notifications[notification_id] = {
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] = {
ATTR_MESSAGE: message, ATTR_MESSAGE: message,
ATTR_NOTIFICATION_ID: notification_id, ATTR_NOTIFICATION_ID: notification_id,
ATTR_STATUS: STATUS_UNREAD, ATTR_STATUS: STATUS_UNREAD,
@ -97,32 +101,39 @@ def async_create(
ATTR_CREATED_AT: dt_util.utcnow(), 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 @callback
@bind_hass @bind_hass
def async_dismiss( def async_dismiss(hass: HomeAssistant, notification_id: str) -> None:
hass: HomeAssistant, notification_id: str, *, context: Context | None = None
) -> None:
"""Remove a notification.""" """Remove a notification."""
if (notifications := hass.data.get(DOMAIN)) is None: notifications = _async_get_or_create_notifications(hass)
notifications = hass.data[DOMAIN] = {} if not (notification := notifications.pop(notification_id, None)):
entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id))
if entity_id not in notifications:
return return
async_dispatcher_send(
hass.states.async_remove(entity_id, context) hass,
SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED,
del notifications[entity_id] UpdateType.REMOVED,
hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) {notification_id: notification},
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the persistent notification component.""" """Set up the persistent notification component."""
notifications = hass.data.setdefault(DOMAIN, {}) notifications = _async_get_or_create_notifications(hass)
@callback @callback
def create_service(call: ServiceCall) -> None: 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[ATTR_MESSAGE],
call.data.get(ATTR_TITLE), call.data.get(ATTR_TITLE),
call.data.get(ATTR_NOTIFICATION_ID), call.data.get(ATTR_NOTIFICATION_ID),
context=call.context,
) )
@callback @callback
def dismiss_service(call: ServiceCall) -> None: def dismiss_service(call: ServiceCall) -> None:
"""Handle the dismiss notification service call.""" """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 @callback
def mark_read_service(call: ServiceCall) -> None: def mark_read_service(call: ServiceCall) -> None:
"""Handle the mark_read notification service call.""" """Handle the mark_read notification service call."""
notification_id = call.data.get(ATTR_NOTIFICATION_ID) notification_id = call.data.get(ATTR_NOTIFICATION_ID)
entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) if notification_id not in notifications:
if entity_id not in notifications:
_LOGGER.error( _LOGGER.error(
( (
"Marking persistent_notification read failed: " "Marking persistent_notification read failed: "
@ -156,9 +164,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
) )
return return
notifications[entity_id][ATTR_STATUS] = STATUS_READ notification = notifications[notification_id]
hass.bus.async_fire( notification[ATTR_STATUS] = STATUS_READ
EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, context=call.context async_dispatcher_send(
hass,
SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED,
UpdateType.UPDATED,
{notification_id: notification},
) )
hass.services.async_register( 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_get_notifications)
websocket_api.async_register_command(hass, websocket_subscribe_notifications)
return True return True
@ -197,19 +210,36 @@ def websocket_get_notifications(
"""Return a list of persistent_notifications.""" """Return a list of persistent_notifications."""
connection.send_message( connection.send_message(
websocket_api.result_message( websocket_api.result_message(
msg["id"], msg["id"], list(_async_get_or_create_notifications(hass).values())
[
{
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()
],
) )
) )
@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)

View File

@ -47,6 +47,9 @@ from homeassistant.helpers import (
) )
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_state_change_event 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.restore_state import RestoreEntity
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from homeassistant.helpers.typing import ConfigType 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: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the person component.""" """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) entity_component = EntityComponent[Person](_LOGGER, DOMAIN, hass)
id_manager = collection.IDManager() id_manager = collection.IDManager()
yaml_collection = collection.YamlCollection( yaml_collection = collection.YamlCollection(

View File

@ -29,7 +29,7 @@ from homeassistant.auth import (
providers as auth_providers, providers as auth_providers,
) )
from homeassistant.auth.permissions import system_policies 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 from homeassistant.components.device_automation import ( # noqa: F401
_async_get_device_automation_capabilities as async_get_device_automation_capabilities, _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): if isinstance(val, list):
for dict_value in val: for dict_value in val:
raise_contains_mocks(dict_value) 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)

View File

@ -55,7 +55,7 @@ async def test_gvh5178_error(hass: HomeAssistant) -> None:
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 0
inject_bluetooth_service_info(hass, GVH5178_SERVICE_INFO_ERROR) inject_bluetooth_service_info(hass, GVH5178_SERVICE_INFO_ERROR)
await hass.async_block_till_done() 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") temp_sensor = hass.states.get("sensor.b51782bc8_remote_temperature")
assert temp_sensor.state == STATE_UNAVAILABLE assert temp_sensor.state == STATE_UNAVAILABLE

View File

@ -25,6 +25,7 @@ from homeassistant.setup import async_setup_component
from . import mock_real_ip from . import mock_real_ip
from tests.common import async_get_persistent_notifications
from tests.typing import ClientSessionGenerator from tests.typing import ClientSessionGenerator
SUPERVISOR_IP = "1.2.3.4" SUPERVISOR_IP = "1.2.3.4"
@ -307,11 +308,10 @@ async def test_ip_bans_file_creation(
assert resp.status == HTTPStatus.FORBIDDEN assert resp.status == HTTPStatus.FORBIDDEN
assert m_open.call_count == 1 assert m_open.call_count == 1
notifications = async_get_persistent_notifications(hass)
assert len(notifications) == 2
assert ( assert (
len(notifications := hass.states.async_all("persistent_notification")) == 2 notifications["http-login"]["message"]
)
assert (
notifications[0].attributes["message"]
== "Login attempt or request with invalid authentication from example.com (200.201.202.204). See the log for details." == "Login attempt or request with invalid authentication from example.com (200.201.202.204). See the log for details."
) )

View File

@ -9,7 +9,7 @@ from homeassistant.components import hue
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry from tests.common import MockConfigEntry, async_get_persistent_notifications
@pytest.fixture @pytest.fixture
@ -162,6 +162,6 @@ async def test_security_vuln_check(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("persistent_notification.hue_hub_firmware") notifications = async_get_persistent_notifications(hass)
assert state is not None assert "hue_hub_firmware" in notifications
assert "CVE-2020-6007" in state.attributes["message"] assert "CVE-2020-6007" in notifications["hue_hub_firmware"]["message"]

View File

@ -34,7 +34,7 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
states = await hass.async_add_executor_job( states = await hass.async_add_executor_job(
get_significant_states, hass, now, None, hass.states.async_entity_ids() 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 entity_states in states.values():
for state in entity_states: for state in entity_states:
assert ATTR_MIN_HUMIDITY not in state.attributes assert ATTR_MIN_HUMIDITY not in state.attributes

View File

@ -21,7 +21,11 @@ from .common import (
simulate_webhook, 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 from tests.components.cloud import mock_cloud
# Fake webhook thermostat mode change to "Max" # 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 config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR
assert hass.config_entries.async_entries(DOMAIN) 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"): for config_entry in hass.config_entries.async_entries("netatmo"):
await hass.config_entries.async_remove(config_entry.entry_id) 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 config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR
assert hass.config_entries.async_entries(DOMAIN) 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"): for config_entry in hass.config_entries.async_entries("netatmo"):
await hass.config_entries.async_remove(config_entry.entry_id) await hass.config_entries.async_remove(config_entry.entry_id)

View File

@ -14,7 +14,7 @@ from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.setup import async_setup_component 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): class MockNotifyPlatform(MockPlatform):
@ -139,7 +139,8 @@ async def test_warn_template(
) )
# We should only log it once # We should only log it once
assert caplog.text.count("Passing templates to notify service is deprecated") == 1 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( async def test_invalid_platform(

View File

@ -4,6 +4,8 @@ import homeassistant.components.persistent_notification as pn
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import async_get_persistent_notifications
async def test_async_send_message(hass: HomeAssistant) -> None: async def test_async_send_message(hass: HomeAssistant) -> None:
"""Test sending a message to notify.persistent_notification service.""" """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() await hass.async_block_till_done()
entity_ids = hass.states.async_entity_ids(pn.DOMAIN) notifications = async_get_persistent_notifications(hass)
assert len(entity_ids) == 1 assert len(notifications) == 1
notification = notifications[list(notifications)[0]]
state = hass.states.get(entity_ids[0]) assert notification["message"] == "Hello"
assert state.attributes.get("message") == "Hello" assert notification["title"] == "Test notification"
assert state.attributes.get("title") == "Test notification"

View File

@ -6,7 +6,6 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import async_capture_events
from tests.typing import WebSocketGenerator from tests.typing import WebSocketGenerator
@ -18,22 +17,14 @@ async def setup_integration(hass):
async def test_create(hass: HomeAssistant) -> None: async def test_create(hass: HomeAssistant) -> None:
"""Test creating notification without title or notification id.""" """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(hass.states.async_entity_ids(pn.DOMAIN)) == 0
assert len(notifications) == 0 assert len(notifications) == 0
pn.async_create(hass, "Hello World 2", title="2 beers") 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 assert len(notifications) == 1
state = hass.states.get(entity_ids[0]) notification = notifications[list(notifications)[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])
assert notification["status"] == pn.STATUS_UNREAD assert notification["status"] == pn.STATUS_UNREAD
assert notification["message"] == "Hello World 2" assert notification["message"] == "Hello World 2"
assert notification["title"] == "2 beers" 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: async def test_create_notification_id(hass: HomeAssistant) -> None:
"""Ensure overwrites existing notification with same id.""" """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(hass.states.async_entity_ids(pn.DOMAIN)) == 0
assert len(notifications) == 0 assert len(notifications) == 0
pn.async_create(hass, "test", notification_id="Beer 2") pn.async_create(hass, "test", notification_id="Beer 2")
assert len(hass.states.async_entity_ids()) == 1
assert len(notifications) == 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["message"] == "test"
assert notification["title"] is None assert notification["title"] is None
pn.async_create(hass, "test 2", notification_id="Beer 2") pn.async_create(hass, "test 2", notification_id="Beer 2")
# We should have overwritten old one # We should have overwritten old one
assert len(hass.states.async_entity_ids()) == 1 notification = notifications[list(notifications)[0]]
state = hass.states.get(entity_id)
assert state.attributes.get("message") == "test 2"
notification = notifications.get(entity_id)
assert notification["message"] == "test 2" assert notification["message"] == "test 2"
async def test_dismiss_notification(hass: HomeAssistant) -> None: async def test_dismiss_notification(hass: HomeAssistant) -> None:
"""Ensure removal of specific notification.""" """Ensure removal of specific notification."""
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 assert len(notifications) == 0
pn.async_create(hass, "test", notification_id="Beer 2") pn.async_create(hass, "test", notification_id="Beer 2")
assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 1
assert len(notifications) == 1 assert len(notifications) == 1
pn.async_dismiss(hass, notification_id="Beer 2") pn.async_dismiss(hass, notification_id="Beer 2")
assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 0
assert len(notifications) == 0 assert len(notifications) == 0
async def test_mark_read(hass: HomeAssistant) -> None: async def test_mark_read(hass: HomeAssistant) -> None:
"""Ensure notification is marked as Read.""" """Ensure notification is marked as Read."""
events = async_capture_events(hass, pn.EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) notifications = pn._async_get_or_create_notifications(hass)
notifications = hass.data[pn.DOMAIN]
assert len(notifications) == 0 assert len(notifications) == 0
await hass.services.async_call( await hass.services.async_call(
@ -99,20 +78,17 @@ async def test_mark_read(hass: HomeAssistant) -> None:
blocking=True, blocking=True,
) )
entity_id = "persistent_notification.beer_2"
assert len(notifications) == 1 assert len(notifications) == 1
notification = notifications.get(entity_id) notification = notifications[list(notifications)[0]]
assert notification["status"] == pn.STATUS_UNREAD assert notification["status"] == pn.STATUS_UNREAD
assert len(events) == 1
await hass.services.async_call( await hass.services.async_call(
pn.DOMAIN, "mark_read", {"notification_id": "Beer 2"}, blocking=True pn.DOMAIN, "mark_read", {"notification_id": "Beer 2"}, blocking=True
) )
assert len(notifications) == 1 assert len(notifications) == 1
notification = notifications.get(entity_id) notification = notifications[list(notifications)[0]]
assert notification["status"] == pn.STATUS_READ assert notification["status"] == pn.STATUS_READ
assert len(events) == 2
await hass.services.async_call( await hass.services.async_call(
pn.DOMAIN, pn.DOMAIN,
@ -121,7 +97,6 @@ async def test_mark_read(hass: HomeAssistant) -> None:
blocking=True, blocking=True,
) )
assert len(notifications) == 0 assert len(notifications) == 0
assert len(events) == 3
async def test_ws_get_notifications( async def test_ws_get_notifications(
@ -172,3 +147,68 @@ async def test_ws_get_notifications(
msg = await client.receive_json() msg = await client.receive_json()
notifications = msg["result"] notifications = msg["result"]
assert len(notifications) == 0 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"

View File

@ -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, {}))

View File

@ -1,5 +1,4 @@
"""The tests for the person component.""" """The tests for the person component."""
import logging
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import patch
@ -24,48 +23,14 @@ from homeassistant.const import (
STATE_UNKNOWN, STATE_UNKNOWN,
) )
from homeassistant.core import Context, CoreState, HomeAssistant, State 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 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.common import MockUser, mock_component, mock_restore_cache
from tests.typing import WebSocketGenerator 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: async def test_minimal_setup(hass: HomeAssistant) -> None:
"""Test minimal config with only name.""" """Test minimal config with only name."""

View File

@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util 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 from tests.components.recorder.common import async_wait_recording_done
@ -18,6 +18,8 @@ async def test_exclude_attributes(
recorder_mock: Recorder, recorder_mock: Recorder,
hass: HomeAssistant, hass: HomeAssistant,
enable_custom_integrations: None, enable_custom_integrations: None,
hass_admin_user: MockUser,
storage_setup,
) -> None: ) -> None:
"""Test update attributes to be excluded.""" """Test update attributes to be excluded."""
now = dt_util.utcnow() now = dt_util.utcnow()

View File

@ -2,9 +2,12 @@
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import async_get_persistent_notifications
async def test_works(hass: HomeAssistant) -> None: async def test_works(hass: HomeAssistant) -> None:
"""Test safe mode works.""" """Test safe mode works."""
assert await async_setup_component(hass, "safe_mode", {}) assert await async_setup_component(hass, "safe_mode", {})
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_entity_ids()) == 1 notifications = async_get_persistent_notifications(hass)
assert len(notifications) == 1

View File

@ -46,6 +46,8 @@ from .common import (
mock_integration, mock_integration,
) )
from tests.common import async_get_persistent_notifications
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_handlers() -> Generator[None, None, None]: 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"} 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 # Start first discovery flow to assert that reconfigure notification fires
flow1 = await hass.config_entries.flow.async_init( flow1 = await hass.config_entries.flow.async_init(
"test", context={"source": config_entries.SOURCE_DISCOVERY} "test", context={"source": config_entries.SOURCE_DISCOVERY}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("persistent_notification.config_entry_discovery") notifications = async_get_persistent_notifications(hass)
assert state is not None assert "config_entry_discovery" in notifications
# Start a second discovery flow so we can finish the first and assert that # Start a second discovery flow so we can finish the first and assert that
# the discovery notification persists until the second one is complete # 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 assert flow1["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("persistent_notification.config_entry_discovery") notifications = async_get_persistent_notifications(hass)
assert state is not None assert "config_entry_discovery" in notifications
flow2 = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) flow2 = await hass.config_entries.flow.async_configure(flow2["flow_id"], {})
assert flow2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert flow2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("persistent_notification.config_entry_discovery") notifications = async_get_persistent_notifications(hass)
assert state is None assert "config_entry_discovery" not in notifications
async def test_reauth_notification(hass: HomeAssistant) -> None: 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() await hass.async_block_till_done()
state = hass.states.get("persistent_notification.config_entry_reconfigure") notifications = async_get_persistent_notifications(hass)
assert state is None assert "config_entry_reconfigure" not in notifications
# Start first reauth flow to assert that reconfigure notification fires # Start first reauth flow to assert that reconfigure notification fires
flow1 = await hass.config_entries.flow.async_init( 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() await hass.async_block_till_done()
state = hass.states.get("persistent_notification.config_entry_reconfigure") notifications = async_get_persistent_notifications(hass)
assert state is not None assert "config_entry_reconfigure" in notifications
# Start a second reauth flow so we can finish the first and assert that # Start a second reauth flow so we can finish the first and assert that
# the reconfigure notification persists until the second one is complete # 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 assert flow1["type"] == data_entry_flow.FlowResultType.ABORT
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("persistent_notification.config_entry_reconfigure") notifications = async_get_persistent_notifications(hass)
assert state is not None assert "config_entry_reconfigure" in notifications
flow2 = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) flow2 = await hass.config_entries.flow.async_configure(flow2["flow_id"], {})
assert flow2["type"] == data_entry_flow.FlowResultType.ABORT assert flow2["type"] == data_entry_flow.FlowResultType.ABORT
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("persistent_notification.config_entry_reconfigure") notifications = async_get_persistent_notifications(hass)
assert state is None assert "config_entry_reconfigure" not in notifications
async def test_discovery_notification_not_created(hass: HomeAssistant) -> None: 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 assert len(hass.config_entries.flow.async_progress()) == 0
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("persistent_notification.config_entry_discovery") notifications = async_get_persistent_notifications(hass)
assert state is None assert "config_entry_discovery" not in notifications
# Let the flow init complete # Let the flow init complete
pause_discovery.set() pause_discovery.set()
@ -2474,8 +2478,8 @@ async def test_partial_flows_hidden(
assert len(hass.config_entries.flow.async_progress()) == 1 assert len(hass.config_entries.flow.async_progress()) == 1
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("persistent_notification.config_entry_discovery") notifications = async_get_persistent_notifications(hass)
assert state is not None assert "config_entry_discovery" in notifications
async def test_async_setup_init_entry(hass: HomeAssistant) -> None: async def test_async_setup_init_entry(hass: HomeAssistant) -> None:

View File

@ -8,7 +8,7 @@ from homeassistant.components import http, hue
from homeassistant.components.hue import light as hue_light from homeassistant.components.hue import light as hue_light
from homeassistant.core import HomeAssistant, callback 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: 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 = loader.Components(hass)
components.persistent_notification.async_create("message") 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: async def test_helpers_wrapper(hass: HomeAssistant) -> None: