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.
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.
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.
- Tag Report Utility Ignition Exchange | Inductive Automation
- Spreadsheet Import Tool Ignition Exchange | Inductive Automation
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.
Make topics.
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...
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
... 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?
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.
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.
{
ā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.