Skip to content

alarmExt 扩展参考 ​

原文: https://finproducts.atlassian.net/wiki/spaces/JIDD3/pages/34793816087/FIN+5.3.1+alarmExt 中文整理基于 FIN 5.3.1

导读: 本页为 FIN 扩展 alarmExt 的 API 参考文档。

以下为 API 参考,保留英文以便对照源码;目录已列出(英文标题便于检索);概述已尽量中译。


目录(导读) ​

  • mixins
  • classes
  • enums
  • Slot Details
    • futures
    • make
    • routes
    • rules
    • ackError
    • ackInProgress
    • ackMessage
    • ackMessageRequired
    • ackRequired
    • acked
    • ackedBy
    • active
    • created
    • curStatus
    • curVal
    • dispatcher
    • isAcked
    • message
    • metadata
    • occurred
    • priority
    • sourceRef
    • state
    • wrap
    • ack
    • _context
    • acknowlegedBy
    • alarmRefs
    • context
    • time
    • alarmRec
    • error
    • failureReason
    • success
    • key
    • op
    • createUpdateAlarm
    • fromStr
    • getAlarmByID
    • getAlarmCount
    • getAlarmsByTarget
    • import
    • poll
    • readAlarms
    • removeAlarms
    • vals
    • alarmCount
    • createOrUpdate
    • get
    • getTargetsAlarms
    • remove
    • runHouseKeeping
    • start
    • stop
    • CONNECTOR
    • CONTROL
    • INTERNAL
    • RULES
    • UNKNOWN
    • Timeout
    • acknowledge
    • lib
    • receive
    • transition
    • update
    • alarmAck
    • alarmDigest
    • alarmHistoryCount
    • alarmHistoryQuery
    • alarmNavCounts
    • alarmTargets
    • alarms
    • alarmsGetById
    • alarmsRemove
    • clearAlarmHistory
    • clearAlarmHistoryBefore
    • readAlarmHistory
    • toAlarm
    • toAlarmNew
    • toAlarmOld
    • toKey
    • toNormal
    • toNormalNew
    • toNormalOld
    • transitionAlarm
    • alarm
    • cancel
    • complete
    • completeErr
    • parent
    • status
    • waitFor
    • wraps
    • merge
    • clear
    • count
    • houseKeeping
    • insert
    • query
    • read
    • removeBefore
    • removeBetween
    • historyCount
    • readStream
    • fromAlarm
    • fromState
    • toState
    • TABLE_NAME
    • _config
    • _engine
    • _sqlContext
    • _utility
    • _walApplyJob
    • _walPurgeJob
    • _writer
    • actor
  • …(其余标题见英文正文)

API 正文(英文) ​

| MockAlarmRoute | | | NotificationRoute | | | OutOfRangeRule | | | PollAlarmsMessage | | | PromisingActor | | | PromisingMessage | | | QuestDBCursor | | | ReadAlarmsMessage | | | RecordChangeMessage | | | RemoveRoutesMessage | | | RemoveRulesMessage | | | RouteAlarmMessage | | | RoutingMessage | | | RuleMessage | | | RulesEngine | | | ValueRule | | | WriterStream | |


enums ​

AlarmDBOp
AlarmDispatcher
AlarmHistoryDBOp
AlarmOp
AlarmPointTags
AlarmsRequestOp
RoutingOp
RuleOp

AckCompleteMessage ​

src const class AckCompleteMessage : AlarmMessage

Constructors

make

Fields

futures

Slot Details ​


futures ​

src const AlarmFuture[] futures


make ​

src new make(AlarmFuture[] futures)


AddRoutesMessage ​

src const class AddRoutesMessage : RoutingMessage

Constructors

make

Fields

routes

Slot Details ​


make ​

src new make(AlarmRoute[] routes)


routes ​

src const AlarmRoute[] routes


AddRulesMessage ​

src const class AddRulesMessage : RuleMessage

Constructors

make

Fields

rules

Slot Details ​


make ​

src new make(AlarmRule[] rules)


rules ​

src const AlarmRule[] rules


Alarm ​

src const class Alarm : TypedDict

Constructors

make
wrap

Fields

ackError
ackInProgress
ackMessage
ackMessageRequired
ackRequired
acked
ackedBy
active
created
curStatus
curVal
dispatcher
message
metadata
occurred
priority
sourceRef
state

Methods

isAcked

Slot Details ​


ackError ​

src @TypedTag const Str? ackError


ackInProgress ​

src @TypedTag const Bool ackInProgress


ackMessage ​

src @TypedTag const Str? ackMessage


ackMessageRequired ​

src @TypedTag const Bool ackMessageRequired


ackRequired ​

src @TypedTag const Bool ackRequired


acked ​

src @TypedTag const DateTime? acked


ackedBy ​

src @TypedTag const Str? ackedBy


active ​

src @TypedTag const Bool active


created ​

src @TypedTag const DateTime? created


curStatus ​

src @TypedTag const Str? curStatus


curVal ​

src @TypedTag const Obj? curVal


dispatcher ​

src @TypedTag const Str dispatcher := AlarmDispatcher.UNKNOWN.toStr


isAcked ​

src Bool isAcked()


make ​

src new make(Dict rec, |This| f)


message ​

src @TypedTag const Str? message


metadata ​

src @TypedTag const Dict? metadata


occurred ​

src @TypedTag const DateTime occurred


priority ​

src @TypedTag const Number priority


sourceRef ​

src @TypedTag const Ref? sourceRef


state ​

src @TypedTag const Str state


wrap ​

src static new wrap(Dict? dict)


AlarmAckMessage ​

src const class AlarmAckMessage : AlarmMessage

Constructors

make

Fields

ack

Slot Details ​


ack ​

src const AlarmAcknowledgement ack


make ​

src new make(AlarmAcknowledgement ack)


AlarmAcknowledgement ​

src const class AlarmAcknowledgement : Obj

Constructors

make

Fields

_context
acknowlegedBy
alarmRefs
message
time

Methods

context

Slot Details ​


_context ​

src const Unsafe _context


acknowlegedBy ​

src const Str? acknowlegedBy


alarmRefs ​

src const Ref[] alarmRefs


context ​

src HxContext context()


make ​

src new make(HxContext context, Ref[] alarmRefs, DateTime time, Str? ackBy := null, Str? message := null)


message ​

src const Str? message


time ​

src const DateTime time


AlarmConnAckResult ​

src const class AlarmConnAckResult : Obj

Constructors

make

Fields

alarmRec
error
failureReason
success

Slot Details ​


alarmRec ​

src const Dict alarmRec


error ​

src const Err? error


failureReason ​

src const Str? failureReason


make ​

src new make(Dict rec, Bool isSuccess, Str? failureReason := null, Err? error := null)


success ​

src const Bool success


AlarmDBMessage ​

src const class AlarmDBMessage : Obj

Constructors

make

Fields

_context
op

Methods

context
key

Slot Details ​


_context ​

src const Unsafe _context


context ​

src HxContext? context()


key ​

src virtual Obj? key()


make ​

src new make(AlarmDBOp op, HxContext? context := null)


op ​

src const AlarmDBOp op


AlarmDBOp ​

src @Serializable { simple=true } enum class AlarmDBOp : Enum

Enum

readAlarms
getAlarmByID
getAlarmsByTarget
createUpdateAlarm
removeAlarms
getAlarmCount
poll
import

Constructors

fromStrReturn the AlarmDBOp instance for the specified name.

Fields

valsList of AlarmDBOp values indexed by ordinal

Slot Details ​


createUpdateAlarm ​

src const static AlarmDBOp createUpdateAlarm := ...


fromStr ​

src static new fromStr(Str name, Bool checked := true)

Return the AlarmDBOp instance for the specified name. If not a valid name and checked is false return null, otherwise throw ParseErr.


getAlarmByID ​

src const static AlarmDBOp getAlarmByID := ...


getAlarmCount ​

src const static AlarmDBOp getAlarmCount := ...


getAlarmsByTarget ​

src const static AlarmDBOp getAlarmsByTarget := ...


import ​

src const static AlarmDBOp import := ...


poll ​

src const static AlarmDBOp poll := ...


readAlarms ​

src const static AlarmDBOp readAlarms := ...


removeAlarms ​

src const static AlarmDBOp removeAlarms := ...


vals ​

src const static AlarmDBOp[] vals := ...

List of AlarmDBOp values indexed by ordinal


AlarmDatabase ​

src const mixin AlarmDatabase

Methods

alarmCount
createOrUpdate
get
getTargetsAlarms
import
poll
readAlarms
remove
runHouseKeeping
start
stop

Slot Details ​


alarmCount ​

src abstract Future alarmCount(Ref[]? targets := null)


createOrUpdate ​

src abstract Future createOrUpdate(Alarm[] alarms, Bool coalesce := true)


get ​

src abstract Future get(HxContext context, Ref[] alarmIds)


getTargetsAlarms ​

src abstract Future getTargetsAlarms(HxContext context, Ref[] targets)


import ​

src abstract Future import(Alarm[] records)


poll ​

src abstract Future poll(HxContext context, Int lastPollTime, Ref[]? targets := null)


readAlarms ​

src abstract Future readAlarms(HxContext context, Ref[]? targets := null, DateSpan? span := null, DatabasePaginationRequest? pagination := null)


remove ​

src abstract Future remove(Ref[] alarmIds)


runHouseKeeping ​

src abstract Future runHouseKeeping()


start ​

src abstract Future start()


stop ​

src abstract Future stop()


AlarmDispatcher ​

src @Serializable { simple=true } enum class AlarmDispatcher : Enum

Enum

INTERNAL
CONTROL
RULES
CONNECTOR
UNKNOWN

Constructors

fromStrReturn the AlarmDispatcher instance for the specified name.

Fields

valsList of AlarmDispatcher values indexed by ordinal

Slot Details ​


CONNECTOR ​

src const static AlarmDispatcher CONNECTOR := ...


CONTROL ​

src const static AlarmDispatcher CONTROL := ...


INTERNAL ​

src const static AlarmDispatcher INTERNAL := ...


RULES ​

src const static AlarmDispatcher RULES := ...


UNKNOWN ​

src const static AlarmDispatcher UNKNOWN := ...


fromStr ​

src static new fromStr(Str name, Bool checked := true)

Return the AlarmDispatcher instance for the specified name. If not a valid name and checked is false return null, otherwise throw ParseErr.


vals ​

src const static AlarmDispatcher[] vals := ...

List of AlarmDispatcher values indexed by ordinal


AlarmEngine ​

src const class AlarmEngine : Actor

Constructors

make

Fields

Timeout
lib

Methods

acknowledge
receive
transition
update

Slot Details ​


Timeout ​

src const static Duration Timeout := 30sec


acknowledge ​

src Future acknowledge(AlarmAcknowledgement ack)


lib ​

src const AlarmLib lib


make ​

src new make(ActorPool pool, AlarmLib lib)


receive ​

src protected virtual override Obj? receive(Obj? message)


transition ​

src Future transition(AlarmTracker tracker, AlarmTransition transition)


update ​

src Future update(Alarm changes)


AlarmFolioDatabase ​

src const class AlarmFolioDatabase : Obj, AlarmDatabase

Constructors

make

Methods

alarmCount
createOrUpdate
get
getTargetsAlarms
import
poll
readAlarms
remove
runHouseKeeping
start
stop

Slot Details ​


alarmCount ​

src virtual override Future alarmCount(Ref[]? targets := null)


createOrUpdate ​

src virtual override Future createOrUpdate(Alarm[] alarms, Bool coalesce := true)


get ​

src virtual override Future get(HxContext context, Ref[] alarmIds)


getTargetsAlarms ​

src virtual override Future getTargetsAlarms(HxContext context, Ref[] targetIds)


import ​

src virtual override Future import(Alarm[] records)


make ​

src new make(AlarmLib lib)


poll ​

src virtual override Future poll(HxContext context, Int lastPollTime, Ref[]? targets := null)


readAlarms ​

src virtual override Future readAlarms(HxContext context, Ref[]? targets := null, DateSpan? span := null, DatabasePaginationRequest? pagination := null)


remove ​

src virtual override Future remove(Ref[] alarmIds)


runHouseKeeping ​

src virtual override Future runHouseKeeping()


start ​

src virtual override Future start()


stop ​

src virtual override Future stop()


AlarmFuncs ​

src class AlarmFuncs : Obj

Methods

alarmAckGiven a set of alarms (or alarm ids), acknowledge them and return the updated alarm records.
alarmDigestGenerate an alarm digest grouped by a given parent type.
alarmHistoryCountGiven an optional target, return the count of alarm history records.
alarmHistoryQueryGiven a Haystack query string, return the alarm history records as a grid.
alarmNavCountsGiven a grid of target records with id, enrich it with rolled-up alarm counts per row.
alarmTargetsGiven a root object, return all alarm target records associated with it.
alarmsGiven optional targets and/or a time span, return the current alarm records as a grid.
alarmsGetByIdGiven an alarm id, return the corresponding alarm record.
alarmsRemoveGiven a set of alarms (or alarm ids), remove them from the database and return the removed alarm records.
clearAlarmHistoryClear alarm history records in the database based on the given parameters.
clearAlarmHistoryBeforeClear alarm history records before the given date in the database.
readAlarmHistoryGiven optional targets and/or a time span, query the alarm history as a grid.
toAlarmGiven a target and either a legacy namespace or a new-style alarm state, transition the given namespace/target to the alarm state.
toAlarmNew
toAlarmOld
toKey
toNormalGiven targets, transition alarms back to normal state using either the legacy API (namespace-based) or the new API (message/occurred/meta).
toNormalNew
toNormalOld
transitionAlarm

Slot Details ​


alarmAck ​

src @Axon static Alarm[] alarmAck(Obj alarms, Str? message := null)

Given a set of alarms (or alarm ids), acknowledge them and return the updated alarm records.

Parameters:

  • alarms (required) - Alarm(s) to acknowledge in any of the following formats: (Grid, Dict[], Ref[], Dict, Str, Ref)
  • message (optional) - acknowledgement message

Examples:

alarmAck(alarms())
alarmAck(@alarmId, "Investigating")
alarmAck("r:demo:alarm123", "Cleared by operator")
alarmAck(readAll(alarm).keepCols(["id"]))

alarmDigest ​

src @Axon static Grid alarmDigest(Str? groupBy := "site", Bool includeUnknown := false)

Generate an alarm digest grouped by a given parent type.

Parameters:

  • groupBy (optional) - the parent type to group by. Default is "site".
  • includeUnknown (optional) - if true, includes alarms with unknown sources in

alarmHistoryCount ​

src @Axon static Number alarmHistoryCount(Obj? target := null)

Given an optional target, return the count of alarm history records.

Parameters:

  • target (optional) - this is the rec or id of the target. If omitted, it will count all records in the database.

alarmHistoryQuery ​

src @Axon static Grid alarmHistoryQuery(Str query)

Given a Haystack query string, return the alarm history records as a grid.

Parameters:

  • query (required) - Haystack query string to filter alarm history

alarmNavCounts ​

src @Axon static Grid alarmNavCounts(Grid grid)

Given a grid of target records with id, enrich it with rolled-up alarm counts per row.

Parameters:

  • grid (required) - grid of target records with id

alarmTargets ​

src @Axon static Dict[] alarmTargets(Obj root)

Given a root object, return all alarm target records associated with it.

Parameters:

  • root (required) - the root object to find alarm targets for

alarms ​

src @Axon static Grid alarms(Obj? targets := null, Expr span := Literal.nullVal, Expr filter := Literal.nullVal, Expr pollHandle := Literal.nullVal)

Given optional targets and/or a time span, return the current alarm records as a grid.

Parameters:

  • targets (optional) - list of targets to query alarms for. If this parameter is null it will query everything
  • span (optional) - DateSpan used to filter alarms. If this is null it will return alarms for all dates
  • filter (optional) - filter used to filter alarm result
  • pollHandle: when provided, performs an incremental poll and returns only changes since the given handle (legacy compatibility). The returned grid includes a new <pollHandle> you can pass back on the next call.

Return:

  • A Grid of alarm records. The grid meta includes:
  • pollHandle: an integer handle representing the time of this query.

Examples:

alarms()
alarms(@someEquipId)
alarms(readAll(equip and siteRef==@siteId))
alarms(null, today())
alarms(null, null, null, 123456)        // poll using previous handle

alarmsGetById ​

src @Axon static Alarm? alarmsGetById(Ref alarmId)

Given an alarm id, return the corresponding alarm record.

Parameters:

  • alarmId: Ref of the alarm to retrieve

Examples:

alarmsGetById(@alarmId)

alarmsRemove ​

src @Axon { admin=true } static Alarm[] alarmsRemove(Obj alarms)

Given a set of alarms (or alarm ids), remove them from the database and return the removed alarm records.

Parameters:

  • alarms (required) - Alarm(s) to remove in any of the following formats: (Grid, Dict[], Ref[], Dict, Str, Ref)

Examples:

alarmsRemove(alarms())
alarmsRemove(@alarmId)
alarmsRemove("r:demo:alarm123")
alarmsRemove(readAll(alarm).keepCols(["id"]))

clearAlarmHistory ​

src @Axon { admin=true } static Bool clearAlarmHistory(Obj? spanOrBefore := null, Bool? rebuild := false)

Clear alarm history records in the database based on the given parameters.

Parameters:

  • spanOrBefore (optional) - DateSpan of entries to clear
  • rebuild (optional) - if spanOrBefore is null and this is true, it will destroy the history database and recreate its schema.

clearAlarmHistoryBefore ​

src @Axon { admin=true } static Bool clearAlarmHistoryBefore(Obj date)

Clear alarm history records before the given date in the database.

Parameters:

  • date (required) - Deletes all alarm history entries before the specified date.

readAlarmHistory ​

src @Axon static Grid readAlarmHistory(Expr targets := Literal.nullVal, Expr span := Literal.nullVal)

Given optional targets and/or a time span, query the alarm history as a grid.

Parameters:

  • targets (optional) - List/Grid of targets or target ids to query database for. If omitted this will query the database for all entities
  • span (optional) - DateSpan to used to query the history database. If omitted this will return records for all dates in the database

toAlarm ​

src @Axon static Obj toAlarm(Obj target, Str nsOrState, Obj? messageOrTags := null, DateTime? occurred := null, Obj? priority := null, Dict? meta := null)

Given a target and either a legacy namespace or a new-style alarm state, transition the given namespace/target to the alarm state.

Parameters:

  • targets (required) - list of targets to transition
  • state (required) - to transition targets to (offNormal, normal, fault, ...)
  • message (optional) - the message for the alarm transition
  • occurred (optional) - the time the alarm occurred. If this is omitted, the system will use the current time
  • priority (optional) - priority of the alarm. if this is omitted, the system will use this priority configured for the state.
  • metadata (optional) - this is a miscellaneous dictionary of tags that will follow the lifecycle of the alarm. For compatibility, if "ackRequired" is set on the metadata, it will take precedence over the state's configured ackRequired flag.

This is an adapter kept for backward compatibility.

  • If <nsOrState> contains ":", it is treated as a legacy namespace

Return:

  • Legacy path returns a single Alarm.
  • New path returns <Alarm[]>.

Examples:

toAlarm(@equipId, "highTemp", "Over temperature")
toAlarm(@equipId, "legacy:ns", {message:"Over temperature"})

toAlarmNew ​

src static Alarm[] toAlarmNew(Obj targets, Str state, Str? message := null, DateTime? occurred := null, Obj? priority := null, Dict? meta := null)


toAlarmOld ​

src static Alarm toAlarmOld(Obj target, Str ns, Dict tags := Etc.emptyDict())


toKey ​

src static Str toKey(Ref source, Str state, DateTime? time := null)


toNormal ​

src @Axon static Obj? toNormal(Obj targets, Str? nsOrMessage := null, Obj? occurredOrTags := null, Dict? meta := null)

Given targets, transition alarms back to normal state using either the legacy API (namespace-based) or the new API (message/occurred/meta).

Parameters:

  • targets (required) - list of targets to transition to normal
  • message (optional) - the message for the alarm transition
  • occurred (optional) - the time the alarm occurred. If this is omitted, the system will use the current time
  • metadata (optional) - this is a miscellaneous dictionary of tags that will follow the lifecycle of the alarm. For compatibility, if "ackRequired" is set on the metadata, it will take precedence over the state's configured ackRequired flag.

This is an adapter kept for backward compatibility:

  • If <nsOrMessage> contains ":", it is treated as a legacy namespace

Return:

  • Legacy path returns a single <Alarm?>.
  • New path returns <Alarm[]>.

Examples:

toNormal(@equipId)
toNormal(@equipId, "Cleared by operator")
toNormal(@equipId, "legacy:ns", {message:"Cleared"})

toNormalNew ​

src static Alarm[] toNormalNew(Obj targets, Str? message := null, DateTime? occurred := null, Dict? meta := null)


toNormalOld ​

src static Alarm? toNormalOld(Obj targets, Str ns, Dict tags := Etc.emptyDict())


transitionAlarm ​

src static Alarm[] transitionAlarm(Obj targets, Str state, Bool enable, Str? message := null, DateTime? occurred := null, Obj? priority := null, Dict? meta := null)


AlarmFuture ​

src const class AlarmFuture : Future

Constructors

make

Fields

alarm
parent

Methods

cancel
complete
completeErr
get
status
waitFor
wraps

Slot Details ​


alarm ​

src const Alarm alarm


cancel ​

src virtual override Void cancel()


complete ​

src virtual override This complete(Obj? val)


completeErr ​

src virtual override This completeErr(Err err)


get ​

src virtual override Obj? get(Duration? timeout := null)


make ​

src new make(Future parent, Alarm alarm)


parent ​

src const Future parent


status ​

src virtual override FutureStatus status()


waitFor ​

src virtual override This waitFor(Duration? timeout := null)


wraps ​

src virtual override Future? wraps()


AlarmHistoryDBMessage ​

src const class AlarmHistoryDBMessage : Obj

Constructors

make

Fields

_context
op

Methods

context
key
merge

Slot Details ​


_context ​

src const Unsafe _context


context ​

src HxContext? context()


key ​

src virtual Obj? key()


make ​

src new make(AlarmHistoryDBOp op, HxContext? context := null)


merge ​

src virtual AlarmHistoryDBMessage merge(AlarmHistoryDBMessage msgB)


op ​

src const AlarmHistoryDBOp op


AlarmHistoryDBOp ​

src @Serializable { simple=true } enum class AlarmHistoryDBOp : Enum

Enum

insert
read
count
removeBetween
removeBefore
clear
houseKeeping
start
stop
query
import

Constructors

fromStrReturn the AlarmHistoryDBOp instance for the specified name.

Fields

valsList of AlarmHistoryDBOp values indexed by ordinal

Slot Details ​


clear ​

src const static AlarmHistoryDBOp clear := ...


count ​

src const static AlarmHistoryDBOp count := ...


fromStr ​

src static new fromStr(Str name, Bool checked := true)

Return the AlarmHistoryDBOp instance for the specified name. If not a valid name and checked is false return null, otherwise throw ParseErr.


houseKeeping ​

src const static AlarmHistoryDBOp houseKeeping := ...


import ​

src const static AlarmHistoryDBOp import := ...


insert ​

src const static AlarmHistoryDBOp insert := ...


query ​

src const static AlarmHistoryDBOp query := ...


read ​

src const static AlarmHistoryDBOp read := ...


removeBefore ​

src const static AlarmHistoryDBOp removeBefore := ...


removeBetween ​

src const static AlarmHistoryDBOp removeBetween := ...


start ​

src const static AlarmHistoryDBOp start := ...


stop ​

src const static AlarmHistoryDBOp stop := ...


vals ​

src const static AlarmHistoryDBOp[] vals := ...

List of AlarmHistoryDBOp values indexed by ordinal


AlarmHistoryDatabase ​

src const mixin AlarmHistoryDatabase

Methods

clear
historyCount
import
insert
query
read
readStream
removeBefore
removeBetween
runHouseKeeping
start
stop

Slot Details ​


clear ​

src abstract Future clear(Bool? rebuild := false)


historyCount ​

src abstract Future historyCount(Ref? target := null)


import ​

src abstract Future import(AlarmHistoryEntry[] data)


insert ​

src abstract Future insert(AlarmHistoryEntry[] entries)


query ​

src abstract Future query(Str query)


read ​

src abstract Future read(HxContext context, DateSpan? range, Ref[]? targets, DatabasePaginationRequest? pagination := null)


readStream ​

src abstract SourceStream? readStream(HxContext context, DateSpan? range, Ref[]? targets, DatabasePaginationRequest? pagination := null)


removeBefore ​

src abstract Future removeBefore(DateTime date)


removeBetween ​

src abstract Future removeBetween(DateSpan range)


runHouseKeeping ​

src abstract Future runHouseKeeping()


start ​

src abstract Future start()


stop ​

src abstract Future stop()


AlarmHistoryEntry ​

src const class AlarmHistoryEntry : TypedDict

Constructors

fromAlarm
make
wrap

Fields

ackMessage
acked
ackedBy
active
created
curStatus
curVal
fromState
message
metadata
occurred
priority
sourceRef
toState

Slot Details ​


ackMessage ​

src @TypedTag const Str? ackMessage


acked ​

src @TypedTag const DateTime? acked


ackedBy ​

src @TypedTag const Str? ackedBy


active ​

src @TypedTag const Bool active


created ​

src @TypedTag const DateTime created


curStatus ​

src @TypedTag const Str? curStatus


curVal ​

src @TypedTag const Obj? curVal


fromAlarm ​

src static new fromAlarm(Str? fromState, Alarm alarm)


fromState ​

src @TypedTag const Str? fromState


make ​

src new make(Dict rec, |This| f)


message ​

src @TypedTag const Str? message


metadata ​

src @TypedTag const Dict? metadata


occurred ​

src @TypedTag const DateTime occurred


priority ​

src @TypedTag const Number priority


sourceRef ​

src @TypedTag const Ref? sourceRef


toState ​

src @TypedTag const Str? toState


wrap ​

src static new wrap(Dict? dict)


AlarmHistoryQuestDB ​

src const class AlarmHistoryQuestDB : Obj, AlarmHistoryDatabase

Constructors

make

Fields

TABLE_NAME
_config
_engine
_sqlContext
_utility
_walApplyJob
_walPurgeJob
_writer
actor
lib
log
pool

Methods

clear
config
engine
historyCount
import
insert
query
read
readStream
receive
removeBefore
removeBetween
runHouseKeeping
sqlContext
start
stop

Slot Details ​


TABLE_NAME ​

src const static Str TABLE_NAME := "alarmHistory"


_config ​

src const AtomicRef _config := AtomicRef.<ctor>()


_engine ​

src const AtomicRef _engine := AtomicRef.<ctor>()


_sqlContext ​

src const AtomicRef _sqlContext := AtomicRef.<ctor>()


_utility ​

src const AtomicRef _utility := AtomicRef.<ctor>()


_walApplyJob ​

src const AtomicRef _walApplyJob := AtomicRef.<ctor>()


_walPurgeJob ​

src const AtomicRef _walPurgeJob := AtomicRef.<ctor>()


_writer ​

src const AtomicRef _writer := AtomicRef.<ctor>()


actor ​

src const Actor actor


clear ​

src virtual override Future clear(Bool? rebuild := false)


config ​

src CairoConfiguration config()


engine ​

src CairoEngine engine()


historyCount ​

src virtual override Future historyCount(Ref? targetId := null)


import ​

src virtual override Future import(AlarmHistoryEntry[] data)


insert ​

src virtual override Future insert(AlarmHistoryEntry[] records)


lib ​

src const AlarmLib lib


log ​

src const Log log := Log.get("QuestHistoryDB")


make ​

src new make(AlarmLib lib)


pool ​

src const ActorPool pool


query ​

src virtual override Future query(Str query)


read ​

src virtual override Future read(HxContext context, DateSpan? range, Ref[]? targets, DatabasePaginationRequest? pagination := null)


readStream ​

src virtual override SourceStream? readStream(HxContext context, DateSpan? range, Ref[]? targets, DatabasePaginationRequest? pagination := null)


receive ​

src protected Obj? receive(Obj? message)


removeBefore ​

src virtual override Future removeBefore(DateTime date)


removeBetween ​

src virtual override Future removeBetween(DateSpan range)


runHouseKeeping ​

src virtual override Future runHouseKeeping()


sqlContext ​

src SqlExecutionContext? sqlContext()


start ​

src virtual override Future start()


stop ​

src virtual override Future stop()


AlarmHistoryResult ​

src const class AlarmHistoryResult : Obj

Constructors

make

Fields

entries
metadata

Methods

toGrid

Slot Details ​


entries ​

src const AlarmHistoryEntry[] entries


make ​

src new make(AlarmHistoryEntry[] entries, Dict? metadata := null)


metadata ​

src const Dict? metadata


toGrid ​

src Grid toGrid(Dict? additionalMeta := Etc.emptyDict())


AlarmLib ​

src const class AlarmLib : HxLib

Constructors

make

Fields

DEFAULT_DB
DEFAULT_HISTORY_DB
engine
houseKeepingFreq
observables
pool
router
rules
web

Methods

db
dbRecordChangeHandler
history
onHouseKeeping
onReady
onRecUpdate
onStart
onStop
onUnready
toAlarm
toNormal

Slot Details ​


DEFAULT_DB ​

src const static Str DEFAULT_DB := "alarmExt::AlarmFolioDatabase"


DEFAULT_HISTORY_DB ​

src const static Str DEFAULT_HISTORY_DB := "alarmExt::AlarmHistoryQuestDB"


db ​

src AlarmDatabase? db()


dbRecordChangeHandler ​

src Void dbRecordChangeHandler(CommitObservation observation)


engine ​

src const AlarmEngine engine


history ​

src AlarmHistoryDatabase? history()


houseKeepingFreq ​

src virtual const override Duration? houseKeepingFreq := Duration.<ctor>("15sec")


make ​

src new make()


observables ​

src virtual const override Observable[] observables := Observable[this.alarmsObs]


onHouseKeeping ​

src virtual override Void onHouseKeeping()


onReady ​

src virtual override Void onReady()


onRecUpdate ​

src virtual override Void onRecUpdate()


onStart ​

src virtual override Void onStart()


onStop ​

src virtual override Void onStop()


onUnready ​

src virtual override Void onUnready()


pool ​

src const ActorPool pool


router ​

src const AlarmRouter router


rules ​

src const RulesEngine rules


toAlarm ​

src Alarm[] toAlarm(Obj targets, Str state, Bool enable, AlarmDispatcher dispatcher, Str? message := null, DateTime? occurred := null, Obj? priority := null, Dict? meta := null, Bool deactivateOtherStates := false)


toNormal ​

src Dict[] toNormal(Obj targets, AlarmDispatcher dispatcher, Str? message := null, DateTime? occurred := null, Dict? meta := null)


web ​

src virtual const override AlarmRestAPI web := AlarmRestAPI.<ctor>(this)


AlarmManagement ​

src const class AlarmManagement : TypedDict

Constructors

make
makeNew
wrap

Fields

sourceRef
stack
stateStack
states

Slot Details ​


make ​

src new make(Dict rec, |This| f)


makeNew ​

src static new makeNew(Ref? sourceRef)


sourceRef ​

src @TypedTag const Ref? sourceRef


stack ​

src const AlarmStateModel[] stack


stateStack ​

src @TypedTag const Obj[] stateStack := Obj[,]


states ​

src const Str[] states


wrap ​

src static new wrap(Dict? dict)


AlarmMessage ​

src const class AlarmMessage : Obj

Constructors

make

Fields

op

Slot Details ​


make ​

src new make(AlarmOp op)


op ​

src const AlarmOp op


AlarmObservation ​

src const class AlarmObservation : TypedDict, Observation

Constructors

make
wrap

Fields

alarm
state
ts

Methods

subType
type

Slot Details ​


alarm ​

src @TypedTag const Alarm alarm


make ​

src new make(Dict rec, |This| f)


state ​

src const AlarmState state


subType ​

src virtual override Str? subType()


ts ​

src @TypedTag virtual const override DateTime ts


type ​

src virtual override Str type()


wrap ​

src static new wrap(Dict record)


AlarmOp ​

src @Serializable { simple=true } enum class AlarmOp : Enum

Enum

transition
acknowledge
update
connAckComplete
getCurrentStates
clearCurrentStates

Constructors

fromStrReturn the AlarmOp instance for the specified name.

Fields

valsList of AlarmOp values indexed by ordinal

Slot Details ​


acknowledge ​

src const static AlarmOp acknowledge := ...


clearCurrentStates ​

src const static AlarmOp clearCurrentStates := ...


connAckComplete ​

src const static AlarmOp connAckComplete := ...


fromStr ​

src static new fromStr(Str name, Bool checked := true)

Return the AlarmOp instance for the specified name. If not a valid name and checked is false return null, otherwise throw ParseErr.


getCurrentStates ​

src const static AlarmOp getCurrentStates := ...


transition ​

src const static AlarmOp transition := ...


update ​

src const static AlarmOp update := ...


vals ​

src const static AlarmOp[] vals := ...

List of AlarmOp values indexed by ordinal


AlarmPointTags ​

src @Serializable { simple=true } enum class AlarmPointTags : Enum

Enum

inAlarm

Constructors

fromStrReturn the AlarmPointTags instance for the specified name.

Fields

valsList of AlarmPointTags values indexed by ordinal

Slot Details ​


fromStr ​

src static new fromStr(Str name, Bool checked := true)

Return the AlarmPointTags instance for the specified name. If not a valid name and checked is false return null, otherwise throw ParseErr.


inAlarm ​

src const static AlarmPointTags inAlarm := ...


vals ​

src const static AlarmPointTags[] vals := AlarmPointTags[AlarmPointTags.inAlarm]

List of AlarmPointTags values indexed by ordinal


AlarmRecord ​

src class AlarmRecord : Obj

Constructors

make

Fields

ackInProgress
ackMessage
ackMessageRequired
ackRequired
acked
ackedBy
created
message
meta
occurred
priority
rec
source
state

Methods

toDict

Slot Details ​


ackInProgress ​

src Bool ackInProgress


ackMessage ​

src Str? ackMessage


ackMessageRequired ​

src Bool ackMessageRequired


ackRequired ​

src Bool ackRequired


acked ​

src DateTime? acked


ackedBy ​

src Ref? ackedBy


created ​

src DateTime? created


make ​

src new make(Dict? rec)


message ​

src Str? message


meta ​

src Dict? meta


occurred ​

src DateTime? occurred


priority ​

src Int? priority


rec ​

src const Dict? rec


source ​

src Ref? source


state ​

src Str? state


toDict ​

src Dict toDict()


AlarmRestAPI ​

src const class AlarmRestAPI : HxLibWeb

Constructors

make

Fields

COMMA_DELIMITED_LANGS

Methods

lib
onGet
onService

Slot Details ​


COMMA_DELIMITED_LANGS ​

src const static Str[] COMMA_DELIMITED_LANGS := Str["en","es","zh"]


lib ​

src virtual override AlarmLib lib()


make ​

src new make(AlarmLib lib)


onGet ​

src virtual override Void onGet()


onService ​

src virtual override Void onService()


AlarmResult ​

src const class AlarmResult : Obj

Constructors

make

Fields

_alarms
metadata

Methods

alarms
toGrid

Slot Details ​


_alarms ​

src const AtomicRef _alarms := AtomicRef.<ctor>()


alarms ​

src Alarm[] alarms()


make ​

src new make(Alarm[] alarms, Dict metadata := Etc.emptyDict())


metadata ​

src const Dict metadata


toGrid ​

src Grid toGrid(Dict additionalMeta := Etc.emptyDict())


AlarmRoute ​

src abstract const class AlarmRoute : TypedDict

Constructors

make

Fields

alarmRoute
disabled
log
routeOn

Methods

include
route

Slot Details ​


alarmRoute ​

src @TypedTag const Str alarmRoute


disabled ​

src @TypedTag const Bool disabled


include ​

src Bool include(Dict record, HaystackContext cx)


log ​

src const static Log log := Log.get("AlarmRoute")


make ​

src new make(Dict rec, |This| f)


route ​

src abstract Future route(HxContext context, Dict alarm, Dict? source)


routeOn ​

src @TypedTag const Str? routeOn


AlarmRouter ​

src const class AlarmRouter : Obj

Constructors

make

Fields

REC_TAG
actor
lib

Methods

CreateRouteFromRec
addRoutes
clearRoutes
getRoutes
removeRoutes
route

Slot Details ​


CreateRouteFromRec ​

src static AlarmRoute? CreateRouteFromRec(Dict record)


REC_TAG ​

src const static Str REC_TAG := "alarmRoute"


actor ​

src const Actor actor


addRoutes ​

src Future addRoutes(AlarmRoute[] routes)


clearRoutes ​

src Future clearRoutes()


getRoutes ​

src Future getRoutes()


lib ​

src const AlarmLib lib


make ​

src new make(ActorPool pool, AlarmLib lib)


removeRoutes ​

src Future removeRoutes(Ref[] routeIds)


route ​

src Future route(Dict alarm, Dict? alarmSource)


AlarmRule ​

src abstract const class AlarmRule : TypedDict

Constructors

make

Fields

alarmRule
disabled
message
metadata
priority
ruleFilter
ruleOn
sourceOverride
state

Methods

alarmState
include
test

Slot Details ​


alarmRule ​

src @TypedTag const Str alarmRule


alarmState ​

src virtual AlarmState alarmState()


disabled ​

src @TypedTag const Bool disabled


include ​

src virtual Bool include(HxContext context, Dict record)


make ​

src new make(Dict rec, |This| f)


message ​

src @TypedTag const Str? message


metadata ​

src @TypedTag const Dict? metadata


priority ​

src @TypedTag const Int priority := 255


ruleFilter ​

src protected const Filter? ruleFilter


ruleOn ​

src @TypedTag const Str ruleOn


sourceOverride ​

src @TypedTag const Ref? sourceOverride


state ​

src @TypedTag const Str state


test ​

src protected abstract Bool test(HxContext context, Dict record)


AlarmState ​

src const class AlarmState : Obj

Constructors

make

Fields

FAULT
HIGH_LIMIT
LOW_LIMIT
NORMAL
OFF_NORMAL
acknowledgementRequired
icon
isAlarm
isFault
name
priority

Methods

getState
states

Slot Details ​


FAULT ​

src const static AlarmState FAULT := ...


HIGH_LIMIT ​

src const static AlarmState HIGH_LIMIT := ...


LOW_LIMIT ​

src const static AlarmState LOW_LIMIT := ...


NORMAL ​

src const static AlarmState NORMAL := ...


OFF_NORMAL ​

src const static AlarmState OFF_NORMAL := ...


acknowledgementRequired ​

src const Bool acknowledgementRequired


getState ​

src static AlarmState? getState(Str? name)


icon ​

src const Str icon


isAlarm ​

src const Bool isAlarm


isFault ​

src const Bool isFault


make ​

src new make(Str name, Bool? alarm := false, Bool? fault := false, Bool? ackRequired := false, Int? priority := 255)


name ​

src const Str name


priority ​

src const Int priority


states ​

src static Str:AlarmState states()


AlarmStateModel ​

src const class AlarmStateModel : TypedDict

Constructors

make
makeNew
wrap

Fields

acked
priority
state

Slot Details ​


acked ​

src @TypedTag const Bool acked


make ​

src new make(Dict rec, |This| f)


makeNew ​

src static new makeNew(Str state, Int priority)


priority ​

src @TypedTag const Int priority


state ​

src @TypedTag const Str state


wrap ​

src static new wrap(Dict? dict)


AlarmTargetNavTree ​

src const class AlarmTargetNavTree : DependentNavTree

Constructors

make

Slot Details ​


make ​

src new make()


AlarmTracker ​

src const class AlarmTracker : Obj

Constructors

make

Fields

_enabledStates
dispatcher
lib
sources

Methods

enabledStates
hasState
transition

Slot Details ​


_enabledStates ​

src const AtomicRef _enabledStates := AtomicRef.<ctor>(Unsafe.<ctor>([:]))


dispatcher ​

src const AlarmDispatcher dispatcher


enabledStates ​

src Str:Bool enabledStates()


hasState ​

src Bool hasState(Str state)


lib ​

src const AlarmLib lib


make ​

src new make(Ref[] sources, AlarmLib lib, AlarmDispatcher dispatcher)


sources ​

src const Ref[] sources


transition ​

src Future? transition(Str proposedState, Bool enable, DateTime occurred, Str? message := null, Bool? ackRequired := null, Obj? priority := null, Dict? meta := null, Bool deactivateOtherStates := false)


AlarmTransition ​

src const class AlarmTransition : Obj

Constructors

make

Fields

acknowledgementRequired
deactivateOtherStates
dispatcher
enable
message
meta
occurred
priority
state

Methods

toStateModel

Slot Details ​


acknowledgementRequired ​

src const Bool acknowledgementRequired


deactivateOtherStates ​

src const Bool deactivateOtherStates


dispatcher ​

src const AlarmDispatcher dispatcher


enable ​

src const Bool enable


make ​

src new make(DateTime occurred, AlarmState state, Bool enable, AlarmDispatcher dispatcher, Obj? ackRequired := null, Str? message := null, Int? priority := null, Dict? meta := null, Bool deactivateOtherStates := false)


message ​

src const Str? message


meta ​

src const Dict? meta


occurred ​

src const DateTime occurred


priority ​

src const Int priority


state ​

src const AlarmState state


toStateModel ​

src AlarmStateModel toStateModel()


AlarmTransitionMessage ​

src const class AlarmTransitionMessage : AlarmMessage

Constructors

make

Fields

tracker
transition

Slot Details ​


make ​

src new make(AlarmTracker tracker, AlarmTransition transition)


tracker ​

src const AlarmTracker tracker


transition ​

src const AlarmTransition transition


AlarmUpdateMessage ​

src const class AlarmUpdateMessage : AlarmMessage

Constructors

make

Fields

changes

Slot Details ​


changes ​

src const Alarm changes


make ​

src new make(Alarm changes)


AlarmsObservable ​

src const class AlarmsObservable : Observable

Methods

name
notify

Slot Details ​


name ​

src virtual override Str name()


notify ​

src Void notify(Alarm[] alarms)


AlarmsRequestMessage ​

src const class AlarmsRequestMessage : AlarmDBMessage

Constructors

make

Fields

_itemIds

Methods

itemIds
key
merge

Slot Details ​


_itemIds ​

src const AtomicRef _itemIds := AtomicRef.<ctor>(Unsafe.<ctor>([,]))


itemIds ​

src Ref[] itemIds()


key ​

src virtual override Obj? key()


make ​

src new make(HxContext? context, Ref[]? itemIds, AlarmsRequestOp op := AlarmsRequestOp.get)


merge ​

src AlarmsRequestMessage merge(AlarmsRequestMessage msgB)


AlarmsRequestOp ​

src @Serializable { simple=true } enum class AlarmsRequestOp : Enum

Enum

get
getTarget
getCount
remove

Constructors

fromStrReturn the AlarmsRequestOp instance for the specified name.

Fields

valsList of AlarmsRequestOp values indexed by ordinal

Methods

toAlarmDBOp

Slot Details ​


fromStr ​

src static new fromStr(Str name, Bool checked := true)

Return the AlarmsRequestOp instance for the specified name. If not a valid name and checked is false return null, otherwise throw ParseErr.


get ​

src const static AlarmsRequestOp get := ...


getCount ​

src const static AlarmsRequestOp getCount := ...


getTarget ​

src const static AlarmsRequestOp getTarget := ...


remove ​

src const static AlarmsRequestOp remove := ...


toAlarmDBOp ​

src AlarmDBOp toAlarmDBOp()


vals ​

src const static AlarmsRequestOp[] vals := ...

List of AlarmsRequestOp values indexed by ordinal


CallFuncRoute ​

src const class CallFuncRoute : AlarmRoute

Constructors

make
wrap

Fields

TYPE
funcName

Methods

route

Slot Details ​


TYPE ​

src const static Str TYPE := "callFunc"


funcName ​

src @TypedTag const Str funcName


make ​

src new make(Dict rec, |This| f)


route ​

src virtual override Future route(HxContext context, Dict alarm, Dict? source)


wrap ​

src static new wrap(Dict? dict)


CommandPointsRoute ​

src const class CommandPointsRoute : AlarmRoute

Constructors

make
wrap

Fields

TYPE
targetFilter
targetFilterInst
value
writeLevel

Methods

route

Slot Details ​


TYPE ​

src const static Str TYPE := "commandPoints"


make ​

src new make(Dict rec, |This| f)


route ​

src virtual override Future route(HxContext context, Dict alarm, Dict? source)


targetFilter ​

src @TypedTag const Str targetFilter


targetFilterInst ​

src const Filter targetFilterInst


value ​

src @TypedTag const Obj? value


wrap ​

src static new wrap(Dict? dict)


writeLevel ​

src @TypedTag const Int? writeLevel


CreateOrUpdateMessage ​

src const class CreateOrUpdateMessage : AlarmDBMessage

Constructors

make

Methods

alarms
key
merge

Slot Details ​


alarms ​

src Alarm[] alarms()


key ​

src virtual override Obj? key()


make ​

src new make(Alarm[] alarms, Bool allowCoalesce)


merge ​

src CreateOrUpdateMessage merge(CreateOrUpdateMessage msgB)


DatabasePaginationRequest ​

src const class DatabasePaginationRequest : Obj

Constructors

make

Fields

entityOffset
filter
resultSize
sortColumnName
sortDirection

Methods

currentPage

Slot Details ​


currentPage ​

src Int currentPage()


entityOffset ​

src const Int entityOffset


filter ​

src const Str? filter


make ​

src new make(Int resultSize, Int entityOffset, Str sortCol := "occurred", Str sortDirection := "DESC", Str? filter := null)


resultSize ​

src const Int resultSize


sortColumnName ​

src const Str sortColumnName


sortDirection ​

src const Str sortDirection


DatabasePaginationResult ​

src const class DatabasePaginationResult : Obj

Constructors

make

Fields

currentOffset
resultSize
totalItems
totalResults

Slot Details ​


currentOffset ​

src const Int currentOffset


make ​

src new make(Int currentOffset, Int resultSize, Int totalItems, Int totalResults)


resultSize ​

src const Int resultSize


totalItems ​

src const Int totalItems


totalResults ​

src const Int totalResults


DisabledHistoryDB ​

src const class DisabledHistoryDB : Obj, AlarmHistoryDatabase

Methods

clear
historyCount
import
insert
query
read
readStream
removeBefore
removeBetween
runHouseKeeping
start
stop

Slot Details ​


clear ​

src virtual override Future clear(Bool? rebuild := false)


historyCount ​

src virtual override Future historyCount(Ref? target := null)


import ​

src virtual override Future import(AlarmHistoryEntry[] data)


insert ​

src virtual override Future insert(AlarmHistoryEntry[] entries)


query ​

src virtual override Future query(Str query)


read ​

src virtual override Future read(HxContext context, DateSpan? range, Ref[]? targets, DatabasePaginationRequest? pagination := null)


readStream ​

src virtual override SourceStream? readStream(HxContext context, DateSpan? range, Ref[]? targets, DatabasePaginationRequest? pagination := null)


removeBefore ​

src virtual override Future removeBefore(DateTime date)


removeBetween ​

src virtual override Future removeBetween(DateSpan range)


runHouseKeeping ​

src virtual override Future runHouseKeeping()


start ​

src virtual override Future start()


stop ​

src virtual override Future stop()


ExecuteAxonRoute ​

src const class ExecuteAxonRoute : AlarmRoute

Constructors

make
wrap

Fields

TYPE
axonExpr

Methods

route

Slot Details ​


TYPE ​

src const static Str TYPE := "executeAxon"


axonExpr ​

src @TypedTag const Str axonExpr


make ​

src new make(Dict rec, |This| f)


route ​

src virtual override Future route(HxContext context, Dict alarm, Dict? source)


wrap ​

src static new wrap(Dict? dict)


FINWorkerPoolManager ​

src class FINWorkerPoolManager : Obj

Constructors

make

Fields

db

Slot Details ​


db ​

src AlarmHistoryQuestDB db


make ​

src new make(AlarmHistoryQuestDB db)


FuncEvaluationRule ​

src const class FuncEvaluationRule : AlarmRule

Constructors

make
wrap

Fields

TYPE
funcName

Methods

test

Slot Details ​


TYPE ​

src const static Str TYPE := "funcEval"


funcName ​

src @TypedTag const Str funcName


make ​

src new make(Dict rec, |This| f)


test ​

src protected virtual override Bool test(HxContext context, Dict record)


wrap ​

src static new wrap(Dict? dict)


HistoryClearMessage ​

src const class HistoryClearMessage : AlarmHistoryDBMessage

Constructors

make

Fields

rebuild

Slot Details ​


make ​

src new make(Bool? rebuild := false)


rebuild ​

src const Bool rebuild


HistoryCountMessage ​

src const class HistoryCountMessage : AlarmHistoryDBMessage

Constructors

make

Fields

targetId

Slot Details ​


make ​

src new make(Ref? targetId := null)


targetId ​

src const Ref? targetId


HistoryImportMessage ​

src const class HistoryImportMessage : AlarmHistoryDBMessage

Constructors

make

Fields

data

Slot Details ​


data ​

src const AlarmHistoryEntry[] data


make ​

src new make(AlarmHistoryEntry[] data)


HistoryInsertMessage ​

src const class HistoryInsertMessage : AlarmHistoryDBMessage

Constructors

make

Fields

records

Methods

key
merge

Slot Details ​


key ​

src virtual override Obj? key()


make ​

src new make(AlarmHistoryEntry[] records)


merge ​

src virtual override AlarmHistoryDBMessage merge(AlarmHistoryDBMessage msgB)


records ​

src const AlarmHistoryEntry[] records


HistoryQueryMessage ​

src const class HistoryQueryMessage : AlarmHistoryDBMessage

Constructors

make

Fields

query

Slot Details ​


make ​

src new make(Str query)


query ​

src const Str query


HistoryReadMessage ​

src const class HistoryReadMessage : AlarmHistoryDBMessage

Constructors

make

Fields

_stream
pagination
range
targets

Methods

stream

Slot Details ​


_stream ​

src const Unsafe? _stream


make ​

src new make(HxContext context, DateSpan? range := null, Ref[]? targets := null, DatabasePaginationRequest? pagination := null, SourceStream? stream := null)


pagination ​

src const DatabasePaginationRequest? pagination


range ​

src const DateSpan? range


stream ​

src SourceStream? stream()


targets ​

src const Ref[]? targets


HistoryRemoveBeforeMessage ​

src const class HistoryRemoveBeforeMessage : AlarmHistoryDBMessage

Constructors

make

Fields

date

Slot Details ​


date ​

src const DateTime date


make ​

src new make(DateTime date)


HistoryRemoveBetweenMessage ​

src const class HistoryRemoveBetweenMessage : AlarmHistoryDBMessage

Constructors

make

Fields

range

Slot Details ​


make ​

src new make(DateSpan range)


range ​

src const DateSpan range


ImportAlarmsMessage ​

src const class ImportAlarmsMessage : AlarmDBMessage

Constructors

make

Fields

records

Slot Details ​


make ​

src new make(Alarm[] records)


records ​

src const Alarm[] records


MockAlarmDatabase ​

src const class MockAlarmDatabase : Obj, AlarmDatabase

Methods

alarmCount
createOrUpdate
get
getTargetsAlarms
import
poll
readAlarms
remove
runHouseKeeping
start
stop

Slot Details ​


alarmCount ​

src virtual override Future alarmCount(Ref[]? targets := null)


createOrUpdate ​

src virtual override Future createOrUpdate(Alarm[] alarms, Bool coalesce := true)


get ​

src virtual override Future get(HxContext context, Ref[] alarmIds)


getTargetsAlarms ​

src virtual override Future getTargetsAlarms(HxContext context, Ref[] targets)


import ​

src virtual override Future import(Alarm[] data)


poll ​

src virtual override Future poll(HxContext context, Int lastPollTime, Ref[]? targets := null)


readAlarms ​

src virtual override Future readAlarms(HxContext context, Ref[]? targets := null, DateSpan? range := null, DatabasePaginationRequest? pagination := null)


remove ​

src virtual override Future remove(Ref[] alarmIds)


runHouseKeeping ​

src virtual override Future runHouseKeeping()


start ​

src virtual override Future start()


stop ​

src virtual override Future stop()


MockAlarmHistoryDatabase ​

src const class MockAlarmHistoryDatabase : Obj, AlarmHistoryDatabase

Fields

recordData
timeData

Methods

clear
historyCount
import
insert
query
read
readStream
records
removeBefore
removeBetween
runHouseKeeping
start
stop

Slot Details ​


clear ​

src virtual override Future clear(Bool? rebuild := false)


historyCount ​

src virtual override Future historyCount(Ref? target := null)


import ​

src virtual override Future import(AlarmHistoryEntry[] data)


insert ​

src virtual override Future insert(AlarmHistoryEntry[] records)


query ​

src virtual override Future query(Str query)


read ​

src virtual override Future read(HxContext context, DateSpan? range, Ref[]? targets, DatabasePaginationRequest? pagination := null)


readStream ​

src virtual override SourceStream? readStream(HxContext context, DateSpan? range, Ref[]? targets, DatabasePaginationRequest? pagination := null)


recordData ​

src const Unsafe recordData := Unsafe.<ctor>([,])


records ​

src Dict[] records()


removeBefore ​

src virtual override Future removeBefore(DateTime date)


removeBetween ​

src virtual override Future removeBetween(DateSpan range)


runHouseKeeping ​

src virtual override Future runHouseKeeping()


start ​

src virtual override Future start()


stop ​

src virtual override Future stop()


timeData ​

src const Unsafe timeData := Unsafe.<ctor>([,])


MockAlarmRoute ​

src const class MockAlarmRoute : AlarmRoute

Constructors

make
wrap

Fields

testFunc

Methods

route

Slot Details ​


make ​

src new make(Dict rec, |This| f)


route ​

src virtual override Future route(HxContext context, Dict alarm, Dict? source)


testFunc ​

src const AtomicRef testFunc := AtomicRef.<ctor>()


wrap ​

src static new wrap(Func testFunc)


NotificationRoute ​

src const class NotificationRoute : AlarmRoute

Constructors

make
wrap

Fields

TYPE

Methods

route

Slot Details ​


TYPE ​

src const static Str TYPE := "notification"


make ​

src new make(Dict rec, |This| f)


route ​

src virtual override Future route(HxContext context, Dict alarm, Dict? source)


wrap ​

src static new wrap(Dict? dict)


OutOfRangeRule ​

src const class OutOfRangeRule : AlarmRule

Constructors

make
wrap

Fields

TYPE
highLimit
lowLimit

Methods

test

Slot Details ​


TYPE ​

src const static Str TYPE := "outOfRange"


highLimit ​

src @TypedTag const Number? highLimit


lowLimit ​

src @TypedTag const Number? lowLimit


make ​

src new make(Dict rec, |This| f)


test ​

src protected virtual override Bool test(HxContext context, Dict record)


wrap ​

src static new wrap(Dict? dict)


PollAlarmsMessage ​

src const class PollAlarmsMessage : AlarmDBMessage

Constructors

make

Fields

lastPollTime
targets

Slot Details ​


lastPollTime ​

src const Int lastPollTime


make ​

src new make(HxContext context, Int lastPollTime, Ref[]? targets)


targets ​

src const Ref[]? targets


PromisingActor ​

src const class PromisingActor : Actor

Constructors

make

Fields

POOL
log

Methods

receive
resolveAfter
resolveAfterComplete

Slot Details ​


POOL ​

src const static ActorPool POOL := ActorPool.<ctor>()


log ​

src const Log log := Log.get("PromisingActor")


make ​

src new make()


receive ​

src protected virtual override Obj? receive(Obj? message)


resolveAfter ​

src static Future resolveAfter(Future[] futures, Duration? timeout := null)


resolveAfterComplete ​

src Future resolveAfterComplete(Future[] futures, Duration? timeout := null)


PromisingMessage ​

src const class PromisingMessage : Obj

Constructors

make

Fields

futures
timeout

Slot Details ​


futures ​

src const Future[] futures


make ​

src new make(Future[] futures, Duration? timeout := null)


timeout ​

src const Duration? timeout


QuestDBCursor ​

src class QuestDBCursor : Obj

Constructors

make

Fields

context
includeCounter

Methods

count
reset
shouldEnd
shouldInclude

Slot Details ​


context ​

src HxContext context


count ​

src Int count(RecordCursor cursor)


includeCounter ​

src Int includeCounter


make ​

src new make(HxContext context, QuestDBUtility util, DatabasePaginationRequest? pagination)


reset ​

src Void reset()


shouldEnd ​

src Bool shouldEnd()


shouldInclude ​

src Bool shouldInclude(Record record)


ReadAlarmsMessage ​

src const class ReadAlarmsMessage : AlarmDBMessage

Constructors

make

Fields

pagination
span
targets

Slot Details ​


make ​

src new make(HxContext context, Ref[]? targets, DateSpan? span, DatabasePaginationRequest? pagination)


pagination ​

src const DatabasePaginationRequest? pagination


span ​

src const DateSpan? span


targets ​

src const Ref[]? targets


RecordChangeMessage ​

src const class RecordChangeMessage : RuleMessage

Constructors

make

Fields

record
recordId
time

Methods

compare
key

Slot Details ​


compare ​

src virtual override Int compare(Obj item)


key ​

src virtual override Obj? key()


make ​

src new make(Dict record, DateTime time)


record ​

src const Dict record


recordId ​

src const Ref recordId


time ​

src const DateTime time


RemoveRoutesMessage ​

src const class RemoveRoutesMessage : RoutingMessage

Constructors

make

Fields

routeIds

Slot Details ​


make ​

src new make(Ref[] routeIds)


routeIds ​

src const Ref[] routeIds


RemoveRulesMessage ​

src const class RemoveRulesMessage : RuleMessage

Constructors

make

Fields

ruleIds

Slot Details ​


make ​

src new make(Ref[] ruleIds)


ruleIds ​

src const Ref[] ruleIds


RouteAlarmMessage ​

src const class RouteAlarmMessage : RoutingMessage

Constructors

make

Fields

alarm
alarmSource

Methods

key
merge

Slot Details ​


alarm ​

src const Dict alarm


alarmSource ​

src const Dict? alarmSource


key ​

src virtual override Obj? key()


make ​

src new make(Dict alarm, Dict? alarmSource)


merge ​

src virtual override RoutingMessage merge(RoutingMessage msgB)


RoutingMessage ​

src const class RoutingMessage : Obj

Constructors

make

Fields

op
time

Methods

key
merge

Slot Details ​


key ​

src virtual Obj? key()


make ​

src new make(RoutingOp op)


merge ​

src virtual RoutingMessage merge(RoutingMessage msgB)


op ​

src const RoutingOp op


time ​

src const DateTime time


RoutingOp ​

src @Serializable { simple=true } enum class RoutingOp : Enum

Enum

clearRoutes
addRoutes
removeRoutes
getRoutes
route

Constructors

fromStrReturn the RoutingOp instance for the specified name.

Fields

valsList of RoutingOp values indexed by ordinal

Slot Details ​


addRoutes ​

src const static RoutingOp addRoutes := ...


clearRoutes ​

src const static RoutingOp clearRoutes := ...


fromStr ​

src static new fromStr(Str name, Bool checked := true)

Return the RoutingOp instance for the specified name. If not a valid name and checked is false return null, otherwise throw ParseErr.


getRoutes ​

src const static RoutingOp getRoutes := ...


removeRoutes ​

src const static RoutingOp removeRoutes := ...


route ​

src const static RoutingOp route := ...


vals ​

src const static RoutingOp[] vals := ...

List of RoutingOp values indexed by ordinal


RuleMessage ​

src const class RuleMessage : Obj

Constructors

make

Fields

op

Methods

compare
key
merge

Slot Details ​


compare ​

src virtual override Int compare(Obj item)


key ​

src virtual Obj? key()


make ​

src new make(RuleOp op)


merge ​

src virtual RuleMessage merge(RuleMessage msgB)


op ​

src const RuleOp op


RuleOp ​

src @Serializable { simple=true } enum class RuleOp : Enum

Enum

addRules
removeRules
clearRules
recordChange

Constructors

fromStrReturn the RuleOp instance for the specified name.

Fields

valsList of RuleOp values indexed by ordinal

Slot Details ​


addRules ​

src const static RuleOp addRules := ...


clearRules ​

src const static RuleOp clearRules := ...


fromStr ​

src static new fromStr(Str name, Bool checked := true)

Return the RuleOp instance for the specified name. If not a valid name and checked is false return null, otherwise throw ParseErr.


recordChange ​

src const static RuleOp recordChange := ...


removeRules ​

src const static RuleOp removeRules := ...


vals ​

src const static RuleOp[] vals := ...

List of RuleOp values indexed by ordinal


RulesEngine ​

src const class RulesEngine : Obj

Constructors

make

Fields

REC_TAG
actor
enabled
lib
management

Methods

CreateRuleFromRec
addRule
addRules
clearRules
disable
enable
recordChange
removeRules

Slot Details ​


CreateRuleFromRec ​

src static AlarmRule? CreateRuleFromRec(Dict record)


REC_TAG ​

src const static Str REC_TAG := "alarmRule"


actor ​

src const Actor actor


addRule ​

src Void addRule(Str ruleQname)


addRules ​

src Future addRules(AlarmRule[] rules)


clearRules ​

src Future clearRules()


disable ​

src Void disable()


enable ​

src Void enable()


enabled ​

src const AtomicBool enabled


lib ​

src const AlarmLib lib


make ​

src new make(ActorPool pool, AlarmLib lib)


management ​

src const FolioFlatFile management


recordChange ​

src Future recordChange(Dict record, DateTime? time)


removeRules ​

src Future removeRules(Ref[] ruleIds)


ValueRule ​

src const class ValueRule : AlarmRule

Constructors

make
wrap

Fields

TYPE
tagName
value

Methods

test

Slot Details ​


TYPE ​

src const static Str TYPE := "value"


make ​

src new make(Dict rec, |This| f)


tagName ​

src @TypedTag const Str tagName := "curVal"


test ​

src protected virtual override Bool test(HxContext context, Dict record)


value ​

src @TypedTag const Obj? value


wrap ​

src static new wrap(Dict? dict)


WriterStream ​

src class WriterStream : TerminalStream

Constructors

make

Fields

funcName

Methods

onRun
onStart

Slot Details ​


funcName ​

src virtual override Str funcName := "toWriter"


make ​

src new make(SourceStream source, Obj target)


onRun ​

src virtual override Obj? onRun()


onStart ​

src virtual override Void onStart(Signal sig)


const class AckCompleteMessage : AlarmMessage 
{
  const AlarmFuture[] futures
  
  new make(AlarmFuture[] futures): super(AlarmOp.connAckComplete)
  {
    this.futures = futures
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


const class AddRoutesMessage : RoutingMessage
{
  const AlarmRoute[] routes
  
  new make(AlarmRoute[] routes) : super(RoutingOp.addRoutes)
  {
    this.routes = routes
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


const class AddRulesMessage : RuleMessage 
{
  const AlarmRule[] rules
  
  new make(AlarmRule[] rules) : super(RuleOp.addRules)
  {
    this.rules = rules
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack

const class Alarm : TypedDict
{
  @TypedTag
  const Str state
  
  @TypedTag
  const Number priority
  
  @TypedTag
  const DateTime occurred
  
  @TypedTag
  const Bool active
  
  @TypedTag
  const DateTime? created
    
  @TypedTag
  const Ref? sourceRef
  
  @TypedTag
  const Obj? curVal
  
  @TypedTag
  const Str? curStatus
  
  @TypedTag
  const Str? message
  
  @TypedTag
  const Str? ackedBy
  
  @TypedTag
  const DateTime? acked
  
  @TypedTag
  const Str? ackMessage
  
  @TypedTag
  const Str? ackError
  
  @TypedTag
  const Bool ackRequired
  
  @TypedTag
  const Bool ackInProgress
  
  @TypedTag
  const Bool ackMessageRequired
  
  @TypedTag
  const Str dispatcher := AlarmDispatcher.UNKNOWN.toStr
  
  @TypedTag
  const Dict? metadata
  
  new make (Dict rec, | This | f) : super(rec)
  {
    f(this)
  }
  
  public Bool isAcked()
  {
    return this.acked != null
  }
  
  static new wrap(Dict? dict)
  {
    if (dict == null)
      return null
    
    return create(Alarm#, dict)
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


const class AlarmAckMessage : AlarmMessage 
{
  const AlarmAcknowledgement ack
  
  new make(AlarmAcknowledgement ack): super(AlarmOp.acknowledge)
  {
    this.ack = ack
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using hx

const class AlarmAcknowledgement 
{
  const Unsafe _context
  const Ref[] alarmRefs; // Empty list to ack everything
  const DateTime time;
  const Str? acknowlegedBy;
  const Str? message;
  
  new make(HxContext context, Ref[] alarmRefs, DateTime time, Str? ackBy := null, Str? message := null)
  {
    this._context = Unsafe(context)
    this.alarmRefs = alarmRefs
    this.time = time
    this.acknowlegedBy = ackBy
    this.message = message
  }
  
  public HxContext context()
  {
    return this._context.val
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
const class AlarmConnAckResult
{
  const Dict alarmRec
  const Bool success
  const Str? failureReason
  const Err? error
  
  new make (Dict rec, Bool isSuccess, Str? failureReason := null, Err? error := null)
  {
    this.alarmRec = rec
    this.success = isSuccess
    this.failureReason = failureReason
    this.error = error
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using hx

enum class AlarmDBOp {
  readAlarms,
  getAlarmByID,
  getAlarmsByTarget,
  createUpdateAlarm,
  removeAlarms,
  getAlarmCount,
  poll,
  import
}

const class AlarmDBMessage 
{
  const Unsafe _context 
  const AlarmDBOp op
    
  new make (AlarmDBOp op, HxContext? context := null)
  {
    this.op = op
    this._context = Unsafe(context)
  }
  
  public virtual Obj? key()
  {
    return null
  }
  
  public HxContext? context()
  {
    return this._context.val
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using concurrent
using axon
using hx

const mixin AlarmDatabase
{
  public abstract Future readAlarms(HxContext context, Ref[]? targets := null, DateSpan? span := null, DatabasePaginationRequest? pagination := null) // Future<AlarmResult>
  public abstract Future get(HxContext context, Ref[] alarmIds) // Future<Alarm[]>
  public abstract Future getTargetsAlarms(HxContext context, Ref[] targets) // Future<Alarm[]>
  public abstract Future createOrUpdate(Alarm[] alarms, Bool coalesce := true) // Future<Alarm[]>
  public abstract Future alarmCount(Ref[]? targets := null) // Future<Int>
  public abstract Future remove(Ref[] alarmIds)
  
  public abstract Future poll(HxContext context, Int lastPollTime, Ref[]? targets := null)
  
  public abstract Future runHouseKeeping()
  
  public abstract Future import(Alarm[] records)
  
  public abstract Future start()
  public abstract Future stop()
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


enum class AlarmDispatcher 
{
  INTERNAL,
  CONTROL,
  RULES,
  CONNECTOR,
  UNKNOWN
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using concurrent
using haystack
using connExt
using hxConn
using hx
using folio
using skyarc

const class AlarmEngine : Actor
{
  static const Duration Timeout := 30sec
  
  const AlarmLib lib
  
  new make(
    ActorPool pool,
    AlarmLib lib) : super(pool)
  {
    this.lib = lib
  }
  
  public Future acknowledge(AlarmAcknowledgement ack)
  {
    return this.send(
      AlarmAckMessage(
        ack
      )
    )
  }
  
  public Future transition(AlarmTracker tracker, AlarmTransition transition)
  {
    return this.send(
      AlarmTransitionMessage(
        tracker,
        transition
      )
    )
  }
  
  public Future update(Alarm changes)
  {
    return this.send(
      AlarmUpdateMessage(
        changes
      )
    )
  }
  
  override protected Obj? receive(Obj? message)
  {
    alarmMessage := (AlarmMessage) message
    
    switch (alarmMessage.op)
    {
      case AlarmOp.transition:
        return this.commitTransition(
          ((AlarmTransitionMessage) alarmMessage).tracker,
          ((AlarmTransitionMessage) alarmMessage).transition
        )
      
      case AlarmOp.acknowledge:
        return this.commitAcknowledge(
          ((AlarmAckMessage) alarmMessage).ack
        )
      
      case AlarmOp.update:
        return this.commitUpdate(
          ((AlarmUpdateMessage) alarmMessage).changes
        )
      
      case AlarmOp.connAckComplete:
        this.commitAckComplete(alarmMessage)
    }
    
    return null
  }
  
  private AlarmDatabase db()
  {
    return this.lib.db
  }
  
  private AlarmHistoryDatabase history()
  {
    return this.lib.history
  }
  
  ////////////////////////////////////////
  // IMPLEMENTATION
  ////////////////////////////////////////
  
  
  private Alarm[] commitTransition(AlarmTracker tracker, AlarmTransition transition)
  {   
    Alarm[] toAlarmCommit := [,]
    Alarm[] toReturn := [,]
    Alarm[] toNotify := [,]
    Ref[] toRemove := [,]   
    Diff[] toDBCommit := [,]
    Alarm[] alarmCommitOnly := [,]

    Ref:Dict sourceMap := [:]
    
    tracker.sources.each | sourceRef | 
    {  
      Str alarmId := AlarmFuncs.toKey(sourceRef, transition.state.name, transition.occurred)
      
      //////////////////////////////////////////////////////////////
      // ALARM RECORD CONFIGURATION
      //////////////////////////////////////////////////////////////
      
      // Find existing alarm
      Dict? alarm := ((Alarm[]) db.get(
        this.lib.rt.context.create(User.alarmEngine),
        [Ref(alarmId)]
      ).get).getSafe(0)
      
      
      // Find source
      sourceRec := this.lib.rt.db.readById(sourceRef, false)
      
      // Get State
      newState := transition.state
                  
      // If we're removing a state that we do not have, exit
      if (!transition.enable && (alarm == null || !((Alarm) alarm).active))
      {
        // State already returned to normal.
        if (alarm != null)
          toReturn.add(alarm)
        
        return
      }
      
      // Transition Determination
      if (alarm != null)
      {
        if (transition.occurred.compare(alarm.get("occurred")) == 0)
        {
          // Same alarm (Same source, state, occurrence), already routed, do nothing.
          toReturn.add(alarm)
          return
        }
                
        // Configure new transition.
        alarm = this.configureAlarmRecord(
          Etc.dictToMap(alarm),
          transition
        )
      }
      else
      {       
        // Create Alarm if didn't exist
        alarm = this.configureAlarmRecord(
          [
            AlarmRecord.MARKER_NAME: Marker.val,
            "id": Ref(alarmId),
            "sourceRef": sourceRef
          ],
          transition
        )
      }
      
      //////////////////////////////////////////////////////////////
      // STATE MANAGEMENT
      //////////////////////////////////////////////////////////////
      
      // Configure active status
      alarm = Etc.dictSet(
        alarm,
        "active",
        transition.enable
          ? Marker.val
          : Remove.val
      )
                  
      // Find all alarms
      existingAlarms := this.getAlarmsByPriority(sourceRef)
    
      // Remove existing acknowledged alarms where the point is not in that state.
      toRemove.addAll(
        existingAlarms.mapNotNull |existingAlarm|
        {
          if (existingAlarm.id == alarm.id)
            existingAlarm = Alarm.wrap(alarm)         
                    
          // Normal deactivates all states. Remove if we're going normal
          if ((existingAlarm.active && !transition.deactivateOtherStates) && !(newState == AlarmState.NORMAL && transition.enable))
            return null // Do not remove alarms that are current active
          
          // if this alarm has been acknowledged or doesn't require acknowledgement remove it.
          if (!existingAlarm.has("ackRequired") || existingAlarm.has("acked"))
            return existingAlarm.id
          
          return null
        }
      )
      
      // If we are enabling normal (set entity to normal) deactivate all other alarms.
      // TODO: Add transition property to deactive all other states.
      // This will enable things like BACnet that functions off of a state machine to work
      // properly.
      if ((newState == AlarmState.NORMAL && transition.enable) || transition.deactivateOtherStates)
      {
        // Set all states for this entity to inactive.
        existingAlarms.each |existingAlarm| 
        {  
          if (toRemove.contains(existingAlarm.id) || existingAlarm.id == alarm.id)
            return
          
          if (existingAlarm.active)
          {
            alarmCommitOnly.add(
              Alarm.wrap(
                Etc.dictSet(existingAlarm, "active", Remove.val)
              )
            )
          }
        }
      }
        
      // Validate New Alarm
      // ...
            
      // Calculate next state
      highestPriorityItem := existingAlarms.last
      
      targetState := AlarmState.NORMAL
      lastState := newState.name
      
      if (highestPriorityItem != null)
      {
        highestPriortyState := AlarmState.getState(highestPriorityItem.state)
        
        // if highest priority's state is higher than the new state. use the highest priority state
        if (highestPriortyState.priority < newState.priority)
          targetState = highestPriortyState
        else
        if (transition.enable)
          targetState = newState
      }
      else
      if (transition.enable)
        targetState = newState
          
      // Update Source's "inAlarm" tag.
      if (sourceRec != null)
      {
        sourceMap.set(sourceRec.id, sourceRec)
        
        if (sourceRec.has(AlarmPointTags.inAlarm.toStr))
          lastState = sourceRec.get(AlarmPointTags.inAlarm.toStr)
  
        toDBCommit.add(
          Diff.make(
            sourceRec,
            [
              AlarmPointTags.inAlarm.toStr: (targetState.isAlarm || targetState.isFault)
                ? targetState.name
                : Remove.val
            ]
          )
        )
        
        alarmMeta := transition.meta
        
        // Value
        if (alarmMeta != null && alarmMeta.has("sourceValue"))
        {
          alarm = Etc.dictSet(alarm, "curVal", alarmMeta.get("sourceValue"))
        }
        else
        if (sourceRec.has("curVal"))
          alarm = Etc.dictSet(alarm, "curVal", sourceRec.get("curVal"))
        
        // Status
        if (alarmMeta != null && alarmMeta.has("sourceStatus"))
        {
          alarm = Etc.dictSet(alarm, "curStatus", alarmMeta.get("sourceStatus"))
        }
        else
        if (sourceRec.has("curStatus"))
          alarm = Etc.dictSet(alarm, "curStatus", sourceRec.get("curStatus"))
      }
  
      // If we're removing the last state, 
      // create a normal alarm if alarm isn't for the normal state.
    
      if ((targetState == AlarmState.NORMAL) &&
        (alarm.get("state") != AlarmState.NORMAL.name))
      {
        // Generate normal state
        normalState := AlarmState.NORMAL
        normalAlarm := this.generateAlarm(
          normalState,
          "Return to normal".toLocale,
          sourceRef,
          alarm
        )
        
        if (normalState.acknowledgementRequired)
          toAlarmCommit.add(normalAlarm)
        else
          toNotify.add(normalAlarm)
      }
      
      //////////////////////////////////////////////////////////////
      // DIRECT ALARM
      //////////////////////////////////////////////////////////////

      if (!(alarm is Alarm))
        alarm = Alarm.wrap(alarm)
      
      if (transition.enable)
      {
        if (transition.acknowledgementRequired || 
          newState.isAlarm || 
          newState.isFault)
        {
          // Persist Alarm
          toAlarmCommit.add(alarm)
        }
        else
        {
          // This alarm isn't committing but should notify and go into history.
          toNotify.add(alarm)
        }
      }
      else
      if (!toRemove.contains(alarm.id))
      {
        // If we're not removing the alarm (because it requires ack), update its status
        toAlarmCommit.add(alarm)
      }
    }
    
    //////////////////////////////////////////////////////////////
    // PERSIST & NOTIFY
    //////////////////////////////////////////////////////////////

    
    // Commit Removals
    if (toRemove.size > 0)
      this.db.remove(toRemove)
                    
    // Commit DB Records (Alarm sources)
    if (toDBCommit.size > 0)
      this.lib.rt.db.commitAll(toDBCommit)
    
    // Commit Alarms
    if (toAlarmCommit.size > 0)
    {
      db.createOrUpdate(toAlarmCommit).get
    
      // Send notifications
      toAlarmCommit.each | alarm |
      {
        try
        {
          sourceRec := (alarm.sourceRef != null)
            ? sourceMap.getChecked(alarm.sourceRef, false)
            : null
          
          this.notifySubsystems(alarm, sourceRec, transition)
        }
        catch (Err error)
        {
          this.lib.log.err(
            "Error sending alarm notification for ${alarm.sourceRef} @ ${alarm.state}"
          )
        }
      }

      // Notify Committed Alarms
      this.lib.alarmsObs.notify(toAlarmCommit)
    }
    
    // Send notifications for alarms that are notify only
    if (toNotify.size > 0)
    {
      // Send notifications
      toNotify.each | alarm | 
      {
        try
        {
          sourceRec := (alarm.sourceRef != null)
            ? sourceMap.getChecked(alarm.sourceRef, false)
            : null
          
          this.notifySubsystems(alarm, sourceRec, transition)
        }
        catch (Err error)
        {
          this.lib.log.err(
            "Error sending alarm notification for ${alarm.sourceRef} @ ${alarm.state}"
          )
        }
        
        
      }

      // Notify Committed Alarms
      this.lib.alarmsObs.notify(toNotify)
    }
    
    // Commit changes to alarms that are commit only (no notify)
    if (alarmCommitOnly.size > 0)
      this.db.createOrUpdate(alarmCommitOnly)
    
    return toAlarmCommit.addAll(toReturn).addAll(toNotify).map |alarm| {
      Alarm.wrap(Etc.dictRemoveAllWithVal(alarm, Remove.val))
    }
  }
  
  
  private Alarm[] getAlarmsByPriority(Ref sourceRef)
  {
    return this.sortAlarmByPriority(
      ((AlarmResult) db.readAlarms(
        this.lib.rt.context.create(User.alarmEngine),
        [sourceRef]
      ).get).alarms
    )
  }
  
  private Alarm[] sortAlarmByPriority(Alarm[] alarms)
  {
    return alarms.rw.sort |alarmA, alarmB|
    {
      stateA := AlarmState.getState(alarmA.get("state").toStr)
      stateB := AlarmState.getState(alarmB.get("state").toStr)
      
      return stateA.priority <=> stateB.priority
    }
  }
  
  private Alarm[] commitAcknowledge(AlarmAcknowledgement ack)
  {
    Alarm[] toUpdate := [,]
    Alarm[] toRemove := [,]
    Alarm[] inProgress := [,]
    
    //////////////////////////////////////////////////////////////
    // Setup & Configure Ack Process
    //////////////////////////////////////////////////////////////
    
    Alarm[] originalAlarms := (ack.alarmRefs.size > 0)
      ? this.db.get(
        this.lib.rt.context.create(User.alarmEngine),
        ack.alarmRefs
      ).get
      : ((AlarmResult) this.db.readAlarms(this.lib.rt.context.create(User.alarmEngine)).get).alarms // ack all
    
    // Get Source Records
    sourceRecordList := this.lib.rt.db.readByIdsList(
      originalAlarms.mapNotNull | alarm | { alarm.sourceRef },
      false
    )
    
    // Filter for Access to record
    // TODO: Filter for Access to be able to ack record a record that they can't write to.
    sourceRecordList = sourceRecordList.findAll | rec |
    { 
      filterStr := ack.context.user.meta["alarmAckAccessFilter"] as Str
      
      if(filterStr != null )
        return Filter(filterStr).matches(rec)

      return ack.context.canWrite(rec) 
    }
    
    Ref:Dict sourceRecs := [:]

    sourceRecs.setList(
      sourceRecordList
    ) | source | { source.id }
    
    // Remove alarms with inaccessible sources
    originalAlarms = originalAlarms.findAll |alarm| 
    {
      // Alarms without a source are valid. Possibly an externally triggered alarm
      if (alarm.sourceRef == null)
        return true
      
      return sourceRecs.containsKey(alarm.sourceRef)
    }
    
    // Set ack in progress
    this.db.createOrUpdate(originalAlarms.map |alarm| {  
      Alarm.wrap(Etc.dictSet(alarm, "ackInProgress", Marker.val))
    }, false)
    
    AlarmFuture[] connAckFutures := [,]
    
    //////////////////////////////////////////////////////////////
    // Process Ack Messages
    //////////////////////////////////////////////////////////////
    
    originalAlarms.eachNotNull | originalAlarm | {
      // Acknowledge Alarm
      alarmMap := Etc.dictToMap(originalAlarm)
      alarmMap.set("acked", ack.time)
            
      if (ack.acknowlegedBy != null) 
        alarmMap.set("ackedBy", ack.acknowlegedBy)
      
      if (ack.message != null)
        alarmMap.set("ackMessage", ack.message)
      
      alarm := Alarm.wrap(Etc.makeDict(alarmMap))
      conn  := this.lib.rt.conn.point(alarm.sourceRef, false)?.conn

      if (conn == null)
        conn = alarm.metadata?.get("connRef") != null ? this.lib.rt.conn.conn((Ref) alarm.metadata["connRef"], false) : null
                  
      // Forward to Connectivity Engine if the alarm hasn't been acked
      if (alarm.dispatcher == AlarmDispatcher.CONNECTOR.toStr && conn != null && !originalAlarm.has("acked"))
      {
        try
        {
          AlarmFuture? future
          
          if (conn is ConnActor)
          {
            future = AlarmFuture(
              ((ConnActor) conn).send(
                ConnMsg(
                  "alarmAck",
                  [alarm]
                )
              ),
              alarm
            )
          }
          else
          if (conn is hxConn::Conn)
          {
            future = AlarmFuture(
              ((hxConn::Conn) conn).send(
                HxMsg(
                  "alarmAck",
                  [alarm]
                )
              ),
              alarm
            )
          }
          
          if (future != null)
            connAckFutures.add(future)
        }
        catch (Err error)
        {
          this.lib.log.err("Error sending ack to connector", error)
          alarm = Alarm.wrap(
            Etc.dictSet(
              alarm,
              "ackError",
              error.msg
            )
          )
        }
        
        inProgress.add(alarm)
        
        return // We will update connector alarms after completion
      }
                        
      // If alarm is not the current state, remove
        
      // Get current state from point value (is Point still in alarm state)
      //alarmState := AlarmState.getState(alarm.state)
      sourceRec := sourceRecs.getChecked(alarm.sourceRef, false)
      //currentState := AlarmState.getState(sourceRec?.get(AlarmPointTags.inAlarm.toStr))

      // Persist Alarm
      if (alarm.active)
      {
        // If the record is still in this state
        toUpdate.add(
          this.removeTags(alarm, ["ackInProgress"], true)
        )
      }
      else
      {
        toRemove.add(alarm)
      }
    }
    
    // Queue to update records when acks are complete
    if (connAckFutures.size > 0)
    {
      this.sendWhenComplete(
        PromisingActor.resolveAfter(connAckFutures),
        AckCompleteMessage(connAckFutures)
      )
    }
    
            
    // Remove stale alarms (We only remove acknowledged alarms that aren't active)
    if (toRemove.size > 0)
    {
      this.db.remove(toRemove.map |alarm| { alarm.id })
            
      toRemove.each | alarm | {
        sourceRec := sourceRecs.getChecked(alarm.sourceRef, false)
        this.notifySubsystems(alarm, sourceRec)
      }
      
      this.lib.alarmsObs.notify(toRemove)
    }
    
    if (toUpdate.size > 0)
    {
      this.db.createOrUpdate(toUpdate, false)
                  
      toUpdate.each | alarm | {
        sourceRec := sourceRecs.getChecked(alarm.sourceRef, false)
        this.notifySubsystems(alarm, sourceRec)
      }   
      
      // Notify Committed Alarms
      this.lib.alarmsObs.notify(toUpdate)
    }

    return toUpdate.addAll(toRemove).addAll(inProgress).map |alarm| {
      Alarm.wrap(Etc.dictRemoveAllWithVal(alarm, Remove.val))
    }
  }
  
  private Void notifySubsystems(Alarm alarm, Dict? sourceRec := null, AlarmTransition? transition := null)
  {
    alarm = Alarm.wrap(Etc.dictRemoveAllWithVal(
      alarm,
      Remove.val)
    )
    
    // Update dis prior to notification, possibly find a more efficient way
    alarm.each | tag, tagName | {
      if (tagName == "id")
        return
      
      if (tag is Ref)
      {
        newId := this.lib.rt.db.internRef(tag)
        ((Ref) tag).disVal = newId.dis
      }
    }
        
    currentState := AlarmState.getState(sourceRec?.get(AlarmPointTags.inAlarm.toStr))
    
    if (transition != null && !transition.enable)
    {
      // If we're disabling a state, update the message in the history to be the message of the transition.
      if (transition.message != null)
      {
        alarm = Alarm.wrap(
          Etc.dictSet(alarm, "message", transition.message)
        )
      }
    }
    
    // Track in history
    this.insertHistoricalEntry(currentState?.name, alarm)
        
    // Route Alarm
    this.routeAlarm(alarm, sourceRec)
  }
  
  private Void commitAckComplete(AckCompleteMessage msg)
  {
    Alarm[] toUpdate := [,]
    Alarm[] toRemove := [,]
    
    try
    {
    // Get Source Records
    Ref:Dict sourceRecs := [:]

    sourceRecs.setList(
      this.lib.rt.db.readByIdsList(
        msg.futures.map | future | { future.alarm.sourceRef},
        false
      )
    ) | alarm | { alarm.id }
    
    Ref:Alarm alarms := [:]
    
    alarms.addList(
      ((Alarm[]) this.lib.db.get(
        this.lib.rt.context.create(User.alarmEngine),
        msg.futures.map |future| { future.alarm.id }).get)
    ) | item | { item.id  }
    
    msg.futures.each | future | 
    {
      ackResult := ((AlarmConnAckResult[]) future.get()).get(0)
      alarm := alarms.get(future.alarm.id)
      
      try
      {               
        if (ackResult.success)
        {
          // Successful       
          
          // Get current state from point value (is Point still in alarm state)
          //alarmState := AlarmState.getState(future.alarm.state)
          //sourceRec := sourceRecs.getChecked(future.alarm.sourceRef, false)
          //currentState := AlarmState.getState(sourceRec?.get(AlarmPointTags.inAlarm.toStr))
          
          // The future.alarm has all of the ack tags on it.
          alarm = this.removeTags(future.alarm, ["ackInProgress", "ackError"], true)
          
          // Persist Alarm
          if (alarm.active && alarm.state != AlarmState.NORMAL.name)
          {
            // If the record is still in this state, update it adding the ack tags and removing the progress tag
            toUpdate.add(alarm)
          }
          else
          {
            // Remove & Notify
            toRemove.add(alarm)
          }
        }
        else
        {
          // Failed
          toUpdate.add(
            removeTags(
              Alarm.wrap(
                Etc.dictSet(
                  alarm,
                  "ackError",
                  ackResult.failureReason
                )
              ),
              [
                "acked",
                "ackedBy",
                "ackMessage",
                "ackInProgress"
              ]
            )
          )
        }
      }
      catch (Err error)
      {
        this.lib.log.err("Error completing connector acknowledgement", error)
        
        toUpdate.add(
          removeTags(
            Alarm.wrap(
              Etc.dictSet(
                alarm,
                "ackError",
                error.msg
              )
            ),
            [
              "acked",
              "ackedBy",
              "ackMessage",
              "ackInProgress"
            ]
          )
        )
      }
    }
    
    // Remove stale alarms (We only remove acknowledged alarms that are not active)
    if (toRemove.size > 0)
    {
      this.db.remove(toRemove.map |alarm| { alarm.id })
                  
      toRemove.each | alarm | {
        sourceRec := sourceRecs.getChecked(alarm.sourceRef, false)
        this.notifySubsystems(alarm, sourceRec)
      }
      
      this.lib.alarmsObs.notify(toRemove)
    }
    
    if (toUpdate.size > 0)
    {
      this.db.createOrUpdate(toUpdate)
                  
      toUpdate.each | alarm | {
        sourceRec := sourceRecs.getChecked(alarm.sourceRef, false)
        this.notifySubsystems(alarm, sourceRec)
      }   
      
      // Notify Committed Alarms
      this.lib.alarmsObs.notify(toUpdate)
      
    }
    }
    catch (Err error)
    {
      this.lib.log.err(" ----------- ACK ERR: $error", error)
    }
  }
  
  private Alarm removeTags(Alarm alarm, Str[] tagNames, Bool force := false)
  {
    alarmMap := Etc.dictToMap(alarm)
    
    tagNames.each | tagName | {
      if (alarmMap.containsKey(tagName) || force)
        alarmMap.set(tagName, Remove.val)
    }
    
    return Alarm.wrap(Etc.makeDict(alarmMap))
  }
  
  private Dict commitUpdate(Alarm updatedAlarm)
  {
    // Validate Update
    
    // Persist Alarm
    Dict alarm := this.db.createOrUpdate([updatedAlarm]).get
    return alarm
  }
  
  private Alarm configureAlarmRecord([Str:Obj] record, AlarmTransition transition)
  {
    record.set("alarm", Marker.val)
    record.set("created", DateTime.now)
    record.set("state", transition.state.name)
    record.set("priority", Number.makeInt(transition.priority))
    record.set("dispatcher", transition.dispatcher.toStr)
    record.set("occurred", transition.occurred)
    record.set("metadata", transition.meta ?: Remove.val)
    record.set("message", transition.message ?: Remove.val)
    
    if (transition.enable)
    {
      if (transition.acknowledgementRequired)
        record.set("ackRequired", Marker.val)
      
    }
    
    result := Alarm.wrap(Etc.makeDict(record))
    
    
    // Creating a new alarm.
    if (transition.enable)
    {
      if (record.containsKey("acked"))
      {
        result = this.removeTags(
          result,
          [
            "acked",
            "ackedBy",
            "ackMessage",
            "ackInProgress",
            "ackError"
          ]
        )
      }
      else
      if (record.containsKey("ackInProgress"))
      {
        result = this.removeTags(
          result,
          [
            "ackInProgress",
            "ackError"
          ]
        )
        
      }
    }
    
    return result
  }
  
  private Alarm generateAlarm(AlarmState state, Str message, Ref? sourceRef := null, Dict? fromAlarm := null)
  {
    alarmMap := [:]
    
    alarmMap.set("alarm", Marker.val)
    alarmMap.set("created", DateTime.now)
    alarmMap.set("state", state.name)
    alarmMap.set("priority", Number.makeInt(state.priority))
    alarmMap.set("occurred", DateTime.now)
    alarmMap.set("message", message)
    
    if (sourceRef != null)
      alarmMap.set("sourceRef", sourceRef)
    
    if (state.acknowledgementRequired)
      alarmMap.set("ackRequired", Marker.val)
    
    if (fromAlarm != null)
      alarmMap.set("fromState", fromAlarm.get("state"))
    
    return Alarm.wrap(Etc.makeDict(alarmMap))
  }
  
  private Void routeAlarm(Alarm alarmRec, Dict alarmSource)
  {
    try
    {
      this.lib.router.route(
        alarmRec,
        alarmSource
      )
    }
    catch (Err err)
    {
      this.lib.log.err("Error routing alarm", err)
    }
  }
  
  private Void insertHistoricalEntry(Str? fromState, Alarm alarm)
  {
    if (fromState == null)
      fromState = AlarmState.NORMAL.name
    
    try
    {
      this.history.insert(
        [
          AlarmHistoryEntry.fromAlarm(
            fromState,
            alarm
          )
        ]
      )
    }
    catch (Err err)
    {
      this.lib.log.err("Error inserting alarm history entry", err)
    }
  }
  
  private Bool onlyHasNormal(AlarmStateModel[] stack)
  {
    return stack.all |item| { item.state == AlarmState.NORMAL.name }
  }
  
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using folio
using concurrent
using haystack
using skyarc
using hx

const class AlarmFolioDatabase : AlarmDatabase
{
  private const AlarmLib lib
  private const AtomicRef _db := AtomicRef()
  private const ActorPool pool := ActorPool()
  
  private const Actor actor
  
  new make(AlarmLib lib)
  {
    this.lib = lib
    
    this.actor = Actor.makeCoalescing(
      this.pool,
      |msg -> Obj?| {
        return this.msgToKey(msg)
      },
      |msgA, msgB -> Obj?| { 
        return this.mergeMessages(msgA, msgB)
      },
      |msg -> Obj?| {
        return this.receive(msg)
      }
    )
  }
  
  private Folio? db()
  {
    return this._db.val as Folio
  }
  
  private Obj? msgToKey(AlarmDBMessage msg)
  {
    return msg.key
  }
  
  private AlarmDBMessage mergeMessages(AlarmDBMessage msgA, AlarmDBMessage msgB)
  {
    if (msgA.op == AlarmDBOp.createUpdateAlarm) // TODO: Compare by time latest merges into previous
      return ((CreateOrUpdateMessage) msgA).merge(msgB)
    
    return msgA
  }
  
  private Obj? receive(AlarmDBMessage msg)
  {
    switch (msg.op)
    {
      case AlarmDBOp.createUpdateAlarm:
        return this.commitCreateOrUpdate(msg)
      
      case AlarmDBOp.readAlarms:
        return this.commitReadAlarms(msg)
      
      case AlarmDBOp.getAlarmByID:
        return this.commitGet(msg)
      
      case AlarmDBOp.getAlarmsByTarget:
        return this.commitGetTargetsAlarms(msg)
      
      case AlarmDBOp.getAlarmCount:
        return this.commitAlarmCount(msg)
      
      case AlarmDBOp.removeAlarms:
        return this.commitRemove(msg)
      
      case AlarmDBOp.poll:
        return this.commitPoll(msg)
      
      // Import and Create do the exact same thing
      // maybe enable import to clear before importing later
      case AlarmDBOp.import:
        return this.commitImport(msg)
    }
    
    return null
  }
  
  public override Future readAlarms(HxContext context, Ref[]? targets := null, DateSpan? span := null, DatabasePaginationRequest? pagination := null) // Future<Dict[]>
  {
    return this.actor.send(
      ReadAlarmsMessage(
        context,
        targets,
        span,
        pagination
      )
    )
  }
  
  public override Future get(HxContext context, Ref[] alarmIds)
  {
    return this.actor.send(
      AlarmsRequestMessage(context, alarmIds)
    )
  }
  
  public override Future getTargetsAlarms(HxContext context, Ref[] targetIds)
  {
    return this.actor.send(
      AlarmsRequestMessage(
        context,
        targetIds,
        AlarmsRequestOp.getTarget
      )
    )
  }
  
  public override Future createOrUpdate(Alarm[] alarms, Bool coalesce := true)
  {
    return this.actor.send(
      CreateOrUpdateMessage(alarms, coalesce)
    )
  }
  
  public override Future alarmCount(Ref[]? targets := null)
  {
    return this.actor.send(
      AlarmsRequestMessage(null, targets, AlarmsRequestOp.getCount)
    )
  }
  
  public override Future remove(Ref[] alarmIds)
  {
    return this.actor.send(
      AlarmsRequestMessage(null, alarmIds, AlarmsRequestOp.remove)
    )
  }
  
  public override Future poll(HxContext context, Int lastPollTime, Ref[]? targets := null)
  {
    return this.actor.send(
      PollAlarmsMessage(
        context,
        lastPollTime,
        targets
      )
    )
  }
  
  public override Future import(Alarm[] records)
  {
    return this.actor.send(
      ImportAlarmsMessage(records)
    )
  }
  
  public override Future runHouseKeeping()
  {
    return Future.makeCompletable.complete(null)
  }
  
  private Alarm[] commitImport(ImportAlarmsMessage msg)
  {
    diffs := msg.records.map | alarm |
    {
      alarmId := alarm.id

      alarm = Alarm.wrap(
        Etc.dictFindAll(
          Etc.dictRemove(alarm, "id")
        ) |value, tagName| {
          
          // curVal and curStatus are in the list of uncommittable tags.
          if (tagName == "curVal" || tagName == "curStatus")
            return true
          
          // Strip uncommittable tags, FolioUtil#stripUncommittables is unsupported by FolioFlatFile
          return !FolioUtil.isUncommittable(tagName) 
        }
      )
      
      existing := this.db.readById(alarmId, false)      
      
      // Using Makex/MakeAll because I need to bypass Diff checks for transient only tags
      // In alarming curVal/curStatus are persistent but the Diff checks fail.
      return (existing == null)
        ? Diff.makex(
          alarmId,
          DateTime.now,
          alarm,
          Diff.add)
        : Diff.makeAll(
          alarmId,
          existing.get("mod"),
          existing,
          null,
          null,
          alarm,
          0)
    }
    
    alarms := this.db.commitAll(diffs).map | diff | {  
      diff.newRec
    }

    return this.processAlarmResult(alarms)
  }
    
  private AlarmResult commitReadAlarms(ReadAlarmsMessage msg)
  {
    builder := StrBuf()
    
    // FolioFlatFile is not indexed, using a complex filter is extremely slow
    // filtering a list is much faster.
    alarms := this.db.readAllList(Filter.fromStr("id")).rw
    
    // Filter by access
    alarms = this.filterAlarmsByAccess(msg.context, alarms)
    
    
    Str:Obj metadata := [:]
    
    
    if (msg.span != null)
    {
      alarms = alarms.findAll | alarm | {
        occurred := (DateTime) alarm.get("occurred")
        
        return msg.span.contains(occurred.date)
      }
    }
    
    if (msg.targets != null)
    {
      alarms = alarms.findAll | alarm | {
        msg.targets.contains(alarm.get("sourceRef"))
      }
    }

    metadata.set("totalRecordCount", Number.makeInt(alarms.size))
    metadata.set("resultCount", Number.makeInt(alarms.size))
    
    if (msg.pagination != null)
    {
      // Filter alarm records
      if (msg.pagination.filter != null)
      {
        context := this.lib.rt.context.create(User.alarmEngine)
        filter := Filter.fromStr(msg.pagination.filter)
        alarms = alarms.findAll |rec| { filter.matches(rec, context) }
        metadata.set("totalRecordCount", Number.makeInt(alarms.size))
        metadata.set("resultCount", Number.makeInt(alarms.size))
      }
      
      
      totalPages :=
        (msg.pagination.resultSize > 0)
          ? Number.make(
            alarms.size.toFloat.divInt(
              msg.pagination.resultSize
            ).ceil
          )
          : 1
      
      currentPage := Number.makeInt(msg.pagination.currentPage)
      
      metadata.set("totalPageCount", totalPages)
      metadata.set("currentPage", currentPage)
      
      if (msg.pagination.entityOffset >= alarms.size)
      {
        metadata.set("resultCount", Number.makeInt(0))
        return AlarmResult([,], Etc.makeDict(metadata))
      }
      
      alarms.sort |Dict alarmA, Dict alarmB -> Int|
      {
        try
        {
          aValue := alarmA.get(msg.pagination.sortColumnName)
          bValue := alarmB.get(msg.pagination.sortColumnName)
          
          if (aValue == bValue)
            return 0
          
          if (aValue == null)
            return 1
          
          if (bValue == null)
            return -1
          
          if (aValue is Ref)
            aValue = ((Ref) aValue).dis
          
          if (bValue is Ref)
            bValue = ((Ref) bValue).dis

          if (msg.pagination.sortDirection.lower == "desc")
            return bValue.compare(aValue)
                
          return aValue.compare(bValue)
          
        }
        catch (Err error)
        {
          echo ("Error sorting: $error")
        }
        
        return 0
      }
      
      if (msg.pagination.resultSize > 0)
      {
        endIndex := msg.pagination.entityOffset
                .max(0)
                .plus(msg.pagination.resultSize)
                .minus(1)
                .min(alarms.size - 1)
        
        alarms = alarms.getRange(Range.makeInclusive(msg.pagination.entityOffset, endIndex))
      }
      
      metadata.set("resultCount", Number.makeInt(alarms.size))
      
    }   
    
    // Add dis to refs
    alarms.each |rec| {  
      rec.each | tag, tagName | {
        if (tagName == "id")
          return
        
        if (tag is Ref)
        {
          newId := this.lib.rt.db.internRef(tag)
          ((Ref) tag).disVal = newId.dis
        }
      }
    }
        
    return AlarmResult(
      this.processAlarmResult(alarms),
      Etc.makeDict(metadata)
    )
  }
  
  private Alarm[] commitGet(AlarmsRequestMessage msg)
  {
    alarms := this.db.readByIdsList(msg.itemIds, false)
    
    // Filter by access
    alarms = this.filterAlarmsByAccess(msg.context, alarms.findNotNull)
      
    return this.processAlarmResult(alarms.findNotNull)
  }
  
  private Alarm[] commitGetTargetsAlarms(AlarmsRequestMessage msg)
  {
    filters := msg.itemIds.map | id | { "sourceRef == $id.toProjRel.toCode" }
    
    alarms := this.db.readAllList(
      Filter.fromStr(filters.join(" or "))
    )
    
    // Filter by access
    alarms = this.filterAlarmsByAccess(msg.context, alarms)

    return this.processAlarmResult(alarms)
  }
    
  private Alarm[] commitCreateOrUpdate(CreateOrUpdateMessage msg)
  {
    diffs := msg.alarms.map | alarm | {
      alarmId := alarm.id

      alarm = Alarm.wrap(
        Etc.dictFindAll(
          Etc.dictRemove(alarm, "id")
        ) |value, tagName| {
          
          // curVal and curStatus are in the list of uncommittable tags.
          if (tagName == "curVal" || tagName == "curStatus")
            return true
          
          // Strip uncommittable tags, FolioUtil#stripUncommittables is unsupported by FolioFlatFile
          return !FolioUtil.isUncommittable(tagName) 
        }
      )
      
      existing := this.db.readById(alarmId, false)      
      
      // Using Makex/MakeAll because I need to bypass Diff checks for transient only tags
      // In alarming curVal/curStatus are persistent but the Diff checks fail.
      return (existing == null)
        ? Diff.makex(
          alarmId,
          DateTime.now,
          alarm,
          Diff.add)
        : Diff.makeAll(
          alarmId,
          existing.get("mod"),
          existing,
          null,
          null,
          alarm,
          0)
    }
    
    alarms := this.db.commitAll(diffs).map | diff | {  
      diff.newRec
    }

    return this.processAlarmResult(alarms)
  }
  

  private Int commitAlarmCount(AlarmsRequestMessage msg)
  {
    filters := msg.itemIds.map | ref | { "sourceRef == $ref.toProjRel.toCode" }
    
    filter := (filters.size < 1)
      ? Filter.fromStr("id")
      : Filter.fromStr(filters.join(" or "))
    
    return this.db.readCount(filter)
  }
  
    
  private Alarm[] commitRemove(AlarmsRequestMessage msg)
  {
    toRemove := this.db.readByIdsList(msg.itemIds.unique)
    
    try
    {
      result := db.commitAll(
        toRemove.map | alarm | {  
          Diff.make(alarm, null, Diff.remove.or(Diff.force))
        }
      )
    }
    catch (Err error)
    {
      this.lib.log.err("Error Acknowledging alarms", error)
    }
            
    return toRemove
  }
  
  
  private AlarmResult commitPoll(PollAlarmsMessage msg)
  {
    lastMod := DateTime.fromJava(msg.lastPollTime)

    Alarm[] alarms := this.db.readAllList(
      Filter.fromStr("mod")
    ).map | rec |{ Alarm.wrap(rec) }
    
    // Filter by access
    alarms = this.filterAlarmsByAccess(msg.context, alarms)
    
    alarms = alarms.findAll | alarm |
    {
      modified := (DateTime) alarm.get("mod")
      return modified.compare(lastMod) > 0
    }
    
    if (msg.targets != null)
    {
      relativeTargets := msg.targets.map |target| { target.toProjRel }
      
      alarms = alarms.findAll | alarm |
      {
        relativeTargets.contains(alarm.sourceRef)
      }
    }
    
    // Add dis to refs
    alarms.each |rec| {  
      rec.each | tag, tagName | {
        if (tagName == "id")
          return
        
        if (tag is Ref)
        {
          newId := this.lib.rt.db.internRef(tag)
          ((Ref) tag).disVal = newId.dis
        }
      }
    }
    
    return AlarmResult(
      this.processAlarmResult(alarms)
    )
  }
  
  private Dict[] filterAlarmsByAccess(HxContext context, Dict[] alarms)
  {
    if (context.user.isSu || context.user.isAdmin)
      return alarms
    
    // Filter by access
    return alarms.findAll |alarm|
    {
      // Alarm doesn't have a source
      if (!alarm.has("sourceRef"))
        return true
      
      source := context.db.readById(alarm.get("sourceRef"), false)
      
      // Alarm source doesn't exist in this database
      if (source == null)
        return true
      
      return context.canRead(source)
    }
  }
  
  public override Future start()
  {
    this._db.getAndSet(FolioFlatFile.open(
      FolioConfig {
        it.name = "Alarm Status Database"
        it.dir = lib.rt.dir.plus(`alarms/status/`)
        it.pool = this.lib.pool
      }
    ))
  
    return Future.makeCompletable.complete(true)
  }
  
  public override Future stop()
  {
    return this.db.closeAsync
  }
  
  private Alarm[] processAlarmResult(Dict[]? alarms)
  {   
    if (alarms == null)
      return alarms

    return alarms.map | alarm |
    {
      // Add id dis to alarms
      alarm.id.disVal = ((Str?) alarm.get("state"))?.toDisplayName        
      return Alarm.wrap(alarm)
    }
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using axon
using hx

class AlarmFuncs 
{
  // This adapter method is for backward compatibility with
  // older code that utilizes alarms
  **
  ** Given a target and either a legacy namespace or a new-style alarm state, transition the given namespace/target to the alarm state.
  **
  ** Parameters:
  ** - targets (required) - list of targets to transition
  ** - state (required) - to transition targets to (offNormal, normal, fault, ...)
  ** - message (optional) - the message for the alarm transition
  ** - occurred (optional) - the time the alarm occurred. If this is omitted, the system will use the current time
  ** - priority (optional) - priority of the alarm. if this is omitted, the system will use this priority configured for the state.
  ** - metadata (optional) - this is a miscellaneous dictionary of tags that will follow the lifecycle of the alarm. 
  ** For compatibility, if "ackRequired" is set on the metadata, it will take precedence over the state's configured ackRequired flag.
  **
  ** This is an adapter kept for backward compatibility.
  ** - If `nsOrState` contains `":"`, it is treated as a legacy namespace
  **
  ** Return:
  ** - Legacy path returns a single `Alarm`.
  ** - New path returns `Alarm[]`.
  **
  ** Examples:
  **   toAlarm(@equipId, "highTemp", "Over temperature")
  **   toAlarm(@equipId, "legacy:ns", {message:"Over temperature"})
  **
  @Axon
  public static Obj toAlarm(
    Obj target,
    Str nsOrState,
    Obj? messageOrTags := null,
    DateTime? occurred := null,
    Obj? priority := null,
    Dict? meta := null)
  {
    // Namespaces have special characters in them
    // if the string is not all alpha chars then we assume its a namespace
    // therefore its a call to the legacy API
    if (nsOrState.contains(":") || messageOrTags is Dict)
      return toAlarmOld(target, nsOrState, messageOrTags)
    
    return toAlarmNew(
      target,
      nsOrState,
      messageOrTags,
      occurred,
      priority,
      meta)
  }
  
  
  public static Alarm toAlarmOld(Obj target, Str ns, Dict tags := Etc.emptyDict)
  {
    return toAlarmNew(
      target,
      AlarmState.OFF_NORMAL.name,
      tags.get("message"),
      null,
      null,
      tags).getSafe(0)
  }
  
  public static Alarm[] toAlarmNew(
    Obj targets,
    Str state,
    Str? message := null,
    DateTime? occurred := null,
    Obj? priority := null,
    Dict? meta := null)
  {
    cx := HxContext.curHx
    AlarmLib lib := cx.rt.lib("alarm")
    
    // TODO: Security Check
    
    if (targets is Expr)
      targets = ((Expr) targets).eval(cx)
    
    alarms := lib.toAlarm(targets, state, true, AlarmDispatcher.INTERNAL, message, occurred, priority, meta)
  
    return alarms
  }
  
  // This adapter method is for backward compatibility with
  // older code that utilizes alarms
  **
  ** Given targets, transition alarms back to normal state using either the legacy API
  ** (namespace-based) or the new API (message/occurred/meta).
  **
  ** Parameters:
  ** - targets (required) - list of targets to transition to normal
  ** - message (optional) - the message for the alarm transition
  ** - occurred (optional) - the time the alarm occurred. If this is omitted, the system will use the current time
  ** - metadata (optional) - this is a miscellaneous dictionary of tags that will follow the lifecycle of the alarm.
  ** For compatibility, if "ackRequired" is set on the metadata, it will take precedence over the state's configured ackRequired flag.
  **
  ** This is an adapter kept for backward compatibility:
  ** - If `nsOrMessage` contains `":"`, it is treated as a legacy namespace
  **
  ** Return:
  ** - Legacy path returns a single `Alarm?`.
  ** - New path returns `Alarm[]`.
  **
  ** Examples:
  **   toNormal(@equipId)
  **   toNormal(@equipId, "Cleared by operator")
  **   toNormal(@equipId, "legacy:ns", {message:"Cleared"})
  **
  @Axon
  public static Obj? toNormal(
    Obj targets,
    Str? nsOrMessage := null,
    Obj? occurredOrTags := null,
    Dict? meta := null)
  {
    // Namespaces have special characters in them
    // if the string is not all alpha chars then we assume its a namespace
    // therefore its a call to the legacy API
    if (nsOrMessage != null && nsOrMessage.contains(":"))
      return toNormalOld(targets, nsOrMessage, occurredOrTags)
    
    return toNormalNew(
      targets,
      nsOrMessage,
      occurredOrTags,
      meta
    )
  }
  
  public static Alarm? toNormalOld(Obj targets, Str ns, Dict tags := Etc.emptyDict)
  {
    return toNormalNew(
      targets,
      tags.get("message"),
      null,
      tags
    ).getSafe(0)
  }
  
  public static Alarm[] transitionAlarm(
    Obj targets,
    Str state,
    Bool enable,
    Str? message := null,
    DateTime? occurred := null,
    Obj? priority := null,
    Dict? meta := null)
  {
    cx := HxContext.curHx
    AlarmLib lib := cx.rt.lib("alarm")
    
    // TODO: Security Check
    
    if (targets is Expr)
      targets = ((Expr) targets).eval(cx)
    
    alarms := lib.toAlarm(
      targets,
      state,
      enable,
      AlarmDispatcher.INTERNAL,
      message,
      occurred,
      priority,
      meta
    )
  
    return alarms
  }
  
  public static Alarm[] toNormalNew(
    Obj targets,
    Str? message := null,
    DateTime? occurred := null,
    Dict? meta := null)
  {
    return toAlarmNew(
      targets,
      AlarmState.NORMAL.name,
      message,
      occurred,
      null,
      meta
    )
  }
  
  **
  ** Given a set of alarms (or alarm ids), acknowledge them and return the updated alarm records.
  **
  ** Parameters:
  ** - alarms (required) - Alarm(s) to acknowledge in any of the following formats: (Grid, Dict[], Ref[], Dict, Str, Ref)
  ** - message (optional) - acknowledgement message
  **
  ** Examples:
  **   alarmAck(alarms())
  **   alarmAck(@alarmId, "Investigating")
  **   alarmAck("r:demo:alarm123", "Cleared by operator")
  **   alarmAck(readAll(alarm).keepCols(["id"]))
  **
  @Axon
  public static Alarm[] alarmAck(Obj alarms, Str? message := null)
  {
    cx := HxContext.curHx
    
    // TODO: Security Check
    
    Ref[] alarmIds := [,]
    
    if (alarms is Grid)
      alarmIds.addAll(((Grid) alarms).toRows.map |row| { row.id })
    else
    if (alarms is Dict)
      alarmIds.add(((Dict) alarms).id)
    else
    if (alarms is Str)
      alarmIds.add(Ref((Str) alarms))
    else
    if (alarms is Ref)
      alarmIds.add(alarms)
    else
    if (alarms is List)
      alarmIds = alarms
    
    AlarmLib lib := cx.rt.lib("alarm")
    
    Alarm[] results := lib.engine.acknowledge(
      AlarmAcknowledgement(
        cx,
        alarmIds,
        DateTime.now,
        cx.user.dis,
        message
      )
    ).get
    
    return results
  }
  
  **
  ** Given a root object, return all alarm target records associated with it.
  **
  ** Parameters:
  ** - root (required) - the root object to find alarm targets for  
  **
  @Axon
  public static Dict[] alarmTargets(Obj root)
  {
    cx := HxContext.curHx
    
    if (root is Expr)
      root = ((Expr) root).eval(cx)
    
    if (root is Ref)
      root = HxCoreFuncs.toRec(root)
    
    typeInfo := cx.rt.ns.reflect(root)
    typeDef := typeInfo.entityTypes.getSafe(0, null)
    recId := ((Dict) root).id
    
    if (typeDef == null)
      return [,]

    Def[] defs := cx.ns.implement(typeDef)
    
    filterStr := defs.reduce("") |Str result, def, index->Str| 
    {
      result += " or ${def.name}Ref == ${recId.toCode}"
      return result
    }
    
    // Change to "containedBy? @target" when supported
    return cx.db.readAllList(
      Filter.fromStr(
        "id == ${recId.toCode} ${filterStr}"
      )
    )
  }

  **
  ** Given optional targets and/or a time span, return the current alarm records as a grid.
  **
  ** Parameters:
  ** - targets (optional) - list of targets to query alarms for. If this parameter is null it will query everything
  ** - span (optional) - DateSpan used to filter alarms. If this is null it will return alarms for all dates
  ** - filter (optional) - filter used to filter alarm result
  ** - pollHandle: when provided, performs an incremental poll and returns only changes since the
  **   given handle (legacy compatibility). The returned grid includes a new `pollHandle` you can
  **   pass back on the next call.
  **
  ** Return:
  ** - A Grid of alarm records. The grid meta includes:
  ** - pollHandle: an integer handle representing the time of this query.
  **
  ** Examples:
  **   alarms()
  **   alarms(@someEquipId)
  **   alarms(readAll(equip and siteRef==@siteId))
  **   alarms(null, today())
  **   alarms(null, null, null, 123456)        // poll using previous handle
  **
  @Axon
  public static Grid alarms(
    Obj? targets := null,
    Expr span := Literal.nullVal,
    Expr filter := Literal.nullVal,
    Expr pollHandle := Literal.nullVal) // filter and poll handle to support legacy code
  {
    cx := HxContext.curHx
    AlarmLib lib := cx.rt.lib("alarm")
    
    if (targets is Expr)
      targets = ((Expr) targets).eval(cx)
    
    if (targets is Grid && ((Grid) targets).size == 1)
    {
      // Stack mobile uses "alarmTargets" nav filter which sometimes returns a single
      // row with @null id
      firstItem := ((Grid) targets).get(0)
      
      if (firstItem.id == Ref.nullRef)
        targets = null
    }
    
    // Not null or empty string
    ids := (targets != null && targets != "")
      ? Etc.toRecs(targets, cx).map |rec| { rec.id  }
      : null
    
    range := (span.isNull)
      ? null
      : span.eval(cx)
    
    if (range != null && !(range is DateSpan))
      range = Etc.toDateSpan(range, cx)
    
    AlarmResult? result
    
    if (!pollHandle.isNull)
    {
      result = lib.db.poll(cx, pollHandle.eval(cx).toStr.toInt, ids).get
    }
    else
    {
      result = lib.db.readAlarms(
        cx,
        ids,
        range
      ).get
    }
    
    // TODO: Implement alarm filter
    
    return result.toGrid(
      Etc.makeDict([
        "pollHandle": Number.makeInt(
          DateTime.now.toJava
        )
      ])
    )
  }
  
  **
  ** Given a set of alarms (or alarm ids), remove them from the database and return the removed alarm records.
  **
  ** Parameters:
  ** - alarms (required) - Alarm(s) to remove in any of the following formats: (Grid, Dict[], Ref[], Dict, Str, Ref)
  **
  ** Examples:
  **   alarmsRemove(alarms())
  **   alarmsRemove(@alarmId)
  **   alarmsRemove("r:demo:alarm123")
  **   alarmsRemove(readAll(alarm).keepCols(["id"]))
  **
  @Axon{admin = true}
  public static Alarm[] alarmsRemove(Obj alarms)
  {
    if (alarms is Dict[])
      alarms = ((Dict[]) alarms).map |Dict item| { item.id }
    
    if (alarms is Grid)
      alarms = ((Grid) alarms).toRows.map | item | { item.id }
    
    if (alarms is Dict)
      alarms = [((Dict) alarms).id]
    
    cx := HxContext.curHx
    
    AlarmLib lib := cx.rt.lib("alarm")
    
    return lib.db.remove(alarms).get
  }

  **
  ** Given an alarm id, return the corresponding alarm record.
  **
  ** Parameters:
  ** - alarmId: Ref of the alarm to retrieve
  **
  ** Examples:
  **   alarmsGetById(@alarmId)
  **
  @Axon
  public static Alarm? alarmsGetById(Ref alarmId)
  {
    cx := HxContext.curHx
    
    AlarmLib lib := cx.rt.lib("alarm")
    
    alarm := ((Alarm[]) lib.db.get(cx, [alarmId]).get).getSafe(0)

    return alarm
  }

  **
  ** Given optional targets and/or a time span, query the alarm history as a grid.
  **
  ** Parameters:
  ** - targets (optional) - List/Grid of targets or target ids to query database for. If omitted this will query the database for all entities
  ** - span (optional) - DateSpan to used to query the history database. If omitted this will return records for all dates in the database
  **
  @Axon
  public static Grid readAlarmHistory(
    Expr targets := Literal.nullVal,
    Expr span := Literal.nullVal)
  {
    cx := HxContext.curHx
    
    
    ids := (!targets.isNull)
      ? Etc.toRecs(targets.eval(cx), cx).map |rec| { rec.id  }
      : null
    
    
    range := (span.isNull)
      ? null
      : span.eval(cx)
    
    if (range != null && !(range is DateSpan))
      range = Etc.toDateSpan(range, cx)
        
    AlarmLib lib := cx.rt.lib("alarm")
    AlarmHistoryResult result := lib.history.read(cx, range, ids).get()
    
    return result.toGrid
  }

  **
  ** Clear alarm history records before the given date in the database.
  **
  ** Parameters:
  ** - date (required) - Deletes all alarm history entries before the specified date.
  **
  @Axon{admin = true}
  public static Bool clearAlarmHistoryBefore(Obj date)
  {
    cx := HxContext.curHx
    
    if (date is Expr)
      date = ((Expr) date).eval(cx)
    
    if (date is Date)
      date = ((Date) date).toDateTime(Time.make(0, 0))
    
    if (date is DateTime)
    {
      AlarmLib lib := cx.rt.lib("alarm")
      lib.history.removeBefore(
        date
      ).get
      
      return true
    }
    
    throw Err("Date must resolve to a Date or DateTime")
    
    return false
  }

  ** Clear alarm history records in the database based on the given parameters.
  **
  ** Parameters:
  ** - spanOrBefore (optional) - DateSpan of entries to clear
  ** - rebuild (optional) - if spanOrBefore is null and this is true, it will destroy the history database and recreate its schema.
  **
  @Axon{admin = true}
  public static Bool clearAlarmHistory(Obj? spanOrBefore := null, Bool? rebuild := false)
  {
    cx := HxContext.curHx
    AlarmLib lib := cx.rt.lib("alarm")
    
    if (spanOrBefore == null)
      lib.history.clear(rebuild)
    else
    if (spanOrBefore is Expr)
    {
      span := Etc.toDateSpan(spanOrBefore, cx)
      lib.history.removeBetween(span)
    }
    else
    if (spanOrBefore is DateSpan)
      lib.history.removeBetween(spanOrBefore)
    else
    if (spanOrBefore is DateTime)
      lib.history.removeBetween(
        Etc.toDateSpan(
          ((DateTime)spanOrBefore).date
        )
      )
    else
    if (spanOrBefore is Date)
      lib.history.removeBetween(
        Etc.toDateSpan(
          spanOrBefore
        )
      )
    else
      return false
    
    return true
  }

  **
  ** Given an optional target, return the count of alarm history records.
  **
  ** Parameters:
  ** - target (optional) - this is the rec or id of the target. If omitted, it will count all records in the database.
  **
  @Axon
  public static Number alarmHistoryCount(Obj? target := null)
  {
    cx := HxContext.curHx
    
    id := (target != null)
      ? Etc.toRec(target, cx).id
      : null
    
    AlarmLib lib := cx.rt.lib("alarm")
    return lib.history.historyCount(id).get
  }
  
  **
  ** Given a grid of target records with id, enrich it with rolled-up alarm counts per row.
  **
  ** Parameters:
  ** - grid (required) - grid of target records with id
  **
  @Axon
  public static Grid alarmNavCounts(Grid grid)
  {
    return AlarmTargetCounter(
      HxContext.curHx,
      grid.mapToList |row| { row.id  })
        .count(grid)
  }
  
  **
  ** Generate an alarm digest grouped by a given parent type.
  **
  **  Parameters:
  ** - groupBy (optional) - the parent type to group by. Default is "site".
  ** - includeUnknown (optional) - if true, includes alarms with unknown sources in
  @Axon
  public static Grid alarmDigest(Str? groupBy := "site", Bool includeUnknown := false)
  {
    context := HxContext.curHx
    
    // Orphan parent dict in the case the parent doesn't exist
    orphanParent := Etc.makeDict([
      "id": Ref.makeWithDis(Ref("unknown"), "Unknown Source")
    ])
    
    // Query alarms
    currentAlarms := AlarmFuncs.alarms
    
    // Group Alarms
    Dict:Row[] groups := currentAlarms.toRows
      .groupBy | row | 
      {
        alarmSource := row.get("sourceRef")
        
        parentRef := (alarmSource != null)
          ? context.db.readById(alarmSource)?.get("${groupBy}Ref")
          : null
        
        parentItem := (parentRef != null)
          ? context.db.readById(parentRef)
          : null
        
        return parentItem ?: orphanParent
      }
    
    if (!includeUnknown)
    {
      // Remove unknown
      unknownGroup := groups.keys.find | parent | 
      {
        parent == orphanParent
      }
      
      if (unknownGroup != null)
        groups.remove(unknownGroup)
    }
    
    // Reduce groups to a list of parents with an "alarms" tag that contains the alarms
    Dict[] rows := groups.reduce([,]) | Dict[] result, alarms, parent| 
    {
      return result.add(
        Etc.dictSet(parent, "alarms", alarms)
      )
    }
    
    return Etc.makeDictsGrid(["digest": Marker.val], rows)
  }
  
  **
  ** Given a Haystack query string, return the alarm history records as a grid.
  **  
  ** Parameters:
  ** - query (required) - Haystack query string to filter alarm history
  **
  @Axon
  public static Grid alarmHistoryQuery(Str query)
  {
    context := HxContext.curHx
    
    AlarmLib lib := context.rt.lib("alarm")
    return Etc.makeDictsGrid(null, lib.history.query(query).get)
    
  }
  
  public static Str toKey(Ref source, Str state, DateTime? time := null)
  {
    // There should be an alarm record for each state per source.
    // eg. Going back into an unacknowledged High Limit, overwrites that high limit. 
    return "${source.toProjRel.id}_${state}"
  }
  
  
  /* Have not seen this used yet, if so port.
  @Axon
  public static Grid alarmRollup(Obj? groups, Obj? dates)
  {
    
  }
  */
  
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using concurrent

const class AlarmFuture : Future
{
  const Future parent
  const Alarm alarm
  
  new make (Future parent, Alarm alarm)
  {
    this.parent = parent
    this.alarm = alarm
  }
  
  override Obj? get(Duration? timeout := null)
  {
    return this.parent.get(timeout)
  }
  
  override FutureStatus status()
  {
    return this.parent.status()
  }
  
  override Void cancel()
  {
    this.parent.cancel
  }
  
  override This complete(Obj? val) 
  {
    // Do nothing, this is not complete-able
    return this
  }
  
  override This completeErr(Err err)
  {
    // Do nothing, this is not complete-able
    return this 
  }
  
  override Future? wraps()
  {
    return this.parent.wraps
  }
  
  override This waitFor(Duration? timeout := null)
  {
    this.parent.waitFor(timeout)
    return this
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using hx

enum class AlarmHistoryDBOp {
  insert,
  read,
  count,
  removeBetween,
  removeBefore,
  clear,
  
  houseKeeping,
  
  start,
  stop,
  
  query,
  import
}

const class AlarmHistoryDBMessage
{
  const Unsafe _context
  const AlarmHistoryDBOp op
  
  new make(AlarmHistoryDBOp op, HxContext? context := null)
  {
    this.op = op
    this._context = Unsafe(context)
  }
  
  public HxContext? context()
  {
    return this._context.val
  }
  
  public virtual Obj? key()
  {
    return null
  }
  
  public virtual AlarmHistoryDBMessage merge(AlarmHistoryDBMessage msgB)
  {
    return this
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using concurrent
using axon
using hx

const mixin AlarmHistoryDatabase 
{
  public abstract Future insert(AlarmHistoryEntry[] entries) // Future<AlarmHistoryEntry[]>
  public abstract Future read(HxContext context, DateSpan? range, Ref[]? targets, DatabasePaginationRequest? pagination := null) // Future<AlarmHistoryResult>
  public abstract SourceStream? readStream(HxContext context, DateSpan? range, Ref[]? targets, DatabasePaginationRequest? pagination := null) // SourceStream<AlarmHistoryEntry>
  public abstract Future historyCount(Ref? target := null) // Future<Int>
  public abstract Future removeBetween(DateSpan range)
  public abstract Future removeBefore(DateTime date)
  public abstract Future clear(Bool? rebuild := false)
  
  public abstract Future start()
  public abstract Future stop()
  
  public abstract Future runHouseKeeping()
  
  public abstract Future import(AlarmHistoryEntry[] data)
  
  public abstract Future query(Str query)
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack

const class AlarmHistoryEntry : TypedDict
{ 
  @TypedTag
  const Str? toState
  
  @TypedTag
  const Bool active
  
  @TypedTag
  const Number priority
  
  @TypedTag
  const Str? fromState
  
  @TypedTag
  const DateTime occurred
  
  @TypedTag
  const DateTime created
    
  @TypedTag
  const Ref? sourceRef
  
  @TypedTag
  const Obj? curVal
  
  @TypedTag
  const Str? curStatus
  
  @TypedTag
  const Str? message
  
  @TypedTag
  const DateTime? acked
  
  @TypedTag
  const Str? ackedBy
    
  @TypedTag
  const Str? ackMessage
  
  @TypedTag
  const Dict? metadata
  
  new make (Dict rec, | This | f) : super(rec)
  {
    f(this)
  }
  
  static new wrap(Dict? dict)
  {
    if (dict == null)
      return null
    
    return create(AlarmHistoryEntry#, dict)
  }
  
  static new fromAlarm(Str? fromState, Alarm alarm)
  {
    his := [:]
    
    his.set("created", DateTime.now)
    
    if (fromState != null)
      his.set("fromState", fromState)
    
    his.set("toState", alarm.state)
    his.set("occurred", alarm.occurred)
    his.set("priority", alarm.priority)
    
    if (alarm.active)
      his.set("active", Marker.val)
    
    if (alarm.curVal != null)
      his.set("curVal", alarm.curVal)
    
    if (alarm.curStatus != null)
      his.set("curStatus", alarm.curStatus)
    
    if (alarm.sourceRef != null)
      his.set("sourceRef", alarm.sourceRef)
    
    if (alarm.message != null)
      his.set("message", alarm.message)
            
    if (alarm.acked != null)
    {
      his.set("acked", alarm.acked)
      his.set("fromState", alarm.state)
      
      // Set occurrence to acked time so that the entry shows up in the proper order.
      his.set("occurred", alarm.acked) 
      
      // Remove "toState" for acknowledgements
      his.remove("toState")
    }
    
    if (alarm.ackedBy != null)
      his.set("ackedBy", alarm.ackedBy)
    
    if (alarm.ackMessage != null)
      his.set("ackMessage", alarm.ackMessage)
    
    if (alarm.metadata != null)
      his.set("metadata", alarm.metadata)
    
    Str[] idParts := [,] 
    idParts.add(((DateTime) his.get("created")).toJava.toStr)
    idParts.add(alarm.sourceRef?.id ?: "soureceless")
    idParts.add(
      (alarm.acked == null)
        ? alarm.state
        : "${alarm.state}-acked"
    )
    
    his.set(
      "id",
      Ref(idParts.join("::"))
    )
    
    return create(AlarmHistoryEntry#, Etc.makeDict(his))
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using concurrent
using haystack
using skyarcd
using axon
using hx

using [java] io.questdb.cairo::CairoConfiguration
using [java] io.questdb.cairo::DefaultCairoConfiguration
using [java] io.questdb.cairo::CairoEngine
using [java] io.questdb.cairo::O3PartitionJob
using [java] io.questdb.cairo::TableUtils
using [java] io.questdb.cairo::TableWriterAPI
using [java] io.questdb.cairo.security::AllowAllSecurityContext
using [java] io.questdb.cairo.wal::ApplyWal2TableJob
using [java] io.questdb.cairo.wal::WalPurgeJob
using [java] io.questdb.cairo.sql::Record
using [java] io.questdb.griffin::SqlExecutionContext
using [java] io.questdb.griffin::SqlExecutionContextImpl

using [java] java.util.concurrent::TimeUnit

const class AlarmHistoryQuestDB : AlarmHistoryDatabase
{
  public static const Str TABLE_NAME := "alarmHistory"
  const Log log := Log.get("QuestHistoryDB")
  const ActorPool pool
  const Actor actor
  const AlarmLib lib
  
  const AtomicRef _config := AtomicRef()
  const AtomicRef _engine := AtomicRef()
  const AtomicRef _sqlContext := AtomicRef()
  const AtomicRef _utility := AtomicRef()
  const AtomicRef _walApplyJob := AtomicRef()
  const AtomicRef _walPurgeJob := AtomicRef()
  const AtomicRef _writer := AtomicRef()

  new make(AlarmLib lib)
  {
    this.lib = lib
                            
    this.pool = ActorPool()
    
    // Using coalescing to batch history inserts together 
    this.actor = Actor.makeCoalescing(
      pool,
      | AlarmHistoryDBMessage msg -> Obj? | { 
        return msg.key
      },
      | AlarmHistoryDBMessage msgA, AlarmHistoryDBMessage msgB -> Obj? | { 
        return msgA.merge(msgB)
      },
      | msg -> Obj? | { 
        return this.receive(msg)
      }
    )
  }
  
  public CairoConfiguration config()
  {
    return ((Unsafe) this._config.val).val
  }
  
  public CairoEngine engine()
  {
    return ((Unsafe) this._engine.val).val
  }
  
  public SqlExecutionContext? sqlContext()
  {
    return ((Unsafe?) this._sqlContext.val)?.val
  }
  
  private ApplyWal2TableJob? walApplyJob()
  {
    return ((Unsafe?) this._walApplyJob.val)?.val
  }
  
  private WalPurgeJob? walPurgeJob()
  {
    return ((Unsafe?) this._walPurgeJob.val)?.val
  }
  
  private TableWriterAPI? writer()
  {
    return ((Unsafe?) this._writer.val)?.val
  }
  
  private QuestDBUtility? utility()
  {
    return this._utility.val
  }
  
  protected Obj? receive(Obj? message)
  {
    AlarmHistoryDBMessage msg := message
    
    switch (msg.op)
    {
      case AlarmHistoryDBOp.start:
        this.commitStart
      
      case AlarmHistoryDBOp.stop:
        this.commitStop
      
      case AlarmHistoryDBOp.import:
        return this.commitImport(msg)
      
      case AlarmHistoryDBOp.insert:
        return this.commitInsert(msg)
      
      case AlarmHistoryDBOp.read:
        return this.commitRead(msg)
      
      case AlarmHistoryDBOp.query:
        return this.commitQuery(msg)
      
      case AlarmHistoryDBOp.count:
        return this.commitHistoryCount(msg)
      
      case AlarmHistoryDBOp.removeBefore:
        this.commitRemoveBefore(msg)
      
      case AlarmHistoryDBOp.removeBetween:
        this.commitRemoveBetween(msg)
      
      case AlarmHistoryDBOp.clear:
        this.commitClear(msg)
      
      case AlarmHistoryDBOp.houseKeeping:
        this.commitHouseKeeping
    }
    
    return null
  }
  
  public override Future insert(AlarmHistoryEntry[] records) // Future<AlarmHistoryEntry>
  {
    return this.actor.send(
      HistoryInsertMessage(records)
    )
  }
  
  public override Future query(Str query) // Future<AlarmHistoryEntry[]>
  { 
    return this.actor.send(
      HistoryQueryMessage(query)
    )
  }
  
  public override Future read(
    HxContext context,
    DateSpan? range,
    Ref[]? targets,
    DatabasePaginationRequest? pagination := null) // Future<AlarmHistoryEntry[]>
  {
    return this.actor.send(
      HistoryReadMessage(
        context,
        range,
        targets,
        pagination
      )
    )
  }
  
  public override SourceStream? readStream(
    HxContext context,
    DateSpan? range,
    Ref[]? targets,
    DatabasePaginationRequest? pagination := null)
  {
    /* -- Need to override source stream
    stream := SourceStream()
    
    this.actor.send(
      HistoryReadMessage(
        context,
        range,
        targets,
        pagination,
        stream
      )
    )
    
    return stream
    */
    
    return null
  }
  
  public override Future historyCount(Ref? targetId := null) // Future<Int>
  {
    return this.actor.send(
      HistoryCountMessage(targetId)
    )
  }
  
  public override Future removeBetween(DateSpan range)
  {
    return this.actor.send(
      HistoryRemoveBetweenMessage(range)
    )
  }
  
  public override Future removeBefore(DateTime date)
  {
    return this.actor.send(
      HistoryRemoveBeforeMessage(date)
    )
  }
  
  public override Future clear(Bool? rebuild := false)
  {
    return this.actor.send(
      HistoryClearMessage(rebuild)
    )
  }
  
  public override Future runHouseKeeping()
  {
    return this.actor.send(
      AlarmHistoryDBMessage(AlarmHistoryDBOp.houseKeeping)
    )
  }
  
  public override Future start()
  {
    return this.actor.send(
      AlarmHistoryDBMessage(AlarmHistoryDBOp.start)
    )
  }
  
  public override Future stop()
  {
    return this.actor.send(
      AlarmHistoryDBMessage(AlarmHistoryDBOp.stop)
    )
  }
  
  public override Future import(AlarmHistoryEntry[] data)
  {
    return this.actor.send(
      HistoryImportMessage(data)
    )
  }
  
  ///////////////////////////////////////
  // Implementation
  ///////////////////////////////////////
  
  private Void commitHouseKeeping()
  {
    // Apply WALs
    this.runWalApply

    // Purge WALs
    this.runWalPurge
            
    // Run Engine Maintenance
    while(this.engine.getEngineMaintenanceJob.run(0)) {}        
  }
  
  private AlarmHistoryEntry[] commitImport(HistoryImportMessage msg)
  {
    if (this.writer == null)
    {
      this._writer.getAndSet(
        Unsafe(
          engine.getTableWriterAPI(
            TABLE_NAME,
            "insert"
          )
        )
      )
    }
    
    msg.data.each | entry | {
      try
      {
        this.utility.insertRecord(entry, this.writer)
      }
      catch (Err error)
      {
        this.log.err("Error inserting history record", error)
      }
    }
    
    this.writer.commit
    
    this.runWalApply
    
    return this.addDisToEntries(msg.data)
  }
  
  private AlarmHistoryEntry[] commitInsert(HistoryInsertMessage msg)
  {
    if (this.writer == null)
    {
      this._writer.getAndSet(
        Unsafe(
          engine.getTableWriterAPI(
            TABLE_NAME,
            "insert"
          )
        )
      )
    }
    
    if (this.writer == null)
      echo ("Writer is null after check and before writing")
    
    // Somehow the writer becomes null while inserting resulting in an NPE
    
    msg.records.each | entry | {
      try
      {
        if (this.writer == null)
          echo ("Writer is null when writing")
        
        this.utility.insertRecord(entry, this.writer)
      }
      catch (Err error)
      {
        this.log.err("Error inserting history record", error)
      }
    }
    
    this.writer.commit
    
    this.runWalApply
    
    return this.addDisToEntries(msg.records)
  }
  
  private AlarmHistoryResult commitRead(HistoryReadMessage msg) // Future<AlarmHistoryResult>
  {
    Str[] query := [,]
    query.add("SELECT * FROM ${TABLE_NAME}")
    
    if (msg.range != null || msg.targets != null)
      query.add("WHERE")
    
    Str[] whereClause := [,]
    
    if (msg.targets != null)
    {
      ids := msg.targets.map | id | { "'${id.toCode}'" }.join(",")
      whereClause.add("sourceRef in(${ids})")
    }
    
    if (msg.range != null)
    {
      startDate := msg.range.start.toDateTime(Time.make(0,0,0)).toUtc.toLocale("YYYY-MM-DD:hh:mm:ss.fff")
      endDate := msg.range.end.toDateTime(Time.make(23, 59, 59)).toUtc.toLocale("YYYY-MM-DD:hh:mm:ss.fff") // Inclusive
      whereClause.add(
        "(occurred >= to_timestamp('$startDate', 'yyyy-MM-dd:HH:mm:ss.SSS') AND occurred <= to_timestamp('$endDate', 'yyyy-MM-dd:HH:mm:ss.SSS'))"
      )
    }
    
    if (whereClause.size > 0)
      query.add(whereClause.join(" AND "))
    
    // Sort
    if (msg.pagination != null)
    {
      query.add("ORDER BY ${msg.pagination.sortColumnName} ${msg.pagination.sortDirection}")
    }
    else
      query.add("ORDER BY occurred DESC")
    
    shouldCheckPermissions := !(msg.context.user.isAdmin || msg.context.user.isSu) 
    
    if (!shouldCheckPermissions && msg.pagination?.resultSize > 0)
    {
      query.add("LIMIT ${msg.pagination.entityOffset}, ${msg.pagination.entityOffset + msg.pagination.resultSize}")
    }
    
        
        
    AlarmHistoryEntry[] results := [,]
    
    try
    {
      factory := this.engine.select(query.join(" "), this.sqlContext)
      cursor := factory.getCursor(this.sqlContext)
      QuestDBCursor? questCursor := shouldCheckPermissions
        ? QuestDBCursor(msg.context, this.utility, msg.pagination)
        : null
      
      if (msg.stream != null)
        msg.stream.signalStart

      while (cursor.hasNext)
      {
        try
        {
          dbRecord := cursor.getRecord
          
          if (questCursor != null && !questCursor.shouldInclude(dbRecord))
            continue
          
          record := this.utility.entryFromRecord(
            dbRecord
          )
  
          if (msg.stream != null)
          {
            msg.stream.submit(
              this.addDisToEntry(record)
            )
          }
          else
          {
            results.add(
              record
            )
          }
          
          if (questCursor != null && questCursor.shouldEnd)
            break
        }
        catch (Err error)
        {
          this.log.err("Error reading history record", error)
        }
      }
      
      cursor.close
      factory.close
      
      if (msg.stream != null)
        msg.stream.signalComplete
    }
    catch (Err error)
    {
      this.log.err("Error reading history database", error)
    }
    
    
    // Get Metadata
    
    Str:Obj metadata := [:]
    recordCount := Number.makeInt(-1)
    
    Str[] metaQuery := [,]
        
    if (!shouldCheckPermissions)
    {
      metaQuery.add("SELECT count() FROM $TABLE_NAME")
      
      if (msg.pagination?.resultSize > 0)
      {
        metaQuery.add(
          query.getRange(1..-3).join(" ") // Removes "SELECT", "ORDER BY", and "LIMIT"
        )
      }
      else
      {
        metaQuery.add(
          query.getRange(1..-2).join(" ") // Removes "SELECT", "ORDER BY"
        )
      }
      
      countFactory := this.engine.select(metaQuery.join(" "), this.sqlContext)
      countCursor := countFactory.getCursor(this.sqlContext)
      countRecord := countCursor.getRecord
      
      recordCount = (!countCursor.hasNext)
        ? Number.makeInt(0)
        : Number.makeNum(countRecord.getLong(0))
      
      countCursor.close
      countFactory.close
    }
    else
    {
      // Utilizing QuestDBCursor to calculate permitted records
      metaQuery.add("SELECT sourceRef FROM $TABLE_NAME")
      
      metaQuery.add(
        query.getRange(1..-2).join(" ") // Removes "SELECT", "ORDER BY"
      )
      
      countFactory := this.engine.select(metaQuery.join(" "), this.sqlContext)
      dbCursor := countFactory.getCursor(this.sqlContext)
      
      questCursor := QuestDBCursor(msg.context, this.utility, msg.pagination)
            
      recordCount = Number.makeInt(
        questCursor.count(dbCursor)
      )
      
      dbCursor.close
      countFactory.close
    }
    
    metadata.set("totalRecordCount", recordCount)
    metadata.set("resultCount", Number.makeInt(results.size))
    
    if (msg.pagination?.resultSize > 0)
    {
      totalPages := Number.make(
        recordCount.toFloat.divInt(
          msg.pagination.resultSize
        ).ceil.max(1.0f)
      )
      
      currentPage := Number.makeInt(msg.pagination.currentPage)
      
      metadata.set("totalPageCount", totalPages)
      metadata.set("currentPage", currentPage)
    }
    
    // Add dis to refs
    results.each |rec| {  
      rec.each | tag, tagName | {
        if (tagName == "id")
          return
        
        if (tag is Ref)
        {
          newId := this.lib.rt.db.internRef(tag)
          ((Ref) tag).disVal = newId.dis
        }
      }
    }
    
    return AlarmHistoryResult(
      this.addDisToEntries(results),
      Etc.makeDict(metadata)
    )
  }
  
  private AlarmHistoryEntry[] commitQuery(HistoryQueryMessage msg)
  {
    AlarmHistoryEntry[] results := [,]
    
    try
    {
      factory := this.engine.select(msg.query, this.sqlContext)
      cursor := factory.getCursor(this.sqlContext)
      
      while (cursor.hasNext)
      {
        try
        {
          record := this.utility.entryFromRecord(
            cursor.getRecord
          )
  
          results.add(
            record
          )
        }
        catch (Err error)
        {
          this.log.err("Error reading history record", error)
        }
      }
      
      cursor.close
      factory.close     
    }
    catch (Err error)
    {
      this.log.err("Error reading history database", error)
    }
    
    return results
  }
  
  private Number commitHistoryCount(HistoryCountMessage msg) // Future<Number>
  {
    query := StrBuf()
    query.add("SELECT count() FROM $TABLE_NAME")
    
    if (msg.targetId != null)
      query.add(" WHERE sourceRef = '$msg.targetId.toCode'")
    
    factory := this.engine.select(query.toStr, this.sqlContext)
    cursor := factory.getCursor(this.sqlContext)
    
    try
    {
      if (!cursor.hasNext)
        return Number.makeInt(0)
      
      rec := cursor.getRecord
      
      return Number.makeNum(rec.getLong(0))
    }
    catch (Err error)
    {
      this.log.err("Error reading history count", error)
    }
    finally
    {
      cursor.close
      factory.close
    }
    
    return Number.makeInt(0)
  }
  
  private Void commitRemoveBetween(HistoryRemoveBetweenMessage msg)
  {
    Str[] partitions := [,]
    
    msg.range.eachDay | date |
    {
      partitions.add("'" + SysLib.format(date, "YYYY-MM-DD") + "'")
    }
    
    partitionQuery := partitions.join(",")
    
    this.engine.execute(
      "ALTER TABLE $TABLE_NAME DROP PARTITION LIST ${partitionQuery}",
      this.sqlContext
    )
    
    this.runWalApply
  }
  
  private Void commitRemoveBefore(HistoryRemoveBeforeMessage msg)
  {
    factory := this.engine.select(
      "SHOW PARTITIONS FROM $TABLE_NAME",
      this.sqlContext
    )
    
    try
    {
      cursor := factory.getCursor(this.sqlContext)
      
      Str[] partitions := [,]
      
      Int deleteBeforeDate := msg.date.toUtc.toJava
      
      while (cursor.hasNext)
      {
        row := cursor.getRecord
        partitionMaxDate := row.getTimestamp(4).div(1000) // maxTimestamp
              
        if (partitionMaxDate < deleteBeforeDate)
        {
          date := DateTime.fromJava(partitionMaxDate, TimeZone.utc)
          partitions.add("'" + SysLib.format(date, "YYYY-MM-DD") + "'")
        }
      }
      
      if (partitions.size < 1)
      {
        this.lib.log.warn("No partitions found before $msg.date")
        return
      }
      
      partitionQuery := partitions.join(",")
      
      this.engine.execute(
        "ALTER TABLE $TABLE_NAME DROP PARTITION LIST ${partitionQuery}",
        this.sqlContext
      )
      
      this.runWalApply
      
    }
    catch (Err error)
    {
      this.lib.log.err("Error Removing Before", error)
    }
  }
  
  private Void commitClear(HistoryClearMessage msg)
  {
    if (msg.rebuild)
    {
      tableToken := this.engine.getTableTokenIfExists(TABLE_NAME)
      
      try
      {
        tableMeta := this.engine.getTableMetadata(tableToken)
      
        if (tableMeta != null)
          tableMeta.close
        
        this.engine.dropTableOrMatView(
          null,
          tableToken
        )
        
        this.commitHouseKeeping
      }
      catch (Err error)
      {
        this.log.err("Error destroying table", error)
      }
      finally
      {
        this.commitStop
        this.commitStart
      }
    }
    else
    {
      this.engine.execute("TRUNCATE TABLE $TABLE_NAME", this.sqlContext)
      this.runWalApply
    }
  }
  
  private Void runWalApply()
  {
    if (this.walApplyJob == null)
      return
    
    try
    {
      while (this.walApplyJob.run(0)) { }
    }
    catch (Err error)
    {
      this.log.err("Error applying write ahead log", error)
    }
  }
  
  private Void runWalPurge()
  {
    if (this.walPurgeJob == null)
      return
    
    if (this.walPurgeJob.getRunLock.isLocked)
      return
    
    try
    {
      while (this.walPurgeJob.run(0)) {}
    }
    catch (Err error)
    {
      this.log.err("Error purging write ahead logs", error)
    }
  }
  
  private Void destroyCurrentContext()
  {
    if (this.writer != null)
      this.writer.close
    
    this._writer.getAndSet(Unsafe(null))
            
    this.sqlContext.close
    
    if (walApplyJob != null)
      this.walApplyJob.close
    
    if (walPurgeJob != null)
      this.walPurgeJob.close
    
    this.engine.clear
  }
  
  private Void createSQLContext()
  {
    this._sqlContext.getAndSet(
      Unsafe(
        SqlExecutionContextImpl(
          this.engine,
          1
        ).with(AllowAllSecurityContext.INSTANCE, null)
      )
    )
  }
  
  private Void createWALJob()
  {
    this._walApplyJob.getAndSet(
      Unsafe(
        ApplyWal2TableJob(this.engine, 1)
      )
    )
    
    this._walPurgeJob.getAndSet(
      Unsafe(
        this.config.getFactoryProvider.getWalJobFactory.createWalPurgeJob(this.engine)
      )
    )
    
    this.engine.setWalPurgeJobRunLock(this.walPurgeJob.getRunLock)
    this.walPurgeJob.delayByHalfInterval
  }
  
  private Void createTable()
  {
    // Create Table if it doesn't exist
    if (this.engine.getTableStatus(TABLE_NAME) == TableUtils.TABLE_DOES_NOT_EXIST)
    {
      this.engine.execute(
        QuestDBUtility.CreateHistoryDDL,
        this.sqlContext,
        null
      )
      
      this.engine.awaitTable(TABLE_NAME, 30, TimeUnit.SECONDS)      
    }
    
    // Build Utility and Process Metadata
    tableMeta := this.engine.getTableMetadata(this.engine.getTableTokenIfExists(TABLE_NAME))
    this._utility.getAndSet(
      QuestDBUtility(tableMeta)
    )
    
    tableMeta.close
  }
  
  private Void commitStart()
  {
    // Setup Folder
    historyFolder := lib.rt.dir.plus(`alarms/history/`)
    
    if (!historyFolder.exists)
      historyFolder.create
    
    // Construct config
    this._config.getAndSet(
      Unsafe(
        DefaultCairoConfiguration(
          historyFolder.osPath
        )
      )
    )
        
    // Construct Engine
    this._engine.getAndSet(
      Unsafe(
        CairoEngine(this.config)
      )
    )
    
    
    this.engine.load
            
    // Create SQL Context
    this.createSQLContext
    
    this.createTable
    
    this.createWALJob
  }
  
  private Void commitStop()
  {
    try
    {
      this.writer?.close
      
      this._writer.getAndSet(Unsafe(null))
      
      this.config.getFactoryProvider?.close
      this.walApplyJob?.close
      this.walPurgeJob?.close
      this.sqlContext?.close
      this.engine.close
    }
    catch (Err error)
    {
      
      this.lib.log.err("Error Shutting Down Quest DB", error)
      throw error
    }   
  }
  
  private AlarmHistoryEntry[] addDisToEntries(AlarmHistoryEntry[] entries)
  {
    entries.each | entry | {
      this.addDisToEntry(entry)
    }
    
    return entries
  }
  
  private AlarmHistoryEntry addDisToEntry(AlarmHistoryEntry entry)
  {
    entry.id.disVal = (entry.acked != null)
      ? "${entry.fromState.toDisplayName} [Acked]"
      : "${entry.fromState.toDisplayName} -> ${entry.toState.toDisplayName}"
    
    return entry
  }
  
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack

const class AlarmHistoryResult
{
  const Dict? metadata
  const AlarmHistoryEntry[] entries
  
  new make(AlarmHistoryEntry[] entries, Dict? metadata := null)
  {
    this.entries = entries
    this.metadata = metadata ?: Etc.emptyDict
  }
  
  public Grid toGrid(Dict? additionalMeta := Etc.emptyDict)
  {
    return Etc.makeDictsGrid(
      Etc.dictMerge(this.metadata, additionalMeta),
      this.entries
    )
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using hx
using concurrent
using haystack
using obs
using skyarc

const class AlarmLib : HxLib
{
  static const Str DEFAULT_DB := "alarmExt::AlarmFolioDatabase"
  static const Str DEFAULT_HISTORY_DB := "alarmExt::AlarmHistoryQuestDB"
  
  override const AlarmRestAPI web := AlarmRestAPI(this)
  override const Duration? houseKeepingFreq := Duration("15sec")
  
  const ActorPool pool;
  const AlarmEngine engine;
  const AlarmRouter router;
  const RulesEngine rules;
  
  private const AtomicRef _db;
  private const AtomicRef _history;
  
  internal const AlarmsObservable alarmsObs := AlarmsObservable()
  override const Observable[] observables := [alarmsObs]
  
  new make(): super()
  {
    this.pool = ActorPool()
    this._db = AtomicRef()
    this._history = AtomicRef()
        
    // Create Alarming Actors
    this.engine = AlarmEngine(ActorPool(), this)
    this.router = AlarmRouter(ActorPool(), this)
    this.rules = RulesEngine(ActorPool(), this)
  }
  
  public AlarmDatabase? db()
  {
    return this._db.val
  }
  
  public AlarmHistoryDatabase? history()
  {
    return this._history.val
  }
    
  public Void dbRecordChangeHandler(CommitObservation observation)
  {
    // TODO: Ignore alarm specific tag changes, "mod" changes causes everything to come through
    // Would be nice if the observation contained the actual Diff.
    
    this.rules.recordChange(
      observation.newRec,
      observation.ts
    )
  }
  
  public override Void onHouseKeeping()
  {
    try
    {
      if (this.db != null)
        this.db.runHouseKeeping
    }
    catch (Err error)
    {
      this.log.err("Error running DB Housekeeping", error)
    }
    
    try
    {
      if (this.history != null)
        this.history.runHouseKeeping
    }
    catch (Err error)
    {
      this.log.err("Error running history Housekeeping", error)
    }
  }
  
  
  // TODO: Possibly move toAlarm & toNormal execution into an actor
  public Alarm[] toAlarm(
    Obj targets,
    Str state,
    Bool enable,
    AlarmDispatcher dispatcher,
    Str? message := null,
    DateTime? occurred := null,
    Obj? priority := null,
    Dict? meta := null,
    Bool deactivateOtherStates := false)
  {
    // TODO: Security & Validation Check
    if (targets is Dict)
      targets = [((Dict) targets).id]
    
    if (targets is Grid)
      targets = ((Grid) targets).toRows.map |row| { row.id }
    
    if (!(targets is List))
      targets = [targets]
    
    if (occurred == null)
      occurred = DateTime.now
            
    ackRequired := null
    
    if (meta?.has("ackRequired") == true)
      ackRequired = meta.get("ackRequired") == Marker.val || meta.get("ackRequired") == true
  
    return AlarmTracker(targets, this, dispatcher).transition(
      state,
      enable,
      occurred,
      message,
      ackRequired,
      priority,
      meta,
      deactivateOtherStates
    ).get(AlarmEngine.Timeout)
  }
  
  public Dict[] toNormal(
    Obj targets,
    AlarmDispatcher dispatcher,
    Str? message := null,
    DateTime? occurred := null,
    Dict? meta := null)
  {
    // If we're enabling the normal state directly, this should probably clear all other states.
    // the intention is to make the entity normal.
    return toAlarm(targets, AlarmState.NORMAL.name, true, dispatcher, message, occurred)
  }
  
  private Void ruleRouteDBHandler(CommitObservation observation)
  {
    switch (observation.subType)
    {
      case CommitObservationAction.added.name:
        // Add alarm routes
        if (observation.newRec.has(AlarmRouter.REC_TAG))
        {
          this.router.addRoutes([
            AlarmRouter.CreateRouteFromRec(observation.newRec)
          ])
        }
        
        // Add alarm rules
        if (observation.newRec.has(RulesEngine.REC_TAG))
        {
          this.rules.addRules([
            RulesEngine.CreateRuleFromRec(observation.newRec)
          ])
        }
      
      case CommitObservationAction.removed.name:
        // Remove alarm routes
        if (observation.oldRec.has(AlarmRouter.REC_TAG))
          this.router.removeRoutes([observation.id])
      
        // Remove alarm rules
        if (observation.oldRec.has(RulesEngine.REC_TAG))
          this.rules.removeRules([observation.id])
      
      case CommitObservationAction.updated.name:
        // Change alarm routes
        if (observation.oldRec.has(AlarmRouter.REC_TAG))
        {
          this.router.removeRoutes([observation.id])

          this.router.addRoutes([
            AlarmRouter.CreateRouteFromRec(observation.newRec)
          ])
        }
      
        // Change alarm rules
        if (observation.oldRec.has(RulesEngine.REC_TAG))
        {
          this.rules.removeRules([observation.id])
          
          this.rules.addRules([
            RulesEngine.CreateRuleFromRec(observation.newRec)
          ])
        }
    }
  }
  
  private Void loadRoutesAndRules()
  {
    try
    {
      routes := this.rt.db.readAllList(Filter.fromStr(AlarmRouter.REC_TAG))
      
      this.router.addRoutes(
        routes.map | route | { AlarmRouter.CreateRouteFromRec(route)  }
      )
    }
    catch (Err err)
    {
      log.err("Error loading routes", err)
    }
    
    try
    {
      rules := this.rt.db.readAllList(Filter.fromStr(RulesEngine.REC_TAG))
      
      this.rules.addRules(
        rules.map | rule | { RulesEngine.CreateRuleFromRec(rule) }
      )
    }
    catch (Err err)
    {
      log.err("Error loading rules", err)
    }
  }
  
  override Void onStart()
  {
    super.onStart
    
    dbType := Type.find(this.rec.get("alarmDb") ?: AlarmLib.DEFAULT_DB)
    historyType := Type.find(this.rec.get("alarmHistoryDb") ?: AlarmLib.DEFAULT_HISTORY_DB)

    this._db.getAndSet(
      (dbType.method("make").params.size > 0)
        ? dbType.make([this])
        : dbType.make()
    )

    this._history.getAndSet(
      (historyType.method("make").params.size > 0)
        ? historyType.make([this])
        : historyType.make()
    )

    this.db.start.get
    this.history.start.get
    
    this.loadRoutesAndRules
    
    // Rules Engine: Monitoring Observables
    this.observe(
      "obsCurVals",
      Etc.makeDict([:]),
      #dbRecordChangeHandler
    )
    
    this.observe(
      "obsCommits",
      Etc.makeDict([
        "obsAdds": Marker.val,
        "obsUpdates": Marker.val
      ]),
      #dbRecordChangeHandler
    )
    
    // Rule or Route Record Monitoring
    this.observe(
      "obsCommits",
      Etc.makeDict([
        "obsAdds": Marker.val,
        "obsUpdates": Marker.val,
        "obsRemoves": Marker.val,
        "obsFilter": "$RulesEngine.REC_TAG or $AlarmRouter.REC_TAG"
      ]),
      #ruleRouteDBHandler
    )
  }
  
  override Void onRecUpdate()
  {
    super.onRecUpdate
    
    if (rec.has("alarmDb") && 
        (
          this.db == null ||
          !rec.get("alarmDb").equals(this.db.typeof.qname)
        )
      )
    {
      if (this.db != null)
        this.db.stop.get
      
      dbType := Type.find(this.rec.get("alarmDb"))
      
      this._db.getAndSet(
        (dbType.method("make").params.size > 0)
          ? dbType.make([this])
          : dbType.make()
      )
      
      this.db.start.get
    }
    
    if (rec.has("alarmHistoryDb") &&
        (
          this.history == null ||
          !rec.get("alarmHistoryDb").equals(this.history.typeof.qname)
        )
      )
    {
      if (this.history != null)
        this.history.stop.get
      
      historyType := Type.find(this.rec.get("alarmHistoryDb"))
      
      this._history.getAndSet(
        (historyType.method("make").params.size > 0)
          ? historyType.make([this])
          : historyType.make()
      )
      
      this.history.start.get
    }
  }
  
  override Void onStop()
  { 
    this.db.stop.get
    this.history.stop.get
    
    this.pool.stop
    this.pool.join
    super.onStop
  }
  
  override Void onReady()
  {
    super.onReady
  }
  
  override Void onUnready()
  {
    super.onUnready
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack

const class AlarmManagement : TypedDict
{
  @TypedTag
  const Obj[] stateStack := [,] // Dict[] using Obj because Fantom breaks when passing a TypedDict
    
  @TypedTag
  const Ref? sourceRef
  
  const AlarmStateModel[] stack
  const Str[] states
  
  new make (Dict rec, | This | f) : super(rec)
  {
    f(this)
        
    this.stack = this.stateStack.map |item| { AlarmStateModel.wrap(item)  }
    this.states = this.stack.map |item| { item.state }
  }
    
  static new wrap(Dict? dict)
  {
    if (dict == null)
      return null
    
    return create(AlarmManagement#, dict, | error | { echo("Error creating management dict: $error")})
  }
  
  static new makeNew(Ref? sourceRef)
  {
    return wrap(
      Etc.makeDict([
        "sourceRef": sourceRef
      ])
    )
  }
}

const class AlarmStateModel : TypedDict
{
  @TypedTag
  const Str state
  
  @TypedTag
  const Bool acked // TODO: use acked when sorting. Acked states are of the lowest priority.

  @TypedTag
  const Int priority
  
  new make (Dict rec, | This | f) : super(rec)
  {
    f(this)
  }
  
  static new wrap(Dict? dict)
  {
    if (dict == null)
      return null
    
    return create(AlarmStateModel#, dict)
  }
  
  static new makeNew(Str state, Int priority)
  {
    return wrap(
      Etc.makeDict([
        "state": state,
        "priority": priority
      ])
    )
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using folio

enum class AlarmOp {transition, acknowledge, update, connAckComplete, getCurrentStates, clearCurrentStates}

const class AlarmMessage 
{
  const AlarmOp op;

  new make(
    AlarmOp op
  ) {
    this.op = op
  } 
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using legacy
using haystack
using skyarcd

const class AlarmTargetNavTree : DependentNavTree
{
  new make(): super("alarmTarget", "equip", NavPath("/site/equip/point"))
  {
    
  }
}

**
** AlarmNavCounter
**
internal class AlarmNavCounter
{
  new make(Context cx, Grid src)
  {
    this.cx = cx
    this.lib = cx.rt.lib("alarm", false)
    this.src = src
    this.tree = NavTree.find(cx, "alarmTarget")
  }

  Grid count()
  {
    readAlarms
    mapByTargetRef
    src.each |row| { countRow(row) }
    return addCols
  }

  private Void readAlarms()
  {
    // read open alarms for today
    //this.alarms = ext.db.readAll(Date.today) |a| { a["alarmStatus"] == "alarm" }
    
  // TODO: May need to filter by entities that are currently in alarm.
  // We could read "inAlarm", map to id, then query alarm db for those sources
  this.alarms = this.lib.db.readAlarms(this.cx, null, Etc.toDateSpan(Date.today)).get
  }

  private Void mapByTargetRef()
  {
    this.byTargetRef = Ref:AlarmCounts[:]
    this.alarms.each |alarm|
    {
      targetRef := alarm.sourceRef
      ac := byTargetRef[targetRef]
      if (ac == null) byTargetRef[targetRef] = ac = AlarmCounts()
      ac.numAlarms++
      if (alarm.acked == null) ac.numUnackAlarms++
    }
  }

  private Void countRow(Dict row)
  {
    targets := tree.targets(cx, cx.readById(row.id))
    total := AlarmCounts()
    targets.each |target|
    {
      ac := byTargetRef[target.id]
      if (ac != null) total.add(ac)
    }
    counts.add(total)
  }

  private Grid addCols()
  {
    gb :=  GridBuilder().setMeta(src.meta)
    src.cols.each |c| { gb.addCol(c.name, c.meta) }
    gb.addCol("numAlarms")
    gb.addCol("numUnackAlarms")
    src.each |srcRow, i|
    {
      newRow := Obj?[,]
      newRow.capacity = gb.numCols
      src.cols.each |col| { newRow.add(srcRow.val(col)) }
      ac := counts[i]
      newRow.add(Number(ac.numAlarms))
      newRow.add(Number(ac.numUnackAlarms))
      gb.addRow(newRow)
    }
    return gb.toGrid
  }

  Context cx
  AlarmLib lib
  Grid src
  AlarmCounts[] counts := [,]
  NavTree tree
  Alarm[]? alarms
  [Ref:AlarmCounts]? byTargetRef
}

internal class AlarmCounts
{
  Void add(AlarmCounts x)
  {
    this.numAlarms += x.numAlarms
    this.numUnackAlarms += x.numUnackAlarms
  }

  override Str toStr() { "$numUnackAlarms / $numAlarms" }

  Int numAlarms
  Int numUnackAlarms
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using obs

const class AlarmObservation : TypedDict, Observation 
{
  @TypedTag
  const Alarm alarm
  
  @TypedTag
  override const DateTime ts
  
  const AlarmState state
  
  new make (Dict rec, | This | f): super(rec)
  {
    f(this)
    
    this.state = AlarmState.getState(rec["alarm"]->state)
  }
  
  override Str? subType()
  {
    return this.alarm.state
  }
  
  override Str type()
  {
    if (this.alarm.acked != null)
      return "ack"
    
    return "transition"
  }
  
  static new wrap(Dict record)
  {
    return create(AlarmObservation#, record)
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


enum class AlarmPointTags 
{
  inAlarm
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack

class AlarmRecord 
{
  internal static const Str MARKER_NAME := "alarm"
  
  const Dict? rec;
  
  DateTime? created
  DateTime? occurred
  Ref? source
  Str? state
  Int? priority
  Str? message
  Bool ackRequired
  
  DateTime? acked
  Ref? ackedBy
  Bool ackMessageRequired
  Str? ackMessage
  Bool ackInProgress
  Dict? meta
  
  new make(Dict? rec)
  {
    this.rec = rec
    
    if (rec != null)
    {
      if (rec.has("created"))
        this.created = rec.get("created")
      
      if (rec.has("occurred"))
        this.occurred = rec.get("occurred")
      
      if (rec.has("source"))
        this.source = rec.get("source")
      
      if (rec.has("state"))
        this.state = rec.get("state")
      
      if (rec.has("priority"))
        this.priority = rec.get("priority")
      
      if (rec.has("message"))
        this.message = rec.get("message")
      
      this.ackRequired = rec.has("ackRequired")
      
      if (rec.has("acked"))
        this.acked = rec.get("acked")
      
      if (rec.has("ackedBy"))
        this.ackedBy = rec.get("ackedBy")
      
      this.ackMessageRequired = rec.has("ackMessageRequired")
      
      if (rec.has("ackMessage"))
        this.ackMessage = rec.get("ackMessage")
      
      this.ackInProgress = rec.has("ackInProgress")
      
      if (rec.has("meta"))
        this.meta = rec.get("meta")
    }
  }
  
  public Dict toDict()
  {
    [Str:Obj] results := [:]
    results.set(MARKER_NAME, Marker.val)
    
    if (rec != null)
      results.set("id", rec.id)
    
    if (this.created != null)
      results.set("created", this.created)
    
    if (this.occurred != null)
      results.set("occurred", this.occurred)
    
    if (this.source != null)
      results.set("source", this.source)
    
    if (this.state != null)
      results.set("state", this.state)
    
    if (this.priority != null)
      results.set("priority", this.priority)
    
    if (this.message != null)
      results.set("message", this.message)
    
    results.set("ackRequired", this.ackRequired)
    
    if (this.acked != null)
      results.set("acked", this.acked)
    
    if (this.ackedBy != null)
      results.set("ackedBy", this.ackedBy)
    
    results.set("ackMessageRequired", this.ackMessageRequired)
    
    if (this.ackMessage != null)
      results.set("ackMessage", this.ackMessage)
    
    results.set("ackInProgress", this.ackInProgress)
    
    if (this.meta != null)
      results.set("meta", this.meta)    
    
    return Etc.makeDict(results)
  }
  
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using hx
using web 
using haystack
using util

const class AlarmRestAPI : HxLibWeb
{
  static const Str[] COMMA_DELIMITED_LANGS := ["en", "es", "zh"]
  
  override AlarmLib lib()
  {
    return super.lib
  }
  
  new make(AlarmLib lib): super(lib)
  {
  }
  
  override Void onService()
  {
    context := this.rt.user.authenticate(req, res)
    
    appAccess := ((Str?) context.user.meta.get("appAccess"))?.split(',')
    
    if (appAccess != null && !appAccess.contains("alarms"))
    {
      res.sendErr(401, "$<unauthorized>")
      return
    }
    
    
    if (req.method.lower() == "patch")
    {
      this.onPatch()
      return
    }
    
    super.onService()
  }
  
  override Void onGet()
  {
    acceptTypes := this.req.headers.get("Accept", "application/vnd.haystack+json").split(',')
    download := this.req.headers.containsKey("download")
    
    operation := this.req.modRel.path.getSafe(0, "default")
    context := this.rt.user.authenticate(req, res)

    Grid? result

    range := this.req.modRel.query.getChecked("range", false)

    offset := this.req.modRel.query.containsKey("offset")
        ? Int.fromStr(this.req.modRel.query.getChecked("offset", false))
        : 0

    limit := this.req.modRel.query.containsKey("limit")
        ? Int.fromStr(this.req.modRel.query.getChecked("limit", false))
        : -1

    sortOn := this.req.modRel.query.get("sortOn", "occurred")
    sortDirection := this.req.modRel.query.get("sortDirection", "DESC")

    switch (operation)
    {
      case "default":
      case "current":
        source := this.req.modRel.query.getChecked("source", false)
        filterStr := this.req.modRel.query.getChecked("filter", false)
      
        // Validate Source
        if (source != null && !Ref.isId(source))
          source = null
              
        // Resolve all targets under source
        sources := (source != null)
          ? AlarmFuncs.alarmTargets(context.db.readById(Ref(source))).map | rec | { rec.id }
          : null
        
        // Query alarm database
        queryResult := this.lib.db.readAlarms(
          context,
          sources,
          (range != null)
            ? Etc.toDateSpan(context.eval(range), context)
            : null,
          DatabasePaginationRequest(
            limit,
            offset,
            sortOn,
            sortDirection,
            filterStr
          )
        ).get as AlarmResult

        result = queryResult.toGrid
      
      case "history":
        Ref[]? sources
        
        Str[]? sourceList := (this.req.modRel.path.size > 1)
          ? [this.req.modRel.path.get(1)]
          : (this.req.modRel.query.containsKey("sources")
            ? ((Str) this.req.modRel.query.get("sources")).split(',')
            : null)

        // Resolve all targets
        if (sourceList != null)
        {
          sources = sourceList
            .mapNotNull |source| 
            {
              // Validate source
              if (!Ref.isId(source))
                return null
              
              return Ref(source)
            }
            .flatMap |ref|
            { 
              return AlarmFuncs.alarmTargets(ref)
                .map | rec |
                {
                  rec.id
                }
              
            }
          
          if (sources.size == 0)
            sources = null
          
        }

        result = ((AlarmHistoryResult) this.lib.history.read(
          context,
          (range != null)
            ? Etc.toDateSpan(context.eval(range), context)
            : null,
          sources,
          DatabasePaginationRequest(
            limit,
            offset,
            sortOn,
            sortDirection
          )
        ).get).toGrid
        
    }
    
    // TODO: Filter by user access
    
    if (result != null)
    {
      // Add dis to refs
      result.each |rec| {  
        rec.each | tag, tagName | {
          if (tagName == "id")
            return
          
          if (tag is Ref)
          {
            newId := context.db.internRef(tag)
            ((Ref) tag).disVal = newId.dis
          }
        }
      }
    }
    
    
    if (download)
    {
      downloadFileName := "Alarms"
      
      if (operation == "history")
        downloadFileName += "-History"
      // TODO: add source to file name
      downloadFileName += "-${Date.today.toStr}"
      downloadFileName += ".csv"
      
      this.res.headers.set("Content-Disposition", "attachment; filename=\"$downloadFileName\"")
    }
    
    GridWriter? writer;
    
    if (acceptTypes.contains("text/csv"))
    {
      result = this.appendMetadata(result)
      this.res.headers.set("Content-Type", "text/csv")
      writer = CsvWriter(this.res.out)
      
      locale := (context.user.meta.has("locale"))
        ? context.user.meta.get("locale")
        : "en"
      
      if (!COMMA_DELIMITED_LANGS.contains(locale))
        ((CsvWriter) writer).delimiter = ';'
    }
    else
    {
      // TODO: Create utility to select proper encoder based off of the request's accepts header.
      this.res.headers.set("Content-Type", "application/vnd.haystack+json")
      writer = JsonWriter(this.res.out)
    }
            
    writer.writeGrid(result)
    this.res.done
  }

  // Returns a grid with individual metadatcolumns from the "metadata" column of the source grid
  private Grid appendMetadata(Grid sourceGrid)
  {
    Str METADATA_COL := "metadata"
    doesMetadataColExist := sourceGrid.cols.find |col| { col.name == METADATA_COL }
    if (sourceGrid.isEmpty || doesMetadataColExist == null)
      return sourceGrid

    [Str:Dict] uniqueColumns := [:]
    sourceGrid.each |row| {
      Dict? meta := row.get(METADATA_COL, null) as Dict
      if (meta != null) {
        flattenColumns(meta).each |col| {
          uniqueColumns[col->path] = col 
        }
      }
    }

    if (uniqueColumns.isEmpty)
      return sourceGrid

    gb := GridBuilder()
    gb.setMeta(sourceGrid.meta)

    // Add new columns
    sourceGrid.cols.each |col|
    {
      if (col.name != METADATA_COL)
        gb.addCol(col.name)
    }
    uniqueColumns.each |col| { gb.addCol("${METADATA_COL}_${col->name}") }
    
    // Add new rows
    sourceGrid.each |row|
    {
      Dict? meta := row.get(METADATA_COL, null) as Dict
      Obj?[] vals := sourceGrid
        .removeCol(METADATA_COL)
        .cols
        .map |col -> Obj?| { row.get(col.name, null) }

      uniqueColumns.each |col|
      {
        vals.add(getValueAtPath(meta, col.get("path", "")))
      }
      gb.addRow(vals)
    }

    sourceGrid = gb.toGrid
    return sourceGrid
  }

  // Gets a flattened representation of a dictionary into a list of dictionaries with path, name, and dis keys for each unique path in the original dictionary.
  private Dict[] flattenColumns(Dict dic)
  {
    if (dic.isEmpty) return Dict[,]

    Str[][] output := [,]
    dic.each |val, key|
    {
      Int maxDepth := 1
      output.addAll(getDictPaths(val, [key], maxDepth))
    }
    
    return output.map |path -> Dict| 
    { 
      Etc.makeDict([
        "path": path.join("."),
        "name": path.join("_"),
        "dis":  path.last
      ])
    }
  }

  // Recursively traverses a nested dictionary and returns a list of paths to each leaf node. Each path is represented as an array of strings.
  private Str[][] getDictPaths(Obj? node, Str[] path, Int maxDepth := Int.maxVal)
  { 
    Str[][] output := [,]
    if (maxDepth <= 0)
      return output

    if (node is Dict)
    {
      ((Dict) node).each |val, key|
      {
        output.addAll(getDictPaths(val, path.dup.add(key), maxDepth - 1))
      }
    }

    output.add(path)
    return output
  }

  // Utility to get value at a given path in a nested dictionary. Path segments are delimited by dots.
  private Obj? getValueAtPath(Dict? dic, Str path)
  {
    if (dic == null || path.isEmpty) return null
    pathSegments := path.split('.')
    Obj? node := dic
    pathSegments.eachWhile |Str segment -> Obj?|
    {
      if (node is Dict) 
      {
        node = ((Dict) node).get(segment, null)
      } 
      return null
    }
    return node
  }
  
  private Void onPatch()
  {
    operation := this.req.modRel.path.getSafe(0, "error")
    context := this.rt.user.authenticate(req, res)

    switch (operation)
    {
      case "ack":
        id := this.req.modRel.path.getSafe(1, null)
        body := JsonReader(this.req.in)
        
        AlarmAcknowledgement? acknowledgement
        
        if (id != null)
        {
          // Request via "/ack/{id}
          ackToken := (Dict) body.readVal(true)
        
          acknowledgement = AlarmAcknowledgement(
            context,
            [Ref(id)],
            DateTime.now,
            context.user.dis,
            ackToken.get("message")
          )
        }
        else
        {         
          // Request via "/ack" - body contains ack models 
          ackToken := (Dict) body.readVal(true)
          
          Ref[]? ids
          
          if (ackToken.has("ackAll"))
          {
            ids = AlarmFuncs.alarms(
              ackToken.get("ackAll")
            ).mapToList |alarm| {
              alarm.id
            }
          }
          else
          {
            ids = ((Str[]) ackToken.get("ids")).map | ackId |
            {
              Ref(ackId)
            }
          }
          
          acknowledgement = AlarmAcknowledgement(
            context,
            ids,
            DateTime.now,
            context.user.dis,
            ackToken.get("message")
          )
        }
      
        if (acknowledgement != null)
        {
          Alarm[] alarms := this.lib.engine.acknowledge(acknowledgement).get
          this.res.statusCode = 200
          
          this.res.headers.set("Content-Type", "application/vnd.haystack+json")         
          writer := JsonWriter(this.res.out)
          writer.writeGrid(Etc.makeDictsGrid(null, alarms))
          writer.flush
          writer.close
          
          this.res.done
        }       
      
      case "error":
        this.res.sendErr(404)
        return
    }
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using concurrent

const class AlarmResult
{
  const Dict metadata
  const AtomicRef _alarms := AtomicRef()
  
  new make(Alarm[] alarms, Dict metadata := Etc.emptyDict)
  {
    this._alarms.getAndSet(alarms.toImmutable)
    this.metadata = metadata
  }
  
  public Alarm[] alarms()
  {
    return this._alarms.val
  }

  public Grid toGrid(Dict additionalMeta := Etc.emptyDict)
  {
    return Etc.makeDictsGrid(
      Etc.dictMerge(this.metadata, additionalMeta),
      this.alarms
    )
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using concurrent
using hx

abstract const class AlarmRoute : TypedDict
{
  static const Log log := Log.get("AlarmRoute")
  
  @TypedTag
  const Str alarmRoute // Route Type
  
  @TypedTag
  const Bool disabled
  
  @TypedTag
  const Str? routeOn
  
  private const Filter? routeFilter
  
  new make(Dict rec, | This | f) : super(rec)
  {
    f(this)
    
    if (this.routeOn != null)
      this.routeFilter = Filter.fromStr(this.routeOn)
  }
  
  public Bool include(Dict record, HaystackContext cx)
  {
    if (this.disabled)
      return false
    
    if (this.routeFilter != null)
      return this.routeFilter.matches(record, cx)
    
    // If no routeOn filter is defined, this means route on everything.
    return true
  }
  
  abstract Future route(HxContext context, Dict alarm, Dict? source)
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using concurrent
using haystack
using skyarc
using hx

const class AlarmRouter 
{
  public static const Str REC_TAG := "alarmRoute"
  
  const AlarmLib lib
  const Actor actor
    
  new make(
    ActorPool pool,
    AlarmLib lib)
  {
    this.lib = lib
    this.actor = Actor.makeCoalescing(
      pool,
      | msg -> Obj? | {
        return this.messageToKey(msg)
      },
      | msgA, msgB -> Obj? | {
        return this.mergeMessage(msgA, msgB)
      },
      | msg -> Obj? | { 
        return this.receive(msg)
      }
    )
  }
  
  public Future clearRoutes()
  {
    return this.actor.send(RoutingMessage(RoutingOp.clearRoutes))
  }
  
  public Future addRoutes(AlarmRoute[] routes)
  {
    return this.actor.send(
      AddRoutesMessage(
        routes
      )
    )
  }
  
  public Future removeRoutes(Ref[] routeIds)
  {
    return this.actor.send(
      RemoveRoutesMessage(
        routeIds
      )
    )
  }
  
  public Future getRoutes()
  {
    return this.actor.send(
      RoutingMessage(RoutingOp.getRoutes)
    )   
  }
  
  public Future route(Dict alarm, Dict? alarmSource)
  {
    return this.actor.send(
      RouteAlarmMessage(
        alarm,
        alarmSource
      )
    )
  }
  
  private Obj? messageToKey(RoutingMessage msg)
  {
    return msg.key 
  }
  
  private RoutingMessage mergeMessage(RoutingMessage msgA, RoutingMessage msgB)
  {
    return msgA.merge(msgB)
  }
  
  private Obj? receive(RoutingMessage msg)
  {
    switch (msg.op)
    {
      case RoutingOp.addRoutes:
        return this.handleAddRoutes(msg)
      
      case RoutingOp.removeRoutes:
        return this.handleRemoveRoutes(msg)
      
      case RoutingOp.getRoutes:
        return this.routes.vals.toImmutable
      
      case RoutingOp.clearRoutes:
        this.routes.clear
      
      case RoutingOp.route:
        return this.handleRouteAlarm(msg)
    }
    
    return null
  }
  
  private Ref:AlarmRoute routes
  {
    get {
      if (!Actor.locals.containsKey("routes"))
        Actor.locals["routes"] = [:]
      
      return Actor.locals["routes"]
    }
    
    set {
      Actor.locals["routes"] = it
    }
  }

  
  private AlarmRoute[] handleAddRoutes(AddRoutesMessage msg)
  {
    return this.routes.addList(msg.routes) | route | { route.id }.vals
  }
  
  private AlarmRoute[] handleRemoveRoutes(RemoveRoutesMessage msg)
  {
    msg.routeIds.each | id | {
      this.routes.remove(id)
    }
    
    return this.routes.vals
  }
  
  private Future handleRouteAlarm(RouteAlarmMessage msg)
  {
    context := this.lib.rt.context.create(User.alarmEngine)
    
    this.routes.each | route | 
    {
      // Note: this uses a send it and forget it method
      // If we want to know if it completed, we can await the route using ".get"
      // Then return a list of the route results
      try
      {
        if (route.include(msg.alarm, context))
        {
          route.route(
            this.lib.rt.context.create(User.alarmEngine),
            msg.alarm,
            msg.alarmSource
          )
          
        }
      }
      catch (Err err)
      {
        // TODO: Handle routing error
        this.lib.log.err("Error routing alarm", err)
      }
    }
    
    return Future.makeCompletable().complete(true)
  }
  
  public static AlarmRoute? CreateRouteFromRec(Dict record)
  {
    type := ROUTER_REGISTRY.get(record.get(REC_TAG))
    
    constructorMethod := type.method("wrap", false)
    
    if (constructorMethod != null)
      return constructorMethod.call(record) as AlarmRoute
      
    return null
  }
  
  private static const Str:Type ROUTER_REGISTRY := [
    CommandPointsRoute.TYPE: CommandPointsRoute#,
    ExecuteAxonRoute.TYPE: ExecuteAxonRoute#,
    NotificationRoute.TYPE: NotificationRoute#,
    CallFuncRoute.TYPE: CallFuncRoute#
  ]
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using hx

abstract const class AlarmRule : TypedDict
{
  @TypedTag
  const Str alarmRule // Rule Type
  
  @TypedTag
  const Str ruleOn
  
  @TypedTag
  const Str state
  
  @TypedTag
  const Int priority := 255
  
  @TypedTag
  const Bool disabled
  
  @TypedTag
  const Str? message
  
  @TypedTag
  const Dict? metadata
  
  @TypedTag
  const Ref? sourceOverride
  
  protected const Filter? ruleFilter
  
  new make(Dict rec, | This | f): super(rec)
  {
    f(this)
    
    this.ruleFilter = Filter.fromStr(this.ruleOn)
  }
  
  public virtual AlarmState alarmState()
  {
    return AlarmState.getState(this.state)
  }
    
  public virtual Bool include(HxContext context, Dict record)
  {
    if (this.disabled)
      return false
    
    return this.ruleFilter.matches(record, context) && this.test(context, record)
  }
  
  // TODO: Allow this method to return alarm specs (target, metadata, and etc...)
  // if it returns True (then the record is alarm, if it returns specs,
  // then it uses those specs)
  protected abstract Bool test(HxContext context, Dict record)
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using concurrent

const class AlarmState 
{
  const Str name
  const Bool isAlarm
  const Bool isFault
  const Bool acknowledgementRequired
  const Int priority
  const Str icon
  
  new make(
    Str name,
    Bool? alarm := false,
    Bool? fault := false,
    Bool? ackRequired := false,
    Int? priority := 255)
  {
    this.name = name
    this.isAlarm = alarm
    this.isFault = fault
    this.acknowledgementRequired = ackRequired
    this.priority = priority
    this.icon = ""
  }
  
  static const AlarmState NORMAL := AlarmState("normal", false, false, false, 200)
  static const AlarmState HIGH_LIMIT := AlarmState("highLimit", true, false, true, 150)
  static const AlarmState LOW_LIMIT := AlarmState("lowLimit", true, false, true, 150)
  static const AlarmState OFF_NORMAL := AlarmState("offNormal", true, false, true, 150) // For Backwards Compatibility
  static const AlarmState FAULT := AlarmState("fault", true, true, true, 100)
  
  // TODO: Populate states from user configurable states
  private static const Unsafe _states := Unsafe([
    "normal": AlarmState.NORMAL,
    "highlimit": AlarmState.HIGH_LIMIT,
    "lowlimit": AlarmState.LOW_LIMIT,
    "offnormal": AlarmState.OFF_NORMAL,
    "fault": AlarmState.FAULT
  ])
  
  public static Str:AlarmState states()
  {
    return _states.val
  }
  
  public static AlarmState? getState(Str? name)
  {
    if (name == null)
      return AlarmState.NORMAL
    
    
    stateKey := name.lower
    
    if (states.containsKey(stateKey))
      return states.get(stateKey)
      
    // Support Arbitrary Alarm States
    state := AlarmState(
      name,
      true,
      false,
      true,
      150
    )
    
    states.set(stateKey, state)
    
    return state
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using hx
using skyarcd

internal class AlarmTargetCounter 
{
  private HxContext context
  private Ref[] targets
  private AlarmLib lib
  
  private Alarm[]? alarms
  private [Ref:AlarmCount]? byRef
  private [Ref:Dict]? refToTarget
  
  private [Ref:AlarmCount] totals := [:]
  
  new make (HxContext context, Ref[] targets)
  {
    this.context = context
    this.targets = targets
    
    this.lib = context.rt.lib("alarm")
  }
  
  public Grid count(Grid? sourceGrid := null)
  {
    this.readAlarms
    this.organizeByRef
    this.targets.each | target | { this.countTarget(target) }
    return this.processTotals(sourceGrid)
  }
  
  private Void readAlarms()
  {
    this.alarms = lib.db.readAlarms(this.context).get
  }
  
  private Void organizeByRef()
  {
    this.alarms.each | alarm |
    {
      count := this.byRef.getOrAdd(alarm.sourceRef) { AlarmCount() }
      
      count.alarmCount++
      
      if (!alarm.isAcked)
        count.unackedCount++
    }
  }
  
  private Void countTarget(Ref targetRef)
  {
    childTargets := this.context.call("alarmTargets", [targetRef]) as Dict[]
    
    total := AlarmCount(context.db.readById(targetRef))
    
    childTargets.each | target |
    {
      targetCount := this.byRef.getChecked(target.id, false)
            
      if (targetCount != null)
        total.add(targetCount)
    }
    
    this.totals.set(targetRef, total)
  }
  
  private Grid processTotals(Grid? sourceGrid)
  {
    builder := GridBuilder()
    
    if (sourceGrid != null)
      sourceGrid.cols.each | col | { builder.addCol(col.name, col.meta)}
    
    builder.addCol("numAlarms")
    builder.addCol("numUnackAlarms")
    
    if (sourceGrid != null)
    {
      sourceGrid.each | record |
      {  
        count := totals.get(record.id)
        
        row := Obj?[,]
        row.capacity = builder.numCols
        
        sourceGrid.cols.each | col | {
          row.add(record.val(col))
        }
        
        row.add(Number(count.alarmCount))
        row.add(Number(count.unackedCount))
        builder.addRow(row)
      }
    }
    else
    {
      this.totals.vals.each | count | 
      {
        row := Obj?[,]
        row.capacity = builder.numCols        
        row.add(Number(count.alarmCount))
        row.add(Number(count.unackedCount))
        builder.addRow(row)
      }
    }
    
    return builder.toGrid
  }
}

internal class AlarmCount
{
  Int alarmCount
  Int unackedCount
  Dict? target
  
  new make (Dict? target := null)
  {
    this.target = target
  }
  
  override Str toStr()
  {
    return "$unackedCount / $alarmCount"
  }
  
  public Void add(AlarmCount count)
  {
    this.alarmCount += count.alarmCount
    this.unackedCount += count.unackedCount
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using concurrent
using haystack

const class AlarmTracker 
{
  const Ref[] sources
  const AlarmDispatcher dispatcher
  const AlarmLib lib
  
  const AtomicRef _enabledStates := AtomicRef(Unsafe([:]))
  
  new make(Ref[] sources, AlarmLib lib, AlarmDispatcher dispatcher)
  {
    this.sources = sources
    this.lib = lib
    this.dispatcher = dispatcher
  }
  
  public Str:Bool enabledStates()
  {
    return ((Unsafe) this._enabledStates.val).val
  }
  
  public Bool hasState(Str state)
  {
    return this.enabledStates.containsKey(state)
  }
  
  public Future? transition(
    Str proposedState,
    Bool enable,
    DateTime occurred,
    Str? message := null,
    Bool? ackRequired := null, // Overrides State's acknowledgement configuration
    Obj? priority := null, // Overrides State's priority
    Dict? meta := null,
    Bool deactivateOtherStates := false)
  {
    if (enable && this.hasState(proposedState))
      return Future.makeCompletable.complete(false)
    
    if (!enable && !this.hasState(proposedState))
      return Future.makeCompletable.complete(false)
    
    state :=  AlarmState.getState(proposedState)
            
    if (priority is Number)
      priority = ((Number) priority).toInt
    
    if (enable)
    {
      this.enabledStates.set(state.name, true)
    }
    else
    {
      this.enabledStates.remove(state.name)
    }
    
    return this.lib.engine.transition(
      this,
      AlarmTransition(
        occurred,
        state,
        enable,
        this.dispatcher,
        ackRequired,
        message,
        priority,
        meta,
        deactivateOtherStates
      )
    )
  }
}

const class AlarmTransition
{
  const AlarmState state
  const Bool enable
  const DateTime occurred
  const Bool acknowledgementRequired
  const Int priority
  const Str? message
  const Dict? meta
  const Bool deactivateOtherStates
  const AlarmDispatcher dispatcher
  
  new make(
    DateTime occurred,
    AlarmState state,
    Bool enable,
    AlarmDispatcher dispatcher,
    Obj? ackRequired := null,
    Str? message := null,
    Int? priority := null,
    Dict? meta := null,
    Bool deactivateOtherStates := false)
  {
    this.occurred = occurred
    this.state = state
    this.enable = enable
    this.dispatcher = dispatcher
    this.acknowledgementRequired = ackRequired ?: state.acknowledgementRequired
    this.priority = priority ?: state.priority
    this.message = message
    this.meta = meta
    this.deactivateOtherStates = deactivateOtherStates
  }
  
  public AlarmStateModel toStateModel()
  {
    return AlarmStateModel.makeNew(this.state.name, this.priority)
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


const class AlarmTransitionMessage : AlarmMessage 
{
  const AlarmTracker tracker;
  const AlarmTransition transition;
  
  new make(
    AlarmTracker tracker,
    AlarmTransition transition): super(AlarmOp.transition)
  {
    this.tracker = tracker
    this.transition = transition
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using folio

const class AlarmUpdateMessage : AlarmMessage 
{
  const Alarm changes
  
  new make(Alarm changes): super(AlarmOp.update)
  {
    this.changes = changes
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using concurrent
using haystack
using obs

const class AlarmsObservable : Observable 
{
  override Str name() { "obsAlarms" }
  
  public Void notify(Alarm[] alarms)
  {
    if (!this.hasSubscriptions)
      return
    
    alarms.each | alarm | {  
      this.subscriptions.each | sub | {
        if (!sub.isSubscribed)
          return
        
        sub.send(
          AlarmObservation.wrap(
            Etc.makeDict([
              "ts": DateTime.now,
              "alarm": alarm
            ])
          )
        )
      }
    }
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using hx
using concurrent

enum class AlarmsRequestOp
{
  get,
  getTarget,
  getCount,
  remove
  
  AlarmDBOp toAlarmDBOp()
  {
    switch (this)
    {
      case AlarmsRequestOp.getTarget:
        return AlarmDBOp.getAlarmsByTarget
    
      case AlarmsRequestOp.getCount:
        return AlarmDBOp.getAlarmCount
      
      case AlarmsRequestOp.remove:
        return AlarmDBOp.removeAlarms
    }
    
    return AlarmDBOp.getAlarmByID
  }
}

const class AlarmsRequestMessage : AlarmDBMessage 
{
  const AtomicRef _itemIds := AtomicRef(Unsafe([,]))
  
  new make(HxContext? context, Ref[]? itemIds, AlarmsRequestOp op := AlarmsRequestOp.get): super(op.toAlarmDBOp, context)
  {
    if (itemIds != null)
      this.itemIds.addAll(itemIds)
  }
  
  public Ref[] itemIds()
  {
    return ((Unsafe) this._itemIds.val).val
  }
  
  override public Obj? key()
  {
    //if (this.op == AlarmsRequestOp.remove)
    //  return 0xF1F2F3 // To compare with CreateOrUpdateMessage (Create Update and Delete need to be coalesced appropriately)
    
    return null
  }
  
  public AlarmsRequestMessage merge(AlarmsRequestMessage msgB)
  {
    msgB.itemIds.each |id| 
    {
      if (!this.itemIds.contains(id))
        this.itemIds.add(id)
    }
    
    return this
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using hx
using haystack
using concurrent

const class CallFuncRoute : AlarmRoute
{
  public static const Str TYPE := "callFunc"
  
  @TypedTag
  const Str funcName
    
  new make(Dict rec, | This | f) : super(rec, f) {}
  
  override Future route(HxContext context, Dict alarm, Dict? source)
  {   
    func := context.evalToFunc(this.funcName)
    funcArgs := [alarm, source] 

    while (funcArgs.size > func.arity)
      funcArgs.pop
    
    return Future.makeCompletable().complete(
      func.call(context, funcArgs)
    )
  }
  
  static new wrap(Dict? dict)
  {
    if (dict == null)
      return null
    
    return create(CallFuncRoute#, dict)
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using hx
using concurrent

const class CommandPointsRoute : AlarmRoute 
{
  public static const Str TYPE := "commandPoints"
  
  @TypedTag
  const Str targetFilter
  
  @TypedTag
  const Int? writeLevel
  
  @TypedTag
  const Obj? value
  
  const Filter targetFilterInst
  
  new make(Dict rec, | This | f) : super(rec, f) 
  {
    this.targetFilterInst = Filter.fromStr(this.targetFilter)
  }
  
  override Future route(HxContext context, Dict alarm, Dict? source)
  {
    targets := context.db.readAllList(this.targetFilterInst)
        
    targets.each | target |
    {
      try
      {
        // Note: if we need the status, map the target list to await ".get" each write and return it.
        context.rt.pointWrite.write(
          target, this.value, this.writeLevel ?: 17 , "Route: ${this.dis}"
        )
      }
      catch (Err error)
      {
        log.err("Unable to write point: ${target.id}", error)
      }
    }
    
    return Future.makeCompletable.complete(null)
  }
  
  static new wrap(Dict? dict)
  {
    if (dict == null)
      return null
    
    return create(CommandPointsRoute#, dict)
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack

const class CreateOrUpdateMessage : AlarmDBMessage
{
  private const Unsafe _alarms := Unsafe([:])
  private const Bool allowCoalesce
  
  new make(Alarm[] alarms, Bool allowCoalesce) : super(AlarmDBOp.createUpdateAlarm)
  {
    this.allowCoalesce = allowCoalesce
    this.addAlarms(alarms)
  }
  
  public Alarm[] alarms()
  {
    return ((Ref:Alarm)this._alarms.val).vals
  }
  
  private Ref:Alarm alarmTable()
  {
    return this._alarms.val
  }
  
  private Void addAlarms(Alarm[] alarms)
  {
    alarms.each | alarm | {
      if (this.alarmTable.containsKey(alarm.id))
      {
        existing := this.alarmTable.get(alarm.id)       
        this.alarmTable.set(
          alarm.id,
          Alarm.wrap(
            Etc.dictMerge(existing, alarm)
          )
        )
      }
      else
      {
        this.alarmTable.set(alarm.id, alarm)
      }
    }
  }
  
  override Obj? key()
  {
    if (allowCoalesce)
      return 0xF1F2F3
    
    return null
  }
  
  public CreateOrUpdateMessage merge(
    CreateOrUpdateMessage msgB)
  {
    this.addAlarms(msgB.alarms)
    return this
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


const class DatabasePaginationRequest
{
  const Int resultSize    // Page Size
  const Int entityOffset    // Index # or Page #
  const Str sortColumnName  // Sort column
  const Str sortDirection   // Sort Direction
  const Str? filter     // Filter
  
  new make(
    Int resultSize,
    Int entityOffset,
    Str sortCol := "occurred",
    Str sortDirection := "DESC",
    Str? filter := null)
  {
    this.resultSize = resultSize
    this.entityOffset = entityOffset
    this.sortColumnName = sortCol
    this.sortDirection = sortDirection
    this.filter = filter
  }
  
  public Int currentPage()
  {
    return this.entityOffset.toFloat.divInt(
      this.resultSize
    ).ceil.toInt.max(1)
  }
}

const class DatabasePaginationResult
{
  const Int currentOffset // Index # or Page #
  const Int resultSize  // Page Size
  const Int totalResults  // Total number of pages
  const Int totalItems  // Total number of items
  
  new make(Int currentOffset, Int resultSize, Int totalItems, Int totalResults)
  {
    this.currentOffset = currentOffset
    this.resultSize = resultSize
    this.totalResults = totalResults
    this.totalItems = totalItems
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using concurrent
using haystack
using axon
using hx

const class DisabledHistoryDB : AlarmHistoryDatabase
{
  override public Future insert(AlarmHistoryEntry[] entries) // Future<AlarmHistoryEntry[]>
  {
    return Future.makeCompletable.complete([,])
  }
  
  override public Future read(HxContext context, DateSpan? range, Ref[]? targets, DatabasePaginationRequest? pagination := null) // Future<AlarmHistoryResult>
  {
    return Future.makeCompletable.complete(AlarmHistoryResult([,]))
  }
  
  override public SourceStream? readStream(HxContext context, DateSpan? range, Ref[]? targets, DatabasePaginationRequest? pagination := null) // SourceStream<AlarmHistoryEntry>
  {
    return null
  }
  
  override public Future historyCount(Ref? target := null) // Future<Int>
  {
    return Future.makeCompletable.complete(0)
  }
  
  override public Future removeBetween(DateSpan range)
  {
    return Future.makeCompletable.complete([,])
  }
  
  override public Future removeBefore(DateTime date)
  {
    return Future.makeCompletable.complete([,])
  }
  
  override public Future clear(Bool? rebuild := false)
  {
    return Future.makeCompletable.complete(null)
  }
  
  override public Future start()
  {
    return Future.makeCompletable.complete(null)
  }
  
  override public Future stop()
  {
    return Future.makeCompletable.complete(null)
  }
  
  override public Future runHouseKeeping()
  {
    return Future.makeCompletable.complete(null)
  }
  
  public override Future query(Str query)
  {
    return Future.makeCompletable.complete(null)
  }
  
  public override Future import(AlarmHistoryEntry[] data)
  {
    return Future.makeCompletable.complete(null)
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using hx
using concurrent

const class ExecuteAxonRoute : AlarmRoute
{
  public static const Str TYPE := "executeAxon"
  
  @TypedTag
  const Str axonExpr
    
  new make(Dict rec, | This | f) : super(rec, f) {}
  
  override Future route(HxContext context, Dict alarm, Dict? source)
  {
    // TODO: Support string interpolation with the alarm and source.
    
    return Future.makeCompletable().complete(
      context.eval(this.axonExpr)
    )
  }
  
  static new wrap(Dict? dict)
  {
    if (dict == null)
      return null
    
    return create(ExecuteAxonRoute#, dict)
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using [java] io.questdb::WorkerPoolManager
using [java] io.questdb.mp::WorkerPool
using [java] io.questdb.mp::WorkerPoolUtils
using [java] io.questdb.cairo::CairoEngine
using [java] io.questdb.cairo::CairoConfiguration

using [java] io.questdb.cairo.wal::ApplyWal2TableJob

class FINWorkerPoolManager// : WorkerPoolManager
{
  AlarmHistoryQuestDB db
  
  new make(AlarmHistoryQuestDB db)// : super(db.config)
  {
    this.db = db
  }
  /*
  override protected Void configureWorkerPools(
    WorkerPool? sharedPoolQuery,
    WorkerPool? sharedPoolWrite)
  {
    sharedPoolWrite.assign(this.db.engine.getEngineMaintenanceJob())
    
    // Configure Query Jobs
    WorkerPoolUtils.setupQueryJobs(
      sharedPoolQuery,
      this.db.engine
    )
    
    // Configure Writer Jobs
    WorkerPoolUtils.setupWriterJobs(
      sharedPoolWrite,
      this.db.engine
    )
    
    // Configure WAL Apply Jobs
    sharedPoolWrite.assign(
      this.db.config.getFactoryProvider.getWalJobFactory.createCheckWalTransactionsJob(this.db.engine)
    )
    
    // Create Purge Job
    purgeJob := this.db.config.getFactoryProvider.getWalJobFactory.createWalPurgeJob(this) 
    this.db.engine.setWalPurgeJobRunLock(purgeJob.getRunLock)
    purgeJob.delayByHalfInterval
    sharedPoolWrite.assign(purgeJob)
    sharedPoolWrite.freeOnExit(purgeJob)
    
    // Create Apply Job Per Worker
    for (i := 0; i < sharedPoolQuery.getWorkerCount; i++)
    {
      applyJob := ApplyWal2TableJob(
        this.db.engine,
        sharedPoolQuery.getWorkerCount
      )
      
      sharedPoolWrite.assign(i, applyJob)
      sharedPoolWrite.freeOnExit(applyJob)
    }
  }*/
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using hx

const class FuncEvaluationRule : AlarmRule
{
  public static const Str TYPE := "funcEval"
  
  @TypedTag
  const Str funcName
    
  new make(Dict rec, | This | f) : super(rec, f) {}
  
  protected override Bool test(HxContext context, Dict record)
  {
    return context.call(this.funcName, [record])
  }
  
  static new wrap(Dict? dict)
  {
    if (dict == null)
      return null
    
    return create(FuncEvaluationRule#, dict)
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


const class HistoryClearMessage : AlarmHistoryDBMessage 
{
  const Bool rebuild
  
  new make(Bool? rebuild := false) : super(AlarmHistoryDBOp.clear)
  {
    this.rebuild = rebuild
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack

const class HistoryCountMessage : AlarmHistoryDBMessage
{
  const Ref? targetId
  
  new make(Ref? targetId := null): super(AlarmHistoryDBOp.count)
  {
    this.targetId = targetId    
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack

const class HistoryImportMessage : AlarmHistoryDBMessage
{
  const AlarmHistoryEntry[] data
    
  new make(AlarmHistoryEntry[] data): super(AlarmHistoryDBOp.import)
  {
    this.data = data
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


const class HistoryInsertMessage : AlarmHistoryDBMessage 
{
  const AlarmHistoryEntry[] records
  
  new make(AlarmHistoryEntry[] records): super(AlarmHistoryDBOp.insert)
  {
    this.records = records
  }
  
  public override Obj? key()
  {
    return 0xABCDEF
  }
  
  public override AlarmHistoryDBMessage merge(AlarmHistoryDBMessage msgB)
  {
    records := this.records.rw
    records.addAll(((HistoryInsertMessage) msgB).records)
    
    return HistoryInsertMessage(
      records
    )
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack

const class HistoryQueryMessage : AlarmHistoryDBMessage
{
  const Str query
  
  new make(Str query): super(AlarmHistoryDBOp.query)
  {
    this.query = query
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using axon
using hx

const class HistoryReadMessage : AlarmHistoryDBMessage
{
  const DateSpan? range
  const Ref[]? targets
  const Unsafe? _stream 
  const DatabasePaginationRequest? pagination
  
  new make(
    HxContext context,
    DateSpan? range := null,
    Ref[]? targets := null,
    DatabasePaginationRequest? pagination := null,
    SourceStream? stream := null): super(AlarmHistoryDBOp.read, context)
  {
    this.range = range
    this.targets = targets
    this.pagination = pagination
    this._stream = Unsafe(stream)
  }
  
  public SourceStream? stream()
  {
    return this._stream.val
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


const class HistoryRemoveBeforeMessage : AlarmHistoryDBMessage
{
  const DateTime date
  
  new make(DateTime date): super(AlarmHistoryDBOp.removeBefore)
  {
    this.date = date
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack

const class HistoryRemoveBetweenMessage : AlarmHistoryDBMessage
{
  const DateSpan range
  
  new make(DateSpan range): super(AlarmHistoryDBOp.removeBetween)
  {
    this.range = range
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


const class ImportAlarmsMessage : AlarmDBMessage
{
  const Alarm[] records
  
  new make(Alarm[] records): super(AlarmDBOp.import)
  {
    this.records = records
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using concurrent
using haystack
using hx

const class MockAlarmDatabase : AlarmDatabase 
{
  private const Unsafe data := Unsafe([:])
  
  private const AtomicInt lastPoll := AtomicInt(-1)
  
  public override Future readAlarms(HxContext context, Ref[]? targets := null, DateSpan? range := null, DatabasePaginationRequest? pagination := null)
  {
    Dict[] result := (targets != null) 
      ? this.db.vals.findAll | record |
        {  
          return targets.contains(record.sourceRef)
        }
      : this.db.vals
    
    if (range != null)
    {
      result = result.findAll | record |
      {  
        return range.contains(
          ((DateTime) record.get("occurred")).date
        )
      }
    }
    
    return Future.makeCompletable().complete(
      result
    )
  }
  
  public override Future getTargetsAlarms(HxContext context, Ref[] targets) // Future<Dict[]>
  {
    result := this.db.vals.findAll | record |
    {
      return targets.contains(record.sourceRef)
    }
    
    
    return Future.makeCompletable().complete(
      result
    )
  }
  
  public override Future get(HxContext context, Ref[] alarmIds)
  {
    return Future.makeCompletable().complete(
      alarmIds.map | alarmId | {
        this.db.get(alarmId)
      }
    )
  }
  
  public override Future createOrUpdate(Alarm[] alarms, Bool coalesce := true)
  {
    //echo ("Write to alarm db: ${alarm} - ${alarm.id}")
    
    alarms.each | alarm | {  
      this.db.set(alarm.id, alarm) // TODO: Merge?
    }
    
    return Future.makeCompletable().complete(
      alarms
    )
  }
  
  public override Future alarmCount(Ref[]? targets := null)
  {
    if (targets != null)
    {
      return Future.makeCompletable.complete(
        this.db.vals.findAll | alarm | { targets.contains(alarm.sourceRef) }.size
      )
    }
    
    return Future.makeCompletable.complete(
      this.db.size
    )
  }
  
  public override Future remove(Ref[] alarmIds)
  {
    alarmIds.map | alarmId| { 
      this.db.remove(alarmId)
    }
    
    return Future.makeCompletable().complete(null)
  }
  
  public override Future poll(HxContext context, Int lastPollTime, Ref[]? targets := null)
  {
    values := (targets != null)
      ? targets.map |ref| { this.db.get(ref) }
      : this.db.vals
    
    
    lastIndex := this.lastPoll.getAndSet(values.size - 1)
    
    return Future.makeCompletable().complete(
      values.getRange(
        Range(
          lastIndex,
          values.size - 1,
          false
        )
      )
    )
  }
  
  public override Future runHouseKeeping()
  {
    return Future.makeCompletable.complete(null)
  }
  
  private [Ref:Alarm] db()
  {
    return ([Ref:Alarm]) this.data.val
  }
  
  public override Future import(Alarm[] data)
  {
    data.each | item | {
      this.db.set(item.id, item)
    }
    
    return Future.makeCompletable.complete(this.db.vals)
  }
  
  public override Future start() { return Future.makeCompletable.complete(null) }
  public override Future stop() { return Future.makeCompletable.complete(null) }
  
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using concurrent
using axon
using haystack
using hx

const class MockAlarmHistoryDatabase : AlarmHistoryDatabase 
{
  const Unsafe timeData := Unsafe([,])
  const Unsafe recordData := Unsafe([,])
    
  public override Future insert(AlarmHistoryEntry[] records)
  {
    //echo ("Write to history db: ${record} - ${record.id}")    
    
    //record = Etc.dictSet(record, "created", created)
    
    records.each | record| {
      timestamps.add(record.created.toJava)
      records.add(record)
    }

    return Future.makeCompletable.complete(
      records
    )
  }
  
  public override Future read(HxContext context, DateSpan? range, Ref[]? targets, DatabasePaginationRequest? pagination := null)
  {
    if (range == null)
    {
      return Future.makeCompletable.complete(
        this.records
      )
    }
    
    startTime := range.start.toDateTime(Time.make(0, 0, 0)).toJava
    endTime := range.end.toDateTime(Time.make(23, 59, 59)).toJava
            
    Int firstIndex := this.timestamps.binarySearch(startTime)
    Int lastIndex := this.timestamps.binarySearch(endTime)
    
    if (firstIndex < 0)
      firstIndex = this.timestamps.size + firstIndex
    
    if (lastIndex < 0)
      lastIndex = this.timestamps.size + lastIndex
    
    Dict[] results := [,]
    
    for (i := firstIndex; i <= lastIndex; i++)
      results.add(records.get(i))
    
    return Future.makeCompletable.complete(
      results
    )
  }
  
  public override SourceStream? readStream(HxContext context, DateSpan? range, Ref[]? targets, DatabasePaginationRequest? pagination := null)
  {
    return null
    /* -- this is internal :(
    return ListStream(
      (Obj?[]) this.read(range, targets, pagination).get
    )
    */
  }
  
  public override Future removeBetween(DateSpan range)
  {
    startTime := range.start.toDateTime(Time.make(0, 0, 0)).toJava
    endTime := range.end.toDateTime(Time.make(23, 59, 59)).toJava
            
    Int firstIndex := this.timestamps.binarySearch(startTime)
    Int lastIndex := this.timestamps.binarySearch(endTime)
    
    if (firstIndex < 0)
      firstIndex = this.timestamps.size + firstIndex
    
    if (lastIndex < 0)
      lastIndex = this.timestamps.size + lastIndex
    
    for (i := lastIndex; i >= firstIndex; i--)
    {
      this.timestamps.removeAt(i)
      this.records.removeAt(i)
    }
    
    
    return Future.makeCompletable.complete(null)
  }
  
  public override Future removeBefore(DateTime date)
  {
    endTime := date.toJava
    
    Int lastIndex := this.timestamps.binarySearch(endTime)
    
    for (i := lastIndex; i >= 0; i--)
    {
      this.timestamps.removeAt(i)
      this.records.removeAt(i)
    }   
    
    return Future.makeCompletable.complete(null)
  }
  
  public override Future historyCount(Ref? target := null)
  {
    if (target == null)
      return Future.makeCompletable.complete(this.timestamps.size)
    
    return Future.makeCompletable.complete(
      this.records.findAll | rec | {  
        rec.get("sourceRef").equals(target)
      }.size
    )   
  }
  
  public override Future clear(Bool? rebuild := false)
  {
    this.timestamps.clear
    this.records.clear
    
    return Future.makeCompletable.complete(null)
  }
  
  public override Future runHouseKeeping()
  {
    return Future.makeCompletable.complete(null)
  }
  
  private Int[] timestamps()
  {
    return (Int[]) this.timeData.val
  }
  
  public Dict[] records()
  {
    return (Dict[]) this.recordData.val
  }
  
  public override Future import(AlarmHistoryEntry[] data)
  {
    return Future.makeCompletable.complete(null)
  }
  public override Future start() { return Future.makeCompletable.complete(null) }
  public override Future stop() { return Future.makeCompletable.complete(null) }
  public override Future query(Str query) { return Future.makeCompletable.complete(null) }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using concurrent
using hx

const class MockAlarmRoute : AlarmRoute 
{
  const AtomicRef testFunc := AtomicRef()
  
  new make(Dict rec, | This | f) : super(rec, f)
  {   
  }
  
  override Future route(HxContext context, Dict alarm, Dict? source)
  {
    ((Func) testFunc.val).call    
    return Future.makeCompletable.complete(null)
  }
  
  static new wrap(Func testFunc)
  {
    result := create(MockAlarmRoute#, Etc.makeDict(["routeOn": "testItem"])) as MockAlarmRoute
    result.testFunc.getAndSet(testFunc)
    
    return result
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using concurrent
using hx
using notificationExt

const class NotificationRoute : AlarmRoute
{
  public static const Str TYPE := "notification"
  
  new make(Dict rec, | This | f) : super(rec, f) {}
  
  override Future route(HxContext context, Dict alarm, Dict? source)
  {
    NotificationLib? notification := context.rt.lib("notification", false)
    
    if (notification  == null)
    {
      log.warn("Cannot send notification: Notification Lib doesn't exist or is not enabled.")
      return Future.makeCompletable.complete(null)
    }
    
    return notification.notify(
      context,
      "alarm",
      alarm
    )
  }
  
  static new wrap(Dict? dict)
  {
    if (dict == null)
      return null
    
    return create(NotificationRoute#, dict)
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using hx

const class OutOfRangeRule : AlarmRule 
{
  public static const Str TYPE := "outOfRange"
  
  @TypedTag
  const Number? highLimit
  
  @TypedTag
  const Number? lowLimit
  
  new make(Dict rec, | This | f) : super(rec, f) {}
  
  protected override Bool test(HxContext context, Dict record)
  {
    if (!record.has("curVal"))
      return false
    
    if (highLimit != null && record.get("curVal") > this.highLimit)
      return true;
    
    if (lowLimit != null && record.get("curVal") < this.lowLimit)
      return true;
    
    return false
  }
  
  static new wrap(Dict? dict)
  {
    if (dict == null)
      return null
    
    return create(OutOfRangeRule#, dict)
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using hx

const class PollAlarmsMessage : AlarmDBMessage 
{
  const Int lastPollTime
  const Ref[]? targets
  
  new make(HxContext context, Int lastPollTime, Ref[]? targets) : super(AlarmDBOp.poll, context)
  {
    this.lastPollTime = lastPollTime
    this.targets = targets
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using concurrent

const class PromisingActor : Actor 
{
  static const ActorPool POOL := ActorPool()
  
  const Log log := Log.get("PromisingActor")
  
  new make() : super(POOL)
  {
    
  }
  
  override protected Obj? receive(Obj? message)
  {
    PromisingMessage msg := message
    
    Obj[] results := [,]
    
    msg.futures.each | future | 
    {
      try
      {
        results.add(
          future.get(msg.timeout)
        )
      }
      catch (Err error)
      {
        this.log.err("Error resolving future", error)
      }
    }
    
    return results
  }
  
  public Future resolveAfterComplete(Future[] futures, Duration? timeout := null)
  {
    return this.send(
      PromisingMessage(futures, timeout)
    )
  }
  
  public static Future resolveAfter(Future[] futures, Duration? timeout := null)
  {
    actor := PromisingActor()
    return actor.resolveAfterComplete(futures, timeout)
  }
}

const class PromisingMessage
{
  const Future[] futures
  const Duration? timeout
  
  new make (Future[] futures, Duration? timeout := null)
  {
    this.futures = futures
    this.timeout = timeout
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using [java] io.questdb.cairo.sql::Record
using [java] io.questdb.cairo.sql::RecordCursor
using haystack
using hx

class QuestDBCursor 
{
  HxContext context
  private QuestDBColumn sourceColumn
  private QuestDBUtility util
  private DatabasePaginationRequest? pagination
  
  public Int includeCounter 
  
  new make(HxContext context, QuestDBUtility util, DatabasePaginationRequest? pagination)
  {
    this.context = context
    this.util = util
    this.sourceColumn = util.getColumnByName("sourceRef")
    this.pagination = pagination
    
    this.includeCounter = 0
  }
  
  public Bool shouldInclude(Record record)
  {   
    if (!this.canReadRecord(record))
      return false
    
    this.includeCounter++
    
    // This scans the DB starting from the beginning each time.
    // This will affect performance further down the dataset.
    // This was created to support filting .
    return this.includeCounter >= this.pagination.entityOffset
  }
  
  private Bool canReadRecord(Record record)
  {
    sourceRef := this.sourceColumn.getValue(record) as Ref
    
    if (sourceRef == null)
      return true // Unable to use permission descriptors on this entry.
    
    source := this.context.db.readById(sourceRef, false)
      
    // Alarm source doesn't exist in this database
    // Unable to use permission descriptors on this entry
    if (source == null)
      return true
    
    return context.canRead(source)
  }
  
  public Bool shouldEnd()
  {
    if (this.pagination != null && this.pagination.resultSize > 0)
      return this.includeCounter >= (this.pagination.entityOffset + this.pagination.resultSize)
    
    return false
  }
  
  public Void reset()
  {
    this.includeCounter = 0
  }
  
  public Int count(RecordCursor cursor)
  {
    result := 0
    
    while (cursor.hasNext)
    {
      if (this.canReadRecord(cursor.getRecord))
        result++
    }
    
    return result
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using [java] io.questdb.cairo::TableWriterAPI
using [java] io.questdb.cairo::TableWriter$Row as TableRow
using [java] io.questdb.cairo.sql::Record
using [java] io.questdb.cairo.sql::TableMetadata
using [java] io.questdb.std.str::Utf8String
using [java] java.lang::String

using util
using haystack

@NoDoc
const class QuestDBUtility
{
  static const Log log := Log.get("QuestDB")
  public const QuestDBColumn[] columns
  
  new make(TableMetadata metadata)
  {
    this.columns = QuestDBUtility.Columns.mapNotNull | col |
    {
      try
      {
        result := col.clone(
          metadata.getColumnIndex(col.name)
        )
        return result
      }
      catch (Err error)
      {
        // Column Index doesn't exist. SCHEMA mismatch issue
        // Should rebuild the database schema.        
        log.err("Database schema mismatch, rebuild database with 'clearAlarmHistory(null, true)' then restart.'", error)
      }
    
      return null
    }
  }
  
  public Void insertRecord(AlarmHistoryEntry entry, TableWriterAPI writer)
  {
    row := writer.newRow(entry.created.toUtc.toJava.mult(1000))
    
    this.columns.each | col |
    {
      value := (col.fantomType == Marker#)
        ? entry.has(col.name)
        : entry.get(col.name)

      if (value != null)
        col.writeValue(row, value)
    }
    
    row.append
  }
      
  public AlarmHistoryEntry entryFromRecord(Record record)
  {
    result := [:]
    
    this.columns.each | col |
    {
      try
      {
        value := this.getValue(record, col)
        
        if (value != null)
          result.add(col.name, value)
      }
      catch (Err error)
      {
        log.err("Error Reading Column '${col.name}'", error)
      }
    }
                    
    return AlarmHistoryEntry.wrap(
      Etc.makeDict(result)
    )
  }
  
  public Obj? getValue(Record record, QuestDBColumn column)
  {
    return column.getValue(record)
  }
  
  public QuestDBColumn? getColumnByName(Str columnName)
  {
    return this.columns.find | col | { col.name.lower == columnName.lower }
  }
  
  
  ///////////////////////////////
  // STATICS
  ///////////////////////////////
  
  public static const QuestDBColumn[] Columns := [
    QuestDBColumn("created", "TIMESTAMP", DateTime#),
    QuestDBColumn("id", "VARCHAR", Ref#),
    QuestDBColumn("sourceRef", "VARCHAR", Ref#),
    QuestDBColumn("priority", "INT", Number#),
    QuestDBColumn("toState", "VARCHAR", Str#),
    QuestDBColumn("active", "BOOLEAN", Marker#),
    QuestDBColumn("fromState", "VARCHAR", Str#),
    QuestDBColumn("occurred", "TIMESTAMP", DateTime#),
    QuestDBColumn("curVal", "VARCHAR", Obj#),
    QuestDBColumn("curStatus", "VARCHAR", Str#),
    QuestDBColumn("message", "VARCHAR", Str#),
    QuestDBColumn("acked", "TIMESTAMP", DateTime#),
    QuestDBColumn("ackedBy", "VARCHAR", Str#),
    QuestDBColumn("ackMessage", "VARCHAR", Str#),
    QuestDBColumn("metadata", "VARCHAR", Dict#)
  ]
  
  public static Str CreateHistoryDDL()
  {
    result := StrBuf()
    result.add("CREATE TABLE IF NOT EXISTS ${AlarmHistoryQuestDB.TABLE_NAME}")
    result.add("(\n")
    result.add(QuestDBUtility.Columns.map | col | { "\t${col.toDDL}" }.join(",\n") + "\n")
    result.add(") TIMESTAMP(created) PARTITION BY DAY WAL;")

    return result.toStr
  }
}

const class QuestDBColumn
{
  const Log log := Log.get("QuestDBColumn")
  const Str name
  const Str questType
  const Type fantomType
  const Int? index
  
  new make(Str name, Str questType, Type fantomType, Int? index := null)
  {
    this.name = name
    this.questType = questType
    this.fantomType = fantomType
    this.index = index
  }
  
  Str toDDL()
  {
    return "$name $questType"
  }
  
  /**
    Get value from a table record
  */
  Obj? getValue(Record record)
  {
    switch (questType.lower)
    {
      case "timestamp":
        return decodeValue(record.getTimestamp(this.index))
      
      case "int":
        return decodeValue(record.getInt(this.index))
      
      case "long":
        return decodeValue(record.getLong(this.index))
      
      case "varchar":
        return decodeValue(record.getVarcharA(this.index)?.asAsciiCharSequence?.toString)
      
      case "boolean":
        return decodeValue(record.getBool(this.index))
      
      /*
      case "binary":
      {
        record.getBin(col).
      }*/
    }
    
    return null
  }
  
  /*
    Write Value into a table row
  */
  Void writeValue(TableRow row, Obj? value)
  {
    value = this.encodeValue(value)
    
    if (value == null)
      return
    
    switch (questType.lower)
    {
      case "timestamp":
        row.putTimestamp(this.index, value)
      
      case "int":
        row.putInt(this.index, value as Int)
        
      case "varchar":
        row.putVarchar(this.index, Utf8String(value as String))
      
      case "boolean":
        row.putBool(this.index, value)
    }
  }
  
  /**
    Encode value into QuestDB supported data type
  */
  private Obj? encodeValue(Obj? value)
  {
    if (value == null)
      return null
    
    switch (this.fantomType)
    {
      case Ref#:
        return ((Ref) value).toCode
      
      case DateTime#:
        return ((DateTime) value).toUtc.toJava.mult(1000)
      
      case Number#:
        return ((Number) value).toInt
      
      case Int#:
        return value as Int
      
      case Marker#:
        return value == true
      
      case Obj#:
      case Dict#:
        resultBuf := StrBuf()
        writer := JsonWriter(resultBuf.out)
        writer.writeVal(value)
        return resultBuf.toStr
    }
    
    return value
  }
  
  /**
    Decode value from QuestDB type into Haystack
  */
  private Obj? decodeValue(Obj? value)
  {
    if (value == null)
      return null
    
    try
    {
      switch (this.fantomType)
      {
        case Ref#:
          return Ref.fromCode(value)
        
        case Marker#:
          return (value == true)
            ? Marker.val
            : null
        
        case DateTime#:
          return DateTime.fromJava(
              ((Int) value).div(1000),
              TimeZone.utc
            )?.toTimeZone(TimeZone.cur)
        
        case Number#:
          if (questType.lower == "int")
            return Number.makeInt(value)
          else
            return Number.makeNum(value)
        
        case Obj#:
        case Dict#:
          if (((Str) value).size == 0)
            return null
        
          reader := JsonReader(((Str) value).in)
          result := reader.readVal(true)
          return result
        
        case Str#:
          return value as Str
        
        case Int#:
          return value as Int
      }
    }
    catch (Err error)
    {
      this.log.err("Error decoding value: $value", error)
    }
    
    return value
  }
  
  QuestDBColumn clone(Int? index := null)
  {
    return QuestDBColumn(this.name, this.questType, this.fantomType, index ?: this.index)
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using hx

const class ReadAlarmsMessage : AlarmDBMessage
{
  const Ref[]? targets
  const DateSpan? span
  const DatabasePaginationRequest? pagination
  
  new make (
    HxContext context,
    Ref[]? targets,
    DateSpan? span,
    DatabasePaginationRequest? pagination): super(AlarmDBOp.readAlarms, context)
  {
    this.targets = targets
    this.span = span
    this.pagination = pagination
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack

const class RecordChangeMessage : RuleMessage
{
  const DateTime time
  const Ref recordId
  const Dict record
  
  new make(Dict record, DateTime time) : super(RuleOp.recordChange)
  {
    this.time = time
    this.record = record
    this.recordId = record.id
  }
  
  public override Obj? key()
  {
    return this.record.id.id
  }
  
  override public Int compare(Obj item)
  {
    bMsg := item as RecordChangeMessage
    
    return this.time <=> bMsg.time
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack

const class RemoveRoutesMessage : RoutingMessage
{
  const Ref[] routeIds
  
  new make(Ref[] routeIds) : super(RoutingOp.removeRoutes)
  {
    this.routeIds = routeIds
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack

const class RemoveRulesMessage : RuleMessage 
{
  const Ref[] ruleIds
  
  new make(Ref[] ruleIds) : super(RuleOp.removeRules)
  {
    this.ruleIds = ruleIds
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack

const class RouteAlarmMessage : RoutingMessage
{
  const Dict alarm
  const Dict? alarmSource
  
  new make(Dict alarm, Dict? alarmSource) : super(RoutingOp.route)
  {
    this.alarm = alarm
    this.alarmSource = alarmSource
  }
  
  override Obj? key()
  {
    return alarm.has("id")
      ? alarm.id.id
      : null
  }
  
  override RoutingMessage merge(RoutingMessage msgB)
  {
    if (this.time.compare(msgB.time) > 1)
      return this
    
    return msgB
  }
  
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


enum class RoutingOp {clearRoutes, addRoutes, removeRoutes, getRoutes, route}

const class RoutingMessage
{
  const DateTime time
  const RoutingOp op
  
  new make(RoutingOp op)
  {
    this.time = DateTime.now
    this.op = op
  }
  
  virtual Obj? key()
  {
    return null
  }
  
  virtual RoutingMessage merge(RoutingMessage msgB)
  {
    return msgB
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


enum class RuleOp {addRules, removeRules, clearRules, recordChange}

const class RuleMessage 
{
  const RuleOp op
  
  new make(RuleOp op)
  {
    this.op = op
  }
  
  override public Int compare(Obj item)
  {
    return 0
  }
  
  public virtual RuleMessage merge(RuleMessage msgB)
  {
    return this
  }
  
  public virtual Obj? key()
  {
    return null
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using concurrent
using haystack
using hx
using folio
using skyarc

// TODO: add ability to execute rule test arbitrarily
// This will enable AI based FDD, nightly checks, and etc...
const class RulesEngine
{
  public static const Str REC_TAG := "alarmRule"
  
  const AlarmLib lib
  const Actor actor
  const AtomicBool enabled
  const FolioFlatFile management

  new make(
    ActorPool pool,
    AlarmLib lib)
  {
    this.lib = lib
    
    this.management = FolioFlatFile.open(
      FolioConfig {
        it.name = "Alarm Rules Management"
        it.dir = lib.rt.dir.plus(`alarms/rules/manage/`)
      }
    )
    
    this.actor = Actor.makeCoalescing(
      pool,
      | msg -> Obj? | { 
        return this.messageToKey(msg)
      },
      | msgA, msgB -> Obj? | {
        return this.mergeMessage(msgA, msgB)
      },
      | msg -> Obj? | { 
        return this.receive(msg)
      }
    )
    
    this.enabled = AtomicBool(true)
  }

  public Future recordChange(Dict record, DateTime? time)
  {
    if (!this.enabled.val)
      return Future.makeCompletable.complete(null)
    
    if (time == null)
      time = record.get("mod")
    
    return this.actor.send(
      RecordChangeMessage(
        record,
        time
      )
    ) 
  }
  
  public Future addRules(AlarmRule[] rules)
  {
    return this.actor.send(
      AddRulesMessage(rules)
    )
  }
  
  public Future removeRules(Ref[] ruleIds)
  {
    return this.actor.send(
      RemoveRulesMessage(ruleIds)
    )
  }
  
  public Future clearRules()
  {
    return this.actor.send(
      RuleMessage(
        RuleOp.clearRules
      )
    )
  }
  
  public Void enable()
  {
    this.enabled.getAndSet(true)
  }
  
  public Void disable()
  {
    this.enabled.getAndSet(false)
  }
  
  private Obj? messageToKey(RuleMessage? msg)
  {
    return msg.key
  }
  private RuleMessage mergeMessage(RuleMessage msgA, RuleMessage msgB)
  {
    if (msgA.compare(msgB) > 0)
      return msgA
    
    return msgB
  }
  
  // TODO: Create a sorted filter tree to test the least amount of filters before bailing.
  private Ref:AlarmRule rules
  {
    get {
      if (!Actor.locals.containsKey("rules"))
        Actor.locals["rules"] = [:]
      
      return Actor.locals["rules"]
    }
    
    set {
      Actor.locals["rules"] = it
    }
  }
  
  private Obj? receive(RuleMessage msg)
  {
    switch (msg.op)
    {
      case RuleOp.addRules:
        return this.handleAddRules(msg)
      
      case RuleOp.removeRules:
        return this.handleRemoveRules(msg)
      
      case RuleOp.clearRules:
        this.rules.clear
      
      case RuleOp.recordChange:
        return this.handleRecordChange(msg)
    }
    
    return null
  }
  
  private AlarmRule[] handleAddRules(AddRulesMessage msg)
  {
    msg.rules.each | rule | {  
      this.rules.set(rule.id, rule)
    }
    
    return this.rules.vals
  }
  
  private AlarmRule[] handleRemoveRules(RemoveRulesMessage msg)
  {
    msg.ruleIds.each |id| { this.rules.remove(id)  }
    
    return this.rules.vals
  }
  
  private AlarmRule[] handleRecordChange(RecordChangeMessage msg)
  {
    // Process Record Change
    context := this.lib.rt.context.create(User.alarmEngine)
    
    // Test for alarm conditions
    Ref:AlarmRule matchedRules := this.rules.findAll |rule|
    {
      return rule.include(context, msg.record)
    }   
    
    hasMatches := matchedRules.size > 0
    
    // TODO: Finish implementing sourceOverride for rule
            
    mgtRec := this.management.readById(msg.recordId.toProjRel, false)
    tracker := AlarmTracker.make([msg.record.id], this.lib, AlarmDispatcher.RULES)
    
    if (hasMatches)
    {
      
      matchedRules.each | rule |
      {
        if (((Ref[]?) mgtRec?.get("rules"))?.contains(rule.id) ?: false)
          return // This rule already transitioned
        
        this.lib.engine.transition(
          tracker,
          AlarmTransition(
            DateTime.now,
            rule.alarmState,
            true,
            AlarmDispatcher.RULES,
            null,
            Etc.macro(rule.message, msg.record),
            null,
            rule.metadata
          )
        )
      }

      // Normalize unmatched rules that are in the rules list
      if (mgtRec != null)
      {
        normalRuleIds := ((Ref[]) mgtRec.get("rules")).findAll | ruleId | 
        { 
          !matchedRules.containsKey(ruleId)
        }
        
        normalRuleIds.each |ruleId|
        {
          normalRule := this.rules.get(ruleId)
          ruleName := normalRule.dis("dis", "Rule")
          
          // Remove rule from the state stack
          this.lib.engine.transition(
            tracker,
            AlarmTransition(
              DateTime.now,
              AlarmState.getState(normalRule.state),
              false,
              AlarmDispatcher.RULES,
              null,
              "${ruleName}: '${normalRule.state}' resolved'".toLocale
            )
          )
        }
      }
      
      // Update Management Record
      // Add management record if we don't have one
      mgtRec = this.management.commit(
        (mgtRec == null)
          ? Diff.makeAdd(
            [
              "rules": matchedRules.vals.map |rule| { rule.id },
            ],
            msg.recordId.toProjRel
          )
          : Diff(
            mgtRec,
            [
              "rules": matchedRules.vals.map |rule| { rule.id }
            ],
            Diff.force
          )
      ).newRec
    }
    else
    if (mgtRec != null) // Having a management record means we were previously in alarm.
    {
      // Remove management record
      
      try
      {
        this.management.commit(
          Diff(mgtRec, null, Diff.remove)
        )

        // If the point is at an alarm state, we did not match a rule,
        // and the management record exit
        // Transition the point to normal as it has transitioned to normal
        normalRuleIds := mgtRec.get("rules") as Ref[]
        normalRuleIds.each |ruleId|
        {
          normalRule := this.rules.get(ruleId)
          ruleName := normalRule.dis("dis", "Rule")
          
          // Remove all rules from the state stack
          this.lib.engine.transition(
            tracker,
            AlarmTransition(
              DateTime.now,
              AlarmState.getState(normalRule.state),
              false,
              AlarmDispatcher.RULES,
              null,
              "${ruleName}: '${normalRule.state}' resolved".toLocale
            )
          )
        }
        
      }
      catch (Err error)
      {
        echo ("Error returning to normal $error")
      }
      
    }
    
    return matchedRules.vals
  }
  
  public static AlarmRule? CreateRuleFromRec(Dict record)
  {
    type := RULE_REGISTRY.get(record.get(REC_TAG))
    
    constructorMethod := type.method("wrap", false)
    
    if (constructorMethod != null)
      return constructorMethod.call(record) as AlarmRule
      
    return null
  }
  
  public Void addRule(Str ruleQname)
  {
    ruleType := Type.find(ruleQname, false)
    
    if (ruleType == null)
      return
  
    typeMember := ruleType.field("TYPE")
    typeName := typeMember.get(ruleType)
    
    RULE_REGISTRY.set(typeName, ruleType)
    
    // TODO: Look up and instantiate any rules by this rule type.
  }
  
  private static const AtomicRef _RULE_REGISTRY := AtomicRef(Unsafe([
    ValueRule.TYPE: ValueRule#,
    OutOfRangeRule.TYPE: OutOfRangeRule#,
    FuncEvaluationRule.TYPE: FuncEvaluationRule#
  ]))
  
  private static Str:Type RULE_REGISTRY()
  {
    return ((Unsafe) _RULE_REGISTRY.val).val
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using skyarcd
using skyarc
using folio
using concurrent
using hx

class TestAlarmEngine : HxTest
{
  AlarmLib? lib
  HxContext? context
  Ref? pointId

  override Void setup()
  {
    super.setup()
        
    //this.lib = this.addExt("alarm")
    this.lib = this.addLib("alarm")
    //this.lib.rules.disable
    
    this.context = this.makeContext(User.alarmEngine)
    
    // Alarms can apply to any record.
    point := addRec([
      "testPoint": Marker.val, // Using the point tag requires a license but locks up when using a valid license
      "kind": "Number"
    ])
    
    this.commit(
      point,
      ["curVal": Number.makeInt(104)],
      Diff.transient
    )
    
    this.pointId = point.id
  }
  
  Alarm transitionAlarm(Str state, DateTime occurred, Obj? ackRequired := null)
  {
    // Create Alarm
    tracker := AlarmTracker([this.pointId], this.lib, AlarmDispatcher.INTERNAL)
    
    alarm := (Dict) this.lib.engine.transition(
      tracker,
      AlarmTransition(
        occurred,
        (AlarmState) AlarmState.getState(state), // TODO: Check and throw error if state doesn't exist
        ackRequired,
        AlarmDispatcher.INTERNAL
      )
    ).get
    
    return alarm
  
  }
  
  Dict acknowledgeAlarm(AlarmAcknowledgement ack)
  {
    context := this.makeContext(User.alarmEngine) //Context(proj.sys, User.alarmEngine, proj)
    return this.lib.engine.acknowledge(ack).get
  }
  
  @HxRuntimeTest
  Void testToAlarm()
  {
    alarm := this.transitionAlarm(AlarmState.OFF_NORMAL.name, DateTime.now)
    
    // Verify Alarm
    verifyRefEq(alarm.get("sourceRef"), pointId)
    verifyEq(((Str) alarm.get("state")).lower, "offnormal")
    verify(alarm.has("ackRequired"))
    
  }
  
  @HxRuntimeTest
  Void testAlarmCount(Int? count := -1)
  {
    if (count == -1)
    {
      this.transitionAlarm(AlarmState.FAULT.name, DateTime.now)
      
      verifyEq(this.lib.db.alarmCount.get, 1)
      
      // Different status
      this.transitionAlarm(AlarmState.HIGH_LIMIT.name, DateTime.now)
      
      count = 2
    }   
    
    verifyEq(this.lib.db.alarmCount.get, count)
    verifyEq(this.lib.db.alarmCount([pointId]).get, count)
  }
  
  @HxRuntimeTest
  Void testNewAlarm()
  {
    originalTime := DateTime.now.minus(Duration.fromStr("10day"))
    alarm := this.transitionAlarm(AlarmState.OFF_NORMAL.name, originalTime)
    
    // Verify Original Alarm
    verifyRefEq(alarm.get("sourceRef"), pointId)
    verifyEq(((Str) alarm.get("state")).lower, "offnormal")
    verifyEq(alarm.get("occurred"), originalTime)
    verify(alarm.has("ackRequired"))

    // Test that we have the alarm
    this.testAlarmCount(1)
    
    
    alarmTime := DateTime.now
    
    alarm = this.transitionAlarm(AlarmState.OFF_NORMAL.name, alarmTime)
    
    // Verify Alarm
    verifyRefEq(alarm.sourceRef, pointId)
    verifyEq(alarm.state.lower, "offnormal")
    verifyEq(alarm.occurred, alarmTime)
    verify(alarm.ackRequired)

    // Should overwrite existing alarm
    this.testAlarmCount(1)
  }
  
  @HxRuntimeTest
  Void testPointStatus()
  {
    // Transition to Off Normal
    offNormalAlarm := this.transitionAlarm(AlarmState.OFF_NORMAL.name, DateTime.now)
    
    point := this.readById(this.pointId)
    
    verify(point.has(AlarmPointTags.inAlarm.toStr))
    verifyEq(point.get(AlarmPointTags.inAlarm.toStr), AlarmState.OFF_NORMAL.name)
    
    // Transition to higher priority "FAULT" state
    faultAlarm := this.transitionAlarm(AlarmState.FAULT.name, DateTime.now)
    point = this.readById(this.pointId)
    
    verify(point.has(AlarmPointTags.inAlarm.toStr))
    verifyEq(point.get(AlarmPointTags.inAlarm.toStr), AlarmState.FAULT.name)
    
    // Transition to Normal 
    normalAlarm := this.transitionAlarm(AlarmState.NORMAL.name, DateTime.now)
    point = this.readById(this.pointId)
    
    verify(!point.has(AlarmPointTags.inAlarm.toStr))
  }
  
  @HxRuntimeTest
  Void testAcknowledgeAlarm()
  {
    // Transition to initial state
    offNormalAlarm := this.transitionAlarm(AlarmState.OFF_NORMAL.name, DateTime.now)
    point := this.readById(this.pointId)
    
    // Verify 1 alarm
    this.testAlarmCount(1)
    
    // Verify Point State
    verifyEq(point.get(AlarmPointTags.inAlarm.toStr), AlarmState.OFF_NORMAL.name)
    
    // Acknowledge alarm leaving point in alarm state
    this.acknowledgeAlarm(
      AlarmAcknowledgement(
        this.context,
        [offNormalAlarm.id],
        DateTime.now,
        "Michael",
        "My Acknowledgement"
      )
    )
    
    // Verify Point State (Haven't transitioned so should still be offnormal)
    point = this.readById(this.pointId)
    verifyEq(point.get(AlarmPointTags.inAlarm.toStr), AlarmState.OFF_NORMAL.name)
    
    // Verify Ack Tags
    offNormalAlarm = ((Alarm[]) this.lib.db.get(this.context, [offNormalAlarm.id]).get).getSafe(0)
    verifyEq(offNormalAlarm.get("ackedBy"), "Michael")
    verifyEq(offNormalAlarm.get("ackMessage"), "My Acknowledgement")
    
    // Verify 1 alarm
    this.testAlarmCount(1)
    this.transitionAlarm(AlarmState.NORMAL.name, DateTime.now)
    
    // Verify 0 alarms (Off Normal should be deleted as it is already acknowledged and we transition to normal)
    this.testAlarmCount(0)
        
    // Transition back to off normal
    offNormalAlarm = this.transitionAlarm(AlarmState.OFF_NORMAL.name, DateTime.now)
    // Transition to higher priority state
    faultAlarm := this.transitionAlarm(AlarmState.FAULT.name, DateTime.now)
    // Transition to normal
    normalAlarm := this.transitionAlarm(AlarmState.NORMAL.name, DateTime.now)
    
    // Verify 2 alarms
    this.testAlarmCount(2)
    point = this.readById(this.pointId)
        
    // Acknowledge Fault
    this.acknowledgeAlarm(
      AlarmAcknowledgement(
        this.context,
        [faultAlarm.id],
        DateTime.now,
        "Michael",
        "My Acknowledgement"
      )
    )
    
    this.testAlarmCount(1)
    
    // Acknowledge Off Normal (Should remove "inAlarm")
    this.acknowledgeAlarm(
      AlarmAcknowledgement(
        this.context,
        [offNormalAlarm.id],
        DateTime.now,
        "Michael",
        "My Acknowledgement"
      )
    )
    
    this.testAlarmCount(0)
  }
  
  @HxRuntimeTest
  Void testAlarmHistory()
  {
    verifyEq(this.lib.history.historyCount.get, 0)
    this.transitionAlarm(AlarmState.OFF_NORMAL.name, DateTime.now)  
    Actor.sleep(Duration.fromStr("100ms"))
    verifyEq(this.lib.history.historyCount.get, 1)
    
    
    this.transitionAlarm(AlarmState.OFF_NORMAL.name, DateTime.now)
    Actor.sleep(Duration.fromStr("100ms"))
    verifyEq(this.lib.history.historyCount.get, 2)
    
    
    this.transitionAlarm(AlarmState.FAULT.name, DateTime.now)
    Actor.sleep(Duration.fromStr("100ms"))
    verifyEq(this.lib.history.historyCount.get, 3)
    
    
    this.transitionAlarm(AlarmState.NORMAL.name, DateTime.now)
    Actor.sleep(Duration.fromStr("100ms"))
    verifyEq(this.lib.history.historyCount.get, 4)
    verifyEq(this.lib.history.historyCount(this.pointId).get, 4)
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using skyarcd
using skyarc
using folio
using concurrent
using hx

class TestAlarmRouter : HxTest
{
  AlarmLib? ext
  
  override Void setup()
  {
    super.setup()

    this.ext = this.addLib("alarm")
    this.ext.rules.disable
  }
  
  @HxRuntimeTest
  Void testAddGetRoute()
  {
    verifyEq(((List) this.ext.router.getRoutes.get).size, 0)
    
    this.ext.router.addRoutes([
      MockAlarmRoute.wrap(
        |->Void| {}
      )
    ])
    
    verifyEq(((List) this.ext.router.getRoutes.get).size, 1)
  }
  
  @HxRuntimeTest
  Void testClearRoutes()
  {
    // Verify No Routes
    verifyEq(((List) this.ext.router.getRoutes.get).size, 0)
    
    // Add 2 Routes
    this.ext.router.addRoutes([
      MockAlarmRoute.wrap(
        |->Void| {})
    ])
    
    this.ext.router.addRoutes([
      MockAlarmRoute.wrap(
        |->Void| {})
    ])
    
    // Verify Routes were added
    verifyEq(((List) this.ext.router.getRoutes.get).size, 2)
    
    // Clear Routes
    this.ext.router.clearRoutes().get
    
    // Verify Routes are empty
    verifyEq(((List) this.ext.router.getRoutes.get).size, 0)
  }
  
  @HxRuntimeTest
  Void testRouteExecution()
  {
    executedRoute := AtomicBool(false)
    
    // Create Route
    this.ext.router.addRoutes([
      MockAlarmRoute.wrap(
        |->Void| { executedRoute.getAndSet(true)})
    ])
    
    // Test non-selected item
    this.ext.router.route(
      Etc.makeDict(["alarm": Marker.val, "wrongItem": Marker.val]),
      Etc.makeDict(["source": Marker.val, "testSource": Marker.val])
    ).get
    
    // Should not have executed the route
    verify(!executedRoute.val)
    
    // Test selected item.
    this.ext.router.route(
      Etc.makeDict(["alarm": Marker.val, "testItem": Marker.val]),
      Etc.makeDict(["source": Marker.val, "testSource": Marker.val])
    ).get
    
    // Should have executed the route
    verify(executedRoute.val)
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using skyarcd
using skyarc
using folio
using concurrent
using hx

class TestRulesEngine : HxTest 
{
  AlarmLib? lib
  Ref? pointId
  
  override Void setup()
  {
    super.setup()
    
    this.lib = this.addLib("alarm")
    
    // Alarms can apply to any record.
    point := addRec([
      "testPoint": Marker.val,
      "kind": "Number"
    ])
    
    this.commit(
      point,
      ["curVal": Number.makeInt(104)],
      Diff.transient
    )
    
    this.pointId = point.id
    
    this.lib.rules.addRules([
      OutOfRangeRule.wrap(
        Etc.makeDict([
          "ruleOn": "testPoint",
          "state": AlarmState.OFF_NORMAL.name,
          "highLimit": Number.makeInt(100),
          "lowLimit": Number.makeInt(0),
          "message": "Out of Range"
        
        ])
      ),
      ValueRule.wrap(
        Etc.makeDict([
          "ruleOn": "testPoint",
          "faultValue": Number.makeInt(Int.minVal),
          "message": "In Fault"
        ])
      )
    ]).get
  }
  
  Void verifyAlarmCount(Int count)
  {
    verifyEq(this.lib.db.alarmCount.get, count)
    verifyEq(this.lib.db.alarmCount([pointId]).get, count)
  }
  
  @HxRuntimeTest
  Void testOffNormalFaultRule()
  {
    // Off Normal
    point := Etc.makeDict([
      "id": this.pointId,
      "testPoint": Marker.val,
      "curVal": Number.makeInt(101)
    ])
    
    this.verifyAlarmCount(0)
                    
    transitions := (Future[]) this.lib.rules.recordChange(
      point, 
      DateTime.now
    ).get
    
    transitions.each |future| { future.get }
    
    this.verifyAlarmCount(1)
    
    // Normal
    point = Etc.dictSet(point, "curVal", Number.makeInt(10))
    
    transitions = (Future[]) this.lib.rules.recordChange(
      point, 
      DateTime.now
    ).get
    
    transitions.each |future| { future.get }
    
    this.verifyAlarmCount(1)
    
    // Fault
    point = Etc.dictSet(point, "curVal", Number.makeInt(Int.minVal))
    
    transitions = (Future[]) this.lib.rules.recordChange(
      point, 
      DateTime.now
    ).get
    
    transitions.each |future| { future.get }
    
    this.verifyAlarmCount(2)
    
    
    //alarms := (Dict[]) this.lib.db.getTargetsAlarms(this.pointId).get
  }
  
  @HxRuntimeTest
  Void testTransitionToNormal()
  {
    // Verify no alarms
    this.verifyAlarmCount(0)
    
    // Transition point into off normal
    point := Etc.makeDict([
      "id": this.pointId,
      "testPoint": Marker.val,
      "curVal": Number.makeInt(1000)
    ])
    
    transitions := (Future[]) this.lib.rules.recordChange(
      point, 
      DateTime.now
    ).get
    
    transitions.each |future| { future.get }
    
    // verify offNormal alarm and alarm count
    dbPoint := this.readById(this.pointId)
    verifyEq(dbPoint.get(AlarmPointTags.inAlarm.toStr), AlarmState.OFF_NORMAL.name)
    
    this.verifyAlarmCount(1)
    
    // Transition point into normal state
    point = Etc.dictSet(point, "curVal", Number.makeInt(75))
                    
    transitions = (Future[]) this.lib.rules.recordChange(
      point, 
      DateTime.now
    ).get
    
    transitions.each |future| { future.get }
    
    // Verify point's in normal and unacked alarm is still there.
    dbPoint = this.readById(this.pointId)
    verify(!dbPoint.has(AlarmPointTags.inAlarm.toStr))
    
    this.verifyAlarmCount(1)
    
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using haystack
using hx

const class ValueRule : AlarmRule 
{
  public static const Str TYPE := "value"
  
  @TypedTag
  const Str tagName := "curVal"
  
  @TypedTag
  const Obj? value
    
  new make(Dict rec, | This | f) : super(rec, f) {}
  
  protected override Bool test(HxContext context, Dict record)
  {
    testValue := record.get(this.tagName)
    
    if (testValue is Marker)
      return this.value is Marker || this.value == true
        
    if (this.value is Number)
    {
      // If the entity has a number value and the configured value does not have a unit
      // Strip the unit and test the number itself
      if (testValue is Number &&
        ((Number) this.value).unit == null)
      
      return  ((Number) this.value).toFloat == ((Number) testValue).toFloat
    }
    
    return testValue == this.value
  }
  
  static new wrap(Dict? dict)
  {
    if (dict == null)
      return null
    
    return create(ValueRule#, dict)
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST


using axon

class WriterStream : TerminalStream
{
  override Str funcName := "toWriter"
  
  new make(SourceStream source, Obj target): super(source)
  {
    
  }
  
  override Void onStart(Signal sig)
  {
    // TODO:
  }
  
  override Obj? onRun()
  {
    return null
  }
}

Skyspark 3.1.12 ∙ 20-Oct-2026 14:19 CEST