Alarm Status table - sort order and ack notes

Two questions:
One is about the Sort Order. I want it to sort by "Unacknowledged" first, whether it is cleared or not, secondary sorted by time it occurred, then acknowledged alarms after that, again, secondary sorted by time it occurred.
image

Seems like "State_Time" is the only one that would work, but that one sorts first by "active, Unack", then "active, Ack", then "Cleared, Unack" third. which is not what I want, I want it to ignore the active/cleared state. I can't see a way to change that.
This seems like a pretty basic thing to make it behave the way our current software does and I'm frustrated.

Second question is about the "Ack Notes". I want the option for operators to add ack notes if they want to, but I don't want to force them by flagging all the alarms as "ack notes required". But if it's not set as "required", the text box doesn't appear for them to be able to type anything. I feel like I should be able to do that with scripting, but can't find in the manual where to even look for something like that.

Thanks

Hi tgish,

There seems to be no easy method of doing both your requests. I would suggest you open a ticket with Inductive Automation Support so a Support Engineer can look into this at a deeper level.

I've developed a way to accomplish this, but to me, it seems quite complicated, so in the code below, I have been unusually detailed in my comments to make it easier to modify this code if needed.

Here is the code for the Alarm Status Table's onAcknowledge extension function that will make notes optional when they not required by the tag:

	label = self.getComponent(0).getComponent(1).getComponent(0).getComponent(2)#This label is for the Notes Required Alert
	slideOverPane = self.getComponent(0).getComponent(0)#Responsible for expanding slide up notes panel
	jideTable = self.getComponent(0).getComponent(0).getComponent(1).getComponent(0).getComponent(3).getComponent(0)#this is the table that contains the selection checkboxes
	def clearNoteReqAlert():#Resets Notes Required alert notification
		label.setText('')
	if slideOverPane.isExpanded():#Check to see if the notes panel is already visible; if so, process notes, but if not, make it so
		textArea = self.getComponent(0).getComponent(0).getComponent(0).getComponent(0).getComponent(1).getComponent(0).getComponent(5).getComponent(0).getComponent(0).getComponent(0)#gets text area component
		notes = textArea.getText()#gets the notes from the notes textbox
		if "ackNotesReqd=true" in str(alarms) and len(notes) < 1:#If notes are required, and none are present: stop the loop, alert the operator, and wait for notes
			label.setText('Acknowledgement Notes Required')
			system.util.invokeLater(clearNoteReqAlert, 3000)#Reset note alert automatically after 3 seconds
			return
		else:#if notes are present, or if notes are NOT required: acknowledge the alarm and apply notes if they are present
			for alarm in alarms:
				eventID = []
				eventID.append(str(alarm.get('EventId')))#get the event id for the alarm in the form of a string array
				system.alarm.acknowledge(eventID, notes)
		for row in range(jideTable.getRowCount()):#loops through and deselects all checkboxes after alarms have been acknowledged
			jideTable.setValueAt(False, row, 0)
		textArea.setText('')#Clears notes text area after alarms have been acknowledged
		slideOverPane.collapse()#hides slide over pane
	else:
		slideOverPane.expand()#expand the slide over pane
		buttonPanel = self.getComponent(0).getComponent(1).getComponent(0)#the notes panel lives in this component
		notesPanelField = buttonPanel.getClass().getDeclaredField('notesPanel')#access the notes panel field
		notesPanelField.setAccessible(True)#set the protected field to accessable
		notesPanel = notesPanelField.get(buttonPanel)#get the notes panel
		popupHolderPanel = self.getComponent(0).getComponent(0).getComponent(0).getComponent(0)#This panel should contain the notes panel, but it isn't automatically set
		for specificMethod in popupHolderPanel.getClass().getDeclaredMethods():#Get the protected method that sets the panel
			if specificMethod.name == 'setContents':
				method = specificMethod
				method.invoke(popupHolderPanel, [notesPanel])#set the panel
				break

Explination of the code:
First, the above code checks to see if the notes panel is open at acknowledgement, and if it isn't, it opens it without any regard for whether or not notes are required. No acknowledgements will take place with the first button press.
Note: For reasons that are unknown to me, the acknowledgement notes panel is stored in the footer of the alarm table in a protected field and has to be added to the popup holder panel the first time it is expanded. The above code does this using the setContents method of the PopupHolderPanel, but for reasons that are also unknown to me, this method cannot be accessed directly. For these reasons, this step of the process seems much more complicated than it should be, but if there is a simpler way to invoke this action, I haven't found it yet.

Once the notes panel is opened, if the acknowledgement button is pressed, the code checks to see if acknowledgement notes are required. If notes are required, the code enforces the policy in the normal way, but if notes are not required, the alarms will be acknowledged whether or not notes have been typed. That said, if notes have been typed, the notes will be applied to the acknowledged tags.

Once the alarms have been acknowledged, the checkboxes are unchecked, and the notes panel is cleared and closed.

Edits:
• Implemented much simpler way to check for alarm note requirements
• Changed the way the the setContents method is obtained after testing on a different system and discovering a discrepancy in the method index
• Refactored for loop for better efficiency

1 Like

I messed around with this problem again, and I figured out a way to do it that is actually SUPER SIMPLE!

First, right click on the header of the table and select the checkbox next to the column "Acked?"
image

Second, click on the header for the Acked column twice to sort descending.

Result:

1 Like

wow, thanks so much for this detailed answer. I'll be trying this out right away.

1 Like

ok, this is a decent work around - and should be fine assuming the operators just leave the alarm screen open all the time. I don't really want them to have to re-sort every time they open it, but this should be good enough to get us going at least.

1 Like

No worries; I figured out a way to automate the process. The following script checks to see if the Acked column is visible in the table, and if not, it programmatically adds the column. Afterwards, it programmatically sorts the table by that column, and if the acked column wasn't already visible, it removes the column immediately after the sorting is complete. Here is the code as fired from the actionPerformed script of my test button:

from javax.swing.table import TableColumn
def tableSorter(alarmStatusTable):
	if alarmStatusTable.componentCount > 0:
		for component in alarmStatusTable.getComponents():
			if 'AlarmStatusTable$1' in str(component.__class__):
				table = component
				ackedColumn = None
				rowSorterField = table.getClass().getSuperclass().getDeclaredField('Cc')
				rowSorterField.setAccessible(True)
				rowSorter = rowSorterField.get(table)
				for column in range(component.columnCount):
					if 'Acked' in component.getColumnName(column):
						ackedColumn = column
						columnModel = table.getColumnModel()
						tableColumn = columnModel.getColumn(column)
						rowSorter.sortColumn(tableColumn.getModelIndex())
						break
				if ackedColumn is None:
					tableHeaders= table.getTableHeader()
					columnModel = table.getColumnModel()
					columnModel.addColumn(TableColumn(11))
					tableColumn = columnModel.getColumn(column + 1)
					tableColumn.setHeaderValue("Acked")
					tableHeaders.repaint()
					rowSorter.sortColumn(tableColumn.getModelIndex())
					columnModel.removeColumn(tableColumn)#***Removes Acked Column after sorting
				break
			tableSorter(component)
alarmStatusTable = event.source.parent.getComponent('Alarm Status Table') #From a Test Button in the same container as the alarm status table
'''
alarmStatusTable = system.gui.getParentWindow(event).getComponentForPath('Root Container.Alarm Status Table') #From the parent window's internalFrameOpened event handler
alarmStatusTable = event.source #From the alarm status table's propertyChange event handler
alarmStatusTable = self #from the alarm status table's extension functions or custom methods
'''
tableSorter(alarmStatusTable)

Here is the result:
image

Edit:
• Corrected an erroneous assumption that the Acked Column would always be the last column in the table.
• Discovered that the acked column can be removed after sorting, and the table will still retain the sort order giving the illusion of default sort order. This was accomplished by adding columnModel.removeColumn(tableColumn) to the script.

2 Likes

Thanks again for this code, I've got it working in my project. Can I ask where you found the documentation that describes all those things you are using (like columnModel and getModelIndex and sortColumn() etc.? Googling gets me no hits at all and this thread is the only thing that comes up in the forum. I want to add a secondary sort by time but can't figure out how since I'm only guessing at how your code even works. Right now the time sorting gets out of whack once I apply this "acked" sort.

Vision is built with Java Swing, so all Vision components are also Swing components, and use many of the same inner classes to build the desired functionality. (With various Jide pieces thrown into the mix.)

You just need to learn how JavaDoc works.

2 Likes

Phil nailed it; add those three shortcuts to your browser, and you should be all set as long as you can figure out which one to use. One approach that I use quite often to probe a component is to simply throw a button in the window somewhere. Then, I'll wrap the component in a type statement to see what it actually is.
Example:

alarmStatusTable = event.source.parent.getComponent('Alarm Status Table')
print type(alarmStatusTable)

Output:

<type 'com.inductiveautomation.factorypmi.application.components.AlarmStatusTable'>

Since the class is prefaced with inductiveautomation, I know to search inductive automation's class documentation for the methods, fields, and constructors that are applicable to that component. Likewise, if the script had returned a preface of java or javax, then I know to search the java docs, and with things like alarm status tables and power tables, some of the subcomponents will be prefaced with jidesoft; although, I will admit that sometimes I ignore this and look at the java equivalent component instead because I've spent a lot more time in those documents, so they are simpler for me to navigate.

Another thing that can help is to wrap the component in dir() to get quick and dirty list of the fields and methods.

Example:

print dir(alarmStatusTable)

Abridged Output:

['ABORT', 'ALLBITS', 'AccessibleJComponent', 'BOTT[...] 

Many components are actually collections of subcomponents, so it is possible to drill down level by level doing something like this:

for component in alarmStatusTable.getComponents():
	print type(component)

Output:

<type 'javax.swing.JPanel'> #Yup, this is java docs
1 Like

You can't also use my venerable introspect() function from later.py. { Note how far back my copyright on that goes.... }

1 Like