Skip to content

RFC: Notification Handling in Signal K Server #2856

Description

@dirkwa

RFC: Notification Handling in Signal K Server

Status Draft v4 — for discussion
Scope Notification layer only. Central Alert Management (CAM) will be a separate, follow-up RFC.
Discussion Discord #server thread 2026-07-19/21 (Teppo Kurki, Matti Airas, tobias_r, Dirk); PR #2847 review

1. Summary

Signal K today conflates two different things under notifications.*:

  1. Notification — a statement of fact: "I observed an abnormal condition or noteworthy event, here is what I know."
  2. Alert — a managed demand for human attention: presentation (sound, visual, push), escalation, acknowledgement flow, and triggered actions.

This RFC proposes to make that split explicit and to fix the notification layer:

  • A notification is information only. The emitter states what happened and how severe it is — never how it should be presented. method: ['visual','sound'] is removed from the emitter's authority.
  • The server's NotificationManager is confirmed as the single lifecycle authority for all notifications, including those arriving as raw deltas from gateways and legacy plugins. Much of this already ships enabled by default (§6.2); this RFC declares it the intended architecture and hardens the implementation against ten concrete defects found in the current code (§6.9 — silent removal without tombstones, no restart persistence, zone hysteresis, sensor-loss-as-normal, and more).
  • The lifecycle becomes a standards-aligned state graph (IEC 62682:2022, IEC 62923-1:2018): severity and lifecycle state are separated into two fields, rectified-unacknowledged is retained until acknowledged, silence is a timed data-layer state, and standing normal entries are replaced by explicit rectification semantics with bounded retention.
  • Plugins announce the notifications they can emit (a capabilities catalog, modeled on the Radar API's capability-announcement pattern), so consumers can be configured before a condition ever fires.
  • The Admin UI gets a notification management panel (list, acknowledge) — Teppo's minimal goal — and plugin/server error statuses migrate onto notifications, relieving the overloaded dashboard status list.

Everything about presentation and action — annunciators, sound routing, mobile push, navigation.state-dependent behavior, switching a light on when the anchor drags — is out of scope and deferred to the CAM RFC. This RFC's job is to give CAM a clean substrate to stand on.

2. Motivation — what is broken today

Field evidence from a production 2.30.0 installation (Data Browser, notifications.* subtree, 36 paths — screenshots archived with this RFC):

P1 — The emitter decides presentation. The v1 model { state, method: ['visual','sound'], message } lets the party that raises a notification dictate how it is annunciated. The raiser cannot know the vessel context: a failed cloud backup and a dragging anchor must not sound the same siren, and whether the crew is asleep at anchor or underway at night changes everything. Teppo: "V1 notifications has one glaring error in this respect: the party that raises the notification also decides how it should be presented." This is the core defect.

P2 — No single lifecycle authority. The same path can be written by multiple sources with conflicting states. Observed: notifications.nmea.alarm.navigational.22.8196 simultaneously carries ALARM: Highwater from source IPG100.128 and NORMAL from a ws.<uuid> source. Which one is true? What does acknowledging it mean, and to whom? Matti: "there can be only a single entity managing the alert/notification state."

P3 — Standing NORMAL noise. 15+ permanent entries of the form NORMAL: Value is within normal range (zone handler), NORMAL: Normal communication with BT sensor … (health pings dressed as notifications), and even NORMAL: undefined. The absence of a problem is modeled as a standing object, so consumers cannot distinguish "condition was abnormal and has been rectified" from "nothing ever happened", and every UI that lists notifications drowns in green rows.

P4 — Uncontrolled path taxonomy. Observed in one tree: notifications.nmea.alarm.navigational.22.8196 (gateway-internal instance numbers), notifications.sensors.Victron_Orion_Smart_24V_12V-30A_DC-DC_Converter_F7_73_12_AD_B5_1D (device name + BT MAC as path segments), notifications.buddy.urn:mrn:imo:mmsi:368294880, and a bare notifications. (empty relative path). There is no reliable way to group, filter, or address notifications programmatically.

P5 — No discoverability. A consumer (future CAM, dashboards, dispatch plugins) cannot know which notifications a plugin may emit without waiting for each one to fire once. Configuring rules for "all the notifications my anchor plugin can raise" is guesswork.

P6 — Admin UI has no notification management. The dashboard status list has become the de-facto place plugins surface problems and it no longer scales (the trigger for this discussion). There is no way to see, filter, or acknowledge notifications in the Admin UI.

P7 — Divergence from marine standards. IMO MSC.302(87) / IEC 62923 (Bridge Alert Management) and the NMEA 2000 alert PGNs (126983/126984/126985) define alert categories, priorities, states, and responsibilities. Full alignment is CAM's job, but the notification model must not preclude it — today it does (no identity, no lifecycle states, no category concept).

3. Terminology

Term Meaning in this RFC
Notification An informative statement that a condition or event has been observed, with a severity classification. Carries no presentation semantics.
Alert A managed entity demanding attention, derived from notifications by CAM. Owns presentation, escalation, action triggering. Out of scope here.
Severity The emitter's classification of the condition: `alert
Lifecycle state Where the occurrence stands: active-unacknowledged, active-silenced, active-acknowledged, rectified-unacknowledged, normal (see §6.2). Severity and state are separate fields. Today's ALARM_STATE conflates them — normal/nominal are lifecycle information ("condition cleared"), not severities.
Rectification The underlying condition has returned to normal. A lifecycle transition, not a standing normal value.
Managed notification A notification whose lifecycle (identity, state transitions, acknowledgement) is owned by the server's NotificationManager. Goal state: all notifications are managed.

Mapping to the marine standards (IEC 62923-1:2018 / IMO MSC.302(87) priorities — stated explicitly because of a naming collision: BAM uses alert as the umbrella term for all priorities, while Signal K's alert is the lowest severity):

Signal K severity BAM priority (IEC 62923-1 §6.2.2.1) Semantics
emergency Emergency alarm Immediate danger to life/ship, immediate action. No ack, no silence — only normal ⇄ active.
alarm Alarm Immediate attention and action. Full state machine.
warn Warning Immediate attention, no immediate action. Full state machine; unacknowledged warnings escalate (CAM).
alert Caution Awareness only. Never requires acknowledgement, auto-clears on rectify — only normal ⇄ active.
normal / nominal — (not priorities) Lifecycle information; become the normal state / zone baseline, not severities.

IEC 62682:2022 (process-industry alarm management) uses the same layering with different words: its alarm = "requires timely response" (Signal K alarm/emergency), its operator alert = "evaluate when time allows" (Signal K alert/warn), its event = state-change fact with no response semantics (a plain Signal K delta). Signal K's "notification" as the umbrella term collides with neither standard.

4. Scope and non-goals

In scope: notification data model and identity, lifecycle and single authority, ingestion/adoption of externally sourced notifications, normal-state/retention semantics, capabilities catalog, Admin UI management (list/acknowledge), migration and compatibility.

Explicit non-goals (deferred to the CAM RFC):

  • Presentation: sound, visual, mobile push, annunciator selection and registry (N2K beeper vs. PC audio vs. HMI display).
  • Action triggering (e.g. PUT electrical.switches.… when a notification appears).
  • navigation.state-dependent behavior (different rules anchored vs. underway), mode-dependent zone profiles.
  • Escalation, repetition, transfer of responsibility (MSC.302 state machine).
  • Whether CAM lives in core or as a privileged plugin (signalk-alert-manager is the natural candidate to build on).

5. Current state inventory

5.1 What "v1" actually is

There is no separate v1 notifications API to deprecate as a unit. "v1" is three distinct things, and they have three distinct deprecation answers:

v1 component What it is Can it be deprecated?
The method field Emitter-chosen presentation (['visual','sound']) in the spec'd value schema Yes — this is the deprecation target of this RFC (§6.1, phase 5 in §8)
Write mechanism Plugins/gateways emitting notifications.* deltas via handleMessage No — kept as a first-class ingest path (§6.2 adoption, §6.8). "Pop it in the tree and be done with it" stays valid
Read representation notifications.* in the v1 full model (/signalk/v1/api/…) and on the WS delta stream Not for the foreseeable future — it is how every existing consumer (KIP, WilhelmSK, Freeboard-SK, dispatch/player plugins) sees notifications; it remains the read path, fed by the manager

So the deprecable part of v1 is precisely its one glaring error — presentation authority at the emitter — not the emission or consumption mechanics around it.

5.2 Inventory

What exists today — the RFC builds on this rather than starting over:

  1. v1 delta model (Signal K spec): plugins/gateways write notifications.<path> values { state, method, message } via handleMessage. Consumers (KIP, WilhelmSK, Freeboard-SK, notification-player) read method to decide presentation.
  2. meta.zones: declarative per-path zones plus per-state presentation overrides (alarmMethod, warnMethod, … — ZoneMethods, undocumented in the public MetaValue type); the server's core zones engine emits zone-crossing notifications with method resolved from these overrides (source self.notificationhandler in the field evidence). See §6.7 for the full three-layer picture.
  3. v2 Notifications API — already in the server (src/api/notifications/, packages/server-api/src/notificationsapi.ts): app.notifications.raise/update/mob/silence/acknowledge/clear(…), id-based (NotificationId), AlarmStatus { silenced, acknowledged, canSilence, canAcknowledge, canClear }, REST + AsyncAPI, and a notifications.manageNotifications config toggle. Notably, v2 raise() already does not accept method — the direction proposed here is half-built. Beyond the API surface, adoption is already active by default: a delta-input handler intercepts every notifications.* value, assigns a stable id keyed by (context, path, $source), and syncs it into the NotificationManager (filterNotifications/handleNotificationUpdate in src/api/notifications/index.ts). The managed value even embeds status (AlarmStatus) on the v1 stream, and external N2K acknowledge state (acknowledgeStatus) is already parsed on ingest.
  4. Ecosystem: signalk-alert-manager (Matti — also author of a pending "disable notification manager" PR, reflecting disagreement about the current manager), signalk-notification-dispatcher (tobias_r — rule-based forwarding, already navigation.state-aware), signalk-notification-player (audio), @codekilo/signalk-trigger-event. N2K gateways inject third-party alert systems (Maretron N2KView via IPG100 in the field evidence).
  5. Admin UI: dashboard status list only; PR feat: webapp warn/error status badges in Webapps list and sidebar #2847 (webapp status badges) touches adjacent ground.

6. Proposal

6.1 Notifications carry no presentation (normative core)

  • ALARM_METHOD / method is removed from the emitter's vocabulary. Plugins raise notifications exclusively through the v2 API, whose raise() already has no method parameter — this RFC makes that the rule rather than an accident.
  • The NotificationManager strips or ignores method on managed notifications. During the transition it synthesizes method for legacy consumers, resolved in priority order: (1) the path's per-state *Method overrides from meta (alarmMethod, warnMethod, … — existing vessel-operator config, see §6.7 Layer 1), (2) a server-level severity→method default mapping (e.g. alarm|emergency → visual+sound, warn|alert → visual, configurable). So KIP/WilhelmSK keep working and existing per-path tuning is honored. The shim is transitional and explicitly slated for replacement by CAM.
  • Consequence: a plugin can no longer force a siren. It can say emergency; what happens then is the vessel's (CAM's) decision, not the plugin author's.

6.2 Single lifecycle authority

  • Status: largely shipped, not greenfield. With manageNotifications on (the default), a registerDeltaInputHandler already intercepts every notifications.* value, strips it from the passing delta, assigns a stable id keyed by (context, path, $source), and syncs it into the NotificationManager (src/api/notifications/index.ts). Single authority, adoption of raw gateway/legacy deltas, and per-origin identity exist today. What this RFC does is (a) declare this the intended architecture rather than an implementation detail, (b) harden it — the shipped pipeline has concrete defects listed in §6.9, and (c) resolve the standing disagreement about it (Matti's pending "disable notification manager" PR; see Open Questions — the single-authority requirement is his, so his objection is presumably to the model, which §6.9's fixes must address).

  • Adoption semantics (confirming the shipped behavior as design): raw deltas targeting notifications.* (N2K gateways, legacy plugins, WS clients) are never rejected — they are ingested as managed notifications with the delta source as origin. Direct writes never bypass the manager. For one (context, path, origin) there is exactly one managed notification; conflicting states across origins are distinct managed notifications a UI can show side by side, instead of last-writer-wins on a single path (resolves P2 — and note this means Open Question 3's "per-origin" option is the status quo).

  • Lifecycle — a state graph, not a chain (aligned with IEC 62682:2022 §5.3/Table 2 and IEC 62923-1:2018 §6.3.2.2/Annex G). Both standards factor the model into orthogonal axes — condition (normal/abnormal), lifecycle state, acknowledgement — and both insist that acknowledge and rectify can happen in either order:

    normal ──raise──▶ active-unacknowledged ──ack──▶ active-acknowledged ──rectify──▶ normal
                        │        ▲    │ silence                                        │
                        │        └────┤ (auto-expires, IEC 62923-1 §6.3.4.3)           ▼
                        └──rectify──▶ rectified-unacknowledged ──ack──▶ normal    [removal event
                                       │                                           + history]
                                       └──reoccur──▶ active-unacknowledged  (same identity)
    

    Key rules adopted from the standards:

    • rectified-unacknowledged is retained until acknowledged — a condition that clears before anyone saw it stays visible (IEC 62923-1 §6.3.4.5.1; IEC 62682's "RTNUN" state). Auto-ack on rectify is a per-severity/per-catalog-entry configuration (IEC 62682 §5.3.3.7), not a global default.
    • Per-severity state subsets: emergency and alert (BAM: emergency alarm, caution) use only normal ⇄ active — no ack, no silence (IEC 62923-1 Annex G, Figs G.1/G.4). alarm and warn use the full graph.
    • Acknowledgement survives updates. Only a genuinely new activation (reoccur), or explicitly configured re-alarming (IEC 62682 §5.3.3.4), returns an acknowledged notification to active-unacknowledged. De-escalation and message/data updates never reset ack.
    • Escalation creates a new occurrence. Both standards model severity escalation as a new alarm (IEC 62923-1 §6.3.7.1: warning→alarm terminates the warning and raises a new alert ID; IEC 62682 models each setpoint as its own alarm). Severity is immutable per occurrence; escalation = rectify/terminate + raise, linked via the notification's data (predecessor id).
    • Silence is a data-layer state that auto-expires. active-silenced is a source-side state transition, not a display effect, and reverts to active-unacknowledged on a timer (IEC 62923-1 §6.3.4.3: 30 s for alarms/warnings, configurable ≤ 5 min for category-C-like classes; holding/repeating silence must not extend it; new notifications are never pre-silenced). The timer therefore belongs to the NotificationManager; the audio policy it modulates belongs to CAM. This resolves Open Question 1.
    • Reserved states for CAM and operations, not implemented here but kept in the vocabulary so the model never blocks them: responsibility-transferred (IEC 62923-1 §6.9) and the suppression family shelved / suppressed-by-design / out-of-service (IEC 62682 §5.3.2, §11.7–11.9 — suppressed alarms keep evaluating their condition and stay queryable; suppression is never deletion).
    • Every transition is recorded (IEC 62682 §16.2; IEC 62923-1 §7.2.3: history keeps all state changes, ≥ 24 h / ≥ 5 000 entries) — see §6.3 retention and Open Question 11.

6.3 Normal-state semantics and retention

  • A managed notification leaves the live tree only from the normal state — i.e. rectified and acknowledged (or auto-acked where configured; alert-severity notifications auto-clear on rectify per the caution rules, IEC 62923-1 §6.3.6). rectified-unacknowledged entries stay (§6.2). After reaching normal, a configurable linger applies (minutes, not forever); "normal is not annunciated" (IEC 62682 Table 2) — the absence of a problem is represented by absence of a notification. History belongs to the transition journal / history provider, not the live model.
  • Removal needs a mechanism, not just a policy. The v1 delta protocol has no deletion semantics: today the manager already deletes rectified alarms internally (clean() after ~2 minutes) but emits nothing, so the last normal value survives in the delta cache and full model indefinitely — this, not the zones engine alone, is where the ghost NORMAL rows come from. The RFC must define a tombstone: candidate options are a final delta with value: null plus delta-cache purge, or an explicit removal event on the v2 stream with the v1 tree pruned server-side. Spec-level gap; see Open Questions.
  • nominal/normal as initial state is no longer a notification at all. "BT sensor communication OK" is health/status information, not a notification — it belongs in the source/plugin status mechanism (cf. P6 and the source-status work: absence of a problem should be represented by absence of a notification). This single rule removes most of the noise in the field evidence.

6.4 Identity and paths

  • Every managed notification has a stable NotificationId (exists in v2 already). The path classifies; the id identifies. Acknowledgement targets the id, never the path — so multiple concurrent notifications on one path (P2, P4) are individually addressable.
  • The standards refine this into a three-part identity worth adopting (IEC 62923-1 §8.1.1, Annex C; IEC 62923-2 §4): kind identifier (which condition — a catalog entry, §6.5), instance (which occurrence of that condition — two lost AIS targets are two instances of one kind), and revision counter (monotonically increasing per state change). The revision counter is what makes reconnect/restart resynchronization well-defined for stream consumers — a client that missed deltas can detect it and re-fetch (IEC 62923-1 §8.1.6).
  • Instance data never goes into the identifier or path. IEC 62923-2 Table A.1 uses parameterized titles (Lost <interface>, Fire <location>) with the specifics as parameters — the standard's direct answer to P4's MAC-addresses-and-gateway-numbers-in-paths. Device, location, and target specifics belong in data/message parameters and the origin, never in the classification.
  • A recommended path taxonomy is documented (spec-level guidance, not enforced initially): stable, semantic segments; no device serials/MACs/gateway instance numbers as primary classification; reserved subtrees for server-raised notifications (notifications.server.*).

6.5 Capabilities catalog (discoverability)

Plugins declare the notifications they can emit: identifier/path pattern, possible severities, human-readable description, and optional additional-data schema. Precedent: the Radar API, where the provider announces each radar's capabilities so downstream consumers need no per-model knowledge — same pattern, much smaller maintenance surface than every consumer keeping a matrix.

Candidate mechanisms (to be decided in discussion):

  1. Runtime registration: app.notifications.registerCatalog([...]) at plugin start — dynamic, matches the radar precedent.
  2. Static metadata in package.json (signalk.notifications) — inspectable without loading the plugin, App Store can display it.
  3. Both: static declaration, runtime refinement.

The standards sharpen what the catalog should be (IEC 62923-2):

  • Two-tier registry: a server-curated set of standardized notification kinds for conditions every ecosystem participant shares — IEC 62923-2 Table A.1 is a ready-made seed vocabulary of source-agnostic condition kinds (lost-input, invalid-data, threshold-exceeded, power-fail, system-fault, fallback-active, …) — plus a namespaced free range (<plugin-id>:<name>) for everything else. Standardized conditions must reuse the standardized kind (the standard's "reduce the number of different identifiers used for similar alerts", Clause 1).
  • The catalog is the registry of record: kinds are never reassigned; renames ship an old→new alias map with legacy ids documented but deprecated (the standard's bracketed-legacy-identifier mechanism, IEC 62923-2 A.1). Conformance in the standard is checked against the manufacturer's declared alert list (§4.2) — precedent for validating emitted notifications against the declared catalog (plugin-ci lint).
  • Cluster/domain field: IEC 62923-2 Annex B defines reserved cluster identifiers organized by operational responsibility (navigation, communication, automation/machinery, safety, hotel, ICT, …) — who acts on it, not where the data came from. Catalog entries carry an optional cluster from a small reserved vocabulary plus a free range, statically declared like the standard's manufacturer-declared cluster list.
  • Declared severity set: each entry declares its possible severities (the catalog analog of IEC 62923-2's per-priority identifier blocks) so the manager can validate emissions.
  • Optional iecAlertId/iecCluster attributes per entry, so a future CAM or an N2K alert-PGN bridge (126983–126985) can emit standard-compliant identifiers without remapping.

Payoff: CAM/dispatcher rules configurable before a condition ever fires; Admin UI shows friendly names; plugin-ci can lint "emits undeclared notification".

6.6 Admin UI management panel

  • New Admin UI panel: list active managed notifications (severity, message, origin, age), acknowledge individually or in bulk, view recently rectified. This is deliberately Teppo's minimal scope: show and acknowledge.
  • This is an early, independently shippable win. The REST surface it needs already exists in the v2 API (list, per-id acknowledge/silence, acknowledgeAll, silenceAll — §6.7 Layer 3), so the panel is almost purely a frontend task. It can ship ahead of, and independently of, everything else in this RFC — and delivers the original ask that started this discussion.
  • Server and plugin error statuses are migrated to server-raised notifications (notifications.server.plugins.<id>.…), so the dashboard status list stops being the overloaded catch-all that triggered this discussion.

6.7 Zones — three layers, three dispositions

Zones are not one thing; the server already implements a three-layer stack, and each layer gets a different treatment:

Layer 1 — meta.zones + per-state method overrides (declarative config). Each path's meta carries zones (lower/upper/state/message) plus per-state presentation overrides (alarmMethod, warnMethod, emergencyMethod, … — ZoneMethods in src/zones.ts, set via PUT …/meta, defaults files, or plugin meta deltas; currently absent from the public MetaValue type). Crucially, the *Method overrides are not emitter-decided presentation — they are configured by the vessel operator. They don't violate this RFC's core principle (the emitter must not decide presentation); they are in fact the vessel's only existing per-path presentation policy — a primitive CAM rule embedded in metadata. Disposition: zones/messages stay (declarative severity source); the *Method overrides are grandfathered as vessel policy — they become the highest-priority input to the transition synthesis shim (§6.1), and their long-term home (migrate into CAM rules vs. remain meta-resident config CAM reads) is a CAM-RFC question (see Open Questions).

Layer 2 — core zones engine. Watches values against zones and today emits raw notifications.<path> deltas with method resolved from Layer 1. Disposition: it raises/rectifies managed notifications through the NotificationManager instead of emitting raw deltas, and stops attaching method itself — presentation resolution moves entirely into the synthesis shim (transition) and later CAM. Zone re-entry into normal becomes a rectification event (§6.3), eliminating the standing-NORMAL rows the zone handler produces today.

Layer 3 — NotificationManager. Already provides the managed-alarm wrapper: acknowledge/silence/clear per id, silenceAll/acknowledgeAll REST endpoints, reactivation on severity change, and emergency-cannot-be-silenced rules. This RFC doesn't invent this layer — it makes it the only authority (§6.2) and puts an Admin UI on it (§6.6). "Mute all" and "mute one" already exist server-side; they need surfacing, not building.

Mode-dependent zone profiles (different thresholds anchored vs. underway) remain a CAM-RFC extension point and are intentionally not designed here.

6.8 Two write paths, one authority

Guiding principle (Teppo): "if your plugin has something to say, pop it in the notifications. tree and be done with it."* Emitting a notification must stay that simple — therefore direct delta emission is not deprecated:

  • Simple path (kept, first-class): a plugin emits a notifications.* delta via handleMessage — minus method — and is done. The manager adopts it (6.2): assigns identity, tracks lifecycle, derives rectification from subsequent values on the same (path, origin). Zero new API to learn; gateways and existing plugins keep working unchanged.
  • Rich path (recommended for lifecycle-aware plugins): the v2 API (raise/update/clear with a stable id) for emitters that actively manage a condition over time — e.g. an anchor plugin escalating warnalarm on the same notification.
  • Either way there is one authority: both paths land in the NotificationManager; nothing mutates notification state except the manager.
  • What is deprecated is only method (§6.1) — emitting it logs a deprecation warning (later a plugin-ci lint) and the value is ignored/replaced by the synthesized mapping.
  • Read paths unchanged: the delta stream and v1 full model continue to expose notifications (with synthesized method during transition), so existing consumers see no break.

6.9 Hardening requirements from reading the shipped code

Close reading of the current implementation (2.30-era master: src/api/notifications/, src/zones.ts) surfaces concrete defects this RFC adopts as requirements. They double as evidence for the discussion that the architecture is right but the implementation needs the pass this RFC scopes:

  1. Silent removal / no tombstone. NotificationManager.clean() deletes rectified alarms from its map after ~2 minutes but emits no delta — delta cache and full model keep the last normal value forever (§6.3; the direct cause of the standing-NORMAL field evidence).
  2. Identity-map leak. The adoption pipeline's notiKeys map ((context, path, source) → id) is never pruned when alarms are cleaned; it grows unboundedly and re-associates long-dead ids with re-raised paths.
  3. No persistence. Alarms and their acknowledge/silence state are in-memory only. A server restart silently drops API-raised notifications (gateway ones re-adopt on the next delta; plugin-raised ones don't) and re-alarms previously acknowledged conditions. MSC.302 expects alert state to survive; at minimum the RFC must decide restart semantics deliberately.
  4. Inconsistent reactivation rules. Managed update() clears acknowledged+silenced on any state change — including de-escalation (alarmwarn) and even rectification. The adopted-delta path reactivates only on normal→abnormal. One rule should hold, and de-escalation should not re-trigger an acknowledged alarm (MSC.302 distinguishes escalation from other transitions).
  5. Hardcoded method on the API path. Alarm's default value and disabled-mode raiseViaDelta hardcode ['visual','sound'] for every API-raised notification, while adopted external values keep their emitter's method verbatim. Both are exactly what the §6.1 synthesis shim replaces.
  6. method doubles as the lifecycle-status channel. Acknowledge empties method (emergency: ['visual']), silence strips 'sound' (alignAlarmMethod) — a presentation field is being used to signal lifecycle state to v1 consumers. The replacement already exists (value.status is embedded in emitted values); phase 5 must migrate consumers to status before method disappears.
  7. Zones: no hysteresis. The zones engine deduplicates on zone index only; a value oscillating across a boundary re-raises endlessly, and combined with (4) each oscillation un-acknowledges the alarm — a noisy sensor at a threshold produces a siren that cannot be silenced. Zone evaluation needs deadband and on/off-delays — both are "shall" capabilities in IEC 62682 (§10.5.5, §10.5.6), which also names the failure modes precisely: chattering, fleeting, stale, and nuisance alarms (§3.1.41/54/82/63), with a long-term chattering target of zero (§16.3.7).
  8. Zones: null maps to "normal". A null value falls into the default zone: state: 'normal', message: 'Value is within normal range'sensor loss masquerades as rectification (dead freezer sensor reads as "freezer fine"). Loss of data must be distinct from being in the normal zone; this ties into the staleness/source-status semantics (missing = unknown, not normal — and not rectified).
  9. raise() has no idempotency. Every call mints a fresh uuid; a plugin that restarts and re-raises its standing condition creates a parallel duplicate alarm. The adopted path has stable (context, path, source) keying — the rich API paradoxically doesn't. raise() should accept an optional dedup/upsert key.
  10. Authorization is implicit. The acknowledge/silence/clear REST handlers apply no explicit permission checks (whatever the global security middleware provides is it). Who may acknowledge an alarm — helm, crew, guest, a plugin? — must be defined (MSC.302 is strict about where acknowledgement may come from).
  11. Silence never expires. silence() sets a flag that persists until the next state change — but silencing is temporary by definition: the silenced state must auto-revert to active-unacknowledged on a timer (30 s for alarms/warnings, IEC 62923-1 §6.3.4.3), and repeating the silence command must not extend the period. The current permanent silence is a mute button, not a silence.
  12. No refusal outcome for acknowledge. The API can only succeed or 404. The standards define ack as refusable (IEC 62923-1's ARC response; category-A/C alerts have restricted or no bridge-ack), so the manager needs an explicit "acknowledge not allowed" outcome — this is also the natural hook for (10)'s authorization answer, and canAcknowledge/canSilence/canClear in AlarmStatus already anticipate it.
  13. Bulk acknowledge conflicts with the standards' individual-ack rule. acknowledgeAll() acks every alarm in one sweep; IEC 62923-1 §7.2.1.8.1 (MSC.302/9.9) permits acknowledging alarms and warnings individually only, and IEC 62682 §11.6.2.5 allows multi-ack solely behind access control. Keep silenceAll (standard-blessed, one operator action), but bulk-ack should at minimum become an access-controlled, deliberately-enabled operation.

7. Standards alignment and forward compatibility with CAM

The notification layer proposed here deliberately mirrors the split the standards themselves make. A structural observation from IEC 62923-1 that validates the whole architecture: BAM puts the entire alert state machine at the alert source, not at the display — the CAM/HMI only sends commands (acknowledge, silence, transfer), and the source decides, changes state, and reports it consistently to all displays (§6.3.2.2). In Signal K terms: the server's NotificationManager plays the alert-source role, every client (KIP, Freeboard-SK, HMI displays, CAM itself) is an HMI that requests state changes. That is exactly §6.2's single-authority model.

Adopted into this layer (from §6.2–6.5): the state graph incl. rectified-unacknowledged and timed silence; severity/state separation with immutable severity per occurrence; kind/instance/revision identity; the catalog as registry of record with clusters and declared severities; transition journaling sufficient to compute IEC 62682 Clause 16 quality metrics later (alarm-flood detection, chattering top-offenders, stale-alarm counts — the journal must capture per-transition timestamps so these are computable, even though the dashboards are CAM work). One more rule worth adopting verbatim: on receiving inconsistent external data, coerce to the nearest valid combination rather than reject (IEC 62923-1 Annex C.7 — e.g. a caution claiming active-unacknowledged is treated as active).

Reserved for CAM (the follow-up RFC's inventory, per IEC 62923-1 and IEC 62682):

  • Category / ack-scope (IEC 62923-1 §6.2.2.2, categories A/B/C): where acknowledgement is permitted is a per-alert property, orthogonal to priority — the standardized answer to "who may ack". This RFC reserves an optional category catalog field; CAM enforces it.
  • Presentation: audible patterns, flash/steady/colour per state, display-priority ordering (§6.4.2.1's ten-level sort), icons, the 3–5 s presentation delay.
  • Escalation policy: unacknowledged-warning repetition and warning→alarm escalation timers (§6.3.7) — the mechanism (new occurrence, predecessor link) is defined in §6.2; the policy is CAM.
  • Aggregation (same-kind, instance 0 headers, §6.8) and functional grouping (§6.7); responsibility transfer with heartbeat supervision (§6.9, HBT ≤ 30 s, fallback to source within 2 min).
  • Shelving/suppression/out-of-service workflows with access control, time limits, and audit (IEC 62682 §11.7–11.9, §15) — states reserved in §6.2.
  • The vessel's "alarm philosophy": prioritization method, class definitions, performance targets, retention policy (IEC 62682 Clause 6) — the CAM RFC is, in 62682 terms, Signal K's alarm philosophy document.

What CAM needs from this layer, guaranteed here: stable identity and lifecycle events it can subscribe to (no polling, no path heuristics); severity as clean information uncontaminated by presentation; the capabilities catalog for authoring alert policies; an extension field (data, plus a reserved namespace) so CAM can annotate without schema changes here.

Whether CAM is core or a privileged plugin, and how signalk-alert-manager's model maps onto MSC.302/IEC 62923, is explicitly the next RFC's discussion.

8. Migration and compatibility

Phase Change Breaks anyone?
1 Manager-on and delta adoption are already shipped defaults. Phase 1 = hardening (§6.9): tombstone emission + cache purge, notiKeys pruning, persistence decision, unified reactivation, zone hysteresis and null-handling, raise() dedup key; method synthesis shim replaces the hardcoded defaults No — consumers still see method
2 Deprecation warnings (server log + plugin-ci lint) for plugin-side method emission — direct notifications.* writes themselves stay supported (§6.8) No — warnings only
3 Admin UI panel; server/plugin statuses as notifications No
4 Capabilities catalog API; App Store/plugin-ci integration No — opt-in
5 Spec vNext: method removed from the notification schema; synthesis shim retired once CAM exists Yes — coordinated major, gated on CAM being available

Compatibility note on the severity/state split (§6.2): the managed model carries severity + state internally, but the v1 read path keeps emitting today's conflated value.state (severity while active, 'normal' on rectification) synthesized from the pair — existing consumers see no change until spec vNext. The new fields ride alongside (like value.status does today).

9. Open questions (input wanted)

  1. Silence. Notification-layer or CAM? Proposed answer (from the standards): the silenced state is data-layer — IEC 62923-1 makes active-silenced a source-side, auto-expiring state transition (§6.3.4.3, §7.2.2.1), so the state and its expiry timer belong to the NotificationManager; the audio policy it modulates belongs to CAM. IEC 62682 adds the boundary rule: silence is never acknowledgement (§3.1.2 vs §3.1.81). Remaining discussion point: the timer defaults (30 s per the standard feels short for a short-handed cruising boat — configurable per severity/class?).
  2. manageNotifications=false. Does the opt-out survive this RFC, or does single-authority mean the toggle goes away? (Matti's pending "disable notification manager" PR suggests the current manager's model is the objection — does the model proposed here resolve it?)
  3. Multi-origin policy. When Maretron raises ALARM and another source says the same path is fine: distinct managed notifications per origin (proposed), or a conflict-resolution policy (highest severity wins)?
  4. Retention. Where do rectified notifications go — server log, history provider (signalk-questdb etc.), both? How long is the live linger?
  5. Other contexts. Notifications about non-self contexts exist in the wild (notifications.buddy.urn:mrn:imo:mmsi:…). Is the manager self-only (v2 today effectively is), or context-aware?
  6. Catalog mechanism. Runtime registration, static package.json metadata, or both (6.5)?
  7. Staleness/source events. Should the staleness enforcer and source-status subsystem raise managed notifications, or stay separate health mechanisms (per 6.3 they are health, not notifications — but the dashboard migration in 6.6 blurs this)?
  8. mob() and distress helpers. Distress is where notification and alert are hardest to separate — does mob() stay a notification-layer convenience or move to CAM?
  9. Future of meta *Method overrides. They are grandfathered as vessel presentation policy feeding the synthesis shim (§6.7 Layer 1). Long term: migrate them into CAM rules (and deprecate from meta alongside method), or keep meta as the canonical per-path presentation config that CAM reads? Either way they should be added to the public MetaValue type — today they are undocumented core behavior.
  10. Tombstone mechanism. How does a notification leave the v1 tree (§6.3)? Final value: null delta + delta-cache purge, or a v2 removal event with server-side pruning? What do existing v1 consumers do with a null notification value today?
  11. Persistence. Should managed notifications and their acknowledge/silence state survive a server restart (file in the settings dir? via the history provider?) — and if yes, which classes (all / emergency-only / externally-adopted excluded, since gateways re-send)? The standards' pattern for comparison: IEC 62923-1 relies on source retransmission + revision counters for live-state resync (§8.1.6, §8.2.2 — CAM restarts and rebuilds from the sources) but mandates a transition history of ≥ 24 h / ≥ 5 000 state changes (§7.2.3.7); IEC 62682 requires every state transition recorded (§16.2). So: journal is mandatory; live-state persistence is the open part.
  12. Acknowledge authorization. Which security roles may acknowledge/silence/clear (§6.9 item 10)? Is acknowledgement user-attributed (who acked, when — an audit trail needs it)? The standards' building blocks: a per-alert category/ack-scope property (IEC 62923-1 §6.2.2.2, reserved in §7), a refusable acknowledge with explicit feedback (§6.9 item 12), and access-controlled bulk operations (IEC 62682 §11.6.2.5).

10. References

  • IMO Resolution MSC.302(87) — Performance standards for Bridge Alert Management: https://wwwcdn.imo.org/localresources/en/KnowledgeCentre/IndexofIMOResolutions/MSCResolutions/MSC.302(87).pdf
  • IEC 62923-1:2018 — Bridge Alert Management, Part 1: operational and performance requirements (implements MSC.302; alert state machine §6.3/Annex G, priorities/categories §6.2, silence rules §6.3.4, history §7.2.3, identifiers §8.1/Annex C)
  • IEC 62923-2:2018 — Bridge Alert Management, Part 2: alert and cluster identifiers (identifier registry §4/Annex A, reserved clusters Annex B)
  • IEC 62682:2022 — Management of alarm systems for the process industries (alarm state model §5.3, deadband/delays §10.5, shelving/suppression §11.7–11.9, transition recording §16.2, performance metrics Clause 16)
  • NMEA 2000 PGNs 126983 (Alert), 126984 (Alert Response), 126985 (Alert Text)
  • Signal K specification — notifications.* group and alarm handling
  • signalk-server v2 Notifications API — packages/server-api/src/notificationsapi.ts, src/api/notifications/, docs/develop/rest-api/notifications_api.md
  • Radar API capability-announcement pattern — packages/server-api/src/radarapi.ts
  • PR feat: webapp warn/error status badges in Webapps list and sidebar #2847 — webapp status badges + review discussion that triggered this RFC
  • signalk-alert-manager (Matti Airas), signalk-notification-dispatcher (tobias_r), signalk-notification-player, @codekilo/signalk-trigger-event

Appendix A — Field evidence (2.30.0, summarized from screenshots)

36 paths under notifications.* on one vessel: 10 Maretron-originated NMEA alarm/warning entries with gateway instance numbers in the path (…navigational.22.8196 etc., one of them simultaneously ALARM from IPG100.128 and NORMAL from a ws.<uuid> source); 15 standing NORMAL/NOMINAL zone-handler entries including NORMAL: undefined; 3 BT-sensor "communication OK" health pings modeled as notifications with device-name+MAC paths; one buddy-proximity ALERT; one bare notifications. path with an empty NORMAL value. Screenshots archived alongside this document.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions