Gateway Timer Script to compare conditions, then send Msg Handler

Hello, I've posted here before and found the advice here very helpful. I'm new to Ignition and to coding and may need to seek clarification to understand any suggestions to this post.

I'm writing to try to pinpoint and understand why the steps below fail to execute a series of actions. My intention is for a Gateway Timer Script to constantly run and when certain conditions are met, call a Message Handler that will click a button.

Message Handler:
I've created a Message Handler on a button called "click_button_to_send_email" and written the following script (image also attached):

def onMessageReceived(self, payload):
    # Simulate a button click on the same component
    self.props.onClick()

Button Event Script:
On that button is also a Component Event>onActionPerformed* script that, when clicked, successfully executes and sends an email--this part works.

Gateway Timer Script:
I then created a Gateway Timer Script configured to run at 60,000ms delay at a fixed rate with the following script (image also attached):

from datetime import datetime
import system

def checkReportSettings_callMessageHandler():
    # Get the current day and time
    now = datetime.now()
    current_day = now.strftime("%A")  # Get the current day of the week
    current_time = now.strftime("%m/%d/%y, %I:%M:%S %p")  # Get the current time in the specified format

    print("Current day:", current_day)
    print("Current time:", current_time)

    # Get the custom properties from the view
    view = system.perspective.getView("ProjectName/ViewName")
    selected_day = view.custom.selected_day
    selected_time = view.custom.selected_time.strftime("%m/%d/%y, %I:%M:%S %p")  # Format the selected time

    print("Selected day:", selected_day)
    print("Selected time:", selected_time)

    # Compare the current day and time with the custom properties
    if current_day == selected_day and current_time == selected_time:
        print("Match found! Executing message handler.")
        # Execute the message handler to click the button
        system.perspective.sendMessage("click_button_to_send_email")
    else:
        print("No match found.")

# Call the function
checkReportSettings_callMessageHandler()

Is my script on the Message Handler incorrect? Am I missing a step?

This is the wrong way to go about this. Move your button script to a project script as a function. Then call the function from the button or from your gateway timer script.

Avoid putting scripts in buttons, this is an anti-pattern referred to as the "magic pushbutton." It makes maintaining your scripts a huge pain. A project script is the one source of truth that everything else will refer to.

Since you want to call this from the gateway, make sure to put the project script in your global scripting project. Scripts in this designated project can be called from the gateway scope. More clarification of this in post 4.

2 Likes

Thank you [dkhayes117] for the best practice guidance. I created a project library script as a function, and then a Gateway Timer Script to click the button which calls the function script.
I'm still unable to get it to work. I also tried with a message handler and still no success. I'm not receiving any Gateway Log errors or any errors in the console. It appears that the Gateway Timer executes but the function script isn't getting called.
Note: if I adapt the script from the project library on the button onActionPerformed, the button works.

Here's what I've done:

Gateway Timer Script:
*Note: I've been changing the time 1-2 minutes ahead and then waiting for the Timer Script to execute.

import datetime
from project.Safety_LOTO_Email_Report import send_email
import system

now = datetime.datetime.now()
system.util.getLogger("Safety_LOTO_Report_Timer").info("Timer script executed at: {}".format(now))

# Example: 14:50 is 2:50 PM in 24-hour format
if now.weekday() == 0 and now.hour == 14 and now.minute == 50:
    send_email()

Button onActionPerformed Script:

def runAction(self, event):
    from project.Safety_LOTO_Email_Report import send_email
    send_email()

Project Library Script:

def send_email():
    import system

    logger = system.util.getLogger("EmailDebug")

    try:
        # Get the values from the text boxes
        email_from = system.gui.getParentWindow(event).getComponentForPath('Root Container.Email from.TextField').text
        logger.info("Email from: {}".format(email_from))
        
        email_to = system.gui.getParentWindow(event).getComponentForPath('Root Container.Email to.TextArea_0').text
        logger.info("Email to: {}".format(email_to))
        
        subject = system.gui.getParentWindow(event).getComponentForPath('Root Container.Subject.TextArea').text
        logger.info("Subject: {}".format(subject))
        
        body = system.gui.getParentWindow(event).getComponentForPath('Root Container.Body.TextArea').text
        logger.info("Body: {}".format(body))

        # Split the email_to string into a list of email addresses
        email_to_list = [email.strip() for email in email_to.split(',')]

        # Retrieve the value from the text box
        email_frequency = system.gui.getParentWindow(event).getComponentForPath('Root Container.Email frequency.TextArea').text
        logger.info("Email frequency: {}".format(email_frequency))

        # Explicitly convert the email_frequency to a string and then to an integer
        email_frequency = int(str(email_frequency))

        # Store the current email_frequency value to reassign it later
        current_email_frequency = email_frequency

        # Execute the named query with the parameter
        table_data = system.db.runNamedQuery("get_Safety_LOTO_to_Report", {"email_frequency": email_frequency})
        logger.info("Query executed successfully")

        # Convert the BasicStreamingDataset to a list of dictionaries
        table_data_list = []
        for row in range(table_data.getRowCount()):
            row_dict = {table_data.getColumnName(col): table_data.getValueAt(row, col) for col in range(table_data.getColumnCount())}
            table_data_list.append(row_dict)

        # Log the table data
        logger.info("Table data: {}".format(table_data_list))

        # Clear the existing table data
        system.gui.getParentWindow(event).getComponentForPath('Root Container.Data Table.Table').data = []

        # Set the results to the table data property
        system.gui.getParentWindow(event).getComponentForPath('Root Container.Data Table.Table').data = table_data_list

        # Convert table data to HTML
        table_html = "<table border='1'>"
        table_html += "<tr><th>Employee ID</th><th>LOTO Tag ID</th><th>Date Checked-out</th><th>Date Checked-in</th></tr>"
        for row in table_data_list:
            table_html += "<tr>"
            table_html += "<td>{}</td><td>{}</td><td>{}</td><td>{}</td>".format(row['Employee ID'], row['LOTO Tag ID'], row['Date Checked-out'], row['Date Checked-in'])
            table_html += "</tr>"
        table_html += "</table>"

        # Combine body text and table HTML
        email_body = body + "<br><br>" + table_html

        # Log email details
        logger.info("Sending email from: {}".format(email_from))
        logger.info("Sending email to: {}".format(email_to_list))
        logger.info("Email subject: {}".format(subject))
        logger.info("Email body: {}".format(email_body))

        # Send the email to each recipient individually
        for email in email_to_list:
            try:
                system.net.sendEmail(
                    smtpProfile="email_Safety_LOTO_Report",
                    fromAddr=email_from,
                    to=email,
                    subject=subject,
                    body=email_body,
                    html=True
                )
                logger.info("Email sent to: {}".format(email))
            except Exception as e:
                system.util.getLogger("EmailError").error("Failed to send email to {}: {}".format(email, str(e)))

        # Reassign the stored email_frequency value back to the text box
        system.gui.getParentWindow(event).getComponentForPath('Root Container.Email frequency.TextArea').text = current_email_frequency

    except Exception as e:
        logger.error("Error in send_email: {}".format(str(e)))

ProjectLibraryScript

You don't need to (and shouldn't) do this.

Either:

  1. Directly access your project scripts by their fully qualified path, without an import statement:
    project.Safety_LOTO_Email_Report.send_Email()
    
  2. If you want to refer to them in short-form later, rebind them with a variable:
    send_email = project.Safety_LOTO_Email_Report.send_Email
    
    # later
    send_email()
    

Explicitly importing project library scripts leads to confusion later, because it'll cause conflicts between the automatically loaded instances and the ones you're explicitly importing.

That said, your issue is evidently with your triggering not working as you expect it to, since your function takes no arguments but runs from a Perspective session on the gateway.

I would also recommend you use system.date functions over Python's datetime...but even better, if your condition is "do this thing at a certain time", why not just use a gateway scheduled script?
Cron expressions are perfectly suited for the condition you're using:

3 Likes

This is not correct for buttons. Buttons belong to user interfaces in projects, so must use library scripts in that project or its parents. The gateway scripting project covers tags and any other non-project resource.

More details on this here:

1 Like

Oh, yes, true.

1 Like

I'm at quite an impass coming on a week. My difficulty seems to lie in my attempts to execute scripts from the Gateway to simulate a button click in my project view.

Any specific advice would be greatly appreciated.

Project Library Script to Call Click on Button:
Taking note of dkhayes117's feedback, I decided to create a project library script, named it "ProjectScript_toClick_ReportButton" with the following script:

def runAction(self, event):
    # Retrieve the session ID
    session_id = self.session.props.id
    system.perspective.print("Session ID: " + session_id)
    
    # Call the function to navigate and click the button
    navigate_and_click_button(session_id)

def navigate_and_click_button(session_id):
    # Navigate to the desired view
    system.perspective.navigate("/Safety_LOTO_Settings_Report")
    
    # Simulate a button click by sending a message
    system.perspective.sendMessage(
        "clickButton",
        payload={"path": "root/FlexContainer/Flex_ReportButton/ReportButton"},
        scope="session",
        sessionId=session_id
    )

Memory Tag to call Project Library Script on Boolean value change:
Given pturmel's comment on Gateway Script covering tags, I created a Memory Tag with a Boolean data type and the following script:

def valueChanged(tag, tagPath, previousValue, currentValue, initialChange, missedEvents):
	# Import the project script
	project.ProjectScript_toClick_ReportButton.runAction(None, None)

Gateway Scheduled Script to Tag:
To make the date and time easy to modify in the future, I took PGriffith's suggestion and added this script to the path of the Tag:

def onScheduledEvent():
    # Path to your Memory Tag
    tag_path = "[default]Safety_LOTO/MemoryTag_toProjectScript"

    # Read the current value of the tag
    current_value = system.tag.readBlocking([tag_path])[0].value

    # Toggle the value
    new_value = not current_value

    # Write the new value back to the tag
    system.tag.writeBlocking([tag_path], [new_value])

I want to continue to be able to click on my button manually and execute the event script on it. The button works and I want to leave that script there. But I'm unable to implement a method that will schedule a click on that button.

You've, frankly, fundamentally misunderstood all of our advice.

You should put the actual business logic into the project library script. The thing that you want done based on a varied trigger.

Then, invoking that script from anywhere becomes utterly trivial - you just invoke the project library script.

The overall end goal is to just send an email with a copy of a report at certain times of the day, correct? Why not just configure a schedule for the report?

Or is the expected send time going to vary from day to day?

I'll return to that approach; I previously placed the button script within the project library script in a central location easy to manage, but there too I was unable to call the click on the button and ran into the same problem using a message handler. How about I place all of the script within the project library (eventually manage to get it to click the button), and use Gateway Scheduled Script to run it? Or would you recommend another approach?

Stop trying to click the button from a script.

Instead, make a function called sendLOTOReportEmail in the project library and use system.report.executeAndDistribute in it to generate the report and send it in an email. The function should take the start and end date for the report span, and a list of emails.

Call this from your button, and call this from your gateway scheduled script.
If you want to store information(such as the list of recipients) store it in a gateway memory tag and read that list as part of the sendLOTOReportEmail function.

1 Like

That's a great suggestion. In a sense, yes. I created a form that allows a user to set the frequency of the report, select the day of the week and time of day to send an email which include a selection of a database to certain recipients. I then settled on changing the time the report sends manually making changes upon request.
I could explore the report feature but I would have to face that learning curve on short timeframe which is why I was hoping my above attempts would work.

All of this exists natively in the report action configuration for emailing. The only limitation is that this is limited to those with designer access. If you had the time you could probably write a script to pull this config out of the gateway, modify/add to it, and send it back.