diff --git a/docs/AlarmTracking.md b/docs/AlarmTracking.md index ca550a82..8d57100c 100644 --- a/docs/AlarmTracking.md +++ b/docs/AlarmTracking.md @@ -240,6 +240,15 @@ AB CIP ALMD) route to AVEVA Historian via the Wonderware sidecar: [AlarmHistorian.md §Configuration](AlarmHistorian.md#configuration) for the `AlarmHistorian` appsettings section that enables the real sink. + **Native alarms** (equipment tags carrying an `"alarm"` object in their `TagConfig`) + support the same `HistorizeToAveva` opt-out. The field is `alarm.historizeToAveva` + (`bool?`) and is authored via the **"Historize to AVEVA"** checkbox in the Tag modal's + alarm section. The gate logic is identical (`is not false`): absent or `true` historizes; + explicit `false` suppresses the AVEVA write while leaving the live `/alerts` feed + unaffected. See + [ScriptedAlarms.md §TagConfig alarm fields](ScriptedAlarms.md#tagconfig-alarm-fields) + for the full field reference. + Galaxy-native alarms with `$Alarm*` extensions reach AVEVA Historian directly via System Platform's `HistorizeToAveva` toggle on the alarm primitive — no involvement from OtOpcUa. This sidecar path is diff --git a/docs/Historian.md b/docs/Historian.md index 6ed1b8be..57153022 100644 --- a/docs/Historian.md +++ b/docs/Historian.md @@ -13,9 +13,13 @@ Design reference: [docs/plans/2026-06-14-galaxy-phase-c-historian-design.md](pla ## Historized TagConfig schema -A tag is historized by adding fields to its `TagConfig` blob on the `/uns` equipment page -Tags tab (raw-JSON textarea). No separate UI control exists — Galaxy (the primary use case) -already uses the raw-JSON editor. +A tag is historized by setting the **Historize this tag** checkbox and optional **Historian +tagname (override)** textbox in the Tag modal on the `/uns` equipment page Tags tab. These +controls work for **all drivers** — typed editors (Modbus, S7, OpcUaClient, etc.) and the +raw-JSON textarea (Galaxy) alike. The controls merge `isHistorized` / `historianTagname` +into the existing `TagConfig` JSON blob via the `TagHistorizeConfig` helper, preserving all +other keys byte-stable. Drivers that still use the raw-JSON editor (Galaxy) can also add the +fields directly in the textarea. ### Fields @@ -199,13 +203,36 @@ bit set at materialization. A session without sufficient permissions receives ## Authoring workflow 1. Open the equipment's **Tags** tab on `/uns/equipment/{id}`. -2. Create or edit the tag. Because Galaxy uses the raw-JSON editor, add `"isHistorized":true` - (and optionally `"historianTagname":"..."`) directly in the TagConfig textarea. +2. Create or edit the tag. + - For **typed-editor drivers** (Modbus, S7, OpcUaClient, etc.): check the + **Historize this tag** checkbox and, if needed, fill in the **Historian tagname + (override)** textbox. + - For **raw-JSON editors** (Galaxy): you can check the same first-class checkboxes + (they appear below the JSON textarea), or add `"isHistorized":true` (and optionally + `"historianTagname":"..."`) directly in the textarea. 3. Save and publish. The server rebuilds its address space; the node materialises with `Historizing=true` and the `HistoryRead` AccessLevel bit. 4. Confirm with Client.CLI `read` that the node's `Status` is `Good` and that the value is updating. Then issue a `historyread` to verify the historian connection returns data. +### Native-alarm historian opt-out (`alarm.historizeToAveva`) + +A tag carrying a native `"alarm"` object has a **separate** historian opt-out for its +**alarm transitions** (distinct from tag-value historization). On the Tag modal, check or +uncheck **Historize to AVEVA** in the alarm section. This maps to `alarm.historizeToAveva` +(`bool?`) in the `TagConfig` JSON: + +- **Absent or `true`** (default) — the alarm's transitions are written to AVEVA Historian + via `HistorianAdapterActor`. +- **`false`** — the durable AVEVA write is suppressed for this alarm's transitions. The + live `/alerts` feed and OPC UA condition events are unaffected. + +The gate is applied in `HistorianAdapterActor` using `is not false` semantics, matching the +scripted-alarm `HistorizeToAveva` posture. See +[ScriptedAlarms.md §Native driver alarms](ScriptedAlarms.md#native-driver-alarms-equipment-tag-path) +and [AlarmTracking.md §Historian write-back](AlarmTracking.md#historian-write-back-non-galaxy-alarms) +for the full alarm-historian routing. + --- ## Client.CLI historyread examples diff --git a/docs/ScriptedAlarms.md b/docs/ScriptedAlarms.md index 76334a42..d9babbd1 100644 --- a/docs/ScriptedAlarms.md +++ b/docs/ScriptedAlarms.md @@ -136,12 +136,31 @@ blob and is parsed byte-parity in both the compose (`Phase7Composer`) and deploy |---|---|---| | `alarmType` | `"AlarmCondition"`, `"OffNormalAlarm"`, `"DiscreteAlarm"`, `"LimitAlarm"` | `"AlarmCondition"` | | `severity` | OPC UA 1–1000 scale | `500` | +| `historizeToAveva` | `true` / `false` / absent | absent (historize) | An unknown `alarmType` string falls back to the base `AlarmCondition` OPC UA ObjectType. `severity` seeds the condition's initial severity at materialisation; the driver's live alarm events may carry a different severity that overrides it at runtime. +**`historizeToAveva`** is a `bool?` opt-out for the AVEVA Historian durable write. +Set it to `false` to suppress writing this alarm's transitions to AVEVA Historian; +absent or `true` historizes as usual. The live `/alerts` feed and OPC UA condition +events are always unaffected regardless of this flag. The field is gated in +`HistorianAdapterActor` with `is not false` semantics — a missing field (e.g. from +an older config) defaults to historizing rather than silently dropping an audit row +during a rolling restart. + +On the Tag modal, the **"Historize to AVEVA"** checkbox (in the alarm section) +controls this field. The **Galaxy address picker** pre-fills a default `alarm` +object (`{"alarmType":"OffNormalAlarm","severity":700}`) when it detects that the +selected attribute is itself an alarm (`IsAlarm == true`); the pre-fill never +overwrites an already-authored alarm section. + +> **Distinct from tag-value historization.** `alarm.historizeToAveva` controls +> alarm-transition AVEVA writes only. Tag-value history is controlled by the +> top-level `isHistorized` field (see [Historian.md](Historian.md)). + #### Severity mapping The authored `severity` (1–1000) seeds the OPC UA condition node at materialisation @@ -211,13 +230,6 @@ for the full routing diagram. backing a native alarm has no enable/disable surface distinct from OPC UA; the Part 9 enable/disable concept maps to the scripted-alarm engine only. -One item remains explicitly out of scope: - -1. **AdminUI Galaxy address-picker pre-fill**: the `alarm` object must be authored - by editing the tag's raw `TagConfig` JSON today; a future picker enhancement - could pre-fill `alarmType` / `severity` from driver discovery - (`DriverAttributeInfo.IsAlarm`). - ## Inbound operator ack/shelve Operators interact with active scripted alarms through two surfaces — both converge on the same `alarm-commands` DPS topic consumed by `ScriptedAlarmHostActor`. diff --git a/docs/Uns.md b/docs/Uns.md index 79396f39..af379475 100644 --- a/docs/Uns.md +++ b/docs/Uns.md @@ -20,11 +20,11 @@ Enterprise (read-only grouping — ServerCluster.Enterprise) └─ Equipment (leaf — Equipment; opens its own page) ``` -**Enterprise and Site/Cluster are read-only here.** They are derived from -columns on the cluster record, not entities of their own, so you create and -configure clusters on the **Clusters** pages (`/clusters`). On a cluster row -the **⚙ settings** link jumps to that cluster. Editable UNS entities start -at **Area**. +**Enterprise and Site/Cluster can be deleted from the tree** (see +[Cluster and Enterprise delete](#cluster-and-enterprise-delete) below). +Other than deletion, they are configured on the **Clusters** pages +(`/clusters`) — on a cluster row the **⚙ settings** link jumps to that +cluster. Editable UNS entities start at **Area**. Count badges next to a node show how many direct children it has (for equipment, the combined tag + virtual-tag count). @@ -41,7 +41,7 @@ Every editable row has inline actions: | Node | Actions | |---|---| -| Cluster | **+ Area** | +| Cluster | **+ Area**, Delete | | Area | **+ Line**, Edit, Delete | | Line | **+ Equipment**, Edit, Delete | | Equipment | **Open**, Delete | @@ -106,6 +106,133 @@ the tag **Name**, **DataType**, and **AccessLevel** (default: read-only). There is no alias concept, no `SystemPlatform`-kind namespace, and no relay→alias converter. +## Typed TagConfig editors + +The **Tag modal** dispatches a driver-typed config editor for the following +drivers: + +| Driver | Fields in the typed editor | +|---|---| +| Modbus | Register type, address, data type, word order, etc. | +| S7 | Data block, offset, data type, etc. | +| AB CIP | Tag path, data type, etc. | +| AB Legacy (DF1/DH+) | Address, data type, etc. | +| TwinCAT | Symbol path, data type, etc. | +| FOCAS | PMC address, data type, etc. | +| **OpcUaClient** | `FullName` (the remote OPC UA node id string) | +| **Historian.Wonderware** | `FullName` (the Wonderware tagname to read) | + +**OpcUaClient** and **Historian.Wonderware** were previously raw-JSON +fallback only; they now have first-class typed editors that expose a single +`FullName` field (PascalCase JSON key, consistent with the Galaxy editor +convention). Both are registered in `TagConfigEditorMap` and +`TagConfigValidator`; unknown keys in the stored JSON blob are preserved on +round-trip. + +Drivers not yet listed above (e.g. Galaxy — which uses the Galaxy address +picker described below) still use the generic raw-`TagConfig`-JSON textarea. + +### "Build address" pickers in protocol-driver editors + +The **Modbus, S7, AB CIP, AB Legacy, TwinCAT, and FOCAS** typed editors +include a **Build address** button. Clicking it opens the driver's existing +address-builder UI inside the shared `DriverTagPicker` overlay; confirming a +selection writes the fully constructed address string back into the editor's +address field. This means you can visually compose a register reference +(e.g. select "Holding Register → 100 → Int16") and have it serialised into +the correct JSON without hand-editing. + +## Historizing tags (first-class controls) + +The Tag modal exposes **Historize this tag** and **Historian tagname +(override)** as explicit controls that work for **all drivers** — including +protocol drivers (Modbus, S7, etc.) that use the typed editor and raw-JSON +drivers (Galaxy) alike. + +| Control | JSON key | Type | Behaviour | +|---|---|---|---| +| **Historize this tag** checkbox | `isHistorized` | bool | When checked, the OPC UA node materialises with `Historizing=true` and the `HistoryRead` AccessLevel bit set. | +| **Historian tagname (override)** textbox | `historianTagname` | string (optional) | Explicit tagname the historian backend will query. When left blank, the server defaults to the tag's driver `FullName`. | + +These fields are merged into the `TagConfig` JSON blob via the pure +`TagHistorizeConfig` helper, which preserves all other keys byte-stable. +The server's OPC UA HistoryRead dispatch already consumes these keys from the +`TagConfig` blob — see [Historian.md](Historian.md) for the full server +behaviour, continuation-point paging, and aggregates. + +> **Note — native-alarm `HistorizeToAveva`:** a tag that carries a native +> `"alarm"` object has a **separate** opt-out field `alarm.historizeToAveva` +> (a checkbox labelled "Historize to AVEVA" on the Tag modal's alarm section). +> That field controls whether the alarm's **transition events** are written to +> the AVEVA historian — it does not affect tag-value history (which is +> controlled by `isHistorized`). See [ScriptedAlarms.md §Native driver +> alarms](ScriptedAlarms.md#native-driver-alarms-equipment-tag-path) for +> details. + +## Galaxy address picker — native-alarm pre-fill + +When the Galaxy address picker selects an attribute that is itself an alarm +(`IsAlarm == true` in the Galaxy hierarchy), the Tag modal automatically +seeds a default `alarm` object in the tag config: + +```json +{"alarmType":"OffNormalAlarm","severity":700} +``` + +This lets the operator author the native alarm in a single picker pass +without hand-editing JSON. The pre-fill **never overwrites** an alarm object +that is already present — if the tag already has a custom `alarm` section, the +picker leaves it untouched. + +## Cluster and Enterprise delete + +### Cluster delete + +A **Delete** action is available on Cluster rows in the UNS tree. The server +refuses the delete if the cluster still has any Areas (children) — the same +refuse-if-children guard used by Area and Line delete. Remove all Areas (and +their descendant Lines/Equipment) first, then delete the cluster. + +> **No RowVersion concurrency check.** `ServerCluster` does not carry a +> concurrency token, so the delete does not have the last-writer-wins protection +> that Area/Line/Equipment deletes have. A follow-up migration will add the +> token; for now, coordinate cluster deletes manually. + +### Enterprise delete + +An **Enterprise** row is a read-only grouping label (the `Enterprise` column +of `ServerCluster`) — it is not a separate entity. Deleting an Enterprise row +**deletes all clusters whose `Enterprise` matches that label**, all-or-nothing: + +- If any cluster under the enterprise still has children, the entire delete is + refused and no clusters are removed. +- If all clusters are empty, every cluster under that enterprise is deleted in a + single transaction. + +Remove all Areas under every cluster in the enterprise first, then delete the +enterprise label. + +## Create-new-script inline (virtual-tag panel) + +On the equipment page's **Virtual Tags** tab, when a virtual tag is not yet +bound to any script, the inline script panel shows a **Create new script** +button. Clicking it: + +1. Generates a new blank Script record with an auto-generated `SC-…` id. +2. Binds the virtual tag to that script. +3. Expands the Monaco editor inline so you can begin authoring immediately. + +This removes the previous two-step flow (create a script on a separate page, +then attach it to the virtual tag); the entire lifecycle now lives on the +equipment page. + +## Hosts page per-driver-instance rows (deferred) + +Phase 6 did **not** implement per-driver-instance status rows on the Hosts +page. This item (H7-runtime) is **F7-runtime-blocked**: the runtime plumbing +needed to surface per-instance health rows is not yet in place. It remains on +the backlog. + ## Bulk import **Import equipment CSV** (toolbar) bulk-creates equipment across many lines