Get Entire Tag as Object

Is there a function that allows me to get a tag, with all of it’s properties as a single object? (similar to how tag event scripts have access to the entire tag and it’s properties via tag.propertyName)

Using system.tag.readBlocking() only gives me the value, quality, and timestamp for a tag.

Depending on the information you want:
system.tag.configure gets all the configuration of a tag.
system.tag.browse gets the value of the tag and some configuration information.


In my case I decided it wasn’t really worth it to grab the entire object when I’m only using 3 of the properties in my script. I just made a parallel lists of the properties for each tag.

#create empty lists for holding tag properties
value_paths = []
logging_paths = []
opcPath_paths = []

#add tags of intereset
#. append more tag paths as needed

#create parallel lists with other properties
for i in range(len(value_paths)):
	logging_paths.append(value_paths[i]+".loggingEnabled") #custom prop

#read tags
values = system.tag.readBlocking(value_paths)
logging = system.tag.readBlocking(logging_paths)
opcPaths = system.tag.readBlocking(opcPath_paths)

#log tags
for index in range(len(value_paths)):
	tagHistory.logTagChange(logging[index].value, 0, 0, values[index].value, False, opcPaths[index].value)

A small optimization, and also a suggestion for making this script a bit more “pythonic”.

First, before @pascal.fragnoud comes to rant about it, anytime you have a loop of the form:

for i in range(len(x)): there is a more pythonic way to write it.

Take for instance the first loop, a functionally equivalent script can be written like this:

for value_path in value_paths:
    logging_paths.append(value_path + '.loggingEnabled')
    opcPath_paths.append(value_path +'.OpcItemPath')

The second loop can also be written in a different way. A few different ways actually, but the one I will show here I am using for another reason that I will explain shortly.

for index,path in enumerate(value_paths):
    loggingIndex = index + len(value_paths)
    opcIndex = loggingIndex + len(logging_paths)
    tagHistory.logTagChange(values[loggingIndex].value, 0,0, values[index].value,False,opcPaths[opcIndex].value)

Now, for the optimization. If at all possible, always try to only make 1 call to system.tag.readBlocking(). This function will return the list of qaulifiedValues in a matching order to the list of tag paths that it was provided. We can use this to our advantage here. Instead of calling the function three times, first combine the tag path lists into one list. NOTE: The order will matter here

paths = value_paths + logging_paths + opcPath_paths
values = system.tag.readBlocking(paths)

Maybe there is a more elegant way to do this, but I’m not certain. In your particular situation I might be tempted to first build a a dictionary and store it somewhere, depending on how often it might change.


Considering it currently only takes 93ms to run (and it only runs once every 10 seconds) as written with 19 tags, I’m going to stick with my version just for readability since I’ll not be the only one working on it.

Partially the same reason that I make a bunch of calls to list.append(), rather than just defining the entire thing as one list.

Thanks for the input though.

But really it’s 19 * 3 tag reads, so really 57. I would also say that 93ms for 57 reads, feels like a long time. My system (which admittedly may have very different specs) did 4000 reads in far less time. It isn’t hurting now with the 10 second cycle, but I would also strongly recommend optimizing up front when you can, rather than trying to come back and do it at a later date when it gets painful.

Just my 2 cents, carry on.


Spread the word !

1 Like

After adding more tags (up to 44 now) I saw the execution time ~900 ms.

I took your advice and edited the script, but it only brought it down to ~600 ms, which does seem very slow.

dataLogger = system.tag.readBlocking(["[default]DataLogger"])[0].value

if dataLogger:
	#create empty lists for holding tag properties
	value_paths = []
	logging_paths = []
	opcPath_paths = []
	#filter headloss
	#. more tags
	for path in value_paths:
	tagRead_paths = value_paths + logging_paths + opcPath_paths
	tagsRead = system.tag.readBlocking(tagRead_paths)
	for value_index,path in enumerate(value_paths):
		logging_index = value_index + len(value_paths) #offset logging index from start
		opc_index = logging_index + len(logging_paths) #offset opcPath index from start
		tagHistory.logTagChange(tagsRead[logging_index].value, 0, 0, tagsRead[value_index].value, False, tagsRead[opc_index].value)

Turns out the slowness is in the tagHistory.logTagChange() call. Without it, the new code runs in 2ms.
I had another system.tag.readBlocking() inside that function, so I had 45 total calls to it. I thought this would’ve been the issue, but removing it did nothing to help.

See any other places for optimization?

Here’s tagHistory.logTagChange():

def logTagChange(loggingEnabled, historicalDeadband, minSampleRate, currentValue, initialChange, OpcPath):
	if ((1 == loggingEnabled) and (not initialChange)):
		#trim OPC path for storing in database
		PlcTag = OpcPath.replace("ns=1;s=[PLC]","")

		#get latest DB info
		lastDBvalue = system.db.runNamedQuery("My_Project","Last_Tag_Value",{"tag_name":PlcTag})
		if ( not ((0 == lastDBvalue.rowCount))):
			lastVal = lastDBvalue.getValueAt(0,'val')
			lastTime = lastDBvalue.getValueAt(0,'t_stamp')
			deltaT =,
		#large enough change in value to log the change
		if ((0 == lastDBvalue.rowCount) or (abs(currentValue - lastVal) >= historicalDeadband) or ((minSampleRate > 0) and (deltaT >= minSampleRate))):
			system.db.runNamedQuery("My_Project", "Store_Tag_Change", {"tag_name":PlcTag,"val":currentValue})

Last_Tag_Value Query:

SELECT TOP 1 val, t_stamp FROM [dbo].[Data] where tag_id=:tag_name order by t_stamp desc

Store_Tag_Change Query:

INSERT INTO [dbo].[Data] ([tag_id],[val])
VALUES (:tag_name,:val)

There might be a way to write a different query to insert all of the rows at once, rather than inserting them one at a time with the for loop, but I’m not super familiar with SQL.

If your logTagChange function isn’t returning anything, and you’re passing all the required context into it, you can safely use system.util.invokeAsynchronous to move its execution to a distinct thread.

args = (tagsRead[logging_index].value, 0, 0, tagsRead[value_index].value, False, tagsRead[opc_index].value)
system.util.invokeAsynchronous(tagHistory.logTagChange, args)
1 Like

Perfect! Thanks