外观
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
| fromStr | Return the AlarmDBOp instance for the specified name. |
Fields
| vals | List 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
| fromStr | Return the AlarmDispatcher instance for the specified name. |
Fields
| vals | List 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
| alarmAck | Given a set of alarms (or alarm ids), acknowledge them and return the updated alarm records. |
| alarmDigest | Generate an alarm digest grouped by a given parent type. |
| alarmHistoryCount | Given an optional target, return the count of alarm history records. |
| alarmHistoryQuery | Given a Haystack query string, return the alarm history records as a grid. |
| alarmNavCounts | Given a grid of target records with id, enrich it with rolled-up alarm counts per row. |
| alarmTargets | Given a root object, return all alarm target records associated with it. |
| alarms | Given optional targets and/or a time span, return the current alarm records as a grid. |
| alarmsGetById | Given an alarm id, return the corresponding alarm record. |
| alarmsRemove | Given a set of alarms (or alarm ids), remove them from the database and return the removed alarm records. |
| clearAlarmHistory | Clear alarm history records in the database based on the given parameters. |
| clearAlarmHistoryBefore | Clear alarm history records before the given date in the database. |
| readAlarmHistory | Given optional targets and/or a time span, query the alarm history as a grid. |
| toAlarm | Given 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 | |
| toNormal | Given 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 handlealarmsGetById
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
| fromStr | Return the AlarmHistoryDBOp instance for the specified name. |
Fields
| vals | List 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
| fromStr | Return the AlarmOp instance for the specified name. |
Fields
| vals | List 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
| fromStr | Return the AlarmPointTags instance for the specified name. |
Fields
| vals | List 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
| fromStr | Return the AlarmsRequestOp instance for the specified name. |
Fields
| vals | List 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
| fromStr | Return the RoutingOp instance for the specified name. |
Fields
| vals | List 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
| fromStr | Return the RuleOp instance for the specified name. |
Fields
| vals | List 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