Add event platform to ntfy integration (#143529)

This commit is contained in:
Manu
2025-09-09 17:53:19 +02:00
committed by GitHub
parent 9e73ff06d2
commit 6271765eaf
13 changed files with 607 additions and 55 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ from .const import DOMAIN
from .coordinator import NtfyConfigEntry, NtfyDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.NOTIFY, Platform.SENSOR]
PLATFORMS: list[Platform] = [Platform.EVENT, Platform.NOTIFY, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: NtfyConfigEntry) -> bool:
+41 -2
View File
@@ -38,12 +38,25 @@ from homeassistant.const import (
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from .const import CONF_TOPIC, DEFAULT_URL, DOMAIN, SECTION_AUTH
from .const import (
CONF_MESSAGE,
CONF_PRIORITY,
CONF_TAGS,
CONF_TITLE,
CONF_TOPIC,
DEFAULT_URL,
DOMAIN,
SECTION_AUTH,
SECTION_FILTER,
)
_LOGGER = logging.getLogger(__name__)
@@ -112,6 +125,29 @@ STEP_USER_TOPIC_SCHEMA = vol.Schema(
{
vol.Required(CONF_TOPIC): str,
vol.Optional(CONF_NAME): str,
vol.Required(SECTION_FILTER): data_entry_flow.section(
vol.Schema(
{
vol.Optional(CONF_PRIORITY): SelectSelector(
SelectSelectorConfig(
multiple=True,
options=["5", "4", "3", "2", "1"],
mode=SelectSelectorMode.DROPDOWN,
translation_key="priority",
)
),
vol.Optional(CONF_TAGS): TextSelector(
TextSelectorConfig(
type=TextSelectorType.TEXT,
multiple=True,
),
),
vol.Optional(CONF_TITLE): str,
vol.Optional(CONF_MESSAGE): str,
}
),
{"collapsed": True},
),
}
)
@@ -408,7 +444,10 @@ class TopicSubentryFlowHandler(ConfigSubentryFlow):
return self.async_create_entry(
title=user_input.get(CONF_NAME, user_input[CONF_TOPIC]),
data={CONF_TOPIC: user_input[CONF_TOPIC]},
data={
CONF_TOPIC: user_input[CONF_TOPIC],
**user_input[SECTION_FILTER],
},
unique_id=user_input[CONF_TOPIC],
)
return self.async_show_form(
+6
View File
@@ -6,4 +6,10 @@ DOMAIN = "ntfy"
DEFAULT_URL: Final = "https://ntfy.sh"
CONF_TOPIC = "topic"
CONF_PRIORITY = "filter_priority"
CONF_TITLE = "filter_title"
CONF_MESSAGE = "filter_message"
CONF_TAGS = "filter_tags"
SECTION_AUTH = "auth"
SECTION_FILTER = "filter"
NTFY_EVENT = "ntfy_event"
+43
View File
@@ -0,0 +1,43 @@
"""Base entity for ntfy integration."""
from __future__ import annotations
from yarl import URL
from homeassistant.config_entries import ConfigSubentry
from homeassistant.const import CONF_URL
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import Entity
from .const import CONF_TOPIC, DOMAIN
from .coordinator import NtfyConfigEntry
class NtfyBaseEntity(Entity):
"""Base entity."""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(
self,
config_entry: NtfyConfigEntry,
subentry: ConfigSubentry,
) -> None:
"""Initialize the entity."""
self.topic = subentry.data[CONF_TOPIC]
self._attr_unique_id = f"{config_entry.entry_id}_{subentry.subentry_id}_{self.entity_description.key}"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer="ntfy LLC",
model="ntfy",
name=subentry.title,
configuration_url=URL(config_entry.data[CONF_URL]) / self.topic,
identifiers={(DOMAIN, f"{config_entry.entry_id}_{subentry.subentry_id}")},
via_device=(DOMAIN, config_entry.entry_id),
)
self.ntfy = config_entry.runtime_data.ntfy
self.config_entry = config_entry
self.subentry = subentry
+151
View File
@@ -0,0 +1,151 @@
"""Event platform for ntfy integration."""
from __future__ import annotations
import asyncio
import logging
from typing import TYPE_CHECKING
from aiontfy import Event, Notification
from aiontfy.exceptions import (
NtfyConnectionError,
NtfyForbiddenError,
NtfyHTTPError,
NtfyTimeoutError,
)
from homeassistant.components.event import EventEntity, EventEntityDescription
from homeassistant.config_entries import ConfigSubentry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import CONF_MESSAGE, CONF_PRIORITY, CONF_TAGS, CONF_TITLE
from .coordinator import NtfyConfigEntry
from .entity import NtfyBaseEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
RECONNECT_INTERVAL = 10
async def async_setup_entry(
hass: HomeAssistant,
config_entry: NtfyConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the event platform."""
for subentry_id, subentry in config_entry.subentries.items():
async_add_entities(
[NtfyEventEntity(config_entry, subentry)], config_subentry_id=subentry_id
)
class NtfyEventEntity(NtfyBaseEntity, EventEntity):
"""An event entity."""
entity_description = EventEntityDescription(
key="subscribe",
translation_key="subscribe",
name=None,
event_types=["triggered"],
)
def __init__(
self,
config_entry: NtfyConfigEntry,
subentry: ConfigSubentry,
) -> None:
"""Initialize the entity."""
super().__init__(config_entry, subentry)
self._ws: asyncio.Task | None = None
@callback
def _async_handle_event(self, notification: Notification) -> None:
"""Handle the ntfy event."""
if notification.topic == self.topic and notification.event is Event.MESSAGE:
event = (
f"{notification.title}: {notification.message}"
if notification.title
else notification.message
)
if TYPE_CHECKING:
assert event
self._attr_event_types = [event]
self._trigger_event(event, notification.to_dict())
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
self.config_entry.async_create_background_task(
self.hass,
self.ws_connect(),
"websocket_watchdog",
)
async def ws_connect(self) -> None:
"""Connect websocket."""
while True:
try:
if self._ws and (exc := self._ws.exception()):
raise exc # noqa: TRY301
except asyncio.InvalidStateError:
self._attr_available = True
except asyncio.CancelledError:
self._attr_available = False
return
except NtfyForbiddenError:
if self._attr_available:
_LOGGER.error("Failed to subscribe to topic. Topic is protected")
self._attr_available = False
return
except NtfyHTTPError as e:
if self._attr_available:
_LOGGER.error(
"Failed to connect to ntfy service due to a server error: %s (%s)",
e.error,
e.link,
)
self._attr_available = False
except NtfyConnectionError:
if self._attr_available:
_LOGGER.error(
"Failed to connect to ntfy service due to a connection error"
)
self._attr_available = False
except NtfyTimeoutError:
if self._attr_available:
_LOGGER.error(
"Failed to connect to ntfy service due to a connection timeout"
)
self._attr_available = False
except Exception:
if self._attr_available:
_LOGGER.exception(
"Failed to connect to ntfy service due to an unexpected exception"
)
self._attr_available = False
finally:
if self._ws is None or self._ws.done():
self._ws = self.config_entry.async_create_background_task(
self.hass,
target=self.ntfy.subscribe(
topics=[self.topic],
callback=self._async_handle_event,
title=self.subentry.data.get(CONF_TITLE),
message=self.subentry.data.get(CONF_MESSAGE),
priority=self.subentry.data.get(CONF_PRIORITY),
tags=self.subentry.data.get(CONF_TAGS),
),
name="ntfy_websocket",
)
self.async_write_ha_state()
await asyncio.sleep(RECONNECT_INTERVAL)
@property
def entity_picture(self) -> str | None:
"""Return the entity picture to use in the frontend, if any."""
return self.state_attributes.get("icon") or super().entity_picture
+5
View File
@@ -5,6 +5,11 @@
"default": "mdi:console-line"
}
},
"event": {
"subscribe": {
"default": "mdi:message-outline"
}
},
"sensor": {
"messages": {
"default": "mdi:message-arrow-right-outline"
+3 -29
View File
@@ -8,22 +8,19 @@ from aiontfy.exceptions import (
NtfyHTTPError,
NtfyUnauthorizedAuthenticationError,
)
from yarl import URL
from homeassistant.components.notify import (
NotifyEntity,
NotifyEntityDescription,
NotifyEntityFeature,
)
from homeassistant.config_entries import ConfigSubentry
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import CONF_TOPIC, DOMAIN
from .const import DOMAIN
from .coordinator import NtfyConfigEntry
from .entity import NtfyBaseEntity
PARALLEL_UPDATES = 0
@@ -41,39 +38,16 @@ async def async_setup_entry(
)
class NtfyNotifyEntity(NotifyEntity):
class NtfyNotifyEntity(NtfyBaseEntity, NotifyEntity):
"""Representation of a ntfy notification entity."""
entity_description = NotifyEntityDescription(
key="publish",
translation_key="publish",
name=None,
has_entity_name=True,
)
_attr_supported_features = NotifyEntityFeature.TITLE
def __init__(
self,
config_entry: NtfyConfigEntry,
subentry: ConfigSubentry,
) -> None:
"""Initialize a notification entity."""
self._attr_unique_id = f"{config_entry.entry_id}_{subentry.subentry_id}_{self.entity_description.key}"
self.topic = subentry.data[CONF_TOPIC]
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer="ntfy LLC",
model="ntfy",
name=subentry.title,
configuration_url=URL(config_entry.data[CONF_URL]) / self.topic,
identifiers={(DOMAIN, f"{config_entry.entry_id}_{subentry.subentry_id}")},
via_device=(DOMAIN, config_entry.entry_id),
)
self.config_entry = config_entry
self.ntfy = config_entry.runtime_data.ntfy
async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Publish a message to a topic."""
msg = Message(topic=self.topic, message=message, title=title)
@@ -7,9 +7,7 @@ rules:
status: exempt
comment: the integration does not poll
brands: done
common-modules:
status: exempt
comment: the integration currently implements only one platform and has no coordinator
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
@@ -19,9 +17,7 @@ rules:
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: the integration does not subscribe to events
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
@@ -36,13 +32,9 @@ rules:
status: exempt
comment: the integration has no options
docs-installation-parameters: done
entity-unavailable:
status: exempt
comment: the integration only implements a stateless notify entity.
entity-unavailable: done
integration-owner: done
log-when-unavailable:
status: exempt
comment: the integration only integrates state-less entities
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
test-coverage: done
@@ -102,6 +102,24 @@
"data_description": {
"topic": "Enter the name of the topic you want to use for notifications. Topics may not be password-protected, so choose a name that's not easy to guess.",
"name": "Set an alternative name to display instead of the topic name. This helps identify topics with complex or hard-to-read names more easily."
},
"sections": {
"filter": {
"data": {
"filter_priority": "Filter by priority",
"filter_tags": "Filter by tags",
"filter_title": "Filter by title",
"filter_message": "Filter by message content"
},
"data_description": {
"filter_priority": "Include messages that match any of the selected priority levels. If no priority is selected, all messages are included by default",
"filter_tags": "Only include messages that have all selected tags",
"filter_title": "Include messages with a title that exactly matches the specified text",
"filter_message": "Include messages with content that exactly matches the specified text"
},
"name": "Message filters (optional)",
"description": "Apply filters to narrow down the messages received when Home Assistant subscribes to the topic. Filters apply only to the event entity."
}
}
}
},
@@ -121,6 +139,17 @@
}
},
"entity": {
"event": {
"subscribe": {
"state_attributes": {
"event_type": {
"state": {
"triggered": "Triggered"
}
}
}
}
},
"sensor": {
"messages": {
"name": "Messages published",
@@ -222,5 +251,16 @@
"timeout_error": {
"message": "Failed to connect to ntfy service due to a connection timeout"
}
},
"selector": {
"priority": {
"options": {
"1": "Minimum",
"2": "[%key:common::state::low%]",
"3": "Default",
"4": "[%key:common::state::high%]",
"5": "Maximum"
}
}
}
}
+49 -4
View File
@@ -1,10 +1,11 @@
"""Common fixtures for the ntfy tests."""
from collections.abc import Generator
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, patch
import asyncio
from collections.abc import Callable, Generator
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, Mock, patch
from aiontfy import Account, AccountTokenResponse
from aiontfy import Account, AccountTokenResponse, Event, Notification
import pytest
from homeassistant.components.ntfy.const import CONF_TOPIC, DOMAIN
@@ -40,6 +41,50 @@ def mock_aiontfy() -> Generator[AsyncMock]:
client.generate_token.return_value = AccountTokenResponse(
token="token", last_access=datetime.now()
)
resp = Mock(
id="h6Y2hKA5sy0U",
time=datetime(2025, 3, 28, 17, 58, 46, tzinfo=UTC),
expires=datetime(2025, 3, 29, 5, 58, 46, tzinfo=UTC),
event=Event.MESSAGE,
topic="mytopic",
message="Hello",
title="Title",
tags=["octopus"],
priority=3,
click="https://example.com/",
icon="https://example.com/icon.png",
actions=[],
attachment=None,
content_type=None,
)
resp.to_dict.return_value = {
"id": "h6Y2hKA5sy0U",
"time": datetime(2025, 3, 28, 17, 58, 46, tzinfo=UTC),
"expires": datetime(2025, 3, 29, 5, 58, 46, tzinfo=UTC),
"event": Event.MESSAGE,
"topic": "mytopic",
"message": "Hello",
"title": "Title",
"tags": ["octopus"],
"priority": 3,
"click": "https://example.com/",
"icon": "https://example.com/icon.png",
"actions": [],
"attachment": None,
"content_type": None,
}
async def mock_ws(
topics: list[str], callback: Callable[[Notification], None], **kwargs
):
callback(resp)
while True:
await asyncio.sleep(1)
client.subscribe.side_effect = mock_ws
yield client
@@ -0,0 +1,75 @@
# serializer version: 1
# name: test_event_platform[event.mytopic-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'event_types': list([
'Title: Hello',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'event',
'entity_category': None,
'entity_id': 'event.mytopic',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'ntfy',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'subscribe',
'unique_id': '123456789_ABCDEF_subscribe',
'unit_of_measurement': None,
})
# ---
# name: test_event_platform[event.mytopic-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'actions': list([
]),
'attachment': None,
'click': 'https://example.com/',
'content_type': None,
'entity_picture': 'https://example.com/icon.png',
'event': <Event.MESSAGE: 'message'>,
'event_type': 'Title: Hello',
'event_types': list([
'Title: Hello',
]),
'expires': datetime.datetime(2025, 3, 29, 5, 58, 46, tzinfo=datetime.timezone.utc),
'friendly_name': 'mytopic',
'icon': 'https://example.com/icon.png',
'id': 'h6Y2hKA5sy0U',
'message': 'Hello',
'priority': 3,
'tags': list([
'octopus',
]),
'time': datetime.datetime(2025, 3, 28, 17, 58, 46, tzinfo=datetime.timezone.utc),
'title': 'Title',
'topic': 'mytopic',
}),
'context': <ANY>,
'entity_id': 'event.mytopic',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2025-09-03T22:00:00.000+00:00',
})
# ---
+31 -7
View File
@@ -12,7 +12,12 @@ from aiontfy.exceptions import (
)
import pytest
from homeassistant.components.ntfy.const import CONF_TOPIC, DOMAIN, SECTION_AUTH
from homeassistant.components.ntfy.const import (
CONF_TOPIC,
DOMAIN,
SECTION_AUTH,
SECTION_FILTER,
)
from homeassistant.config_entries import SOURCE_USER, ConfigSubentry
from homeassistant.const import (
CONF_NAME,
@@ -204,7 +209,10 @@ async def test_add_topic_flow(hass: HomeAssistant) -> None:
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
user_input={CONF_TOPIC: "mytopic"},
user_input={
CONF_TOPIC: "mytopic",
SECTION_FILTER: {},
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
subentry_id = list(config_entry.subentries)[0]
@@ -252,14 +260,21 @@ async def test_generated_topic(hass: HomeAssistant, mock_random: AsyncMock) -> N
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
user_input={CONF_TOPIC: ""},
user_input={
CONF_TOPIC: "",
SECTION_FILTER: {},
},
)
mock_random.assert_called_once()
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
user_input={CONF_TOPIC: "randomtopic", CONF_NAME: "mytopic"},
user_input={
CONF_TOPIC: "randomtopic",
CONF_NAME: "mytopic",
SECTION_FILTER: {},
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
@@ -306,7 +321,10 @@ async def test_invalid_topic(hass: HomeAssistant, mock_random: AsyncMock) -> Non
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
user_input={CONF_TOPIC: "invalid,topic"},
user_input={
CONF_TOPIC: "invalid,topic",
SECTION_FILTER: {},
},
)
assert result["type"] == FlowResultType.FORM
@@ -314,7 +332,10 @@ async def test_invalid_topic(hass: HomeAssistant, mock_random: AsyncMock) -> Non
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
user_input={CONF_TOPIC: "mytopic"},
user_input={
CONF_TOPIC: "mytopic",
SECTION_FILTER: {},
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
@@ -360,7 +381,10 @@ async def test_topic_already_configured(
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
user_input={CONF_TOPIC: "mytopic"},
user_input={
CONF_TOPIC: "mytopic",
SECTION_FILTER: {},
},
)
assert result["type"] is FlowResultType.ABORT
+158
View File
@@ -0,0 +1,158 @@
"""Tests for the ntfy event platform."""
import asyncio
from collections.abc import AsyncGenerator
from datetime import UTC, datetime, timedelta
from unittest.mock import AsyncMock, patch
from aiontfy import Event
from aiontfy.exceptions import (
NtfyConnectionError,
NtfyForbiddenError,
NtfyHTTPError,
NtfyTimeoutError,
NtfyUnauthorizedAuthenticationError,
)
from freezegun.api import FrozenDateTimeFactory, freeze_time
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@pytest.fixture(autouse=True)
async def event_only() -> AsyncGenerator[None]:
"""Enable only the event platform."""
with patch(
"homeassistant.components.ntfy.PLATFORMS",
[Platform.EVENT],
):
yield
@pytest.mark.usefixtures("mock_aiontfy")
@freeze_time("2025-09-03T22:00:00.000Z")
async def test_event_platform(
hass: HomeAssistant,
config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Test setup of the ntfy event platform."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
@pytest.mark.usefixtures("mock_aiontfy")
async def test_event(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test ntfy events."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert (state := hass.states.get("event.mytopic"))
assert state.state != STATE_UNKNOWN
assert state.attributes == {
"actions": [],
"attachment": None,
"click": "https://example.com/",
"content_type": None,
"entity_picture": "https://example.com/icon.png",
"event": Event.MESSAGE,
"event_type": "Title: Hello",
"event_types": [
"Title: Hello",
],
"expires": datetime(2025, 3, 29, 5, 58, 46, tzinfo=UTC),
"friendly_name": "mytopic",
"icon": "https://example.com/icon.png",
"id": "h6Y2hKA5sy0U",
"message": "Hello",
"priority": 3,
"tags": [
"octopus",
],
"time": datetime(2025, 3, 28, 17, 58, 46, tzinfo=UTC),
"title": "Title",
"topic": "mytopic",
}
@pytest.mark.parametrize(
("exception", "expected_state"),
[
(
NtfyHTTPError(41801, 418, "I'm a teapot", ""),
STATE_UNAVAILABLE,
),
(
NtfyConnectionError,
STATE_UNAVAILABLE,
),
(
NtfyTimeoutError,
STATE_UNAVAILABLE,
),
(
NtfyUnauthorizedAuthenticationError(40101, 401, "unauthorized"),
STATE_UNAVAILABLE,
),
(
NtfyForbiddenError(403, 403, "forbidden"),
STATE_UNAVAILABLE,
),
(
asyncio.CancelledError,
STATE_UNAVAILABLE,
),
(
asyncio.InvalidStateError,
STATE_UNKNOWN,
),
(
ValueError,
STATE_UNAVAILABLE,
),
],
)
async def test_event_exceptions(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_aiontfy: AsyncMock,
freezer: FrozenDateTimeFactory,
exception: Exception,
expected_state: str,
) -> None:
"""Test ntfy events exceptions."""
mock_aiontfy.subscribe.side_effect = exception
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
freezer.tick(timedelta(seconds=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (state := hass.states.get("event.mytopic"))
assert state.state == expected_state