Could use some help with steps in Shortcuts (grab JSON, search for value, act on value if found)

Hitting an API that returns JSON. I can get that and see it. But I’ve been out of the loop a bit and not sure the cleanest way to use steps, which are available etc.

When I get the JSON back, I’d like to search for a known value. If I find that value, send myself an alert or someone else an iMessage.

I also have Jayson, Pythonista, and Toolbox Pro installed if that helps.

What’s the cleanest way to do this? :slight_smile:

Thanks in advance!

JSON is JavaScript Object Notation, so you can work with it natively in Scriptable, but Shortcuts also converts JSON to its dictionary format so you can work with it there natively too. Toolbox Pro isn’t necessary to work with JSON, butt it does have dictionary-based and dictionary compatible actions.

If you know the key to check where the value is to be found, you can should be able to do that directly. If you don’t know, then you have to enumerate the keys at whatever branch the value appears in the JSON, and if you don’t know the branch, then you have to iterate over each one and traverse the data structure until you find a match or run out of structure.

Jayson will let you view the JSON. It isn’t for processing it.

Pythonista’s possible too. You’ll need to import the json library (example on the Pythonista forum).

1 Like

It’s not the easiest to learn, but GizmoPack provides jq for traversing and searching json very efficiently. These are provided as actions to Shortcuts.

1 Like

Could you show us a JSON-sample?

I also suggest that you think about how to handle edge cases such as if the JSON-string contains an error message. Some API:s also return differently structured JSON-strings depending on the number of items contained (typically 1 vs many), so watch out for that :slight_smile:

1 Like

Sure. Attaching two pics. One showing the highest level of the data structure, and one showing the first two entries.

Let’s say I search for a plane with a ‘type’ value of ‘ULAC’. Right now my simple python script just finds the first hit, and grabs that and returns it to me. But it’s quite possible there could be say 8 entries in the JSON that match that. Ideally, I’d like to search the JSON, build a list of the matches, and return the ICAO values for all of them. :slight_smile:

JSON1

Since you have access to Python, I suggest you use Python since it’s a tool well suited for your task. You can review the solution presented below in it’s entirety here: trinket.io

Your data

This is how I assume your data is structured in the server of the API. Note that I have omitted most attributes for sake of clarity. Also note that I’ve added few entries to show that the solution works for multiple entries as well.

api_data = {
  "ac": # There could be more top-level keys
  [
    # Each flight is a dictionary. All such flights are contained in one list
    {"posttime": "1596223612488", "icao": "AE5E06", "type": ""},
    {"posttime": "1596223613939", "icao": "AB63BC", "type": "ULAC"},
    {"posttime": "1596223613940", "icao": "AB63BD", "type": "ULAC"},
    {"posttime": "1596223612489", "icao": "AE5E07", "type": ""},
    {"posttime": "1596223613941", "icao": "AB63BE", "type": "ULAC"},
  ]
}

Converting the above to a JSON string and printing it:

import json

JSON = json.dumps(api_data)

print(JSON)

results in the following string, which should match the string you receive from the api:

{"ac": [{"posttime": "1596223612488", "type": "", "icao": "AE5E06"}, {"posttime": "1596223613939", "type": "ULAC", "icao": "AB63BC"}, {"posttime": "1596223613940", "type": "ULAC", "icao": "AB63BD"}, {"posttime": "1596223612489", "type": "", "icao": "AE5E07"}, {"posttime": "1596223613941", "type": "ULAC", "icao": "AB63BE"}]}

The solution

First, we convert the JSON string to python objects:

data = json.loads(JSON)

This results in the same data structure as the variable api_data above.

The next step is to extract the planes which match your criterion, i.e. the type should be
“ULAC”:

def criterion(plane):
    return p["type"].lower() == "ulac"
  
selected_planes = [p for p in data["ac"] if criterion(p)]

Note that I isolated the criterion in a function. This way, it’s easy for youth modify the criterion as you see fit. Also note that I compared the type to lowercase “ulac”, which makes the comparison more robust.

Finally, we print the selected planes:

print("Selected the following {} planes:\n".format(len(selected_planes)))
print(selected_planes)

The printout of the above is:

Selected the following 3 planes:

[{'posttime': '1596223613939', 'type': 'ULAC', 'icao': 'AB63BC'}, {'posttime': '1596223613940', 'type': 'ULAC', 'icao': 'AB63BD'}, {'posttime': '1596223613941', 'type': 'ULAC', 'icao': 'AB63BE'}]

i.e. only the planes with type “ULAC”.

1 Like

Oh this is fantastic, thanks so much!!!

I think I have it working. I’ll post more later, thx! :slight_smile:

This is working great, thanks again.

Now I have a list/dictionary of all of the hits or matches. Let’s say there are three in the result set.

Now I’d like to iterate through that list of three items and do something with each, for example, send build up a string and ulitmately send myself a push message. Any recommendations? I’m sure I can hobble my through the syntax but I’m also confident there’s a much cleaner way to do it. Thanks again.

Got it working w a nice for each type of construct. :slight_smile:

Sure!

What you need is a for-loop. Something like this should work:

def assemble_string(plane):
  plane_type = plane[“ttype”]
  return “Plane: {} has left the ground”.format(plane_type)

def send_push_msg(msg):
  # send push containing msg

for plane in planes:
  msg = assemble_string(plane)
  send_push(msg)

I also recommend you to read up on for-loops in general and in Python. It’s a very useful concept :slight_smile:

Good luck!

1 Like

Thanks again!

I ended up with something like this:

szMessageToSend = ""
for i in selected_planes:
	print(i["icao"])
	szMessageToSend = szMessageToSend + "Type: " + i["type"] + "\n"
	szMessageToSend = szMessageToSend + "ICAO: " + i["icao"] + " flying at " + i["alt"] + 'ft\n'
	szMessageToSend = szMessageToSend + 'https://tar1090.adsbexchange.com/?icao=' + i["icao"] + "\n\n"

print(szMessageToSend)

Awesome!

When automating, the most important part is getting something which works. If your supplied code works as intended, that’s all you need for now :grinning:

However, if you aspire to become a better automator/programmer and write cleaner code, I present some ideas for improvement below.

Use the assignment operator

When assembling strings, the following are equivalent:

a = “”
a = a + “The”

b = “”
b += “The”

+=” is called an assignment operator and is a shorter version of “a = a + ”. This makes for increased readability.

Use better variable names

In your code, you name the iteration variable i. Usually, i is used when looping over integers. In your case, you are iterating over a list called selected_planes. Therefore a better variable name could be selected_plane, another plane:

for plane in selected_planes:
  # ...

Define a method for string assembly

Encapsulate your string assembly in a method, e.g.:

def assemble_plane_string(plane):
  message_to_send = ... # assemble the string

By doing this, you not only make it easier to modify or re-use your code in the future, you also make it much clearer for a future reader to understand whats going on. Believe it or not, but in a few weeks (sometimes even days) you will have trouble understanding how the code you wrote works. Do your future self a favor and strive towards code that’s self explanatory.

Apply the method to all planes

Instead of combining lots of small strings in a for-loop the way you did it, you can utilize the str.join() method in the following way:

separator = “\n\n”
plane_strings = []
for plane in selected_planes:
  plane_string = assemble_plane_string(plane)
  plane_strings.append(plane_string)

szMessageToSend = separator.join(plane_strings)

What you do is:

  1. Assemble a list of strings
  2. Join all strings to one large string, separated by the variable separator.

To take this one step further, you can use Python’s list comprehensions:

separator = “\n\n”
plane_strings = [assemble_plane_string(plane) for plane in selected_planes]
szMessageToSend = separator.join(plane_strings)

Note how the combination of utilizing proper methods (str.join()) and concepts (list comprehensions) with suitable variable and method names make the code easy to grasp.

I hope that you found my rambling helpful :blush:

1 Like

This is great, thx! I’m much more familiar w C and don’t know much Python, let alone what’s on the standard and common libs.

For example, the idea that you can just use strings so easily seems like a luxury. And I was very worried about the scope of the local var message_to_send. :slight_smile:

You are absolutely correct in that Python is luxurious compared to C, especially when it comes to string handling.

Since you’re already familiar with programming, albeit not in Python, I recommend you to read the short text The Zen of Python as it covers the mindset of Python. You should also be aware of the official style guide PEP-8.

Regarding your worries about the scope of message_to_send: you would of course have to return the message as well. The variable only exists in the scope of the method.

:blush:

1 Like