Calling Method in driver from App, is there a better way?

I want to call a method in a driver from an app, since I don't want to have to declare each method I need in the driver as a command in order to call it, and I do want return values, I did it in the below way. I also believe it might be possible to do something similar with runInMillis, but that is asynchronous, which is not what I want right here. So while the below works, is there a better way?

In driver (just the relevant parts):

command "deviceCommand", [[name:"cmds*", type: "STRING"]]
def deviceCommand(cmd) {
     def jsonSlurper = new JsonSlurper()
     cmd = jsonSlurper.parseText(cmd)
     logging("deviceCommand: ${cmd}", 1)
     r = this."${cmd['cmd']}"(*cmd['args'])
     logging("deviceCommand return: ${r}", 1)
     updateDataValue('appReturn', JsonOutput.toJson(r))

In App (just the relevant parts):

def sendDeviceCommand(device, cmd, args=[]) {
    def jsonSlurper = new JsonSlurper()
    logging("sendDeviceCommand(device=${device.deviceId.toString()}, cmd=${cmd}, args=${args})", 1)
    device.deviceCommand(JsonOutput.toJson([cmd: cmd, args: args]))
    return jsonSlurper.parseText(device.getDataValue('appReturn'))

def myFunction(selectedDevice) {
     sendDeviceCommand(selectedDevice, 'methodToCall', ['arg1', 2, 'arg3', 'etc'])

To preface...I'm 95-99% sure of all of this but there's always a possibility I'm wrong. I've tried out everything I've said and it works, although it may not be "right" or standard.

Rather than calling your specific method, why not call the parse() method? This method does not have to be publicly declared, so it can have any set of arguments you want. I believe this is the "universal method" that you are looking for. More than one parse method can be defined without having to publicly declare them. When receiving a call to the parse method, the Driver will pick the appropriate method for the type of argument being passed. This is at the driver level though, so if an appropriate method for the argument type you have passed is not available, I don't believe anything will error at the app level. So, a return would not receive anything.

This is in conflict with what the app is trying to pass to "deviceCommand", isn't it? In the metadata you are declaring that the command deviceCommand will receive the string parameter "cmds". But instead, you are passing off to it a JSON object. JsonOutput.toJson is going to return a JSON object, not a string, isn't it?

Also I do not believe that getDataValue is going to work from an app the way you have it. You have:
However, I do not believe that "getDataValue" can be accessed the way you have defined it in your app. It can only be called from a driver. I've tried that on it's own several different ways and it does not return anything, either from the device preferences or state variable. You would need a "helper method" in the driver that would be able to pull that data out for you. Here's the relevant page from the ST documentation. HE documentation is a work in progress but I have seen frequent reference to the ST docs here on the forum and they have been a great source of information for me.

If anything I've said here is not correct, someone, please correct me. Since no one's answered you for 7 hours, I figured an answer with 95% confidence is better than no answer at all. :slight_smile:

Hope this helps!


When I saw this suggestion from you I was really hoping it would work, it doesn't... If I declare something like this, it does work, so back to square one?:
command "parse", [[name:"cmds*", type: "STRING"]]

So the question is, IS there a driver method that CAN be called without declaring it as a command first? I would love to be wrong about parse, but since it works when declared as a command and doesn't when I don't declare it as a command I doubt it, unless there is some other way of calling it?

JsonOutput.toJson returns a String object, which is the beauty of it :slight_smile:

The result of a toJson call is a String containing the JSON code.

It can, and it is working, as I write it above is how I call and successfully get data from my drivers. As long as the object truly is a ChildDeviceWrapper, that wrapper has the method getDataValue.

It did help, so thank you, I'm still hoping there's a way to not have a Command declared.

I also want to explain this one for those that don't know and read this in the future, "*" (spread operator) in front of "cmd['args']" make the list in args to be passed as the arguments to the function with the name as a String in "cmd['cmd']".

The way described in the first post works, but requires an ugly command declaration, though this is an admin interface, so it probably really doesn't matter. Hopefully someone comes along with a neater solution. @bravenel maybe?

Additional Question
What is the maximum length of the String passed this way to the driver? I'm asking because I might want to pass an object which is about 2000 characters when JSONized. I have not yet tried, but thought I could ask? When using the web form there seems to be a limit of way less than that? If it's not possible I'll design around it, but it would be good to know the exact specs of what is allowed here.

You got me thinking, and I realized, although I can't use parse, I can define eg. a refresh method, like this:

def refresh(cmd) {
     def jsonSlurper = new JsonSlurper()
     cmd = jsonSlurper.parseText(cmd)
     logging("refresh deviceCommand: ${cmd}", 1)
     r = this."${cmd['cmd']}"(*cmd['args'])
     logging("refresh deviceCommand return: ${r}", 1)
     updateDataValue('appReturn', JsonOutput.toJson(r))

This is ideal for me, since all my drivers have a need for a refresh button anyway. This would work with any command available to you, for me refresh is the universal one. I do hope all this is possible by design and not something that will be "fixed" in a future version of HE...

I said about 5 times, you do not have to declare the parse method in the driver metadata.

YES!!!! parse()!!!!

You cannot use refresh unless you declare the refresh capability. But you CAN use parse. You do not need to declare it. UGHH!!! I give up.

Yes, and you did help, and I did try with parse, it is not possible, not from inside an App calling a driver. At least it is not possible for me, I get this when doing that:
java.lang.IllegalArgumentException: Command 'parse' is not supported by device.

Unless I declare parse as a command, then it works. I'm not trying to be difficult, not at all. If you can tell me how you can call parse inside a driver from an app, I do really like to know, I get the above error.

EDIT: So, I have tested and verified so as to make sure I've not misunderstood anything. As @bertabcd1234 states below, when a device is a Child Device of an app, all non-private methods are exposed in the ChildDeviceWrapper. When it comes from a device selection list, the DeviceWrapper only exposes methods declared as commands (either directly or indirectly as a capability). By exploiting an already declared command and declaring a method with more arguments than the standard one, it is possible to write a wrapper which allows an app to call any method in a driver which has such a wrapper.

Just curious, is this an arbitrary device or is it one with a special relationship to your app? I have created devices that are child devices of apps (you can't really tell this from the UI), and they have access to any method, regardless of whether they are declared as a command or not, in this case--at least as long as they're not declared private. If your app needs a specific device with a certain method, it sounds like it might be better for your app to create the device, then it could get the child device. I've never really tried it without this but wouldn't be surprised if that is how it is designed to work (if it's not a command--either a custom command, already risky, or one part of a standard capability--you have no guarantee that it's really there).

1 Like

Ah, here we go, this is the reason, thank you! It is a selected device, not a child device. The reason behind this is that the device might have been added manually or by another app. So this is a way to get access to also those devices. Since all my drivers have refresh declared, for me that is enough of a workaround. With this now explained, it does make it very clear!

Thank you @Ryan780 and @bertabcd1234, combined this answered my question!

But your question was about calling a method in your driver. If it was added by another app, then it wouldn't be using your driver.

And you did not try parse the way that I told you to.
YOU DO NOT DECLARE PARSE IN THE METADATA OF A DRIVER....which is exactly what you did wrong. So, a driver that uses parse only would look like this:

metadata {
    definition (name: "EXAMPLE", namespace: "ryan780", author: "Ryan780") {
        capability "Actuator"

def parse(String description) {

You do not declare the parse method. You did do that, that's why it didn't work. But if you would just listen for one minute rather than assume you were right you would have picked up on it the first two times I said it.

Look at some of the drivers out there. Anything that uses parse does not have that method declared in the metadata of the app.

There's legit use-cases for when the above is not true, for a specific example we can take that one of when a device was installed with Sonoff Connect and the user then changed the driver to one of mine without re-installing. Which is the way I would expect it to be done by most. Anyway, there are other times this may occur as well.

I did, exactly like any other normal driver would, like all my drivers do.

Of course I don't, why would I, where did I? I did talk about being able to use parse if 'command "parse"' was declared, just as an example to show that the parse method was indeed correctly declared, just not exposed and callable.

The conclusion, which was not definitive or clear anywhere in the forum to me. It is now very clear and all is working as expected. As I said, the combined efforts of you and @bertabcd1234 solved it and made it very clear.