runIn - JSON vs map

If you run this piece of code in a SmartApp:

def installed() {
  def evt = [name:"lock", data:[usedCode:null, type:"keypad"], value:"locked"]
  someFunc(evt) // This runs fine
  runIn(10, someFunc, [data: evt]) // This will lead to an exception
}

def someFunc(evt) {
  def data = evt.data
  if ((data?.usedCode != null) && (data?.usedCode >= 0)) {
    return true
  } else {
    return false
  }
}

it throws an exception when the someFunc is called from runIn with the parameters

error groovy.lang.GroovyRuntimeException: Cannot compare org.codehaus.groovy.grails.web.json.JSONObject$Null with value ā€˜nullā€™ and java.lang.Integer with value ā€˜0ā€™

According to the docs runIn is supposed to pass a map but apparently itā€™s converting the map to a JSON object which is having trouble with the null object.

@Jim @gausnes - is runIn supposed to pass a JSON object or a map?

1 Like

My guess is that it gets serialized to JSON when written to the scheduler; and thatā€™s whatā€™s causing the mismatch.

1 Like

Now methods are no longer reuseable and one needs to explicitly handle JSONObject.NULL instead of just null.

IMHO since ST says it should be passing a map, they should just convert the JSON object into a map and pass it to the method which will avoid this issue in the first place.

2 Likes

I apologize in advance for even trying to help here because I am not really a programmer but I Brute Force till I find a workaround. I had similar issues trying to pass a map using runIn. Using logs I realized that using the normal method passes different results from runIn:

def installed() {
  def evt = [name:"lock", data:[usedCode:null, type:"keypad"], value:"locked"]
  someFunc(evt) // This runs fine
  runIn(10, someFunc, [data: evt]) // This will lead to an exception
}

def someFunc(evt) {
  def data = evt.data
  log.debug data
  if ((data?.usedCode != null) && (data?.usedCode >= 0)) {
    return true
  } else {
    return false
  }
}

the first run returns
5:39:05 PM: debug [usedCode:null, type:keypad]

and the second (from runIn) returns
5:39:15 PM: debug {"usedCode":null,"type":"keypad"}

A real pain in the you know what. With my smartApp I considered breaking the text string up and manually readding to a new mapā€¦but nope. Too much trouble for me. Since this was for adding delays to execution in my button controller, I ended up just using device.on([delay:xxx]) instead. None of this is probably helpful to you but I will be looking at this thread to see if someone smarter than me has an actual solution.

1 Like

Thatā€™s correct, for the second method the platform is converting the map to a JSON Object which is what youā€™re seeing with { and }, however JSON Objects (with the grails version ST is using) doesnā€™t check for null the way maps do.

Would be nice of @gausnes or someone from ST looked into it and hopefully patch up the platform to return a map instead of a JSON object, a VERY easy fix for ST to do to convert a JSON to map object, far more difficult to handle it in a reuseable method in the SmartApp/Device Handler.

2 Likes

An unfortunate side-effect of the new ā€œSmartThings Cloud APIā€ is that the current / ā€œoldā€ Groovy-based SmartApp environment is less and less likely to receive improvements. I presume if something is fatally broken it will get the necessary attention, but other than thatā€¦, my expectations are low.

(Iā€™m hoping to be proven wrong here!!!).

I will go even further by saying that I expect weā€™ll see an end of life announcement next year.

Sorry for not responding earlier, I was on vacation and then got sick right after I got back. So Iā€™ve been playing catch up for the last week.

Iā€™m a little torn here, I feel like SmartApp should just be getting a map back and not have to worry about parsing JSON. However we have a number of SmartApps assuming that event.data will be JSON, so making this change would require a significant amount of effort across multiple teams. Iā€™m guessing there are also community SmartApps that would break if we just started to return Maps instead of JSON.

I think to change this functionality we need to add a flag to runIn to allow users to migrate their apps over to using Maps instead of JSON.

1 Like

Welcome back Luke.

So Iā€™m a little confused here. Why would a SmartApp think that itā€™s returning a JSON because the documentation indicates otherwise ?

You have a doable solution but I think itā€™s adding more complexity to the issue in the long run.

2 Likes

Because it has been returning JSONā€¦ I realize this is wrong, but if you followed the documentation currently you wonā€™t end up with a working SmartApps so I would imagine that some people have worked around it.

I totally agree with not wanting to add complexity, let me chat with some other people at ST and see what we want to do.

Mmhmm, where are you seeing this in the documentation?

Iā€™m seeing
http://docs.smartthings.com/en/latest/ref-docs/event-ref.html#getdata

A map of any additional data on the Event.

Signature:
String getData()

Returns:
String - A JSON string representing a map of the additional data (if any) on the Event.

Example:
createEvent(name: "myevent", value: "myvalue", data: [key1: "val", key2: 42])
Then in an event handler method, we can get at the data like this:

def eventHandler(evt) {
   def data = parseJson(evt.data)
   log.debug "event data: ${data}"
  log.debug "event key1: ${data.key1}"
   log.debug "event key2: ${data.key2}"
}

runIn() doesnā€™t take an Event as a parameter ā€¦ it takes a Map of Arguments.

http://docs.smartthings.com/en/latest/ref-docs/smartapp-ref.html#runin

1 Like

Yes but we invoke the handler method with an Event, I can see where this would be confusing though.

2 Likes

Itā€™s not confusing to me at allā€¦ with due respect to everyone involved (and the high quality of the documentation in general!); I think it boils down to the Developer Documentation being incomplete or inaccurate for this subject area.

The snip I took from the documentation says explicitly:

A map of data that will be passed to the handler method.

It doesnā€™t say: "A map of data that will be converted to or encapsulated in an Event object that will be passed to the handler method.


Iā€™ve worked a large software company (Sybase), and believe me, Iā€™m completely empathetic here. I worked in a QA testing group that had a role between the Development team, Documentation team, and the Coding team. Getting them to all agree on what the ā€œspecā€ was ā€¦ was impossible in many cases. It wasnā€™t anyoneā€™s fault. It was just a sub-optimal workflow.

The ā€œrealā€ spec and implementation of runIn() is becoming clearā€¦ i.e., the use of the Event invocation and Event object, etcā€¦ But, unfortunately, external developers had and have to rely on the published Documentation.

And, unfortunately, there is no established remediation process for when the documentation is corrected or the spec+implementation+documentation is modified.

2 Likes

Right as @tgauchat pointed out the documentation says map all over so the expectation is to have a map returned to the method ā†’ Input should be = output (map in = map out). See my first post, it also makes for more intuitive code to have reusable functions.

2 Likes

Whatever happened to this??
I suspect THIS is what is causing ME great griefā€¦ 'cuz the documentation says MAP, so Iā€™m trying, but it doesnā€™t workā€¦ this may have been the clue I was looking forā€¦ :scream:

1 Like

As explained in the related Topic, the documentation is not accurate.

The parameters are converted into an Event Object, which in turn is serialized into JSON (or ā€¦ something like that).

A pure map or Object cannot be passed through runIn(), regardless of what data you give it.

Wellā€¦ so if I try parseJSON() I might be able to get a MAP back? :slight_smile: But will I get reference to my Device backā€¦? :question::exclamation:

Speaking of Documentationā€¦
How clear is thisā€¦ ??

https://docs.smartthings.com/en/latest/ref-docs/smartapp-ref.html?highlight=parseJSON#parsejson

parseJson()

Parses the specified string into a JSON data structure.

Signature:
    Map parseJson(stringToParse)
Parameters:
    String stringToParse - The string to parse into JSON
Returns:
    Map - a map that represents the passed-in string in JSON format.

Does it parse Into a JSON format or does it return a MAP!?

Yes and no.ā€¦

You get a representation of the Device, but not the ability to directly access the Device Object. You cannot use this passed in JSON or MAP to access the Deviceā€™s Commands and Attributes. So you can try to use a some key from the map to find your Device in the settings input variable that contained that Device.

You canā€™t access the Device Object itself is for Security / Isolation reasons. SmartThings only lets you access Attributes and Commands of Devices specified in Preferences. If you could access ā€œanyā€ of the customerā€™s Devices (even those not authorized in Preferences), then this eliminates the ability of the Customer to limit access to their specifically input Things. Similarly, you canā€™t obviously must not be able to access Things of other Locations or other Customersā€™ Accounts.

1 Like

Update: After the recent grails update looks like @gausnes fixed this issue

1 Like