How to read properties of existing device connection - Example Code

I had often been disappointed that not all device connection properties for existing devices were easily available through supported scripting (system.device.listDevices()) . I recently had cause to develop a methodology to create/update/remove devices, UDTs (definitions/instances), tags and tag alarms based on an Excel workbook. Without scripted access to device connection properties it was difficult to determine whether a device required an update so I dug in and finally found a way to retrieve all device connection properties.

Disclaimers:

  • This methodology is not officially supported in that it uses classes and functions that are not guaranteed to stay consistent between minor/major Ignition versions. However I expect it will continue to work for all future Ignition v8.1.x releases.
  • This methodology was developed in Ignition 8.1.50, do not expect it to work in Ignition v8.3.x
  • The code provided must run in the Gateway context (run from Gateway message handler, Gateway event script, or Perspective script)
  • As far as scripted update of device properties, probably the most advisable option is to read the existing properties for the device, organize it in such a way that it can can be used to create a new device using system.device.addDevice(), apply whatever modification is needed, then delete/remove the existing device and add a new one with the modified properties.

Credit
This methodology was developed through examination and extrapolation of hints left in other forum posts but none of those threads provided a functioning example. I’ll attempt to leave a list of referenced forum threads here.

Example Code:

import traceback
from com.inductiveautomation.ignition.gateway import IgnitionGateway
def getDeviceConfigurations():
	"""Returns a list of dictionaries, each dictionary containing device configuration.
	This methodology is not officially supported in that it uses classes and functions 
	that are not guaranteed to stay consistent between minor/major Ignition versions. 
	However I expect it will continue to work for all future Ignition v8.1.x releases.
	"""
	logger = system.util.getLogger("getDeviceConfigurations")
	session = IgnitionGateway.get().getPersistenceInterface().getSession()
	try:
		# Query the Ignition Internal Database for the names of tables containing device connection settings/properties
		# It appears these tables end in either "DEVICESETTINGS" or "DRIVERSETTINGS"
		# but it is possible some relevant tables have been overlooked.
		logger.info("Table Names | performing query...")
		sql = "SELECT name FROM sqlite_master WHERE type='table' AND ( name LIKE '%DEVICESETTINGS' OR name LIKE '%DRIVERSETTINGS' )"
		flush=True
		params=[]
		result = session.rawQueryMaps(sql, flush, params)
		tableNames = [item["name"] for item in result]
		logger.infof("Table Names | Count: %d | Result: %s", len(result), tableNames)
		
		# Query each table for device connection settings/properties
		deviceConfigs = {}
		for tableName in tableNames:
			logger.infof("Table '%s' | performing query...", tableName)
			sql = "SELECT * FROM %s" % tableName
			flush=True
			params=[]
			result = session.rawQueryMaps(sql, flush, params)
			logger.infof("Table '%s' | Count %d devices | Result: %s", tableName, len(result), str(result))
			
			for deviceConfig in result:
				# Determine the device setting ID
				# This ID is used to associate the "DEVICESETTINGS" table with the relevant device driver table
				# "DEVICESETTINGS_ID" is used in the "DEVICESETTINGS" table
				# "DEVICESETTINGSID" is used in the relevant device driver tables
				if "DEVICESETTINGS_ID" in deviceConfig:
					deviceSettingsId = deviceConfig["DEVICESETTINGS_ID"]
				else:
					deviceSettingsId = deviceConfig["DEVICESETTINGSID"]
				for propertyKey, propertyValue in deviceConfig.items():
					logger.infof("Table: %s Key: %s Value: %s", tableName, propertyKey, propertyValue)
					if propertyKey in ["DEVICESETTINGSID", "DEVICESETTINGS_ID"]:
						continue
					else:
						# Table "DEVICESETTINGS" contains the standard/common device properties
						# Other tobles contains the driver specific device properties
						deviceConfigs.setdefault(deviceSettingsId, {})[propertyKey]=propertyValue
		for deviceId, deviceConfig in deviceConfigs.items():
			logger.infof("Device ID: %d | Name: %s | Config: %s", deviceId, deviceConfig["NAME"], deviceConfig)
		return deviceConfigs.values()
	except:
		logger.error(traceback.format_exc())
		raise
	finally:
		session.close()

Bonus:
Found out late in the game that the Ignition gateway provides a URL (http://localhost:8088/web/status/sys.internaldb) with interface that can be used to query the internal database.

2 Likes
1 Like

OK, I thought you were joking but I tried it and it works! Apparently I am not cultured enough to recognize that from the world of video games; had to look up what it is, assuming it was a cheat code with historical significance (see Konami Code). I blame my ignorance on never having had an NES. Of course now I am wondering what other hidden Ignition features I am unaware of.

2 Likes

So...
My solution above was tested using a script transform on a custom property of a Perspective view that called the function from the project library.
Today I was doing development and wanted the function results to use in designer script console. So I created a gateway message handler that would call the function in the project library and return the results.
I found that when the function was called from the gateway message handler it would fail with ImportError: cannot import name IgnitionGateway
Eventually I took all the relevant code, including the import, and put it directly in the message handler and then it worked.

Can someone explain why

  • Calling the function from a transform script in Perspective works.
  • Pasting the code directly in the gateway message handler works.
  • BUT... Calling the function from the gateway message handler fails with `ImportError: cannot import name IgnitionGateway?

Update:
Putting the import inside the function (instead of outside at module level) resolves the issue and I can successfully call the function from the gateway message handler.

I found the problem... (hangs head in shame) I had created a function to make the message handler request and put it inside the same module (script file) as the import and the getDeviceConfigurations() ... because they are associated, so when I invoked the function to make the message handler request from script console it caused the script console interpreter to initialize the module and attempt the import which, of course, is not valid in that context.

Gateway events, including message handlers, use a python legacy scope that has peculiar constraints. (Fixed in v8.3.)

Don't write code in gateway events. Just write a one-liner to call a project library script, and do everything else there. Pass any event locals as function arguments.

In general:

  • Never use def or import in events or component custom methods. Call a project library script function.

  • Never use import in functions or methods in your library. Always place imports in the outer level of the script, so they are initialized once for the life of the script.

1 Like