Creating report from alarm data - getting user who acknowlegded alarm and when?

Working on a feature where the user wants to be able to able to print a report for an entire day to see all the alarms that happened in that day, including who acknowledged the alarm.

Right now, I have something like to generate the start/end dates of my alarm query

def generateAlarmFileForFullDay(day):
	"""
	Get's all alarms for a specified day and creates a report that is put into a mapped drive folder.
	Args:
		day: datetime object from a calendar component that will be used to determine what days to use in calculation
	Returns:
		Creates file in specified place or throws an error
	"""
	beginningOfDay = system.date.midnight(day)
	endOfDay = system.date.midnight(system.date.addDays(day, 1))
	print beginningOfDay, endOfDay
	alarmData = system.alarm.queryJournal(startDate=beginningOfDay, endDate=endOfDay)
	for x in alarmData:
		print x

And doing this prints data like :

{Source: 'prov:default:/tag:machine/PLC/Alarms/SFTY_Status:/alm:Safety Relay Opened', Display Path: '', UUID: '002523a5-d71b-4b3a-9f96-374cc5ac5b5b', Current State: 'Active, Acknowledged', Priority: 'Critical', Active Data: {systemAck=true, eventTime=Thu Jun 02 07:18:48 EDT 2022}, Clear Data: null, Ack Data: {ackUser=tag:Auto-Ack, eventTime=Thu Jun 02 07:18:48 EDT 2022}, Runtime Data: {eventState=Active}}
{Source: 'prov:default:/tag:machine/PLC/Alarms/SFTY_Door_Left:/alm:Left Door Open', Display Path: '', UUID: '5f1f05f4-4ef5-4401-ab97-4e4ffb53f7ee', Current State: 'Active, Acknowledged', Priority: 'Critical', Active Data: {systemAck=true, eventTime=Thu Jun 02 07:18:48 EDT 2022}, Clear Data: null, Ack Data: {ackUser=tag:Auto-Ack, eventTime=Thu Jun 02 07:18:48 EDT 2022}, Runtime Data: {eventState=Active}}
{Source: 'evt:System Startup', Display Path: '', UUID: 'c4542634-99e8-4c42-82ea-a80550d1ec48', Current State: 'Active, Unacknowledged', Priority: 'Low', Active Data: {eventTime=Thu Jun 02 13:28:54 EDT 2022}, Clear Data: null, Ack Data: null, Runtime Data: {eventState=Active, isSystemEvent=true}}
{Source: 'evt:System Startup', Display Path: '', UUID: '9365239f-2bb0-41aa-b2c6-feec4162466a', Current State: 'Active, Unacknowledged', Priority: 'Low', Active Data: {eventTime=Thu Jun 02 13:32:53 EDT 2022}, Clear Data: null, Ack Data: null, Runtime Data: {eventState=Active, isSystemEvent=true}}
{Source: 'evt:System Startup', Display Path: '', UUID: 'fdaa4e7a-cb21-4b33-87ec-14eacb804f4f', Current State: 'Active, Unacknowledged', Priority: 'Low', Active Data: {eventTime=Thu Jun 02 13:35:59 EDT 2022}, Clear Data: null, Ack Data: null, Runtime Data: {eventState=Active, isSystemEvent=true}}

I need some help interpreting the dictionaries here-

In the first two rows, I see the alarm was active and then acknowledged, I assume automatically due to ackUser=tag:Auto-Ack. Is that a fair assumption? I can’t find any examples yet in this particular system where ackUser is anything other than tag:Auto-Ack - what would I expect for a user manually acknowledging an alarm, the user name?

Further, inside the AckData dictionary, I assume the eventTime is when the alarm was acknowledged - is that right? In my example the activeData eventTime and the ackData eventTime are identical - presuming this is due to auto-acknowledging, but if a user took a few minutes the ackData would be whenever the user clicked “acknowledged”.

If someone can confirm (or revise) my understanding of what the data in these dictionaries represent that would very helpful. Thank you

Also, what are
Boolean includeSystem - Specifies whether system events are included in the return.
Is this just things like when the gateway is started/turned off? I can’t find documentation on what system alarms is.

Edit:
I think I am almost there but I can’t seem to figure out how to get stuff out of the EventData type objects.

Here is my code so far

	allAlarmData = system.alarm.queryJournal(startDate=beginningOfDay, endDate=endOfDay)
	headers = ['Alarm Occured At', 'Alarm Cleared At', 'Alarm Name', 'User Logged In']
	rows = []
	for alarmRecord in allAlarmData:
		alarmOccuredAt = alarmRecord.???
		alarmClearedAt = alarmRecord.getClearedData().???
		alarmName = alarmRecord.getName()
		userLoggedIn = None
		rows.append([alarmOccuredAt, alarmClearedAt, alarmName, userLoggedIn])
	dataset = system.dataset.toDataSet(headers, rows)

If someone can help me figure out what these questions marks should be, it would greatly help me.

This place currently does not have a audit trail setup but they do manually log who logs in/out of the system so I can manually try to figure that out, but I am curious - is there a way to make sure someone is logged in to clear an alarm - and then to get that data out later on as to who cleared it?

Thanks. Sorry for the shameless bumps lol.

AlarmEvent extends PropertySet. (Technically, you’ll be getting a PyAlarmEventImpl in most cases, which further extends AlarmEvent, but the point is the same.
PropertySets are basically enhanced maps - a Property is a type, an optional value, and a default value.
So with a PropertySet you can get, or getOrDefault, or getOrElse, or various other methods:
https://files.inductiveautomation.com/sdk/javadoc/ignition81/8.1.16/com/inductiveautomation/ignition/common/config/PropertySet.html

But the key you use is, ideally, a Property object. (PyAlarmEvent muddies things by allowing you to request properties by arbitrary keys; however, it does this in a way that’s not very efficient, and less explicit than what we’re about to cover).

So, if you need a specific piece of data off the alarm event itself, you just ask for it:

from com.inductiveautomation.ignition.common.alarming.config import CommonAlarmProperties 

for alarmRecord in allAlarmData:
	alarmOccuredAt = alarmRecord.get(CommonAlarmProperties.EventTime)
	alarmActiveAt = alarmRecord.get(CommonAlarmProperties.ActiveTime)

As a bonus, the clearedData, activeData, etc getters also return PropertySets - so the same technique applies. You can also ask a PropertySet for all of its properties, to just enumerate them and see what it contains:
https://files.inductiveautomation.com/sdk/javadoc/ignition81/8.1.16/com/inductiveautomation/ignition/common/config/PropertyValueSource.html

1 Like

Awesome. This was the missing piece I needed.

Last two questions - I’m looking at CommonAlarmProperties

  1. I see .ActiveTime and .ClearTime. I need the time some user actually cleared the alarm - I am assuming that is .ClearTime is that right?

  2. And I would also need to get the user associated with who cleared the alarm - would that be .AckUser? I guess I would have expected there to be a .ClearedUser since acknowledging and clearing an event seem to be different events.

I lied, this is the last question
3) This system is not currently setup where a user has to be logged into to acknowledge an alarm, how can I make that be the case? Right now they do have a generic login that people can use to view things and clear alarms, and that will be removed to make this system Part 11 compliant, but once that’s done, will it always grab who’s currently logged in who’s clearing the alarm? An audit trail will need to be setup for this system as well, does that relate to keeping track of who cleared the alarm or does the alarming system handle that on its own?

Pretty sure that's the case.

There's no such thing as a clear user, because they're different (orthogonal) events. There are two binaries that compose into the four (ignoring enabled/disabled) possible states for an alarm:

unacked acked
active active, unacked actived, acked
cleared clear, unacked cleared, acked

So we capture an AckUser (ideally) when you move from unacked -> acked for a given event, but there's no user associated with an active -> clear transition, because that's happening whenever the alarm's condition is no longer met.

1 Like

Thank you for clearing that up, that makes a lot of sense.

So given that, how can I better represent the time the event first started and when it was cleared of an alarm? Right now my script looks like

def generateAlarmDataForFullDay(day):
	"""
	Get's all alarms for a specified day and creates a report that is put into a mapped drive folder.
	Args:
		day: datetime object from a calendar component that will be used to determine what days to use in calculation
	Returns:
		DataSet - to be utilized by the reporting module
	"""
	logger = system.util.getLogger("Alarm Report Data Logger")
	beginningOfDay, endOfDay = dates.getMidnightToMidnight(day)
	allAlarmData = system.alarm.queryJournal(startDate=beginningOfDay, endDate=endOfDay, includeSystem=False)
	headers = ['Alarm Occured At', 'Alarm Cleared At', 'Alarm Name', 'User Logged In']
	rows = []
	for i, alarmRecord in enumerate(allAlarmData):
		alarmOccuredAt = alarmRecord.get(CommonAlarmProperties.EventTime)
		alarmClearedAt = alarmRecord.get(CommonAlarmProperties.ActiveTime)
		# Step 0 - get name of alarm
		alarmName = alarmRecord.getName()
		# Step 1 - Way to differentiate between start of alarm and clearing of alarm, determins alarmOccuredAt and alarmCleared at columns
		queryTimeStamp=None
		# This is when alarm was cleared
		if alarmClearedAt is None:
			alarmClearedAt = alarmOccuredAt
			alarmOccuredAt = None
			queryTimeStamp = alarmClearedAt
		# this is when alarm started
		elif alarmClearedAt is not None and alarmOccuredAt is not None:
			alarmClearedAt = None
			queryTimeStamp = alarmOccuredAt
			
		# Step 2 - Chance to determine who was logged in at time
		getUserQuery = "SELECT UserName, EventName, TimeStamp FROM event_log WHERE TimeStamp<? AND EventName IN ('Login','Logout') ORDER BY TimeStamp DESC LIMIT 1"
		userData = system.db.runPrepQuery(getUserQuery, [queryTimeStamp])
		for (username, eventName, TimeStamp) in userData:
			if eventName == 'Logout':
				userLoggedIn = "Auto-Logged In User"
			else:
				userLoggedIn = username
		rows.append([alarmOccuredAt, alarmClearedAt, alarmName, userLoggedIn])
	dataset = system.dataset.toDataSet(headers, rows)
	return dataset

Which gives me data in a report table that looks like

image

I highlighted one instance, Label Head E-Stop Pressed occured and then 1 minute 28 seconds later was cleared. Is that actually true though based on my script? I am nervous I am misinterpreting the alarm objects. Tagging @JordanCClark @nminchin as I believe I have seen you both work a lot with alarms

You need to take the UUID into account.

from com.inductiveautomation.ignition.common.alarming.config import CommonAlarmProperties 

start = '2022-06-04 00:00:00'
end   = '2022-06-06 00:00:00'

beginningOfDay = system.date.parse(start)
endOfDay       = system.date.parse(end)



allAlarmData = system.alarm.queryJournal(journalName='BypassJournal', startDate=beginningOfDay, endDate=endOfDay, includeSystem=False)

dictOut = {}

# Set key names for datapoints associated with each UUID
keys = ['active', 'cleared', 'name']

for alarmRecord in allAlarmData:
	# Get Data points
	eventTime    = alarmRecord.get(CommonAlarmProperties.EventTime)
	alarmActive  = alarmRecord.get(CommonAlarmProperties.ActiveTime)
	alarmCleared = alarmRecord.get(CommonAlarmProperties.ClearTime)
	# Note that I use displaypath instead of name. To each their own. ;)
	# alarmName   = alarmRecord.getName()
	alarmName    = alarmRecord.get(CommonAlarmProperties.DisplayPath)
	# The UUID is what ties it all together.
	uuid         = alarmRecord.id
	
	# Chech if the UUID exists. Make an entry for it if it does not.
	if uuid not in dictOut.keys():
		# Keys full of Nones.  
		dictOut[uuid] = {key:None for key in keys}
	# Set the alarm name, if it doesn't exist
	if not dictOut[uuid]['name']:
		dictOut[uuid]['name'] = alarmName	
	# Set the alarm active time, if it doesn't exist
	if alarmActive and not dictOut[uuid]['active']:
		dictOut[uuid]['active'] = alarmActive
	# Set the alarm cleared time, if it doesn't exist
	if alarmCleared and not dictOut[uuid]['cleared']:
		dictOut[uuid]['cleared'] = alarmCleared

headers = ['Alarm Occured At', 'Alarm Cleared At', 'Alarm Name']
rows = []
for values in dictOut.values():
	rows.append([values[key] for key in keys])

# Create the dataset
dataset = system.dataset.sort(system.dataset.toDataSet(headers, rows), 'Alarm Occured At')

util.printDataSet(dataset)

output from my own journal:

row | Alarm Occured At             | Alarm Cleared At             | Alarm Name                 
-----------------------------------------------------------------------------------------------
0   | Sat Jun 04 00:31:26 EDT 2022 | Sat Jun 04 01:12:09 EDT 2022 | 3427/140/pvc_tape          
1   | Sat Jun 04 05:59:54 EDT 2022 | Sat Jun 04 06:32:39 EDT 2022 | 3425/160/tension           
2   | Sat Jun 04 06:36:22 EDT 2022 | Sat Jun 04 07:05:38 EDT 2022 | 3425/160/tension           
3   | Sat Jun 04 07:00:15 EDT 2022 | Sat Jun 04 07:26:12 EDT 2022 | 3426/140/coax_tape         
4   | Sat Jun 04 07:02:29 EDT 2022 | Sat Jun 04 07:31:13 EDT 2022 | 3426/160/tension           
5   | Sat Jun 04 07:21:47 EDT 2022 | Sat Jun 04 07:31:13 EDT 2022 | 3426/160/handle_pull_effort
6   | Sat Jun 04 07:32:55 EDT 2022 | Sat Jun 04 07:38:14 EDT 2022 | 3426/160/handle_pull_effort
7   | Sat Jun 04 07:32:56 EDT 2022 | Sat Jun 04 07:38:14 EDT 2022 | 3426/160/tension           
8   | Sat Jun 04 07:42:04 EDT 2022 | Sat Jun 04 09:13:50 EDT 2022 | 3426/160/handle_pull_effort
9   | Sat Jun 04 07:42:05 EDT 2022 | Sat Jun 04 09:13:50 EDT 2022 | 3426/160/tension           
3 Likes

Thank you very much. Given what @PGriffith said before about active and cleared being two different events, I suppose the UUID is how you can link them together and that is what this is doing/how it works to tie these two events together? Both distinct events have the same UUID so you’re in essence joining on the UUID is that right? Just want to make sure I understand the logic of this.

1 Like

That is correct. :slight_smile:

2 Likes

Ah ok now it makes much more sense to me. Thank you for your help!

2 Likes