Help with driver - Wiim Music Player

The second of I'm sure many cries for help while trying to get my first driver working.

I'm trying to get a driver working for the Wiim music player. I've managed to add most of the available commands from the Wiim API that I want in the preferences and everything I've added is working. I'm stumped on a few issues and could do with some help.

I've got an issue with a specific command not populating the tile with a space for the number entry and cannot fathom why. The syntax appears to be correct. From the Wiim API the command needed is:

/httpapi.asp?command=setPlayerCmd:seek:<position>

where position is a number in seconds from zero. In my code I have:

def seek(number) {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:seek:${number}"
    sendHttpGetRequest(url)
}

However the tile does not show 'number:' or allow me to key an entry there:

As can be seen in the picture, both 'Set Volume' and 'Preset' which are formatted in the same way correctly provide a space to key an entry. Any ideas? Thanks

Edit: I think I've found the problem. I haven't added ',[number]' for 'seek' in the command definitions :roll_eyes:.

2 Likes

Oooo... I also have a WiiM. Nice to see a driver being worked on! :slight_smile:

1 Like

:rofl: "driver being worked on". I'm stumbling through. I haven't a clue what I'm doing! As its stands it's just set up as an actuator device for individual players as I wanted a way to pause or stop whatever I'm playing when we leave the house (turning things on/off manually is so 1980's). It's just a bunch of commands but I'll post the code in this thread anyway a bit later.

Looking at the Wiim API and the Arylic documents there are commands for group mode. Ideally someone with experience will pick Wiim over Sonos and set something up better than I could ever dream of doing.

@Prometheus_Xex - What I've got working so far

 /*
 * Wiim Media Player
 *
 *  Licensed Virtual the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *  Change History:
 *
 *    Date        Who            	What
 *    ----        ---           	 ----
 *    15Apr2024   John Williamson   Initial attempt - epic fail
 *    16Apr2024	  John Williamson   Fixed ignoreSSLIssues, added and tested all commands
 */

metadata {
    definition (name: "Wiim Media Player", namespace: "John Williamson", author: "John Williamson") {
        capability "Actuator"
        
        command "preset", ["number"]
        command "volumeUp"
        command "volumeDown"
        command "mute"
        command "unmute"
        command "stop"
        command "pause"
        command "seek", ["number"]
        command "resume"
        command "pausePlay"
        command "setVolume", ["number"]
        command "prev"
        command "next"
        command "playURL", ["string"]
        command "playlist", ["string"]
        
        attribute "volume", "number"
        attribute "muted", "bool"
    }

    preferences {
        input("ipAddress", "string", title: "Device IP Address", description: "IP address of Wiim device", required: true)
    }
}

def sendHttpGetRequest(String requestURL) {
    Map requestParams =
	[
        uri: requestURL,
        ignoreSSLIssues: true
	]

    try {
        httpGet(requestParams) { resp ->
            //log.debug resp
            if (resp != null) {
                // success
                //log.debug resp.data // uncomment to log the Get response data 
            } else {
                // failure
            }
        }
    }
    catch (e) {
        // exception thrown
        log.error e
    }
}

def preset(number) {
    def url = "https://${ipAddress}/httpapi.asp?command=MCUKeyShortClick:${number}"
    sendHttpGetRequest(url)
}

def volumeUp() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:vol%2b%2b"
    sendHttpGetRequest(url)
}

def volumeDown() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:vol--"
    sendHttpGetRequest(url)
}

def mute() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:mute:1"
    sendHttpGetRequest(url)
}
def unmute() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:mute:0"
    sendHttpGetRequest(url)
}

def stop() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:stop"
    sendHttpGetRequest(url)
}

def pause() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:pause"
    sendHttpGetRequest(url)
}

def resume() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:resume"
    sendHttpGetRequest(url)
}

def pausePlay() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:onepause"
    sendHttpGetRequest(url)
}

def setVolume(number) {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:vol:${number}"
    sendHttpGetRequest(url)
}

def prev() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:prev"
    sendHttpGetRequest(url)
}

def next() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:next"
    sendHttpGetRequest(url)
}

def seek(number) {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:seek:${number}"
    sendHttpGetRequest(url)
}

def playURL(string) {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:play:${string}"
    sendHttpGetRequest(url)
}

def playlist(string) {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:m3u:play:${string}"
    sendHttpGetRequest(url)
}
1 Like

You an also add Range in the Input statement to limit the allowed values. Maybe you don't want people putting 999999 seconds in there :smiley:

input "humidChangeAmount", "number", title: "<b>Humidity Change Amount (%)?</b>", range: "1..100", description: "<br><i>The percentage the humidity must change to induce an automatic report.</i><br>", defaultValue: 10, required: false

1 Like

Whereabouts would I add that? I was going to ask earlier as I was going to limit the volume range between 1 and 100 and the preset number between 1 and 8 (might be 12 I'll have to check).

I'll want to add code to get events logged I suppose. I'm blindly looking through others driver code for clues

So far SharpTools dosen't see your device... especially as a Media Controller.

'capability actuator' isn't enough.. you'll probably want to add the 3 media__ capabilities along with Music Player.

The built in Sonos Player for example has these:

You'll have to determine the feature list of the Wiim to understand which Capabilities fit the best.

Yes that's expected. The sole purpose of doing this was because I couldn't get a command from RM to work. The actuator capability allows me to use RM to send custom command to the device for anything I added. For my use that was so that I could send a 'stop' to the devices as I leave the house and add a button (or virtual switch with auto off) to a dashboard that would start a radio preset for me. I'll attempt (but will need a lot of help) to get more working. For instance I know I cam get the status of the player (play mode, current source, track artist, album, track in Hex) and I want to get that to be added in current states .

Yes this all baffled me as I don't understand how anything works. With capability 'Music Player' there were unused non functional command buttons on the driver page. I didn't know how to get those buttons to do anything as they weren't referred to anywhere in the code.

An example:

I have a command 'Next' and I've set that to send the command specified to the Wiim and it works. With "MusicPlayer" capability added an additional button - "Next Track" appears which uses "nextTrack". I guess I need to know how to get that "Next Track" button that has been added by the capability, to send the command that I currently do with "Next"; I suppose I would then remove the command I added manually. Hope that makes sense.

1 Like

A few questions:

1 - It appears that if the capability includes a command, I don't need to specifically list it, is that right? For example I originally had command "next" and then defined that with

def next() { def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:next" sendHttpGetRequest(url) }

When I added the MusicPlayer capability, it added a button 'Next Track' that uses 'nextTrack' so I've deleted command "next" and altered def next() to use def nextTrack() instead.

Am I on the right track?

2 - Where the Wiim does not support a particular function, how can I amend the code so that the device page does not show a redundant button? For instance I want to remove the buttons for 'Play Text', 'Restore Track', 'Set Track'

1 Like

Have a look at the docs. Driver Capability List | Hubitat Documentation

Capabilities automatically define attributes and/or declare commands.

For commands you need to supply the implementation in the driver code body (as you did with your custom commands).

Yes

I’m not sure you can if these are mandatory commands associated with the capability. You can however do nothing other than log an error message.

1 Like

Some additional context.

Capabilities are a kind of contract. By declaring a capability for a device, you are "promising" to implement that contract. That’s how the UI can automatically create buttons, but that’s also how apps like RM can present you with appropriate options and list the right devices when setting a trigger or defining actions, etc.

1 Like

I can see that the commands are there automatically. After adding the Music Player capability, where one of the automatically declared commands has matched one that I'd added manually, I've deleted mine and pointed the declared one to the corresponding command in the Wiim API.

I take it I still need to research and list the appropriate attributes that are defined by the capability as they don't magically appear in my code..

Yes I found that in another post. For a total noob like me, the non working commands cause clutter.

Here's a concrete example - using your code from the other thread - of what happens when you declare a capability ("clutter" there is nothing you can do about :wink: ); here, defining an action in RM to control a music player :

They are there :wink: (Groovy being a dynamic language makes this a little more confusing perhaps).

Again, defining a trigger on a custom attribute in RM, for a virtual device using your code from the other day :

1 Like

I think that's almost my point. If the capability Music Player is declared and the 'player commands' are generated automatically by that capability, it poses an issue when the Wiim doesn't support all of the commands. In the device page it is just clutter but I need to remember which commands will work in that RM dropdown list (which I guess is all of the commands created by the capability plus any that I've added specifically). I've worked my through so that most of those capability generated commands do something - IE send the corresponding command to the device.

Summary
 /*
 * Wiim Media Player
 *
 *  Licensed Virtual the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *  Change History:
 *
 *    Date        Who            	What
 *    ----        ---           	 ----
 *    15Apr2024   John Williamson   Initial attempt - epic fail
 *    16Apr2024	  John Williamson   Fixed ignoreSSLIssues, added and tested all commands
 *    17Apr2024   John Williamson   Added MusicPlayer and AudioVolume capability.  Deleted duplicate commands
 */

metadata {
    definition (name: "Wiim Media Player", namespace: "John Williamson", author: "John Williamson") {
        capability "Actuator"
        capability "MusicPlayer"
        capability "AudioVolume"
        capability "Refresh"
        
        command "preset", ["number"]
        command "seek", ["number"]
        command "playlist", ["string"]
        //command "notification", ["string"]
        
        attribute "muted", "bool"
    }

    preferences {
        input("ipAddress", "string", title: "Device IP Address", description: "IP address of Wiim device", required: true)
        //input("soundUrl", "string", title: "Notification", description: "Network path to sound file", required: false)
    }
}

def sendHttpGetRequest(String requestURL) {
    Map requestParams =
	[
        uri: requestURL,
        ignoreSSLIssues: true
	]

    try {
        httpGet(requestParams) { resp ->
            //log.debug resp
            if (resp != null) {
                // success
                //log.debug resp.data // uncomment to log the Get response data 
            } else {
                // failure
            }
        }
    }
    catch (e) {
        // exception thrown
        log.error e
    }
}

def preset(number) {
    def url = "https://${ipAddress}/httpapi.asp?command=MCUKeyShortClick:${number}"
    sendHttpGetRequest(url)
}

def volumeUp() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:vol%2b%2b"
    sendHttpGetRequest(url)
}

def volumeDown() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:vol--"
    sendHttpGetRequest(url)
}

def mute() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:mute:1"
    sendHttpGetRequest(url)
}
def unmute() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:mute:0"
    sendHttpGetRequest(url)
}

def stop() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:stop"
    sendHttpGetRequest(url)
}

def pause() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:onepause"
    sendHttpGetRequest(url)
}

def setLevel(number) {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:vol:${number}"
    sendHttpGetRequest(url)
}

def setVolume(number) {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:vol:${number}"
    sendHttpGetRequest(url)
}

def previousTrack() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:prev"
    sendHttpGetRequest(url)
}

def nextTrack() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:next"
    sendHttpGetRequest(url)
}

def seek(number) {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:seek:${number}"
    sendHttpGetRequest(url)
}


def playTrack(uri) {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:play:${uri}"
    sendHttpGetRequest(url)
}

def playlist(string) {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:m3u:play:${string}"
    sendHttpGetRequest(url)
}

def refresh() {
    def url = "https://${ipAddress}/httpapi.asp?command=getPlayerStatus"
    sendHttpGetRequest(url)
}

//notification not working. Supported by Arylic but not by Wiim device as yet
def notification(string) {
    def url = "https://${ipAddress}/httpapi.asp?command=playPromptUrl:${string}"
    sendHttpGetRequest(url)
}

When you buy a car, you expect a certain amount of similarity. 4 wheels, a steering wheel, gas and break pedals, windows that roll up and down. An individual car, from one manufacturer might not have identical features to a car from a different manufacturer, but we'd hope there was enough overlap that you could reasonably buy a car from either company.

The same is true with Z-Devices. If we buy a dimmer, we expect there to be on/off and levels. However, manufacturers might add in some additional features, such as voltage or power readings, to make their device be superior in the marketplace. Capabilities are those 'expected common elements' that allow us to build automations without having to use Custom commands to do the most basic of tasks.

Maybe "Music Player" is not the closest match to what a Wiim does. There may be closer matches. But if Manufacturer X builds the first device of a category, then with nothing else to compare it to, Capabilities get defined by the market leader. If Manufacturer Y chooses to make a similar device but drops a bunch of features to reduce the cost, then YES there will be 'clutter' of do nothing buttons.

However, "do nothing" is not exactly the expectation.. 'nothing' should include a log message saying "not implemented" at the very least. You can even go so far as to use sendEvent() to populate the Attribute with "off" (or whatever is closest to indicate 'nothing").

Thanks I can't see anything else that might match from the Hubitat docs.

I'm looking through other drivers but cannot make sense of how I can add code to the driver to:

  • log events when a command is used
  • update an attribute value that I guess should be in 'Current States'
if ( !tempRange.contains( tempOffset.abs() as int ) )   { tempOffset = 0 ; log.warn Selection out of Range: Temperature Offset set to 0; }
if ( !humidRange.contains( humidOffset.abs() as int ) ) { humidOffset = 0 ; log.warn Selection out of Range: Humidity Offset set to 0; }

Those are some syntax examples.

    sendEvent(name: 'tamper', value: 'clear', descriptionText: '${device.displayName} tamper cleared')
    sendEvent(name: 'acceleration', value: 'inactive', descriptionText: $device.displayName} acceleration is inactive)

resulting in:
Screenshot 2024-04-17 at 8.07.34 AM

Where acceleration and tamper are reflected in Current States.

1 Like

Thanks. So the sendEvent gubbins created both an event and updates an attribute in 'Current States'. It wasn't creating an event in logs. I added the following in preferences:

input("logEnable", "bool", title: "Enable Text Logging", defaultValue: true)

I've then added that to the command:

def mute() {
    def url = "https://${ipAddress}/httpapi.asp?command=setPlayerCmd:mute:1"
    sendHttpGetRequest(url)
    sendEvent(name: "muted", value: "on", descriptionText: "${device.displayName} muted")
    if (logEnable) log.info "player volume muted"

That works but created more questions:

1 - I guess there's an easier/shorter way of doing it. I wonder if there's a way of globally sending the 'descriptionText' to the log (if logEnable) without typing it beneath every command. I don't mind doing that but assume there's a more efficient way.

2 - I'm sending the commands which is updating the values in states and events. I'm not testing whether the device is responding before updating those values. I can see further up the file that for each command sent, if successful the device responds with 'OK'. Can I test for that 'OK' before sending the event/updating the value and log an error if it fails? Again it would be preferable if I can add that once rather than to every command.

The result of a httpGet() is a response and within that is normal http result codes... 200 for OK, 400 for something wrong, etc. So.. yea, parse the response and issue the sendEvent() when the event got sent :slight_smile:

Regarding log.XXX, you sprinkle them about to assist your users with understanding success vs failure. You will probably want ONE log.info to let people know the device is working. You can add as many log.debug statements that make sense for you to respond to: "This driver isn't working..." Topics. Excess log.info messages are typically behind "if (logEnable)..." and all debug messages are behind "if (debugOutput)..." This enables users to make their logs as quiet or noisy as they need for the occasion. Traditionally, an: input("debugOutput", "bool", title: is added for debug and it is auto disabled after 30 mins.

def updated() {
    if (debugOutput) log.debug "In Updated with settings: ${settings}"
    unschedule()
    if (debugOutput) runIn(1800,logsOff)
...

  
def logsOff(){
	log.warn "debug logging disabled..."
	device.updateSetting("debugOutput",[value:"false",type:"bool"])
}