runIn - JSON vs map


(www.rboyapps.com - Make your home your butler!) #1

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?


Establish Reference/Pointers to a $settings.switch object
(ActionTiles.com co-founder Terry @ActionTiles; GitHub: @cosmicpuppy) #2

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


(www.rboyapps.com - Make your home your butler!) #3

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.


(Stephan H.) #5

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.


(www.rboyapps.com - Make your home your butler!) #6

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.


(ActionTiles.com co-founder Terry @ActionTiles; GitHub: @cosmicpuppy) #7

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!!!).


(Steve White) #8

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


(Luke - Backend Engineer) #9

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.


(www.rboyapps.com - Make your home your butler!) #10

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.


(Luke - Backend Engineer) #11

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.


(Luke - Backend Engineer) #12

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}"
}

(ActionTiles.com co-founder Terry @ActionTiles; GitHub: @cosmicpuppy) #13

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


(Luke - Backend Engineer) #14

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


(ActionTiles.com co-founder Terry @ActionTiles; GitHub: @cosmicpuppy) #15

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.


(www.rboyapps.com - Make your home your butler!) #16

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.


(Khile Klock) #17

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:


(ActionTiles.com co-founder Terry @ActionTiles; GitHub: @cosmicpuppy) #18

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.


(Khile Klock) #19

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!?


(ActionTiles.com co-founder Terry @ActionTiles; GitHub: @cosmicpuppy) #20

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.


(www.rboyapps.com - Make your home your butler!) #21

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