Help Writing Device Driver for RestAPI Call for Sony Soundbar

Im a bit stuck. I have tried to reverse engineer a few apps that i already have like Sony TV, Daikin, and Roku. The only problem is that they all call their API's in different methods. What i'm trying to do is write a driver for my soundbar for hubitat to call the REST API to get powerstatus. After i get that rolling, i will add more capabilities like being able to change my subwoofer level, sound mode, etc. They have a pretty good API and i am able to do all the functions I want in Insomnia.

Have my Work in Progress in Github

Right now i have three buttons that I would like to make post calls to my endpoint. I would like to poll the device with the getPowerStatus and evaluate a state of active=on and standby=off.

Post to
http://192.168.1.206:10000/sony/system

On
"{"method":"setPowerStatus","version":"1.1","params":[{"status":active}],"id":102}"
Off
"{"method":"setPowerStatus","version":"1.1","params":[{"status":standby}],"id":102}"
Refresh
"{"method":"getPowerStatus","version":"1.1","params":[],"id":51}"


Not sure what your question is... Your samples look like you are getting responses from it.

Glad you asked. My code in Github is just snippets regarding the API calls. My question is how to i make a button click from the device GUI make one of my requests. How do i digest the response for the refresh call to update the switch capability based on "Active" or "standby". Then i would like the Refresh function to be polled from my dropdown value. Other than hacking up already made drivers, this is my first groovy attempt.

Ok. So I moved your whole topic over to the "Developers - Coding Questions" category rather than support.

For the first part, you can add a command to your device in the metadata - definition section:

metadata {
  definition (name: "Sony Soundbar API", namespace: "ajones", author: "Alex Jones") {
    capability "Switch"
    capability "Refresh"
    command "DoSomething" // Have it do whatever you want

    attribute "PowerStatus", "string" // Attribute to use for sending events to later...

Then make it DO something later on:

def DoSomething(){
// Your code here

}

That will make your device show a command button labeled "DoSomething" that you can then press to trigger that code.

To make it provide the result somewhere you can send an event (or set a state variable if you want to just have the value for the driver, but not elsewhere in the hub). You need to parse that json for your data, the send the event. So maybe adding something like:

def refresh() {
    def json = "{\"method\":\"getPowerStatus\",\"version\":\"1.1\",\"params\":[],\"id\":102}"
    def result = sendJsonCommand(json)
    Data = parseJson( result )
    sendEvent( name: "PowerStatus", value: "${ Data.result[ 0 ].status }" )
}

Of course you probably want to add some logging into it to make sure of the values being returned and such before you actually start creating a bunch of events.

Great help. that really got me going. I have it working for On Off Status and polling. Where im stuck currently is dealing with the groovy syntax and escaping quotes.

My github has the latest update, but im trying to expand some other functions that use a different path.

Key points im stuck at.

  1. Line 120 = path: '/sony/system',

    I want this to be replaced with line 205 or line 211 just like it replaces the json message. I have tried to replace line 120 with lib, but it doesn't work.

  2. Line 212. I want to pass the number entered in the command button on the driver page where the "5" value is. (command button is line 22)

Thanks again for helping. I plan to capture some current values into states as well, but waiting until i resolve my questions first.

Tough for me to check it all on my phone, but here you go:

  1. I would recommend just having path be a second parameter requested by the function:
private sendJsonRpcCommand(json, Path) {

With the relevant line now:

path: '${ Path }',
  1. You need to make the function accept a parameter and then you can use it in there. So:
def setSubLevel( Level ) {

I updated the function to use my "lib" parm as part of the sendJsonRpcCommand and now i get an error. Maybe i need to change something on the lib parm triggering the function.

Lines 112-126

private sendJsonRpcCommand(json, lib) {

  def headers = [:]

  headers.put("HOST", "${state.device_ip}:${device_port}")

  headers.put("Content-Type", "application/json")

  headers.put("X-Auth-PSK", "${device_psk}")

  def result = new hubitat.device.HubAction(

    method: 'POST',

    path: '${lib}',

    body: json,

    headers: headers

  )

  result

}

Lines 192-196

def getPowerStatus() {

    def lib = "/sony/system"

    def json = "{\"id\":2,\"method\":\"getPowerStatus\",\"version\":\"1.1\",\"params\":[]}"

    def result = sendJsonRpcCommand(json, lib)

}

DebugLog Error

[dev:4513](http://192.168.1.11/logs#dev4513)2020-11-08 02:21:18.742 pm [error](http://192.168.1.11/device/edit/4513)java.net.URISyntaxException: Illegal character in path at index 1: ${lib} on line 118 (refresh)

[dev:4513](http://192.168.1.11/logs#dev4513)2020-11-08 02:20:27.053 pm [error](http://192.168.1.11/device/edit/4513)java.net.URISyntaxException: Illegal character in path at index 1: ${ lib } on line 118 (poll)

I figured this out. Im still hacking my way through groovy, but i took out the variable piece since it was getting defined in a subfunction. Question now is how far to go before releasing to other testers: )

You could just post it as a Project with a disclaimer. My BlinkAPI is an example of such.

I generally feel it is safe to remove the warning when it is fairly stable and maybe a couple people have tried it. Or for ones (like this) that may not have a huge install base, once you have used it for a while for yourself.

Next Problem. I have made each get method its own button for testing and everything works as i want. My JSON msg is parsed and the value i want is saved as a state. Now i want all those get commands to run when polled (UpdateAll for Testing). The problem is where line 204,205,206, the UpdateAll action runs three POST's and should return three separate JSON responses. My problem is that only one json response is getting parsed for action line94-126. In my example, only the last command is getting parsed. Attached is the log when i do an UpdateAll.
Github link

Not sure. Maybe the responses are coming in "too quick" and causing Hubitat to overwrite values before they are processed?

To test for that, put a pauseExecution( 2000 ) between the separate commands. Maybe something like the 2000 (2 seconds) just to try it, maybe a bit longer. There is always the chance the device is getting the requests too fast to handle also and is just replying to the last one.

Added in the delay and even tried it for 5 seconds and it still only runs the parse action for the last action (i switched the order around to verify). Is there anything i can do to parse the action inside the routine. I cant wrap my head around on how line 94
def parse(description) {
is getting invoked by the action on line 129.
private sendJsonRpcCommand(json,lib) {
i dont call parse() anywhere or pass a description, so not sure how to change this.

parse is where responses get directed unless you specifically set a closure. I do not use HubAction though... I am more familiar with using httpGet/Post or asynchttpGet/Post...

https://docs.hubitat.com/index.php?title=Common_Methods_Object

Alright i rewrote most of it using HttpPostJson. But the JSON response is coming back. I think its already parsed as part of the return, so my parseJson wont strip it. I'm trying to return the ID of the response data in order to take action. Perhaps i use the asynchttpPost instead since it wont use HttpResponseDecorator. Any idea if i can make the existing call work to get the ID (and eventually the attributes parsed?

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-11 11:49:50.929 pm [error](http://192.168.1.11/device/edit/4609)groovy.lang.MissingMethodException: No signature of method: user_driver_ajones_Sony_REST_API_1985.parseJson() is applicable for argument types: (groovyx.net.http.HttpResponseDecorator) values: [groovyx.net.http.HttpResponseDecorator@4dde134a] on line 58 (getPowerStatus)

Also i included the response.data that is included from the logs.
"([id:2, result:[[standbyDetail:, status:active]]])"

// Generic Private Functions -------

private postAPICall(lib,json) {

    def requestParams = [

        uri:  "http://${settings.ipAddress}:${settings.ipPort}" ,

        path: lib,

//requestContentType: "application/json",

        query: null,

        body: json,

    ]

    log.debug "${requestParams}"

    httpPostJson(requestParams) { response ->

        def msg = ""

        if (response?.status == 200) {

            msg = "Success"

        }

        else {

            msg = "${response?.status}"

        }

        log.debug "Sony Response: ${msg} (${response.data})"

        def jsonData = parseJson(response)

        jsonreturnaction(jsonData)

    }

}

private jsonreturnaction(jsonData){

  if (jsonData?.id == 2) {

    //Set the Global value of state.device on or off

    log.debug "Result is ${msg.json.result}"

    state.device = (msg.json.result[0]?.status == "active") ? "on" : "off"

    sendEvent(name: "switch", value: state.device)

    log.debug "Device is '${state.device}'"

    state.device_poll_count = 0

  }

  if (jsonData?.id == 78) {

    //Set the Global value of state.devicevolume

    log.debug "Result is ${msg.json.result}"

    state.devicevolume = msg.json.result[0]?.volume

    log.debug "DeviceVolume is '${state.devicevolume}'"

  }

  if (jsonData?.id == 59) {

    //Set the Global value of state.sublevel

    log.debug "Result is ${msg.json.result}"

    state.sublevel = msg.json.result[0]?.currentValue

    log.debug "DeviceSublevel is '${state.SubLevel}'"

  }

}

//API Commands

def getPowerStatus() {

    log.debug "Executing 'getPowerStatus' "

    def lib = "/sony/system"

    def json = "{\"id\":2,\"method\":\"getPowerStatus\",\"version\":\"1.1\",\"params\":[]}"

    postAPICall(lib,json)

}

I would recommend just passing the json response directly to jsonreturnaction:
jsonreturnaction(response)

The response you are getting back is already formatted json (you used the httpPostJson method), so the parseJson is not needed.

Okay, new github file since i decided to restart the driver and remove a bunch of junk i dont need yet.

  1. I was able to parse what i wanted from the httpPostJson method by just listing "response.data.id" and for a value i used response.data.result[0]?.status == "active" I couldnt find much documentation for this and just started trying things with a lot of logging on. (this eliminated having to roll out my own parsing method.
  2. I now can invoke each function in groups and it sends the post and parse in the order called. (YAY)
  3. My last problem is that line 69 works correctly, but line 90 always returns "unmuted", even though logging shows it has [on]. I have also changed "on" to "[on]" on line 90 as well and that doesnt work. It always shows unmuted. Any idea what is going on here?

debug logs are below.

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.565 pm [debug](http://192.168.1.11/device/edit/4609)Devicemute is 'unmuted'

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.563 pm [debug](http://192.168.1.11/device/edit/4609)Mute is [on]

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.562 pm [debug](http://192.168.1.11/device/edit/4609)result is [[[maxVolume:50, minVolume:0, mute:on, output:, step:1, volume:14]]]

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.560 pm [debug](http://192.168.1.11/device/edit/4609)ID is 600

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.557 pm [debug](http://192.168.1.11/device/edit/4609)Sony Response: Success ([id:600, result:[[[maxVolume:50, minVolume:0, mute:on, output:, step:1, volume:14]]]])

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.535 pm [debug](http://192.168.1.11/device/edit/4609)[uri:http://192.168.1.206:10000, path:/sony/audio, query:null, body:{"method":"getVolumeInformation","version":"1.1","params":[{"output":""}],"id":600}]

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.533 pm [debug](http://192.168.1.11/device/edit/4609)Executing 'getMuteStatus'

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.530 pm [debug](http://192.168.1.11/device/edit/4609)no id found for result action

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.528 pm [debug](http://192.168.1.11/device/edit/4609)DeviceSublevel is '[12]'

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.526 pm [debug](http://192.168.1.11/device/edit/4609)SubLevel is [12]

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.525 pm [debug](http://192.168.1.11/device/edit/4609)result is [[[candidate:[[isAvailable:true, max:12, min:0, step:1, title:Subwoofer Volume, titleTextID:sound-subwoofer, value:subwooferLevel]], currentValue:12, deviceUIInfo:sliderHorizon, isAvailable:true, target:subwooferLevel, title:Subwoofer Volume, titleTextID:sound-subwoofer, type:integerTarget]]]

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.522 pm [debug](http://192.168.1.11/device/edit/4609)ID is 59

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.520 pm [debug](http://192.168.1.11/device/edit/4609)Sony Response: Success ([id:59, result:[[[candidate:[[isAvailable:true, max:12, min:0, step:1, title:Subwoofer Volume, titleTextID:sound-subwoofer, value:subwooferLevel]], currentValue:12, deviceUIInfo:sliderHorizon, isAvailable:true, target:subwooferLevel, title:Subwoofer Volume, titleTextID:sound-subwoofer, type:integerTarget]]]])

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.500 pm [debug](http://192.168.1.11/device/edit/4609)[uri:http://192.168.1.206:10000, path:/sony/audio, query:null, body:{"method":"getSoundSettings","version":"1.1","params":[{"target":"subwooferLevel"}],"id":59}]

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.497 pm [debug](http://192.168.1.11/device/edit/4609)Executing 'getSubLevel'

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.495 pm [debug](http://192.168.1.11/device/edit/4609)no id found for result action

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.493 pm [debug](http://192.168.1.11/device/edit/4609)DeviceVolume is '[14]'

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.489 pm [debug](http://192.168.1.11/device/edit/4609)Volume is [14]

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.479 pm [debug](http://192.168.1.11/device/edit/4609)result is [[[maxVolume:50, minVolume:0, mute:on, output:, step:1, volume:14]]]

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.476 pm [debug](http://192.168.1.11/device/edit/4609)ID is 78

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.472 pm [debug](http://192.168.1.11/device/edit/4609)Sony Response: Success ([id:78, result:[[[maxVolume:50, minVolume:0, mute:on, output:, step:1, volume:14]]]])

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.446 pm [debug](http://192.168.1.11/device/edit/4609)[uri:http://192.168.1.206:10000, path:/sony/audio, query:null, body:{"method":"getVolumeInformation","version":"1.1","params":[{"output":""}],"id":78}]

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.443 pm [debug](http://192.168.1.11/device/edit/4609)Executing 'getSoundVolume'

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.441 pm [debug](http://192.168.1.11/device/edit/4609)no id found for result action

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.439 pm [debug](http://192.168.1.11/device/edit/4609)Device State is 'on'

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.437 pm [debug](http://192.168.1.11/device/edit/4609)Status is active

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.436 pm [debug](http://192.168.1.11/device/edit/4609)result is [[standbyDetail:, status:active]]

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.434 pm [debug](http://192.168.1.11/device/edit/4609)ID is 2

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.432 pm [debug](http://192.168.1.11/device/edit/4609)Sony Response: Success ([id:2, result:[[standbyDetail:, status:active]]])

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.403 pm [debug](http://192.168.1.11/device/edit/4609)[uri:http://192.168.1.206:10000, path:/sony/system, query:null, body:{"id":2,"method":"getPowerStatus","version":"1.1","params":[]}]

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.401 pm [debug](http://192.168.1.11/device/edit/4609)Executing 'getPowerStatus'

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-12 04:06:23.398 pm [debug](http://192.168.1.11/device/edit/4609)UpdateAll pushed

I guess this is maybe more a generic groovy question, but

line 89
log.debug "Mute is ${response.data.result[0]?.mute}"

returns either [on] or [off]

so this is an array that i need to get "on" or "off" back from. How do i convert response.data.result.mute to a value without the brackets?

i added the below code to try to get this back (assuming its an array, and this threw a groovy error, expecting an array
def jsonData = parse(response.data.result.mute)

[dev:4609](http://192.168.1.11/logs#dev4609)2020-11-13 12:31:40.128 am [error](http://192.168.1.11/device/edit/4609)groovy.lang.MissingMethodException: No signature of method: user_driver_ajones_Sony_REST_API_1985.parse() is applicable for argument types: (java.util.ArrayList) values: [[[off]]] Possible solutions: use([Ljava.lang.Object;), wait(), run(), mute(), run(), grep() on line 100 (UpdateAll)

Normally you just call the array element name and place it's value into an appropriate variable. I think the problem is that the response has an oddly-layered array. The result is what, 3 layers deep?

A direct call to them would be something like (I could be wrong on the depth there, tough to tell at times for me):
response.data.result[0][0][0].maxVolume
response.data.result[0][0][0].minVolume
response.data.result[0][0][0].mute
response.data.result[0][0][0].output
response.data.result[0][0][0].step
response.data.result[0][0][0].volume

So the mute (line 90) should look like:
state.devicemute = response.data.result[0][0][0].mute

Maybe force it using:
state.devicemute = response.data.result[0][0][0].mute as string

Although the json tool I often use here, does not particularly like this response. I think it is a bit too processed already. You could probably not even use the httpPostJson method and just use httpPost itself because the responses coming back are json (just uncomment your requestContentType to be sure).

You are a lifesaver! By the way appriciate you walking me through this, hopefully another programmer new to groovy(and hubitat) can follow this a bit if they have some basic questions. I went through how many articles online that didn't really have a solution and took the [on] and did a find and replace function to strip the brackets out which is very hacky. it was two deep, so my final call was response.data.result[0][0].mute. I even did this for my volume and sublevel and those worked fine.

So now that i have a pretty simple base, I'm messing with the capabilities. I have added
capability "MusicPlayer
but it added all the command buttons, many i dont want. Is it possible to force hide some of the commands to clean up the page?

1 Like

I don't believe so, but there are a bunch of benefits to using the built-in device classes so that the commands show up in apps that use capability filtering, like RM and the like. When I really don't want to put the effort or just can't figure out how to support a built-in command, I just stub it out and print to debug that it is unsupported. Otherwise you'll get an unsightly exception printed in the log if someone uses it.

With that said, you are on a roll and should totally just implement the rest of MusicPlayer. This was really useful to me for the TTS-related operations.