Skip to content

Add HassModel API support for persistent_notification/subscribe (push-based dismiss detection) #1361

Description

@SatMeNow

The problem

Home Assistant's persistent_notification integration does not use the HA event bus. It
communicates through an internal dispatcher signal and a dedicated WebSocket subscription
(persistent_notification/subscribe). This means that none of the standard HassModel APIs
fire for persistent notification changes:

  • ha.Events — never emits state_changed for persistent_notification.*
  • StateAllChanges() / StateChanges() — never fires for these entities
  • ha.GetAllEntities() — does not include persistent_notification.* entities

There is no error, no warning — the observable simply never emits. This is particularly painful
when trying to detect user-initiated dismissals of persistent notifications from the HA sidebar,
which is a common pattern for device acknowledge flows.

Confirmed root cause: The HA core source (homeassistant/components/persistent_notification/__init__.py)
uses async_dispatcher_send(hass, SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED, ...) rather than
hass.bus.async_fire(...), so EntityStateCache (which feeds ha.Events) never sees these changes.

The proposed solution

Expose a SubscribePersistentNotificationsAsync() helper (or equivalent) that wraps the HA
WebSocket persistent_notification/subscribe command and returns an
IObservable<PersistentNotificationUpdate> (or similar push-based stream).

Suggested API shape

public enum PersistentNotificationUpdateType { Added, Removed, Updated, Current }

public record PersistentNotification(
    string NotificationId,
    string Message,
    string Title,
    string Status,      // "read" | "unread"
    DateTime CreatedAt
);

public record PersistentNotificationUpdate(
    PersistentNotificationUpdateType Type,
    IReadOnlyDictionary<string, PersistentNotification> Notifications
);

Extension on IHaContext (or IHomeAssistantConnection):

// Subscribes via persistent_notification/subscribe WebSocket command.
// Emits on: added, removed, updated, current (initial snapshot).
IObservable<PersistentNotificationUpdate> SubscribePersistentNotifications();

Typical consumer pattern (dismiss detection)

ha.SubscribePersistentNotifications()
    .Where(update => update.Type == PersistentNotificationUpdateType.Removed
                  && update.Notifications.ContainsKey("my_notification_id"))
    .Subscribe(_ =>
    {
        // User dismissed the notification — write back to device
    });

Workaround (current, polling-based)

Without this API, the only reliable approach is to poll persistent_notification/get on every
cyclic tick while the notification is expected to be shown:

private sealed record GetNotificationsCommand : CommandMessage
{
    public GetNotificationsCommand() { Type = "persistent_notification/get"; }
}

private sealed class HassNotificationItem
{
    [JsonPropertyName("notification_id")]
    public string? NotificationId { get; init; }
}

// Called every N seconds while notification is active:
var list = await runner.CurrentConnection!
    .SendCommandAndReturnResponseAsync<GetNotificationsCommand, List<HassNotificationItem>>(
        new GetNotificationsCommand(), cancel);

bool stillActive = list?.Any(n => n.NotificationId == "my_notification_id") == true;

This works but requires polling, bypasses the HassModel abstraction layer, exposes internal
NetDaemon.Client types, and forces apps to manage their own interval logic.

The alternatives

  1. Polling via persistent_notification/get — works but is fragile and not idiomatic
    (see workaround above).

  2. Expose persistent_notification/subscribe directly as a raw IObservable<HassEvent>
    on the connection level — lower effort than a typed helper, still better than nothing.

Additional context

  • HA WebSocket protocol reference: persistent_notification/subscribe emits messages with
    type: "added" | "removed" | "updated" | "current" and a notifications dictionary keyed
    by notification_id. See HA frontend source:
    src/data/persistent_notification.ts

  • NetDaemon version tested: 26.11.0

  • The same gap likely exists for other HA integrations that use the dispatcher signal pattern
    instead of the bus (e.g. repairs, todo lists). A generic SubscribeMessage<T> helper on
    IHaContext would cover all such cases.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions