from datetime import datetime def applyExclusions(items, datasetTagpath): """ Sorts a list of tag dictionary objects. This function assumes that the UDT structure has a '{rootPath}/Manual Exclusions/{Info|Config|Meta}'. We fail gracefully if not. Args: items (List[Dict]): A list of tag dictionaries with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...]. datasetTagpath (String): A tagpath to an L4 UDT Category, i.e "[PHXA2_IG_COMN]System/Device Datasets/RTU/Info". Returns: List[Dict]: A device specific dataset with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...]. """ # parse datasetTagpath rootPath = datasetTagpath[:datasetTagpath.rindex('/')] # "[PHXA2_IG_COMN]System/Device Datasets/RTU" category = datasetTagpath[datasetTagpath.rindex('/')+1:] # "Info" exclusionPath = '{rootPath}/Manual Exclusions/{category}'.format(rootPath=rootPath, category=category) logger = system.util.getLogger("Device Dataset/itemsExcluded()") res = [] # Read sort order, convert from unicode to string, and return unchanged items if empty. data = system.tag.readBlocking(exclusionPath)[0] excludedTags = list(data.value) if (data and data.value is not None) else [] excludedTags = [str(tag) for tag in excludedTags] if len(excludedTags) == 0: return items return list(filter(lambda x: str(x.get('tagName')) not in excludedTags, items)) def getTagInstances(deviceDataset, tagPath, folder=""): """ Iterates through a DeviceDataset and returns instances for a Tag Info FlexRepeater. This function is called by the 'instances' binding in the L4 /Info views. Args: deviceDataset (Document): A device specific dataset with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...]. These are stored in [PHXA2_IG_COMN]System/Device Datasets. tagPath (String): A tagpath to the root of a device, i.e "[PHXA2_IG_COMN]RM2505/PHXA2_RM2505_FCU_4". folder (String): The folder the tags live in, i.e "/Config" or "/Meta". Returns: List[List]: instances to be used in a /Tag Info FlexRepeater. """ # Append /Config or /Meta to the tagpath. tagPath += folder res = [] for item in list(deviceDataset): instance = { "name": item.get("tagName", "N/A"), "dataType": item.get("engUnit", "N/A"), "tagPath": item.get("tagPath", ""), "isUDT": item.get("isUDT", False), "udtType": item.get("udtType", "") } res.append(instance) return res def getSortedTagDictionaries(items, datasetTagpath): """ Sorts a list of tag dictionary objects. This function assumes that the UDT structure has a '{rootPath}/Configuration/{Info|Config|Meta}SortOrder'. We fail gracefully if not. Args: items (List[Dict]): A list of tag dictionaries with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...]. datasetTagpath (String): A tagpath to an L4 UDT Category, i.e "[PHXA2_IG_COMN]System/Device Datasets/RTU/Info". Returns: List[Dict]: A device specific dataset with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...]. """ # parse datasetTagpath rootPath = datasetTagpath[:datasetTagpath.rindex('/')] # "[PHXA2_IG_COMN]System/Device Datasets/RTU" category = datasetTagpath[datasetTagpath.rindex('/')+1:] # "Info" sortOrderPath = '{rootPath}/Sorting Orders/{category}'.format(rootPath=rootPath, category=category) # Read sort order, convert from unicode to string, and return unchanged items if empty. data = system.tag.readBlocking(sortOrderPath)[0] sortingOrder = list(data.value) if (data and data.value is not None) else [] sortingOrder = [str(item) for item in sortingOrder] if len(sortingOrder) == 0: return items # Sort items based on tag names in sorting order. sortedItems = sorted( items, key=lambda x: ( sortingOrder.index(x['tagName']) if x['tagName'] in sortingOrder else len(sortingOrder) # append unsorted tags ) ) return sortedItems def getDiff(previousData, currentData, folder=""): """ Get difference between data before generation and after. This is meant to track manual additions being lost (functionality which will be improved soon^tm). Args: previousData (List[Dict]): A device specific dataset with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...]. currentData (List[Dict]): A device specific dataset with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...]. Returns: String: A timestamped log message detailing tags that existed in previousData and not in currentData. """ logMessage = "" for item in previousInfo: tagName = item.get("tagName", "") if tagName is "": continue print( "{} in {}? {}.".format(tagName, currentData, tagName in currentData) ) logger.info( "{} in {}? {}.".format(tagName, currentData, tagName in currentData) ) if tagName not in currentData: logMessage += "\n{}: {} was removed in {} update.".format(timestamp, folder, item.tagName) return logMessage def generateDeviceDataset(deviceTagpath, datasetTagpath, writeToSystem=True, debug=False): """ Create a new device dataset. Args: deviceTagpath (String): A tagpath to the root of a device, i.e "[PHXA2_IG_COMN]Roof/PHXA2_ROOF_RTU_1". datasetTagpath (String): A tagpath to an L4 UDT category, i.e "[PHXA2_IG_COMN]System/Device Datasets/RTU/{Info|Config|Meta|Alarms}". writeToSystem (Boolean)[Optional]: If specified as false, returns the result instead of writing it to datasetTagpath in the system. Returns: List[Dict]: A device specific dataset with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...]. """ import json allTagsOutput = [] #deviceName = deviceTagpath[deviceTagpath.index('/')+1:] if '/' in deviceTagpath: deviceName = deviceTagpath[deviceTagpath.rindex('/')+1:] else: deviceName = deviceTagpath logger = system.util.getLogger('{} Dataset'.format(deviceName)) # Fetch tag configurations and sort by name nodes = system.tag.getConfiguration(deviceTagpath, True) for item in nodes: if "tags" in item: sortedTags = sorted(item["tags"], key=lambda x: x["name"]) sortedTags = sorted( sortedTags, key=lambda x: ( x.get("engUnit", "misc") if x.get("engUnit", "misc") != "" else "misc" ), ) for tagConfig in sortedTags: name = tagConfig.get("name", "") t_type = str(tagConfig.get("tagType", "")) fullPath = "{}/{}".format(deviceTagpath, name) instance = { "instanceStyle": {"classes": ""}, "instancePosition": {}, "name": name, "tagName": name, "tagPath": fullPath } if t_type == "AtomicTag": instance["dataType"] = tagConfig.get("dataType", "") instance["isUDT"] = False # Group based on name containing 'Power', 'Current', or 'Voltage' allTagsOutput.append(instance) elif t_type =="UdtInstance": exemptions = ['AlarmSummary', 'Meta', 'Maintenance'] if name not in exemptions: instance['dataType'] = 'UDT' instance['udtType'] = tagConfig.get('typeId', 'Unknown UDT') instance['isUDT'] = True allTagsOutput.append(instance) if debug: logger.info('allTagsOutput: {}'.format(allTagsOutput)) print('allTagsOutput: {}'.format(allTagsOutput)) ### FlexRepeater Binding bindingOutput = [] for tag in allTagsOutput: tagName = tag["name"] tagPath = tag['tagPath'] tag["tagPath"] = tagPath + "/" + tagName bindingOutput.append(tag) ### Logan Script res = [] for item in bindingOutput: entry = item.copy() entry['tagName'] = item.get('name','') entry['engUnit'] = item.get('dataType','') #tagPath = item["tagPath"] #tagNameIndex = tagPath.rindex("/") + 1 #res.append({"tagName": tagPath[tagNameIndex:], "engUnit": item["dataType"]}) res.append(entry) ### Excluding excludedTagDictionaries = applyExclusions(items=res, datasetTagpath=datasetTagpath) ### Sorting sortedTagDictionaries = getSortedTagDictionaries(items=excludedTagDictionaries, datasetTagpath=datasetTagpath) if debug: logger.info('sortedTagDictionaries: {}'.format(sortedTagDictionaries)) logger.info('writing to: {}'.format(datasetTagpath)) print('sortedTagDictionaries: {}'.format(sortedTagDictionaries)) print('writing to: {}'.format(datasetTagpath)) if writeToSystem: system.tag.writeBlocking(datasetTagpath, [sortedTagDictionaries]) return sortedTagDictionaries def generateDeviceDatasetTrends(deviceTagpath, datasetTagpath, writeToSystem=True): return {} def generateL4(datasetTagpath, writeToSystem=True, debug=False): """ Highest level function. Can be called by users. Generate Info, Config, Meta, and Trends in a L4 UDT. Requires a UDT to exist with a reference tagpath specified. This function is called by _Manual Refresh in the L4 UDT. Args: datasetTagpath (String): A tagpath to an L4 UDT, i.e "[PHXA2_IG_COMN]System/Device Datasets/RTU". writeToSystem (Boolean)[Optional]: If specified as false, returns the result instead of writing it to datasetTagpath in the system. debug (Boolean)[Optional]: Enable verbose logging. Returns: Dict{key: val}: A dictionary containing each dataset. Keys are ['info','config','meta','trends']. """ timestamp = datetime.now().strftime("[%Y-%m-%d %H:%M:%S]") # Returns "[2025-08-08 08:28:32]". logger = system.util.getLogger("Device Dataset") logMessage = "" if debug: logger.info("Generating L4 for {}.".format(datasetTagpath)) print(datasetTagpath) # Get all relevant UDT data. data = system.tag.readBlocking( [ datasetTagpath + "/Info", datasetTagpath + "/Config", datasetTagpath + "/Meta", datasetTagpath + "/Trends", datasetTagpath + "/Alarms", datasetTagpath + "/_Reference Tagpath", ] ) # Catch bad tagPaths and non-existent data. previousInfo = data[0].value if (data and data[0].value is not None) else {} previousConfig = data[1].value if (data and data[1].value is not None) else {} previousMeta = data[2].value if (data and data[2].value is not None) else {} previousTrends = data[3].value if (data and data[3].value is not None) else {} previousAlarms = data[4].value if (data and data[4].value is not None) else {} referenceTagpath = data[5].value if (data and data[5].value is not None) else {} # If no reference device is given, the script explodes. We catch it here. if referenceTagpath is None or len(referenceTagpath) == 0: logger.info( "\n{}: ERROR: Please verify that a reference tagpath is provided.".format(timestamp) ) logMessage += ( "\n{}: ERROR: Please verify that a reference tagpath is provided.".format(timestamp) ) system.tag.writeAsync([datasetTagpath + "/_Log Message"], [logMessage]) return # exit early if debug: logger.info( "prevInfo: {}\nprevConfig: {}\n prevTrends: {}\n prevMeta: {}\n refTag: {}".format( previousInfo, previousConfig, previousTrends, previousMeta, referenceTagpath, ) ) print( "prevInfo: {}\nprevConfig: {}\n prevTrends: {}\n prevMeta: {}\n refTag: {}".format( previousInfo, previousConfig, previousTrends, previousMeta, referenceTagpath, ) ) # Build the dataset for the Info view. Pull each tagName from the dataset to track which manual additions are lost. currentInfo = Global.DeviceDatasets.generateDeviceDataset( deviceTagpath="{referenceTagpath}".format(referenceTagpath=referenceTagpath), # DON'T APPEND THIS WITH ANYTHING datasetTagpath="{datasetTagpath}/Info".format(datasetTagpath=datasetTagpath), writeToSystem=writeToSystem, ) # Note manual additions that are lost in the update. if debug: currentInfoNames = list(map(lambda x: str(x.get("tagName", "N/A")), currentInfo)) logMessage += Global.DeviceDatasets.getDiff(previousData=previousInfo, currentData=currentInfo, folder="Info") currentConfig = Global.DeviceDatasets.generateDeviceDataset( deviceTagpath="{referenceTagpath}/Config".format(referenceTagpath=referenceTagpath), datasetTagpath="{datasetTagpath}/Config".format(datasetTagpath=datasetTagpath), writeToSystem=writeToSystem, ) if debug: currentConfigNames = list(map(lambda x: str(x.get("tagName", "N/A")), currentConfig)) logMessage += Global.DeviceDatasets.getDiff(previousData=previousConfig, currentData=currentConfigNames, folder="Config") currentMeta = Global.DeviceDatasets.generateDeviceDataset( deviceTagpath="{referenceTagpath}/Meta".format(referenceTagpath=referenceTagpath), datasetTagpath="{datasetTagpath}/Meta".format(datasetTagpath=datasetTagpath), writeToSystem=True, ) if debug: currentMetaNames = list(map(lambda x: str(x.get("tagName", "N/A")), currentMeta)) logMessage += Global.DeviceDatasets.getDiff(previousData=previousMeta, currentData=currentMetaNames, folder="Meta") currentAlarms = Global.DeviceDatasets.generateDeviceDataset( deviceTagpath="{referenceTagpath}/Alarms".format(referenceTagpath=referenceTagpath), datasetTagpath="{datasetTagpath}/Alarms".format(datasetTagpath=datasetTagpath), writeToSystem=True, ) if debug: currentAlarmsNames = list(map(lambda x: str(x.get("tagName", "N/A")), currentAlarms)) logMessage += Global.DeviceDatasets.getDiff(previousData=previousAlarms, currentData=currentAlarmsNames, folder="Alarms") if len(logMessage) == 0: logMessage = "{} Successfully generated.".format(timestamp) # Reset the _Manual Refresh tag back to False. system.tag.writeAsync([datasetTagpath + "/_Manual Refresh"], [False]) # Write the aggregate log message to _Log Message. system.tag.writeAsync([datasetTagpath + "/_Log Message"], [logMessage]) return logMessage