Using Zendesk with the IoT Sandbox to Automate Customer Support

LEVEL 2:  INTERMEDIATE  |

Introduction

In this tutorial, you will learn how to use Zendesk integration with your S5D9 kit to automate customer support. First, you will create a workflow to aggregate daily stats for future Zendesk tickets. Then, you will create a workflow that will request the last 8 digits of a device ID from a customer when they create a ticket. When the customer sends back the device ID, the workflow will send diagnostics information about that device to an internal note on Zendesk, and a thank-you note to the customer. 

 

Here’s what you need to get started:

  1. Synergy S5D9 IoT Fast Prototyping Kit
  2. Ethernet Access
  3. Zendesk Team Account

Estimated Time to Complete: 1- 1.5hrs

Prerequisites:

Step 1: Set up a new project

Activate a new empty S5D9 project here, then provision your board to the new project via step 3 in the tutorial found here.

Alternatively, you can build on top of an existing S5D9 project, as long as it does not have any Zendesk integration yet (for example, the project you created in the Getting Started tutorial will work). Once you have a project, complete parts 1 and 2 here to connect your Zendesk account to this Renesas IoT Sandbox project.

NOTE: If you are using existing S5D9 project, deactivate the workflow that creates respond to ticket creation; in  Linking Zendesk tutorial, part 3: CREATE A WORKFLOW TO RESPOND TO TICKET CREATION). The reason to deactivate is to remove unnecessary responses from this workflow in Zendesk account. To deactivate a workflow, please view Appendix A.

Step 2: Update Zendesk Trigger

You will be updating the Zendesk trigger you created in Part 2 of the Zendesk Linking tutorial to send an event to the Renesas IoT Sandbox every time a ticket is updated, not just when it is created.

Note: Be very careful designing your own workflows once this is selected. If you update a Zendesk ticket from a workflow, that ticket update will trigger an event to be sent back to the IoT Sandbox, which in turn may trigger that same workflow. You will need to keep track of state in order to avoid feedback loops.

On your Zendesk account, click on “Triggers” under “Business rules.” Click on the trigger you previously created.

Add the following under “Meets any of the following conditions.” Click Submit. Now, an event will be sent every time a ticket is created OR updated.

Step 3: Create a new stream

In this step, you’ll create a new stream to later store daily sensor statistics from your board. 

Under Config, click on Data Streams. In the bottom righthand corner, click Create a New Stream.

As the Stream Name, type in “daily_stats.” Click “Save Data Stream”.

 

Step 4: Create Daily Statistics Workflow

In this step, you’ll create a new workflow that will run daily to save statistics about the sensor data in your new stream. This is to aggregate the data for quick access, without having to recalculate previous day’s statistics on command.

Click on Workflow Studio, then click Create New Workflow. Name your workflow “Daily Sensor Statistics.”

Under Tags and Triggers, click Scheduler, then drag Daily onto the screen. Double click on the green box. Under “Users,” type in your device’s API Basic User ID. For time and time zone, enter 11pm and Los Angeles Time. You can choose a different time if you prefer; as long as it runs once a day, the timing doesn’t make a difference.

Click “Save.” Now, under Modules-> Foundation, drag Base Python onto your workflow. Double click on the blue box and replace the code in there with the following python code:

import Analytics
import DateRange
from datetime import datetime, timedelta

def calculate_vibration(sensor_data):
    x_diff = sensor_data['x_accel']['max'] - sensor_data['x_accel']['min']
    y_diff = sensor_data['y_accel']['max'] - sensor_data['y_accel']['min']
    z_diff = sensor_data['z_accel']['max'] - sensor_data['z_accel']['min']
    xyz_diff = x_diff + y_diff + z_diff
    return xyz_diff

now = datetime.utcnow()
day_before = datetime.utcnow() - timedelta(days=1)
dr = DateRange.date_range(day_before, now)
today = Analytics.events('raw', date_range=dr)

payload = {
 'humidity' : {
 'max': None,
 'min': None,
 'avg': None,
 },
 'pressure' : {
 'max': None,
 'min': None,
 'avg': None,
 },
 'temp3' : {
 'max': None,
 'min': None,
 'avg': None,
 },
 'xyz_diff' : {
 'max': None,
 'min': None,
 'avg': None,
 }
 }

humidity_counter = 0
pressure_counter = 0
temp3_counter = 0
vib_counter = 0
for item in today:
    if 'humidity' in item['event_data'] and 'avg' in item['event_data']['humidity']:
        humidity_counter += 1.
        if payload['humidity']['avg']:
            payload['humidity']['max'] = max(payload['humidity']['max'], item['event_data']['humidity']['max'])
            payload['humidity']['min'] = min(payload['humidity']['min'], item['event_data']['humidity']['min'])
            payload['humidity']['avg'] += item['event_data']['humidity']['avg']
        else:
            payload['humidity']['max'] = item['event_data']['humidity']['max']
            payload['humidity']['min'] = item['event_data']['humidity']['min']
            payload['humidity']['avg'] = item['event_data']['humidity']['avg']
    if 'pressure' in item['event_data'] and 'avg' in item['event_data']['pressure']:
        pressure_counter += 1.
        if payload['pressure']['avg']:
            payload['pressure']['max'] = max(payload['pressure']['max'], item['event_data']['pressure']['max'])
            payload['pressure']['min'] = min(payload['pressure']['min'], item['event_data']['pressure']['min'])
            payload['pressure']['avg'] += item['event_data']['pressure']['avg']
        else:
            payload['pressure']['max'] = item['event_data']['pressure']['max']
            payload['pressure']['min'] = item['event_data']['pressure']['min']
            payload['pressure']['avg'] = item['event_data']['pressure']['avg']
    if 'temp3' in item['event_data'] and 'avg' in item['event_data']['temp3']:
        temp3_counter += 1.
        if payload['temp3']['avg']:
            payload['temp3']['max'] = max(payload['temp3']['max'], item['event_data']['temp3']['max'])
            payload['temp3']['min'] = min(payload['temp3']['min'], item['event_data']['temp3']['min'])
            payload['temp3']['avg'] += item['event_data']['temp3']['avg']
        else:
            payload['temp3']['max'] = item['event_data']['temp3']['max']
            payload['temp3']['min'] = item['event_data']['temp3']['min']
            payload['temp3']['avg'] = item['event_data']['temp3']['avg']
    if 'x_accel' in item['event_data'] and 'y_accel' in item['event_data'] and 'z_accel' in item['event_data'] and 'avg' in item['event_data']['x_accel'] and 'avg' in item['event_data']['y_accel'] and 'avg' in item['event_data']['z_accel']:
        vib_counter += 1.
        vib_data = calculate_vibration(item['event_data'])
        if payload['xyz_diff']['avg']:
            payload['xyz_diff']['max'] = max(payload['xyz_diff']['max'], vib_data)
            payload['xyz_diff']['min'] = min(payload['xyz_diff']['min'], vib_data)
            payload['xyz_diff']['avg'] += vib_data
        else:
            payload['xyz_diff']['max'] = vib_data
            payload['xyz_diff']['min'] = vib_data
            payload['xyz_diff']['avg'] = vib_data


if payload['humidity']['avg']:
    payload['humidity']['avg'] = payload['humidity']['avg'] / humidity_counter
if payload['pressure']['avg']:
    payload['pressure']['avg'] = payload['pressure']['avg'] / pressure_counter
if payload['temp3']['avg']:
    payload['temp3']['avg'] = payload['temp3']['avg'] / temp3_counter
if payload['xyz_diff']['avg']:
    payload['xyz_diff']['avg'] = payload['xyz_diff']['avg'] / vib_counter

IONode.set_output('out1', payload)

Click “Save.” Now, under Outputs->Output, drag Processed Stream- Single onto your screen. Connect the green box to the blue box, and the blue box into this new orange box. Your workflow should look like this:

Finally, double click on the orange box. Under “Datastreams” select “daily_stats.” Click “Save and Activate.”

Step 5: Create your Zendesk workflow

Before creating this workflow, recall in part 2 of the Linking Zendesk tutorial found here, you added a new external API under Setup→ External APIs. Navigate there on the IoT Sandbox, and you will see a page similar to the photo below. The number in the red box is the credential ID, and you will need to have it handy in this step.

Go to the Workflow Studio, and click “Create New Workflow.” Name the workflow “Zendesk Ticket Processing.” First, drag a blue Base Python module from under Modules -> Foundation. Double click on the box, then under the dropdown “Inputs/Outputs,” click on “Add Input” three times. There should now be four total inputs listed.

Click “Save.” Under Tags and Triggers-> raw, drag the following four tags and connect them to the respective inputs in the blue box. 

  • Raw.ticket_id goes to in1
  • Raw.ticket_latest_comment goes to in2
  • Raw.current_user_email goes to in3
  • Raw.ticket_all_comments goes to in4

Note: If you don’t see the tags under Tags and Triggers->raw, make sure you enabled them as per Part 1, Step 4 of the Linking Zendesk tutorial. Your workflow should look like this:

Double click on the blue Base Python box again and replace everything in there with the following python code:

# This workflow responds to new or recent Zendesk tickets to gather recent diagnostics information


import Credentials
import Zendesk
import Analytics
import GlobalAnalytics
import DateRange
import Filter
import DateConversion
import re
from datetime import datetime, timedelta

# This function uses regex to search through the most recent comment on the Zendesk ticket 
# for any 8 character alphanumeric strings. It matches any of those 
# strings to the last 8 chacters of each of the users on this project and exits as soon
# as a match is found. 
def find_device():
    regex = r"[a-zA-Z0-9]{8}"
    users = GlobalAnalytics.list_users()
    last_8_characters = re.findall(regex, IONode.get_input('in2')['event_data']['value'])
    log("Regex results: " + str(last_8_characters))
    for user in users:
        for item in last_8_characters:
            if item == user['login_id'][-8:]:
                return user['login_id']
    return None # User not found 

def pretty_print_tag(tag_name):
    return tag_name.replace('_', ' ').replace('mag', 'Magnitude').replace('accel', "Acceleration").replace('temp', 'Temperature ').title()

def pretty_print_datetime(dt):
    return DateConversion.to_py_datetime(dt).strftime('%b %d, %Y at %X UTC')

def calculate_vibration(sensor_data):
    x_diff = sensor_data['x_accel']['max'] - sensor_data['x_accel']['min']
    y_diff = sensor_data['y_accel']['max'] - sensor_data['y_accel']['min']
    z_diff = sensor_data['z_accel']['max'] - sensor_data['z_accel']['min']
    xyz_diff = x_diff + y_diff + z_diff
    return xyz_diff

def print_last_seven_days(device):
    now = datetime.utcnow()
    seven_before = datetime.utcnow() - timedelta(days=7)
    dr = DateRange.date_range(seven_before, now)
    last_seven_days = Analytics.events('daily_stats', date_range=dr, user=device, sort=['event_rcv', 'DESC'])
    if len(last_seven_days) > 0:
        ticket = "Last 7 Day Statistics:\n\n"
        for item in last_seven_days:
            ticket += pretty_print_datetime(item['observed_at']) + ":\n" #TODO add xyz diff
            if not item['event_data']['humidity']['avg'] and not item['event_data']['pressure']['avg'] and not item['event_data']['temp3']['avg'] and not item['event_data']['xyz_diff']['avg']:
                ticket += "No sensor data detected.\n"
            else:
                if item['event_data']['humidity']['avg']:
                    ticket += "Average Humidity: " + str(item['event_data']['humidity']['avg']) + '\n'
                    ticket += "Maximum Humidity: " + str(item['event_data']['humidity']['max']) + '\n'
                    ticket += "Minimum Humidity: " + str(item['event_data']['humidity']['min']) + '\n'
                else:
                    ticket += "No Humidity data detected."
                if item['event_data']['pressure']['avg']:
                    ticket += "Average Pressure: " + str(item['event_data']['pressure']['avg']) + '\n'
                    ticket += "Maximum Pressure: " + str(item['event_data']['pressure']['max']) + '\n'
                    ticket += "Minimum Pressure: " + str(item['event_data']['pressure']['min']) + '\n'
                else:
                    ticket += "No Pressure data detected."
                if item['event_data']['temp3']['avg']:
                    ticket += "Average Temperature: " + str(item['event_data']['temp3']['avg']) + '\n'
                    ticket += "Maximum Temperature: " + str(item['event_data']['temp3']['max']) + '\n'
                    ticket += "Minimum Temperature: " + str(item['event_data']['temp3']['min']) + '\n'
                else:
                    ticket += "No Temperature data detected."
                if item['event_data']['xyz_diff']['avg']:
                    ticket += "Average Vibration: " + str(item['event_data']['xyz_diff']['avg']) + '\n'
                    ticket += "Maximum Vibration: " + str(item['event_data']['xyz_diff']['max']) + '\n'
                    ticket += "Minimum Vibration: " + str(item['event_data']['xyz_diff']['min']) + '\n'
                else:
                    ticket += "No Vibration data detected."
            ticket += '\n'
        return ticket
    else:
            return "No Historical Sensor Data found."

def print_sensor_data(sensor_data):
    ticket = "Most Recent Sensor Data: \n\n"
    ticket += "Vibration: " + str(calculate_vibration(sensor_data)) + "\n"
    ticket += "Temperature: " + str(sensor_data['temp3']['avg']) + "\n"
    ticket += "Humidity: " + str(sensor_data['humidity']['avg']) + "\n"
    ticket += "Pressure: " + str(sensor_data['pressure']['avg']) + "\n\n"
    return ticket


def get_diagnostic_info(device):
    ip = Analytics.last_n_values('raw.ip_address', 1, user=device)

    # This assumes we can filter the events coming from the device in the raw stream based on the string tag
    # raw.device_id. If it has that tag attached to it, it came from the device.
    most_recent = Analytics.events('raw', limit=1, sort=['event_rcv', 'DESC'], user=device, filters = Filter.string_tag('raw.device_id'))

    # Not all events coming in from the board have sensor data in it, so we are filtering events that have sensor
    # data in it. We are assuming that every sensor data event will have the tag raw.pressure.avg, or pressure average.
    # We could have picked any of the other sensor data tags to filter on.
    last_sensor_value =  Analytics.events('raw', limit=1, sort=['event_rcv', 'DESC'], user=device, filters = Filter.numeric_tag('raw.pressure.avg'))
    ticket = "Device Information for board with last 8 digits " + device[-8:] + ':\n\n'
    if len(most_recent) > 0:
        ticket += 'Last Event from Device: ' +  pretty_print_datetime(most_recent[0]['observed_at']) + '\n\n'
    else:
        ticket += 'No recent events from device found\n\n'
    if len(ip) > 0:
        ticket += 'Last IP Address: ' + ip[0]['raw.ip_address'] + '\n\n'
    else:
        ticket += "No IP Address found\n\n"
    if len(last_sensor_value) > 0:
        ticket += print_sensor_data(last_sensor_value[0]['event_data'])
    else:
        ticket += "No sensor data found."
    ticket += print_last_seven_days(device)
    return ticket


def update_ticket_internal(info):
    Zendesk.update_ticket(int(IONode.get_input('in1')['event_data']['value']), info)

def request_device_id():
    response = "Hi, I'm the Renesas IoT Sandbox automated assistant.  Thanks for contacting support. Please respond to this email with the 8 characters of your device's user ID in order to proceed. \nHave a great day!"
    Zendesk.update_ticket(int(IONode.get_input('in1')['event_data']['value']), response, external=True)

def default_external_response():
    response = "Thank you! Someone from support will contact you as soon as possible."
    Zendesk.update_ticket(int(IONode.get_input('in1')['event_data']['value']), response, external=True)

def comment_counter():
    # This counts how many comments are in the whole Zendesk ticket based on this indicator. This prevents 
    # this workflow from continuing past a limited number of comments, especially for the circumstance 
    # when a customer service representative has taken over the ticket.
    comment_indicator = '----------------------------------------------'
    all_comments = IONode.get_input('in4')['event_data']['value']
    return all_comments.count(comment_indicator)

current_email = IONode.get_input('in3')['event_data']['value']
creds = Credentials.use('<Credential ID>')
if current_email != creds['username']: # Do not trigger a response to your own comments!
    comments = comment_counter()
    Zendesk.authenticate(creds['URL'], creds['username'], creds['API Key'])
    log("Comments: " + str(comments))
    if comments == 1:
        request_device_id()
    elif comments < 5: # Do not continue past the five comments in this communication flow
        device_id = find_device()
        if device_id:
            log("Device id: " + device_id)
            update_ticket_internal(get_diagnostic_info(device_id))
            default_external_response()
        else:
            update_ticket_internal("No device could be found")
            default_external_response()

Where it says <Credential ID> above, paste your numeric credential ID you have from above. Make sure to keep the quotations around it. Click “Save and Activate.”

Step 4: Create a Zendesk Ticket

In this step, you will mimick a customer emailing support with a problem. When we use the phrase Zendesk URL, we mean the domain of your Zendesk account. The phrase Zendesk Username means the email address you use to log into your Zendesk account.

Send the following email from an email address different than your Zendesk username, to the email address support@<zendesk url>. This means that if your Zendesk URL is sample.zendesk.com, send the email to support@sample.zendesk.com

Wait a minute or two until you get a response. It should look like this:

Go to the IoT Sandbox. Click on Setup → Manage Users. Copy the last 8 characters of your auto enrolled device ID. This is the long string.

Respond to the email, including these characters somewhere in the message.

Wait a minute or two. You should see another email in your inbox saying:

Now, log into your Zendesk account. Go to “Views” on the sidebar, then click on the ticket you had created.

Click on the ticket. You should see a comment similar to the one below.

If your device was offline in one of the last 7 days, the response may look more like this ticket.

If you did all this project in one day, there will be no historical data and the “Last 7 Day Statistics” segment will not exist. This is normal- your daily statistics aggregate workflow you created in step 4 has not run yet! Try again tomorrow and again in a week to see how the ticket will have historical data in the future.

Congratulations! You now know how to automate customer support!

 

Appendix A: How to deactivate a workflow

To deactivate a workflow, go to Workflow Studio, then click on the workflow you wish to deactivate.

Click on Revisions, then ‘x’ to deactivate.

Note: once the workflow is deactivated, you will not see green ‘activated’ sign next to Autosave (under Revisions tab).