Help: Posting from Gateway Tag Change/Event to Webhook?

Hello everyone, I'm relatively new to Ignition and come from a low-code application development background, not industrial automation or PLC programming. I've taken on a project at my company because we currently don't have anyone else diving into these areas. It's not technically in my job title but trying to help out since we have limited resources. I could really use some guidance on setting up a Gateway Tag Change Script correctly!

Situation

I'm trying to set up a script that monitors specific tags (related to natural gas levels and temperatures) and sends this data to a webhook whenever these tags change. While I can see the tags change in real-time (they are definitely updating), the script meant to post this data isn't triggering.


Note: The webhook in my script is for testing, not our actual target, so I can play around with it and not disrupt anything. The ultimate goal is to post to a 3rd party webhook so they can help us monitor our meters -- we just need a way to get them the data, so this is what we/they came up with.

Steps Taken

1. Script Execution Location

I’ve set up my script under the Gateway Tag Change Scripts in Ignition. This is how I've configured it:

  • Script Name: TagChange_v4
  • Change Triggers: Value (to trigger the script when the value changes)
  • Tag Path(s): I have listed all the tag paths I want to monitor. They all check out correctly when I hit the "Check Tag Paths" button.

image

2. Project Script Library

I created a script in the Project Library called dataPublisher. This project is named STF_Scripting. It does execute on the Gateway, but it doesn't post anything to the webhook or log any information.

Here’s the script I added in dataPublisher -- it's in my Project Library:

# dataPublisher

import system
import json
from java.text.SimpleDateFormat
from java.util import Date

def postTagChangeData(event):
    logger = system.util.getLogger("Losant Data Publisher")
    try:
        # Skip the initial change event
        if event.initialChange:
            logger.info("Initial change event skipped.")
            return

        # URL and headers for the webhook
        url = 'https://ignitionlosant.free.beeceptor.com'
        headers = {"Content-Type": "application/json"}

        # Get the current time in the specified format
        dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
        current_time = dateFormat.format(Date())

        # List of tag paths
        tagPaths = [
            "[default]NaturalGas/Hot_H2O_Tnk_1_Lvl",
            "[default]NaturalGas/Hot_H2O_Tnk_1_Tmp",
            "[default]NaturalGas/Hot_H2O_Tnk_2_Lvl",
            "[default]NaturalGas/Hot_H2O_Tnk_2_Tmp",
            "[default]NaturalGas/Cold_H2O_Tnk_3_Lvl",
            "[default]NaturalGas/Cold_H2O_Tnk_3_Tmp"
        ]

        # Read the tag values
        values = system.tag.readBlocking(tagPaths)

        # Construct data payload
        data_payload = {}
        for i, tagPath in enumerate(tagPaths):
            data_payload[tagPath] = values[i].value
        
        # Log the data payload for debugging
        logger.info("Data payload: {}".format(data_payload))

        # Prepare the final JSON payload
        json_payload = json.dumps({
            "data": data_payload,
            "time": {"$date": current_time}
        })

        # Log the JSON payload for debugging
        logger.info("JSON payload: {}".format(json_payload))

        # Send the HTTP POST request
        client = system.net.httpClient()
        response = client.post(url, headers=headers, data=json_payload)

        # Log the response from the server
        if response.good:
            logger.info("HTTP POST request sent to Losant. Status Code: {}, Response: {}".format(response.statusCode, response.text))
        else:
            logger.error("HTTP POST request failed. Status Code: {}, Response: {}".format(response.statusCode, response.text))

    except Exception as e:
        # Log any exceptions that occur
        logger.error("Error sending data to Losant: " + str(e))

Webhook:

I have verified that the webhook I'm using for testing is fully functional. I can post to it from Ignition using the normal script module and I can post to it from Postman, so seems like it's just an issue with either a) me taking the wrong approach or b) this kind of workflow not being possible and I need to change directions/learn more about how Ignition functions?

Summary

Feel free to make me look like a complete idiot if I'm taking the wrong approach here or missing something obvious!

I haven't been able to find any direct guidance on posting from a script of this nature to a webhook. The idea was:

  • Function postTagChangeData: Reads the values of the specified tags from the event object, constructs a payload, and sends it to the webhook.
  • Gateway Event Script: Triggers on any change in the specified tags and calls postTagChangeData.
1 Like

Good morning KB, welcome to the forum.

And great first post. You are on the right track, I think. But some items to change to ensure you get good diagnostics:

  • Move your creation of your httpClient outside the function. The client object is designed to be re-used, and can cause memory leaks if you keep making more of them. Like so:
#  ....
client = system.net.httpClient()

def postTagChangeData(event):
# ....
  • In your event, remove the import from dataPublisher. Never import from project library scripts. They auto-load when referenced, so the event script should be a one-liner:
dataPublisher.postTagChangeData(event)
  • Your except clause calls out jython's Exception, which does not catch java exceptions. Different inheritance hierarchy. I recommend using two clauses, and making the jython exceptions more logger-friendly. You need java's Throwable class in your imports, like so:
from java.lang import Throwable

Then your catch clauses should look like this:

    except Throwable as t:
        # Log any exceptions that occur
        logger.error("Java Error sending data to Losant", t)
    except Exception as e:
        # Log any exceptions that occur
        logger.error("Jython Error sending data to Losant", later.PythonAsJavaException(e))

Notice that java loggers accept Throwable last arguments and will place the full backtrace in your logs. Place my later.py script in your project library to have the wrapper that converts jython exceptions into java logger-compatible exceptions.

With this last change, the real problem will likely be logged.

Side note: If you are feeling brave, you can try the new, more capable, exception wrapper I posted yesterday:

2 Likes

Good morning KB, welcome to the forum. And great first post. You are on the right track, I think. But some items to change to ensure you get good diagnostics:

Thank you very much for the very detailed response, and for not making me feel like too much of an amateur for my missteps! I will address these issues and report back asap!

Side note: If you are feeling brave, you can try the new, more capable, exception wrapper I posted yesterday:

Very cool! Not sure I'm quite ready to utilize the Toolkit, but it sounds like a valuable resource to refine scripts and debug. Great to see people with your expertise contributing to this community.

In addition to Phil's points (and I'll also echo - great start), some minor additional points to improve things a little bit further. These are by no means required, but getting these habits ingrained now only pays dividends over time.

There's no need to do these imports manually. The system library that's automatically available to you (specifically system.date) has direct analogues to these functions available; specifically system.date.now() and system.date.format().

There's nothing at all wrong with this - it'll work fine and if it's more readable to you, by all means.
But I'll point out, because Python is a language that favors certain idioms, you can also rewrite this as:

data_payload = dict(zip(tagPaths, values))

Similarly to system.date, prefer the use of system.util.jsonEncode/jsonDecode for tasks like this. It's tempting to use the Jython standard library, and it's what lots of online resources will give you, but when working within Ignition it's best to avoid it. In order of preference:

  1. System functions
  2. Java standard library imports
  3. Java third party library imports
  4. Jython standard library imports.

Also, system.net.httpClient will automatically JSON encode a Python dictionary you give it, so you don't need the json import at all.

The LoggerEx objects you get via system.util.logger have an overload for each logging level, e.g. logger.infof that accepts trailing arguments - so you can rewrite this call as:

logger.infof("JSON payload: %s", json_payload)

This is very much a micro-optimization, but the main benefit is that you're deferring the creation of the final string until it's actually logged - this is especially useful for "trace" or "debug" level information.

4 Likes
  • Your except clause calls out jython's Exception, which does not catch java exceptions. Different inheritance hierarchy. I recommend using two clauses, and making the jython exceptions more logger-friendly. You need java's Throwable class in your imports, like so:

You are a lifesaver! Those changes and loading your later.py script into our library caught the exception. I fixed it and we are sending the data without issue to the webhook!

1 Like

PGriffithSoftware Development

Thanks for the tips! The documentation and resources with Ignition are great and I love the software. Glad our org. has embraced it, but I can definitely stand to learn a lot so I appreciate insights from those of you with far more knowledge than myself.

Similarly to system.date, prefer the use of system.util.jsonEncode/jsonDecode for tasks like this. It's tempting to use the Jython standard library, and it's what lots of online resources will give you, but when working within Ignition it's best to avoid it. In order of preference:

  1. System functions
  2. Java standard library imports
  3. Java third party library imports
  4. Jython standard library imports.

Put this in my notes for reference -- completely agree with your point that "getting habits ingrained now pays dividends over time."


Had no idea I was wasting effort with unnecessary imports, or that

system.net.httpClient will automatically JSON encode a Python dictionary you give it, so you don't need the json import at all.


Also, this makes a lot of sense:

The LoggerEx objects you get via system.util.logger have an overload for each logging level, e.g. logger.infof that accepts trailing arguments - so you can rewrite this call as:

logger.infof("JSON payload: %s", json_payload)

This is very much a micro-optimization, but the main benefit is that you're deferring the creation of the final string until it's actually logged - this is especially useful for "trace" or "debug" level information.

I hadn't thought about that kind of optimization, but on the plus side, this is my first real scripting effort in this environment so at least I don't have too many bad habits to undo!