Automation Professionals' Integration Toolkit Module

Agreed, I can't imagine using Ignition without this module anymore. Almost to the point where I'd be willing to turn down work if a client provided no pathway to use it. It's that good.

8 Likes

I still need to take time to look into it. I haven't used it for anything, but I keep hearing everyone sing its praises.

It's hard to wrap your head around the syntax at times, but once you start to use it, it is amazing.

I asked before somewhere, but is there somewhere we can do a wiki-style manual for this so that we could contribute examples? It would require some kind of moderation to prevent it turning into a forum (questions should be asked here, and not on the wiki) and restrict it to working examples.

How would it be structured? A page per function with an alphabetical index page and another with function types / groups?

I'm not going to host a wiki, sorry. New topics here on the IA forum, in this category, are probably the best choice. I'm happy to include tested examples for specific problem types in the official docs, using this forum as a source and with back-links.

Note that it should not be organized by function. Almost any usage will combine several functions for a practical solution, and it is the problem/solution pairs that matter.

2 Likes

There are a few examples with numerous and extremely robust Integration Toolkit uses in the Ignition Exchange. These are worth downloading and inspecting the bindings even if you don't have a use for the actual functionality.

I assumed that you had enough on your plate. That's why the question was to the wider audience.

I could always spin one up easily and just give a handful of people access manually on my home servers (no guarantee of uptime, but usually stays running). I could put it on our company wiki, but wouldn't be able to give anyone write access, but anything sent to me I could post to it.

I think IA should host it, ideally using the forum software somehow.

They do. It is called the "3rd party modules" category. :man_shrugging:

Make topics.

2 Likes

The About the 3rd Party Modules category - #2 pinned post at the top of the says,

A category for module developers (using the Module SDK for Ignition) to post modules.

That would exclude most of us.

Hmmm. I thought it was opened to everyone when the module discussion category went away. Maybe that needs to be revived...

2 Likes

I’m definitely 100% onboard after trying it for a couple weeks but I definitely agree examples might significantly reduce barrier to entry.

I don’t yet know what I can’t do with the expression functions, so I may flail around for a while when I could bang it out in python (or multiple bindings) in 5 minutes. I also feel a bit intimidated to add yet another post to this thread/don’t want to take up Phil’s time by requesting free support for a free module.

I even feel guilty billing customer time while I ā€œlearnā€ how to optimize/refactor a ā€œworkingā€ solution - without a direct request to do so. e.g. i refactored my whole project shortly after I realized this module’s power.

I’m farly certain AI is wrong in this instance:

https://www.perplexity.ai/search/create-an-expression-using-the-PPeJp6OjTMK13lMuo_pI6w#3

(specifically I want to create/replace a nested dictionary based on a value of the parent dictionary in a single binding/transform)

EDIT:
aaaand for the umpteenth time AI’s just not up to the task, though admittedly it took me an hour and I’m sure it could be better:

Binding
{
  "type": "expr-struct",
  "config": {
    "struct": {
      "Node_ID": "coalesce(\r\n\t{view.params.value.Node_ID}, // table subViews\r\n//\t{view.params.value.meta_node.Node_ID}, // table subViews alternative\r\n\t{view.params.Node_ID_static}, // popups\r\n\t{view.params.Node_ID} // url, either node/:Node_ID or node?Node_ID\u003d | (view param bound to urlParam)\r\n)",
      "data": "{session.custom.project.tag.data}"
    },
    "waitOnAll": true
  },
  "transforms": [
    {
      "expression": "asMap(\r\n\tflatten(\r\n\t\tforEach(\r\n\t\t\t\r\n\t\t\twhere(\r\n\t\t\t\t{value}[\u0027data\u0027],\r\n\t\t\t\tit()[\u0027meta_node\u0027][\u0027Node_ID\u0027] \u003d {value}[\u0027Node_ID\u0027]\r\n\t\t\t)[0],\r\n\t\t\tif(\r\n\t\t\t\tit()[0] !\u003d \u0027meta_node\u0027,\r\n\t\t\t\tasPairs(asMap(it()[0], it()[1])),\r\n\t\t\t\tasPairs(\r\n\t\t\t\t\tasMap(\r\n\t\t\t\t\t\t\u0027meta_node\u0027, tags(\r\n\t\t\t\t\t\t\tasList(\r\n\t\t\t\t\t\t\t\twhere(\r\n\t\t\t\t\t\t\t\t\t{value}[\u0027data\u0027],\r\n\t\t\t\t\t\t\t\t\tit()[\u0027meta_node\u0027][\u0027Node_ID\u0027] \u003d {value}[\u0027Node_ID\u0027]\r\n\t\t\t\t\t\t\t\t)[0][\u0027fullPath\u0027] + \u0027.meta_node\u0027\r\n\t\t\t\t\t\t\t)\r\n\t\t\t\t\t\t)[0][1]\r\n\t\t\t\t\t)\r\n\t\t\t\t)\r\n\t\t\t)\r\n\t\t)\r\n\t)\r\n)\r\n",
      "type": "expression"
    },
    {
      "code": "\treturn value\n#\tif not value.data:\n#\t\treturn None\n\treturn next(\n\t\t(\n\t\t\tnode\n\t\t\tfor node in value.data\n\t\t\tif node[\u0027meta_node\u0027].get(\u0027Node_ID\u0027) \u003d\u003d value.Node_ID\n\t\t),\n\t\tNone\n\t)",
      "type": "script"
    }
  ]
}

Intent: overwrite ā€˜static’ tag data retrieved at session start with ā€˜live’ (subscribed) tag data as opposed to triggering a binding refresh via some obtuse/out of band method like gateway/tag change script > gateway/session message handler [ > perspective message handler > component property > property change script ]

(This module is incredible!)

I feel like I’m about to get my hand slapped though :laughing:

... but I definitely agree examples might significantly reduce barrier to entry.

Are you aware of Integration Toolkit Solutions Wiki that was created for this purpose in the past few weeks?

2 Likes

Surprised that link wasn’t already posted. Should I delete?

Nah, but maybe you could create one or more examples in there.
There's a separate thread for discussion about the wiki: https://forum.inductiveautomation.com/t/integration-toolkit-wiki-setup-discussion/105078.

2 Likes

I would suggest moving, "at least", your where() out to a transform, so that you are not re-executing that expression multiple times each time through the loop.

transform(
	where(
		{value}['data'],
		it()['meta_node']['Node_ID'] = {value}['Node_ID']
	)[0],
	asMap(
		flatten(
			forEach(
				
				value(),
				if(
					it()[0] != 'meta_node',
					asPairs(asMap(it()[0], it()[1])),
					asPairs(
						asMap(
							'meta_node', tags(
								asList(
									value()['fullPath'] + '.meta_node'
								)
							)[0][1]
						)
					)
				)
			)
		)
	)
)

I'm not sure I understand enough of what you're trying to get from this to help much further.

1 Like
{
  ā€œname": ā€œtag",
  ā€œtagType": ā€œAtomicTagā€,
  ā€œfullPath": ā€œFolder\tagā€,
  ā€œcustom_tag_property_or_static_data: {	# << replace this nested object
    ā€œtagValue": ā€œa",
    ā€œtagProperty": ā€œb"
  }
}

basically this, but my additional desire is to replace the object with a tags() subscription on the parent object’s tagPath (so the binding automatically updates if the tag data changes, without having to trigger component.refreshBinding()

(the base object is a single element of a system.tag.query() result array)

thank you for the suggestion!

FWIW @pturmel - @lrose’s suggestion above does not have the same result (the object to be replaced is just ’null’) - and perhaps more importantly - it doesn’t seem to match the behavior of breaking it up into two ā€œtraditionalā€ transforms, (simply replacing value() with {value} works as expected)

I’m not complaining, just noting.

single transform()
{
  "type": "expr-struct",
  "config": {
    "struct": {
      "Node_ID": "coalesce(\r\n\t{view.params.value.Node_ID}, // table subViews\r\n//\t{view.params.value.meta_node.Node_ID}, // table subViews alternative\r\n\t{view.params.Node_ID_static}, // popups\r\n\t{view.params.Node_ID} // url, either node/:Node_ID or node?Node_ID\u003d | (view param bound to urlParam)\r\n)",
      "data": "{session.custom.project.tag.data}"
    },
    "waitOnAll": true
  },
  "transforms": [
    {
      "expression": "//overwrite ā€˜static’ tag data retrieved at session start with ā€˜live’ (polled) tag data as opposed to triggering a binding refresh via some obtuse/out of band method like gateway/tag change script \u003e gateway/session message handler [ \u003e perspective message handler \u003e component property \u003e property change script ]\r\ntransform(\r\n\twhere(\r\n\t\t{value}[\u0027data\u0027],\r\n\t\tit()[\u0027meta_node\u0027][\u0027Node_ID\u0027] \u003d {value}[\u0027Node_ID\u0027]\r\n\t)[0],\r\n\tasMap(\r\n\t\tflatten(\r\n\t\t\tforEach(\r\n\t\t\t\t\r\n\t\t\t\tvalue(),\r\n\t\t\t\tif(\r\n\t\t\t\t\tit()[0] !\u003d \u0027meta_node\u0027,\r\n\t\t\t\t\tasPairs(asMap(it()[0], it()[1])),\r\n\t\t\t\t\tasPairs(\r\n\t\t\t\t\t\tasMap(\r\n\t\t\t\t\t\t\t\u0027meta_node\u0027, tags(\r\n\t\t\t\t\t\t\t\tasList(\r\n\t\t\t\t\t\t\t\t\tvalue()[\u0027fullPath\u0027] + \u0027.meta_node\u0027\r\n\t\t\t\t\t\t\t\t)\r\n\t\t\t\t\t\t\t)[0][1]\r\n\t\t\t\t\t\t)\r\n\t\t\t\t\t)\r\n\t\t\t\t)\r\n\t\t\t)\r\n\t\t)\r\n\t)\r\n)",
      "type": "expression"
    },
    {
      "code": "\treturn value\n#\tif not value.data:\n#\t\treturn None\n\treturn next(\n\t\t(\n\t\t\tnode\n\t\t\tfor node in value.data\n\t\t\tif node[\u0027meta_node\u0027].get(\u0027Node_ID\u0027) \u003d\u003d value.Node_ID\n\t\t),\n\t\tNone\n\t)",
      "type": "script"
    }
  ]
}

ā€˜traditional' chained transforms
{
  "type": "expr-struct",
  "config": {
    "struct": {
      "Node_ID": "coalesce(\r\n\t{view.params.value.Node_ID}, // table subViews\r\n//\t{view.params.value.meta_node.Node_ID}, // table subViews alternative\r\n\t{view.params.Node_ID_static}, // popups\r\n\t{view.params.Node_ID} // url, either node/:Node_ID or node?Node_ID\u003d | (view param bound to urlParam)\r\n)",
      "data": "{session.custom.project.tag.data}"
    },
    "waitOnAll": true
  },
  "transforms": [
    {
      "expression": "//overwrite ā€˜static’ tag data retrieved at session start with ā€˜live’ (polled) tag data as opposed to triggering a binding refresh via some obtuse/out of band method like gateway/tag change script \u003e gateway/session message handler [ \u003e perspective message handler \u003e component property \u003e property change script ]\r\n//transform(\r\n\twhere(\r\n\t\t{value}[\u0027data\u0027],\r\n\t\tit()[\u0027meta_node\u0027][\u0027Node_ID\u0027] \u003d {value}[\u0027Node_ID\u0027]\r\n\t)[0]//,\r\n//\tasMap(\r\n//\t\tflatten(\r\n//\t\t\tforEach(\r\n//\t\t\t\tvalue(),\r\n//\t\t\t\tif(\r\n//\t\t\t\t\tit()[0] !\u003d \u0027meta_node\u0027,\r\n//\t\t\t\t\tasPairs(asMap(it()[0], it()[1])),\r\n//\t\t\t\t\tasPairs(\r\n//\t\t\t\t\t\tasMap(\r\n//\t\t\t\t\t\t\t\u0027meta_node\u0027, tags(\r\n//\t\t\t\t\t\t\t\tasList(\r\n//\t\t\t\t\t\t\t\t\tvalue()[\u0027fullPath\u0027] + \u0027.meta_node\u0027\r\n//\t\t\t\t\t\t\t\t)\r\n//\t\t\t\t\t\t\t)[0][1]\r\n//\t\t\t\t\t\t)\r\n//\t\t\t\t\t)\r\n//\t\t\t\t)\r\n//\t\t\t)\r\n//\t\t)\r\n//\t)\r\n//)",
      "type": "expression"
    },
    {
      "expression": "\tasMap(\r\n\tflatten(\r\n\t\tforEach(\r\n\t\t\t{value},\r\n\t\t\tif(\r\n\t\t\t\tit()[0] !\u003d \u0027meta_node\u0027,\r\n\t\t\t\tasPairs(asMap(it()[0], it()[1])),\r\n\t\t\t\tasPairs(\r\n\t\t\t\t\tasMap(\r\n\t\t\t\t\t\t\u0027meta_node\u0027, tags(\r\n\t\t\t\t\t\t\tasList(\r\n\t\t\t\t\t\t\t\t{value}[\u0027fullPath\u0027] + \u0027.meta_node\u0027\r\n\t\t\t\t\t\t\t)\r\n\t\t\t\t\t\t)[0][1]\r\n\t\t\t\t\t)\r\n\t\t\t\t)\r\n\t\t\t)\r\n\t\t)\r\n\t)\r\n\t)",
      "type": "expression"
    },
    {
      "code": "\treturn value\n#\tif not value.data:\n#\t\treturn None\n\treturn next(\n\t\t(\n\t\t\tnode\n\t\t\tfor node in value.data\n\t\t\tif node[\u0027meta_node\u0027].get(\u0027Node_ID\u0027) \u003d\u003d value.Node_ID\n\t\t),\n\t\tNone\n\t)",
      "type": "script"
    }
  ]
}

I'll admit to not looking close since others were helping... I'm traveling with limited net time.