Help Writing Device Driver for RestAPI Call for Sony Soundbar

@ajones: No problem! I have been grinding my way though learning to write drivers and am by no means an expert, but I try to help if I can.

As @tomw mentioned, there is no way to "dynamically" control commands. I always leave the code in (might need it) but comment out the command so it cannot be run. I certainly wish it was possible. That would make it a lot easier for many drivers (I have some that I want to support multiple devices, but then there are extra commands that only work with some).

Preferences are a bit different though. You can control whether those show up so almost all my drivers have a "Show All Preferences" option now, just to hide stuff that most people do not want to see once they have it all set. Plus some drivers end up with LOTS of preferences.

Was plugging away at this and found that Sony pretty much made all TV's backward compatible, but for audio devices, a specific product might be on 1.0, but another is on 1.2. In order to not make a driver for each specific model number, I will detect the current API version for specific methods that are used is the driver and set a state variable for each method. Luckily Sony at least gave an API to detect versions. Okay so on to my question.

My raw response.data is below. I am trying to bring back the version number for "getPowerStatus".
In some cases, like "getSystemInformation", there might be two. Preference would be to use the last, but I could use the first if its easier and will always be there.

Edit: To clarify, I would want to specify "system" and then in there "getPowerStatus" and recieve "1.1" back.

response.data

{
"id": 5,
"result": [
[
{
"apis": [
{
"name": "actSWUpdate",
"versions": [
{
"authLevel": "generic",
"version": "1.0"
}
]
},
{
"name": "connectBluetoothDevice",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getDeviceMiscSettings",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getInterfaceInformation",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getMethodTypes",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getPowerSettings",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getPowerStatus",
"versions": [
{
"version": "1.1"
}
]
},
{
"name": "getSWUpdateInfo",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getSettingsTree",
"versions": [
{
"version": "1.1"
}
]
},
{
"name": "getSleepTimerSettings",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getStorageList",
"versions": [
{
"authLevel": "generic",
"version": "1.1"
},
{
"version": "1.2"
}
]
},
{
"name": "getSystemInformation",
"versions": [
{
"version": "1.3"
},
{
"version": "1.4"
}
]
},
{
"name": "getVersions",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getWuTangInfo",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "setClientInfo",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "setDeviceMiscSettings",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "setPowerSettings",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "setPowerStatus",
"versions": [
{
"version": "1.1"
}
]
},
{
"name": "setSleepTimerSettings",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "setWuTangInfo",
"versions": [
{
"authLevel": "generic",
"version": "1.0"
}
]
},
{
"name": "switchNotifications",
"versions": [
{
"protocols": [
"websocket:jsonizer"
],
"version": "1.0"
}
]
}
],
"notifications": [
{
"name": "notifyPowerStatus",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "notifySWUpdateInfo",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "notifySettingsUpdate",
"versions": [
{
"version": "1.1"
}
]
},
{
"name": "notifyStorageStatus",
"versions": [
{
"authLevel": "generic",
"version": "1.1"
},
{
"version": "1.2"
}
]
}
],
"protocols": [
"xhrpost:jsonizer",
"websocket:jsonizer"
],
"service": "system"
},
{
"apis": [
{
"name": "getAvailablePlaybackFunction",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getBluetoothSettings",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getContentCount",
"versions": [
{
"version": "1.3"
}
]
},
{
"name": "getContentList",
"versions": [
{
"version": "1.4"
}
]
},
{
"name": "getCurrentExternalTerminalsStatus",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getMethodTypes",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getPlaybackModeSettings",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getPlayingContentInfo",
"versions": [
{
"version": "1.2"
}
]
},
{
"name": "getSchemeList",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getSourceList",
"versions": [
{
"version": "1.1"
},
{
"version": "1.2"
}
]
},
{
"name": "getSupportedPlaybackFunction",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getVersions",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "pausePlayingContent",
"versions": [
{
"version": "1.1"
}
]
},
{
"name": "presetBroadcastStation",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "scanPlayingContent",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "seekBroadcastStation",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "setActiveTerminal",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "setBluetoothSettings",
"versions": [
{
"authLevel": "generic",
"version": "1.0"
}
]
},
{
"name": "setPlayContent",
"versions": [
{
"version": "1.2"
}
]
},
{
"name": "setPlayNextContent",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "setPlayPreviousContent",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "setPlaybackModeSettings",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "startContentBrowsing",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "stopPlayingContent",
"versions": [
{
"version": "1.1"
}
]
},
{
"name": "switchNotifications",
"versions": [
{
"protocols": [
"websocket:jsonizer"
],
"version": "1.0"
}
]
}
],
"notifications": [
{
"name": "notifyAvailablePlaybackFunction",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "notifyExternalTerminalStatus",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "notifyPlayingContentInfo",
"versions": [
{
"authLevel": "private",
"version": "1.0"
}
]
}
],
"protocols": [
"xhrpost:jsonizer",
"websocket:jsonizer"
],
"service": "avContent"
},
{
"apis": [
{
"name": "getMethodTypes",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getServiceProtocols",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getSupportedApiInfo",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getVersions",
"versions": [
{
"version": "1.0"
}
]
}
],
"protocols": [
"xhrpost:jsonizer"
],
"service": "guide"
},
{
"apis": [
{
"name": "getCustomEqualizerSettings",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getMethodTypes",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getSoundSettings",
"versions": [
{
"version": "1.1"
}
]
},
{
"name": "getSpeakerSettings",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getVersions",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "getVolumeInformation",
"versions": [
{
"version": "1.1"
}
]
},
{
"name": "setAudioMute",
"versions": [
{
"version": "1.1"
}
]
},
{
"name": "setAudioVolume",
"versions": [
{
"version": "1.1"
}
]
},
{
"name": "setCustomEqualizerSettings",
"versions": [
{
"authLevel": "generic",
"version": "1.0"
}
]
},
{
"name": "setSoundSettings",
"versions": [
{
"version": "1.1"
}
]
},
{
"name": "setSpeakerSettings",
"versions": [
{
"authLevel": "generic",
"version": "1.0"
}
]
},
{
"name": "switchNotifications",
"versions": [
{
"protocols": [
"websocket:jsonizer"
],
"version": "1.0"
}
]
}
],
"notifications": [
{
"name": "notifyVolumeInformation",
"versions": [
{
"version": "1.0"
}
]
},
{
"name": "notifyWirelessSurroundInfo",
"versions": [
{
"version": "1.0"
}
]
}
],
"protocols": [
"xhrpost:jsonizer",
"websocket:jsonizer"
],
"service": "audio"
}
]
]
}

I have put the string into a json parser to try and make sense of it, but am not sure how to bring it back.

My other queries are using simpler responses like on the below, i use
response.data.result[0]?.modelName

response.data

{
"id": 33,
"result": [
{
"interfaceVersion": "4.0.0",
"modelName": "HT-CT800",
"productCategory": "homeTheaterSystem",
"productName": "Bar",
"serverName": ""
}
]
}

Try something like this: https://stackoverflow.com/a/14044969

Total shot in the dark, not tested (EDIT: and definitely missing some needed defensive null-safe additions).

def theKey = response?.data?.result[0].find { it.protocols[0] == "system" }?.key
log.debug "response.data.result[0][theKey].apis"

If that works then you can do something similar to find the one you want within that entry.

Ahh I see. It's like a vlookup for the json map. Will try this tomorrow. Wife grounded me for the night. Said I was squinting too much today. :slight_smile:

1 Like

Yeah, that comes back with null. Whats the null safe addition?

I misunderstood the Json diagram. Thanks for sharing the raw data -- that helped connect the dots.

Try this:

def theKey = (response?.data?.result[0].find { it.service == "system" })?.key
log.debug "response.data.result[0][theKey].apis"

its not recognizing the "theKey". When i log what that is, it shows null.

My code snippet is below.

if (response.data?.id == 998) {

    //Set the Global value of state.SupportedAPIs

    if (logEnable) log.debug "SupportedAPIs is ${response.data}"

    def sprtapirespX = response.data

    //state.sprtapiresp = response.data

    sendEvent(name: "SupportedAPI", value: sprtapirespX, isStateChange: true)

    //system version lookups

    def theKey = (response?.data?.result[0].find { it.service == "system" })?.key

    if (logEnable) log.debug "'thekey' is ${theKey}"

    if (logEnable) log.debug "response.data.result[0][theKey].apis"

My code example from the other night was probably wrong. Plus I still think maybe I don't understand the JSON structure, and trying to read the screenshot of the debug print makes my eyes start to cross.

Here's an example where I built up a JSON sample that I think mirrors what your data will look like. Then I start digging out various parts of it to get the data you want.

If the structure isn't quite right, hopefully this gives enough clues to keep you going.

def traceRandomCode()
{
def result = '''
{"data":
    {"id":"5", 
        "result":
        [        
            [
                {
                "apis":
                [
                    {"name":"getPowerStatus", "versions":[{"version":"1.1"}]},
                    {"name":"getPowerValue", "versions":[{"version":"1.2"}]}
                ], 
                "protocols":
                [
                    {"name":"protocolA"},
                    {"name":"protocolB"}
                ],
                "service":"system"
                },

                {
                "apis":
                [
                    {"name":"getPowerLevel", "versions":[{"version":"1.3"}]}
                ], 
                "protocols":
                [
                    {"name":"protocolC"},
                    {"name":"protocolD"}
                ],
                "service":"avContent"
                }
            ]        
        ]
    }
}'''
    def resJson = new groovy.json.JsonSlurper().parseText(result)

    def theRes = resJson?.data?.result?.get(0)?.find { it.service == "system" }
    log.debug "theRes = ${theRes}"
    def theApi = theRes?.apis?.find { it.name == "getPowerStatus" }
    log.debug "theApi = ${theApi}"
    for(thisVers in theApi?.versions) {log.debug "${thisVers?.version}"}
}

Did you get any better results with that, @ajones?

Umm not so much. Im not sure if its because i use the default json parser and dont run it through my own. I use httpPostJSON which already brings back a parsed response. I posted my develop branch at the below link for full reference.

However a few key snippets as to what im doing.

My button to send the JSON parms to the httpPostJSON call

Summary

def getSupportedAPIInfo(){

    if (logEnable) log.debug "Executing 'getSupportedAPIInfo' "

def lib = "/sony/guide"

def json = "{\"method\":\"getSupportedApiInfo\",\"id\":998,\"params\":[{\"services\":[\"system\",\"avContent\",\"guide\",\"appControl\",\"audio\",\"videoScreen\"]}],\"version\":\"1.0\"}"

postAPICall(lib,json)

}

My httpPOSTjson

Summary

private postAPICall(lib,json) {

def headers = [:]

    headers.put("HOST", "${settings.ipAddress}:${settings.ipPort}")

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

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



def requestParams = [

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

    path: lib,

    headers: headers,

//requestContentType: "application/json",

    query: null,

    body: json,

]

if (logEnable) log.debug "${requestParams}"

httpPostJson(requestParams) { response ->

    def msg = ""

    if (response?.status == 200) {

        msg = "Success"

    }

    else {

        msg = "${response?.status}"

    }

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

    if (response.data.id != 999){

        jsonreturnaction(response)

    }

    if (response.data.id == 999){

        jsonreturnaction(response)

    }

}

}

A shortened version of my response logic for the ID 998.

Summary

private jsonreturnaction(response){

if (logEnable) log.debug "ID is ${response.data.id}"

if (logEnable) log.debug "raw data result is ${response.data.result}"

String responsedataerror = response.data.error

if (logEnable) log.debug "dataerrorstring is ${responsedataerror}"

if (responsedataerror != null){

log.warn "data error is ${response.data.error}"

}

if (response.data?.id == 998) {

//Set the Global value of state.SupportedAPIs

if (logEnable) log.debug "SupportedAPIs is ${response.data}"

def sprtapirespX = response.data

sendEvent(name: "SupportedAPI", value: sprtapirespX, isStateChange: true)



def resJson = new groovy.json.JsonSlurper().parseText(response)

def theRes = resJson?.data?.result?.get(0)?.find { it.service == "system" }

log.debug "theRes = ${theRes}"

def theApi = theRes?.apis?.find { it.name == "getPowerStatus" }

log.debug "theApi = ${theApi}"

for(thisVers in theApi?.versions) {log.debug "${thisVers?.version}"}



else {if (logEnable) log.debug "no id found for result action"}

}

Debug Log when i send the request.

Summary

dev:52492020-12-17 09:23:30.917 pm errorjava.lang.IllegalArgumentException: Text must not be null or empty on line 268 (getCapability)

dev:52492020-12-17 09:23:30.807 pm debugSupportedAPIs is [id:998, result:[[[apis:[[name:actSWUpdate, versions:[[authLevel:generic, version:1.0]]], [name:connectBluetoothDevice, versions:[[version:1.0]]], [name:getDeviceMiscSettings, versions:[[version:1.0]]], [name:getInterfaceInformation, versions:[[version:1.0]]], [name:getMethodTypes, versions:[[version:1.0]]], [name:getPowerSettings, versions:[[version:1.0]]], [name:getPowerStatus, versions:[[version:1.1]]], [name:getSWUpdateInfo, versions:[[version:1.0]]], [name:getSettingsTree, versions:[[version:1.1]]], [name:getSleepTimerSettings, versions:[[version:1.0]]], [name:getStorageList, versions:[[authLevel:generic, version:1.1], [version:1.2]]], [name:getSystemInformation, versions:[[version:1.3], [version:1.4]]], [name:getVersions, versions:[[version:1.0]]], [name:getWuTangInfo, versions:[[version:1.0]]], [name:setClientInfo, versions:[[version:1.0]]], [name:setDeviceMiscSettings, versions:[[version:1.0]]], [name:setPowerSettings, versions:[[version:1.0]]], [name:setPowerStatus, versions:[[version:1.1]]], [name:setSleepTimerSettings, versions:[[version:1.0]]], [name:setWuTangInfo, versions:[[authLevel:generic, version:1.0]]], [name:switchNotifications, versions:[[protocols:[websocket:jsonizer], version:1.0]]]], notifications:[[name:notifyPowerStatus, versions:[[version:1.0]]], [name:notifySWUpdateInfo, versions:[[version:1.0]]], [name:notifySettingsUpdate, versions:[[version:1.1]]], [name:notifyStorageStatus, versions:[[authLevel:generic, version:1.1], [version:1.2]]]], protocols:[xhrpost:jsonizer, websocket:jsonizer], service:system], [apis:[[name:getAvailablePlaybackFunction, versions:[[version:1.0]]], [name:getBluetoothSettings, versions:[[version:1.0]]], [name:getContentCount, versions:[[version:1.3]]], [name:getContentList, versions:[[version:1.4]]], [name:getCurrentExternalTerminalsStatus, versions:[[version:1.0]]], [name:getMethodTypes, versions:[[version:1.0]]], [name:getPlaybackModeSettings, versions:[[version:1.0]]], [name:getPlayingContentInfo, versions:[[version:1.2]]], [name:getSchemeList, versions:[[version:1.0]]], [name:getSourceList, versions:[[version:1.1], [version:1.2]]], [name:getSupportedPlaybackFunction, versions:[[version:1.0]]], [name:getVersions, versions:[[version:1.0]]], [name:pausePlayingContent, versions:[[version:1.1]]], [name:presetBroadcastStation, versions:[[version:1.0]]], [name:scanPlayingContent, versions:[[version:1.0]]], [name:seekBroadcastStation, versions:[[version:1.0]]], [name:setActiveTerminal, versions:[[version:1.0]]], [name:setBluetoothSettings, versions:[[authLevel:generic, version:1.0]]], [name:setPlayContent, versions:[[version:1.2]]], [name:setPlayNextContent, versions:[[version:1.0]]], [name:setPlayPreviousContent, versions:[[version:1.0]]], [name:setPlaybackModeSettings, versions:[[version:1.0]]], [name:startContentBrowsing, versions:[[version:1.0]]], [name:stopPlayingContent, versions:[[version:1.1]]], [name:switchNotifications, versions:[[protocols:[websocket:jsonizer], version:1.0]]]], notifications:[[name:notifyAvailablePlaybackFunction, versions:[[version:1.0]]], [name:notifyExternalTerminalStatus, versions:[[version:1.0]]], [name:notifyPlayingContentInfo, versions:[[authLevel:private, version:1.0]]]], protocols:[xhrpost:jsonizer, websocket:jsonizer], service:avContent], [apis:[[name:getMethodTypes, versions:[[version:1.0]]], [name:getServiceProtocols, versions:[[version:1.0]]], [name:getSupportedApiInfo, versions:[[version:1.0]]], [name:getVersions, versions:[[version:1.0]]]], protocols:[xhrpost:jsonizer], service:guide], [apis:[[name:getCustomEqualizerSettings, versions:[[version:1.0]]], [name:getMethodTypes, versions:[[version:1.0]]], [name:getSoundSettings, versions:[[version:1.1]]], [name:getSpeakerSettings, versions:[[version:1.0]]], [name:getVersions, versions:[[version:1.0]]], [name:getVolumeInformation, versions:[[version:1.1]]], [name:setAudioMute, versions:[[version:1.1]]], [name:setAudioVolume, versions:[[version:1.1]]], [name:setCustomEqualizerSettings, versions:[[authLevel:generic, version:1.0]]], [name:setSoundSettings, versions:[[version:1.1]]], [name:setSpeakerSettings, versions:[[authLevel:generic, version:1.0]]], [name:switchNotifications, versions:[[protocols:[websocket:jsonizer], version:1.0]]]], notifications:[[name:notifyVolumeInformation, versions:[[version:1.0]]], [name:notifyWirelessSurroundInfo, versions:[[version:1.0]]]], protocols:[xhrpost:jsonizer, websocket:jsonizer], service:audio]]]]

OK, digesting some. Here are my initial impressions:

You can cut this line if your response is already JSON in a map.

def resJson = new groovy.json.JsonSlurper().parseText(response)

In that case, you can just replace all of my instances of resJson with just response like you were doing.

Let's start there and see if it is better.

You magnificent genius! This really just broadened the number of devices that I can make my driver easily support! I will update this to see how to best use, but soo awesome!

Below is my debug logs..

dev:52492020-12-17 10:03:05.637 pm debug1.1

dev:52492020-12-17 10:03:05.634 pm debugtheApi = [name:getPowerStatus, versions:[[version:1.1]]]

dev:52492020-12-17 10:03:05.623 pm debugtheRes = [apis:[[name:actSWUpdate, versions:[[authLevel:generic, version:1.0]]], [name:connectBluetoothDevice, versions:[[version:1.0]]], [name:getDeviceMiscSettings, versions:[[version:1.0]]], [name:getInterfaceInformation, versions:[[version:1.0]]], [name:getMethodTypes, versions:[[version:1.0]]], [name:getPowerSettings, versions:[[version:1.0]]], [name:getPowerStatus, versions:[[version:1.1]]], [name:getSWUpdateInfo, versions:[[version:1.0]]], [name:getSettingsTree, versions:[[version:1.1]]], [name:getSleepTimerSettings, versions:[[version:1.0]]], [name:getStorageList, versions:[[authLevel:generic, version:1.1], [version:1.2]]], [name:getSystemInformation, versions:[[version:1.3], [version:1.4]]], [name:getVersions, versions:[[version:1.0]]], [name:getWuTangInfo, versions:[[version:1.0]]], [name:setClientInfo, versions:[[version:1.0]]], [name:setDeviceMiscSettings, versions:[[version:1.0]]], [name:setPowerSettings, versions:[[version:1.0]]], [name:setPowerStatus, versions:[[version:1.1]]], [name:setSleepTimerSettings, versions:[[version:1.0]]], [name:setWuTangInfo, versions:[[authLevel:generic, version:1.0]]], [name:switchNotifications, versions:[[protocols:[websocket:jsonizer], version:1.0]]]], notifications:[[name:notifyPowerStatus, versions:[[version:1.0]]], [name:notifySWUpdateInfo, versions:[[version:1.0]]], [name:notifySettingsUpdate, versions:[[version:1.1]]], [name:notifyStorageStatus, versions:[[authLevel:generic, version:1.1], [version:1.2]]]], protocols:[xhrpost:jsonizer, websocket:jsonizer], service:system]

1 Like

Also, trying to find more info on the topic, but not sure how to phrase my question on the internet.

Your JSON query is
def theRes = response?.data?.result?.get(0)?.find { it.service == "system" }

I have some of mine showing
def soundfield = response.data.result[0][0]?.currentValue

You have used parentheses (0) instead of [0]. Why?

Also, sometimes there is question mark sprinkled in behind the json levels, whats the significance of this.

If anyone has a video explaining json queries or an article, I'm game. Not sure what to look for.

They're related.

The question mark thing is the safe navigation operator. It helps avoid null pointer exceptions if the thing you're trying to access (really, it's 'children') don't exist. For example, response.data will eval to null if data doesn't exist. But response.data.result will throw an exception if data doesn't exist.

I always use brackets for array accesses, except when I need to use the safe navigation operator to go deeper within the array members. Using get() has some potential issues, as discussed here, but it seems pretty safe to me if you're iterating over the known bounds of the collection (like with each).