Efficiently Compare and Analyze Tag Configuration Lists with This Python Tool

Managing and tracking changes in tag configurations can be a complex task, especially when dealing with large systems. Whether you are overseeing industrial automation setups or managing intricate data structures, knowing the differences between configurations is crucial for version control, debugging, and ensuring system integrity. This is where a robust Compare List Tool becomes invaluable.

This article introduces a powerful Python script designed to meticulously compare two sets of tag configurations, represented in JSON format. Leveraging the littletree library, this script provides a detailed report of all modifications, additions, and deletions between your original and modified tag lists. It’s an efficient solution for anyone needing to track changes within complex, hierarchical data.

What is the Tag Compare Script?

This script functions as a specialized compare list tool, tailored for JSON-formatted tag configurations. It takes two JSON files (or Python objects) representing different versions of your tag structure and performs a deep comparison. The output is a clear, CSV-formatted report that outlines every alteration detected.

Here’s what the script meticulously analyzes:

  • Tag Additions and Deletions: Identifies tags that have been added or removed between the two configurations.
  • Tag Property Modifications: Detects changes to the properties of existing tags, highlighting which properties were modified, their original values (“from”), and their new values (“to”).

This level of detail makes it an exceptional tool for:

  • Version Control: Easily track changes between different versions of your tag configurations.
  • Configuration Auditing: Understand exactly what has been modified in your system’s setup.
  • Debugging and Troubleshooting: Pinpoint configuration discrepancies that might be causing issues.
  • Change Management: Document and review every alteration made to your tag structures.

Key Features of this Compare List Tool

  • Detailed CSV Output: The script generates a CSV report that is both human-readable and easily parsed by other systems. This report includes columns for tagpath, tagtype, change, from, and to, providing a comprehensive overview of all detected differences.
  • Flexible Input: It can accept input as either JSON file paths or Python list/dictionary objects, offering flexibility in how you integrate it into your workflow.
  • Exclusion Options: The script includes parameters to exclude tag changes within removed folders or added UDT (User-Defined Type) instances. This is particularly useful in complex systems where you might want to focus on specific areas of change and ignore changes that are part of larger structural modifications.
  • Uses littletree Library: Built upon the efficient littletree Python library, the script is optimized for handling hierarchical data structures, ensuring accurate and fast comparisons.

How to Use the Compare List Tool

To utilize this compare list tool, you’ll need to have Python 3.10 or later installed, along with the littletree library.

Prerequisites:

  1. Python 3.10+: Ensure you have Python version 3.10 or greater installed on your system.

  2. littletree Library: Install the littletree library using pip:

    pip install littletree

Python Script:

from littletree import Node
import json

def compare_tag_json(json_orig_filepath, json_new_filepath, exclude_tags_in_removed_folders=True, exclude_tags_in_added_udt_instances=True):
    """
    This is a wrapper for function compare_tag_obj.
    Takes two tag JSON exports to files and produces a report of changes between the tag structures, including:
    - additions/deletions of tags
    - additions/modifications/removal of tag props

    Args:
        json_orig_filepath: The filepath to the original tag json file for the comparison.
        json_new_filepath: The filepath to the new tag json file for the comparison.
        exclude_tags_in_removed_folders: For folders removed in the new file, this option will exclude the tag changes (i.e. deletions) to the tags contained within the folder. Default: True
        exclude_tags_in_added_udt_instances: For UDT Instances added in the new file, this option will exclude the tag changes (i.e. additions) to the tags contained within the UDT Instance. Default: True

    Returns:
        The table of changes in CSV format with headers: tagpath, tagtype, change, from, to
        Also prints the results as a CSV to the console.
    """
    with open(json_orig_filepath, 'r') as f:
        json_orig = f.read()
    with open(json_new_filepath, 'r') as f:
        json_new = f.read()
    original = json.loads(json_orig)
    modified = json.loads(json_new)
    return compare_tag_obj(original, modified, exclude_tags_in_removed_folders, exclude_tags_in_added_udt_instances)


def compare_tag_obj(tags_original, tags_modified, exclude_tags_in_removed_folders=True, exclude_tags_in_added_udt_instances=True):
    """
    Takes two tag JSON as Py lists/dicts and produces a report of changes between the tag structures, including:
    - additions/deletions of tags
    - additions/modifications/removal of tag props

    Args:
        tags_original: The list/dict structure of the original tag json file for the comparison.
        tags_modified: The list/dict structure of the new tag json file for the comparison.
        exclude_tags_in_removed_folders: For folders removed in the new file, this option will exclude the tag changes (i.e. deletions) to the tags contained within the folder. Default: True
        exclude_tags_in_added_udt_instances: For UDT Instances added in the new file, this option will exclude the tag changes (i.e. additions) to the tags contained within the UDT Instance. Default: True

    Returns:
        The table of changes in CSV format with headers: tagpath, tagtype, change, from, to
        Also prints the results as a CSV to the console.
    """
    original_tree = Node.from_dict(tags_original, identifier_name='name', children_name='tags')
    modified_tree = Node.from_dict(tags_modified, identifier_name='name', children_name='tags')

    # Collect changes in a list
    changes = []
    for diff_node in original_tree.compare(modified_tree).iter_tree():
        diff_data = diff_node.data
        tagpath = str(diff_node.path)[1:]
        if not diff_data:
            continue  # Data was the same

        if 'self' not in diff_data:
            if not exclude_tags_in_added_udt_instances or not any(change['tagpath'] in tagpath and change['change'] == 'Tag added' and change['tagtype'] == 'UdtInstance' for change in changes):
                changes.append({'tagpath': tagpath, 'tagtype': diff_data['other']['tagType'], 'change': 'Tag added', 'from': None, 'to': None})
        elif 'other' not in diff_data:
            if not exclude_tags_in_removed_folders or not any(change['tagpath'] in tagpath and change['change'] == 'Tag removed' for change in changes):
                changes.append({'tagpath': tagpath, 'tagtype': diff_data['self']['tagType'], 'change': 'Tag removed', 'from': None, 'to': None})
        else:
            original_data, modified_data = diff_data['self'], diff_data['other']
            all_keys = set(list(original_data.keys()) + list(modified_data.keys()))
            for key in all_keys:
                if key in original_data and key in modified_data:
                    if original_data[key] != modified_data[key]:
                        changes.append({'tagpath': f'{tagpath}.{key}', 'tagtype': original_data['tagType'], 'change': 'Modified property', 'from': original_data[key], 'to': modified_data[key] })
                elif key in original_data:
                    changes.append({'tagpath': f'{tagpath}.{key}', 'tagtype': original_data['tagType'], 'change': 'Removed property', 'from': original_data[key], 'to': None })
                elif key in modified_data:
                    changes.append({'tagpath': f'{tagpath}.{key}', 'tagtype': modified_data['tagType'], 'change': 'Added property', 'from': None, 'to': modified_data[key]})

    if changes: # Check if changes list is not empty before proceeding
        changes_csv = '"' + '","'.join(changes[0].keys()) + '"rn'
        changes_csv += 'rn'.join('"' + '","'.join(map(str, change.values())) + '"' for change in changes)
        print(changes_csv)
        return changes_csv
    else:
        print("No changes detected.")
        return "No changes detected."

Example Usage:

Assuming you have saved the script as compare_tags.py, you can use it in your Python environment. Below is example test code and the resulting output.

Test Code:

original = {
    'name': 'A', 'tagType': 'Folder', 'tags': [
        { 'valueSource': 'memory', 'name': 'New Tag 1', 'tagType': 'AtomicTag' },
        {
            'name': 'New Instance', 'typeId': 'Alarm Summary', 'parameters': {
                'MainTagPath': { 'dataType': 'String', 'value': 'Bob' },
                'Description': { 'dataType': 'String', 'value': 'Descrp' }
            }, 'tagType': 'UdtInstance', 'tags': [
                { 'name': 'Area PLCs for Alarm Reset', 'tagType': 'AtomicTag' },
                { 'name': 'Active Ack', 'tagType': 'AtomicTag' },
                { 'name': 'Active Unack', 'tagType': 'AtomicTag' },
                { 'name': 'Clear Unack', 'tagType': 'AtomicTag' }
            ]
        },
        { 'valueSource': 'memory', 'name': 'New Tag 2', 'tagType': 'AtomicTag' },
        {
            'name': 'New Instance 1', 'typeId': 'Alarm Summary', 'parameters': {
                'MainTagPath': { 'dataType': 'String', 'value': 'Bob' },
                'Description': { 'dataType': 'String', 'value': 'Descrp' }
            }, 'tagType': 'UdtInstance', 'tags': [
                { 'name': 'Active Ack', 'tagType': 'AtomicTag' },
                { 'name': 'Area PLCs for Alarm Reset', 'tagType': 'AtomicTag' },
                { 'name': 'Clear Unack', 'tagType': 'AtomicTag' },
                { 'name': 'Active Unack', 'tagType': 'AtomicTag' }
            ]
        },
        { 'valueSource': 'memory', 'name': 'New Tag', 'value': 2, 'tagType': 'AtomicTag' }
    ]
}

modified = {
    'name': 'A', 'tagType': 'Folder', 'tags': [
        {'name': 'Folder B', 'tagType': 'Folder' },
        { 'valueSource': 'memory', 'name': 'New Tag', 'value': 1, 'tagType': 'AtomicTag' },
        { 'valueSource': 'memory', 'name': 'New Tag 1', 'value': 1, 'tagType': 'AtomicTag' },
        {
            'name': 'New Instance 3', 'typeId': 'Alarm Summary', 'parameters': {
                'MainTagPath': { 'dataType': 'String', 'value': 'Bob' },
                'Description': { 'dataType': 'String', 'value': 'Descrp' }
            }, 'tagType': 'UdtInstance', 'tags': [
                { 'name': 'Area PLCs for Alarm Reset', 'tagType': 'AtomicTag' },
                { 'name': 'Clear Unack', 'tagType': 'AtomicTag' },
                { 'name': 'Active Unack', 'tagType': 'AtomicTag' },
                { 'name': 'Active Ack', 'tagType': 'AtomicTag' }
            ]
        }
    ]
}

compare_tag_obj(original, modified, exclude_tags_in_removed_folders=True, exclude_tags_in_added_udt_instances=True)

Example Output:

The script will print the comparison results in CSV format to your console, and also return the CSV string. The output for the test code above will be:

"tagpath","tagtype","change","from","to"
"A/New Tag 1.value","AtomicTag","Added property","None","1"
"A/New Instance","UdtInstance","Tag removed","None","None"
"A/New Tag 2","AtomicTag","Tag removed","None","None"
"A/New Tag.value","AtomicTag","Modified property","2","1"
"A/Folder B","Folder","Tag added","None","None"
"A/New Instance 3","UdtInstance","Tag added","None","None"

Another example output showing more complex changes in tag properties:

"tagpath","tagtype","change","from","to"
"Analogue Input 01.parameters","UdtType","Modified property","{'Alarm_ParentDevice': {'dataType': 'String'}, 'Unit': {'dataType': 'String', 'value': ''}, 'Alarm_Area': {'dataType': 'String'}, 'Format': {'dataType': 'String', 'value': '#,#\#0.0'}, 'PLCName': {'dataType': 'String', 'value': {'bindType': 'parameter', 'binding': '{PLCName}'}}, 'Description': {'dataType': 'String', 'value': ''}, 'DeviceName': {'dataType': 'String', 'value': ''}, 'Global.': {'dataType': 'String', 'value': ''}}","{'Alarm_ParentDevice': {'dataType': 'String'}, 'Unit': {'dataType': 'String'}, 'Alarm_Area': {'dataType': 'String'}, 'Format': {'dataType': 'String', 'value': '#,#\#0.0'}, 'PLCName': {'dataType': 'String', 'value': {'bindType': 'parameter', 'binding': '{PLCName}'}}, 'Description': {'dataType': 'String', 'value': ''}, 'DeviceName': {'dataType': 'String', 'value': ''}, 'Global.': {'dataType': 'String', 'value': ''}}"
"Analogue Input 01/Low Alarm SP","AtomicTag","Tag removed","None","None"
"Analogue Input 01/Alarms/High","AtomicTag","Tag removed","None","None"
"Analogue Input 01/Alarms/_AnyActive.executionMode","AtomicTag","Removed property","EventDriven","None"
"Analogue Input 01/Alarms/_AnyActive.expression","AtomicTag","Removed property","{[.\]High High}","None"
"Analogue Input 01/Alarms/_AnyActive.opcItemPath","AtomicTag","Modified property","{'bindType': 'parameter', 'binding': 'ns=1;s=[{PLCName}]{Global.}{DeviceName}.Alarms'}","{'bindType': 'parameter', 'binding': 'ns=1;s=[{PLCName}]{Global.}{DeviceName}.AnyAlarm'}"
"Analogue Input 01/Alarms/Low Low","AtomicTag","Tag removed","None","None"
"Analogue Input 01/Alarms/Low","AtomicTag","Tag removed","None","None"
"Analogue Input 01/Alarms/Comms Fault","AtomicTag","Tag removed","None","None"
"Analogue Input 01/Alarms/Signal Fault","AtomicTag","Tag added","None","None"
"Analogue Input 01/Alarms/Over or Under Range","AtomicTag","Tag added","None","None"
"Analogue Input 01/Simulation","AtomicTag","Tag removed","None","None"
"Analogue Input 01/High Alarm SP","AtomicTag","Tag removed","None","None"
"Analogue Input 01/Low Low Alarm SP","AtomicTag","Tag removed","None","None"
"Analogue Input 01/Low Alarm Time","AtomicTag","Tag removed","None","None"
"Analogue Input 01/High High Alarm Time","AtomicTag","Tag removed","None","None"
"Analogue Input 01/High Alarm Time","AtomicTag","Tag removed","None","None"
"Analogue Input 01/Low Low Alarm Time","AtomicTag","Tag removed","None","None"
"Analogue Input 01/High High Alarm SP","AtomicTag","Tag removed","None","None"

Conclusion

This Python compare list tool provides a robust and efficient way to track changes in your tag configurations. By providing detailed CSV reports of additions, deletions, and modifications, it simplifies version control, debugging, and change management. Integrate this script into your workflow to gain better visibility and control over your tag configurations.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *