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 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)

View File

@ -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(

View File

@ -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)

View File

@ -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

View File

@ -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."
)

View File

@ -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"]

View File

@ -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

View File

@ -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)

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.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(

View File

@ -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"

View File

@ -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"

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."""
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."""

View File

@ -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()

View File

@ -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

View File

@ -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:

View File

@ -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: