Json compare tool for Views and Tags?

Just wondering if anyone’s written a JSON compare tool for comparing Views and Tags?
The generic JSON compare tools online are a bit useless for Ignition.

I want be able to see what’s changed between different versions of the project, and it would be particularly useful to show all the differences between an offline dev and live system as a way to catch any missed resources when deploying new changes, especially larger projects developed offline.

Idea added:
https://ideas.inductiveautomation.com/ignition-features-and-ideas/p/add-json-compare-tool-for-views-and-tags-to-show-differences

Is the way the json structure\order for views consistent? Almost seems like you’d just want to use github to track changes.

Yes it’s consistent. Github will be able to tell you all of the changes made, but not in a friendly way. It won’t tell you “these tags were added, these tags were deleted, these tags had these properties change value, etc.”
It’ll tell you “this part of the json is different, this part was removed” but it won’t be able to associate it to a tag which is the uid for us

I wrote something over the weekend in Python to compare tags. It works pretty good! Although I’m finding that there are a lot of properties deleted between tags that were upgraded by a gateway upgrade and the same tags exported from v7 as XML and imported into the v8 system. Lots of properties like “tagGroup”, “readOnly”, “read/writePermissions” and their sub-objects.
I also found that migration from v7 to v8 doesn’t migrate UDT parameters across with their correct dataType. All of my UDT definitions use String-type parameters, however all of these parameters were converted to Integer. The v7 XML export/import to v8 had the correct String dataType.

I ran the tool over a UDT that had some minor changes made to it by another engineer (we change-freezed to upgrade, but urgent on-site work meant that changes had to be made, so they noted which resources they’re made changes to so I could migrate them back into the upgraded project), and it picked up 1400 or so changes. However, when I ran through them, I found there were only 2 modifications that were actually useful, with the rest being all of the other things mentioned above. I assume it will report more useful things however once only working in v8.

I recently rewrote my tag compare script which was a bit dopey, to use a Python library littletree I found via StackOverflow. Most of the kudos goes to its developer who answered my SO question with the majority of the code, but I modified it to include more detail and spit out a CSV.

I haven't checked, but I'd expect this to only run in CPython, I use PyCharm as my IDE. It requires a min of Python 3.10.
The only library dependency not included in the standard libraries is littletree (pip install littletree)

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):
	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)

	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):
	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]})

	changes_csv = '"' + '","'.join(changes[0].keys()) + '"\r\n'
	changes_csv += '\r\n'.join('"' + '","'.join(map(str, change.values())) + '"' for change in changes)
	print(changes_csv)

The result is a table like this:

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

or this:

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}
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
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)
8 Likes