Fixed formatting

This commit is contained in:
Henric Andersson
2015-09-03 13:09:12 -07:00
parent 7e5d6e99d1
commit d9a2d8109e
2 changed files with 377 additions and 377 deletions
@@ -1,7 +1,7 @@
/**
* Bose SoundTouch
*
* Copyright 2015 Henric Andersson
* Copyright 2015 SmartThings
*
* Licensed under 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:
@@ -19,37 +19,37 @@ import groovy.xml.XmlUtil
// for the UI
metadata {
definition (name: "Bose SoundTouch", namespace: "smartthings", author: "Henric.Andersson@smartthings.com") {
/**
* List our capabilties. Doing so adds predefined command(s) which
* belong to the capability.
*/
capability "Switch"
capability "Refresh"
capability "Music Player"
capability "Polling"
definition (name: "Bose SoundTouch", namespace: "smartthings", author: "SmartThings") {
/**
* List our capabilties. Doing so adds predefined command(s) which
* belong to the capability.
*/
capability "Switch"
capability "Refresh"
capability "Music Player"
capability "Polling"
/**
* Define all commands, ie, if you have a custom action not
* covered by a capability, you NEED to define it here or
* the call will not be made.
*
* To call a capability function, just prefix it with the name
* of the capability, for example, refresh would be "refresh.refresh"
*/
command "preset1"
command "preset2"
command "preset3"
command "preset4"
command "preset5"
command "preset6"
command "aux"
/**
* Define all commands, ie, if you have a custom action not
* covered by a capability, you NEED to define it here or
* the call will not be made.
*
* To call a capability function, just prefix it with the name
* of the capability, for example, refresh would be "refresh.refresh"
*/
command "preset1"
command "preset2"
command "preset3"
command "preset4"
command "preset5"
command "preset6"
command "aux"
command "everywhereJoin"
command "everywhereLeave"
}
command "everywhereJoin"
command "everywhereLeave"
}
/**
/**
* Define the various tiles and the states that they can be in.
* The 2nd parameter defines an event which the tile listens to,
* if received, it tries to map it to a state.
@@ -59,8 +59,8 @@ metadata {
* single quotes, otherwise it will only be interpreted at time of
* launch, instead of every time the event triggers.
*/
valueTile("nowplaying", "device.nowplaying", width: 2, height: 1, decoration:"flat") {
state "nowplaying", label:'${currentValue}', action:"refresh.refresh"
valueTile("nowplaying", "device.nowplaying", width: 2, height: 1, decoration:"flat") {
state "nowplaying", label:'${currentValue}', action:"refresh.refresh"
}
standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) {
@@ -94,12 +94,12 @@ metadata {
}
controlTile("volume", "device.volume", "slider", height:1, width:3, range:"(0..100)") {
state "volume", action:"music Player.setLevel"
state "volume", action:"music Player.setLevel"
}
standardTile("playpause", "device.playpause", decoration: "flat") {
state "pause", label:'', icon:'st.sonos.play-btn', action:'music Player.play'
state "play", label:'', icon:'st.sonos.pause-btn', action:'music Player.pause'
state "pause", label:'', icon:'st.sonos.play-btn', action:'music Player.play'
state "play", label:'', icon:'st.sonos.pause-btn', action:'music Player.pause'
}
standardTile("prev", "device.switch", decoration: "flat", canChangeIcon: false) {
@@ -110,23 +110,23 @@ metadata {
}
valueTile("everywhere", "device.everywhere", width:2, height:1, decoration:"flat") {
state "join", label:"Join\nEverywhere", action:"everywhereJoin"
state "leave", label:"Leave\nEverywhere", action:"everywhereLeave"
state "join", label:"Join\nEverywhere", action:"everywhereJoin"
state "leave", label:"Leave\nEverywhere", action:"everywhereLeave"
// Final state is used if the device is in a state where joining is not possible
state "unavailable", label:"Not Available"
}
// Defines which tile to show in the overview
// Defines which tile to show in the overview
main "switch"
// Defines which tile(s) to show when user opens the detailed view
details ([
"nowplaying", "refresh", // Row 1 (112)
"prev", "playpause", "next", // Row 2 (123)
"volume", // Row 3 (111)
"1", "2", "3", // Row 4 (123)
"4", "5", "6", // Row 5 (123)
"aux", "everywhere"]) // Row 6 (122)
"nowplaying", "refresh", // Row 1 (112)
"prev", "playpause", "next", // Row 2 (123)
"volume", // Row 3 (111)
"1", "2", "3", // Row 4 (123)
"4", "5", "6", // Row 5 (123)
"aux", "everywhere"]) // Row 6 (122)
}
/**************************************************************************
@@ -179,35 +179,35 @@ def parse(String event) {
// List of permanent root node handlers
def handlers = [
"nowPlaying" : "boseParseNowPlaying",
"nowPlaying" : "boseParseNowPlaying",
"volume" : "boseParseVolume",
"presets" : "boseParsePresets",
"zone" : "boseParseEverywhere",
]
// No need to deal with non-XML data
// No need to deal with non-XML data
if (!data.headers || !data.headers?."content-type".contains("xml"))
return null
return null
// Move any pending callbacks into ready state
// Move any pending callbacks into ready state
prepareCallbacks()
def xml = new XmlSlurper().parseText(data.body)
// Let each parser take a stab at it
def xml = new XmlSlurper().parseText(data.body)
// Let each parser take a stab at it
handlers.each { node,func ->
if (xml.name() == node)
actions << "$func"(xml)
if (xml.name() == node)
actions << "$func"(xml)
}
// If we have callbacks waiting for this...
actions << processCallbacks(xml)
// Be nice and helpful
if (actions.size() == 0) {
log.warn "parse(): Unhandled data = " + lan
if (actions.size() == 0) {
log.warn "parse(): Unhandled data = " + lan
return null
}
}
// Issue new actions
// Issue new actions
return actions.flatten()
}
@@ -231,21 +231,21 @@ def installed() {
def onAction(String user, data=null) {
log.info "onAction(${user})"
// Keep IP address current (since device may have changed)
// Keep IP address current (since device may have changed)
state.address = parent.resolveDNI2Address(device.deviceNetworkId)
// Process action
def actions = null
switch (user) {
case "on":
actions = boseSetPowerState(true)
switch (user) {
case "on":
actions = boseSetPowerState(true)
break
case "off":
boseSetNowPlaying(null, "STANDBY")
actions = boseSetPowerState(false)
case "off":
boseSetNowPlaying(null, "STANDBY")
actions = boseSetPowerState(false)
break
case "volume":
actions = boseSetVolume(data)
actions = boseSetVolume(data)
break
case "aux":
boseSetNowPlaying(null, "AUX")
@@ -257,43 +257,43 @@ def onAction(String user, data=null) {
case "4":
case "5":
case "6":
actions = boseSetInput(user)
actions = boseSetInput(user)
break
case "refresh":
boseSetNowPlaying(null, "REFRESH")
actions = [boseRefreshNowPlaying(), boseGetPresets(), boseGetVolume(), boseGetEverywhereState()]
boseSetNowPlaying(null, "REFRESH")
actions = [boseRefreshNowPlaying(), boseGetPresets(), boseGetVolume(), boseGetEverywhereState()]
break
case "play":
actions = [boseSetPlayMode(true), boseRefreshNowPlaying()]
actions = [boseSetPlayMode(true), boseRefreshNowPlaying()]
break
case "pause":
actions = [boseSetPlayMode(false), boseRefreshNowPlaying()]
actions = [boseSetPlayMode(false), boseRefreshNowPlaying()]
break
case "previous":
actions = [boseChangeTrack(-1), boseRefreshNowPlaying()]
break
actions = [boseChangeTrack(-1), boseRefreshNowPlaying()]
break
case "next":
actions = [boseChangeTrack(1), boseRefreshNowPlaying()]
break
actions = [boseChangeTrack(1), boseRefreshNowPlaying()]
break
case "mute":
actions = boseSetMute(true)
break
actions = boseSetMute(true)
break
case "unmute":
actions = boseSetMute(false)
break
actions = boseSetMute(false)
break
case "ejoin":
actions = boseZoneJoin()
actions = boseZoneJoin()
break
case "eleave":
actions = boseZoneLeave()
actions = boseZoneLeave()
break
default:
log.error "Unhandled action: " + user
log.error "Unhandled action: " + user
}
// Make sure we don't have nested lists
if (actions instanceof List)
return actions.flatten()
// Make sure we don't have nested lists
if (actions instanceof List)
return actions.flatten()
return actions
}
@@ -309,15 +309,15 @@ def poll() {
* Joins this speaker into the everywhere zone
*/
def boseZoneJoin() {
def results = []
def results = []
def posts = parent.boseZoneJoin(this)
for (post in posts) {
if (post['endpoint'])
results << bosePOST(post['endpoint'], post['body'], post['host'])
}
for (post in posts) {
if (post['endpoint'])
results << bosePOST(post['endpoint'], post['body'], post['host'])
}
sendEvent(name:"everywhere", value:"leave")
results << boseRefreshNowPlaying()
results << boseRefreshNowPlaying()
return results
}
@@ -326,15 +326,15 @@ def boseZoneJoin() {
* Removes this speaker from the everywhere zone
*/
def boseZoneLeave() {
def results = []
def posts = parent.boseZoneLeave(this)
def results = []
def posts = parent.boseZoneLeave(this)
for (post in posts) {
if (post['endpoint'])
results << bosePOST(post['endpoint'], post['body'], post['host'])
}
for (post in posts) {
if (post['endpoint'])
results << bosePOST(post['endpoint'], post['body'], post['host'])
}
sendEvent(name:"everywhere", value:"join")
results << boseRefreshNowPlaying()
results << boseRefreshNowPlaying()
return results
}
@@ -346,7 +346,7 @@ def boseZoneLeave() {
* cause the zone to collapse (for example, AUX)
*/
def boseZoneReset() {
parent.boseZoneReset()
parent.boseZoneReset()
}
/**
@@ -359,9 +359,9 @@ def boseZoneReset() {
* @return command
*/
def boseParseNowPlaying(xmlData) {
def result = []
def result = []
// Perform display update, allow it to add additional commands
// Perform display update, allow it to add additional commands
if (boseSetNowPlaying(xmlData)) {
result << boseRefreshNowPlaying()
}
@@ -376,7 +376,7 @@ def boseParseNowPlaying(xmlData) {
* @return command
*/
def boseParseVolume(xmlData) {
def result = []
def result = []
sendEvent(name:"volume", value:xmlData.actualvolume.text())
sendEvent(name:"mute", value:(Boolean.toBoolean(xmlData.muteenabled.text()) ? "unmuted" : "muted"))
@@ -390,7 +390,7 @@ def boseParseVolume(xmlData) {
* @param xmlData
*/
def boseParseEverywhere(xmlData) {
// No good way of detecting the correct state right now
// No good way of detecting the correct state right now
}
/**
@@ -400,13 +400,13 @@ def boseParseEverywhere(xmlData) {
* @return command
*/
def boseParsePresets(xmlData) {
def result = []
def result = []
state.preset = [:]
def missing = ["1", "2", "3", "4", "5", "6"]
for (preset in xmlData.preset) {
def id = preset.attributes()['id']
def id = preset.attributes()['id']
def name = preset.ContentItem.itemName[0].text().replaceAll(~/ +/, "\n")
if (name == "##TRANS_SONGS##")
name = "Local\nPlaylist"
@@ -418,8 +418,8 @@ def boseParsePresets(xmlData) {
}
for (id in missing) {
state.preset["$id"] = null
sendEvent(name:"station${id}", value:"Preset $id\n\nNot set")
state.preset["$id"] = null
sendEvent(name:"station${id}", value:"Preset $id\n\nNot set")
}
return result
@@ -435,21 +435,21 @@ def boseParsePresets(xmlData) {
* @return true if it would prefer a refresh soon
*/
def boseSetNowPlaying(xmlData, override=null) {
def needrefresh = false
def nowplaying = null
def needrefresh = false
def nowplaying = null
if (xmlData && xmlData.playStatus) {
switch(xmlData.playStatus) {
case "BUFFERING_STATE":
nowplaying = "Please wait\nBuffering..."
needrefresh = true
switch(xmlData.playStatus) {
case "BUFFERING_STATE":
nowplaying = "Please wait\nBuffering..."
needrefresh = true
break
case "PLAY_STATE":
sendEvent(name:"playpause", value:"play")
sendEvent(name:"playpause", value:"play")
break
case "PAUSE_STATE":
case "STOP_STATE":
sendEvent(name:"playpause", value:"pause")
sendEvent(name:"playpause", value:"pause")
break
}
}
@@ -480,21 +480,21 @@ def boseSetNowPlaying(xmlData, override=null) {
if (xmlData.ContentItem.itemName[0])
nowplaying += "[${xmlData.ContentItem.itemName[0].text()}]\n\n"
case "STORED_MUSIC":
nowplaying += "${xmlData.track.text()}"
nowplaying += "${xmlData.track.text()}"
if (xmlData.artist)
nowplaying += "\nby\n${xmlData.artist.text()}"
nowplaying += "\nby\n${xmlData.artist.text()}"
if (xmlData.album)
nowplaying += "\n\n(${xmlData.album.text()})"
break
break
default:
if (xmlData != null)
nowplaying = "${xmlData.ContentItem.itemName[0].text()}"
else
nowplaying = "Unknown"
else
nowplaying = "Unknown"
}
}
// Some last parsing which only deals with actual data from device
// Some last parsing which only deals with actual data from device
if (xmlData) {
if (xmlData.attributes()['source'] == "STANDBY") {
log.trace "nowPlaying reports standby: " + XmlUtil.serialize(xmlData)
@@ -502,21 +502,21 @@ def boseSetNowPlaying(xmlData, override=null) {
} else {
sendEvent(name:"switch", value:"on")
}
boseSetPlayerAttributes(xmlData)
boseSetPlayerAttributes(xmlData)
}
// Do not allow a standby device or AUX to be master
if (!parent.boseZoneHasMaster() && (override ? override : xmlData.attributes()['source']) == "STANDBY")
sendEvent(name:"everywhere", value:"unavailable")
sendEvent(name:"everywhere", value:"unavailable")
else if ((override ? override : xmlData.attributes()['source']) == "AUX")
sendEvent(name:"everywhere", value:"unavailable")
else if (boseGetZone()) {
log.info "We're in the zone: " + boseGetZone()
log.info "We're in the zone: " + boseGetZone()
sendEvent(name:"everywhere", value:"leave")
} else
sendEvent(name:"everywhere", value:"join")
sendEvent(name:"everywhere", value:"join")
sendEvent(name:"nowplaying", value:nowplaying)
sendEvent(name:"nowplaying", value:nowplaying)
return needrefresh
}
@@ -533,16 +533,16 @@ def boseSetPlayerAttributes(xmlData) {
def trackData = [:]
switch (xmlData.attributes()['source']) {
case "STANDBY":
trackData["station"] = trackText = trackDesc = "Standby"
break
case "STANDBY":
trackData["station"] = trackText = trackDesc = "Standby"
break
case "AUX":
trackData["station"] = trackText = trackDesc = "Auxiliary Input"
trackData["station"] = trackText = trackDesc = "Auxiliary Input"
break
case "AIRPLAY":
trackData["station"] = trackText = trackDesc = "Air Play"
trackData["station"] = trackText = trackDesc = "Air Play"
break
case "SPOTIFY":
case "SPOTIFY":
case "DEEZER":
case "PANDORA":
case "IHEART":
@@ -550,25 +550,25 @@ def boseSetPlayerAttributes(xmlData) {
trackText = trackDesc = "${xmlData.track.text()}"
trackData["name"] = xmlData.track.text()
if (xmlData.artist) {
trackText += " by ${xmlData.artist.text()}"
trackText += " by ${xmlData.artist.text()}"
trackDesc += " - ${xmlData.artist.text()}"
trackData["artist"] = xmlData.artist.text()
trackData["artist"] = xmlData.artist.text()
}
if (xmlData.album) {
trackText += " (${xmlData.album.text()})"
trackData["album"] = xmlData.album.text()
trackText += " (${xmlData.album.text()})"
trackData["album"] = xmlData.album.text()
}
break
case "INTERNET_RADIO":
trackDesc = xmlData.stationName.text()
trackDesc = xmlData.stationName.text()
trackText = xmlData.stationName.text() + ": " + xmlData.description.text()
trackData["station"] = xmlData.stationName.text()
break
break
default:
trackText = trackDesc = xmlData.ContentItem.itemName[0].text()
}
sendEvent(name:"trackDescription", value:trackDesc, descriptionText:trackText)
sendEvent(name:"trackDescription", value:trackDesc, descriptionText:trackText)
}
/**
@@ -577,7 +577,7 @@ def boseSetPlayerAttributes(xmlData) {
* @return command
*/
def boseGetEverywhereState() {
return boseGET("/getZone")
return boseGET("/getZone")
}
/**
@@ -591,7 +591,7 @@ def boseGetEverywhereState() {
* the second key info.
*/
def boseKeypress(key) {
def press = "<key state=\"press\" sender=\"Gabbo\">${key}</key>"
def press = "<key state=\"press\" sender=\"Gabbo\">${key}</key>"
def release = "<key state=\"release\" sender=\"Gabbo\">${key}</key>"
return [bosePOST("/key", press), bosePOST("/key", release)]
@@ -605,8 +605,8 @@ def boseKeypress(key) {
* @return command
*/
def boseSetPlayMode(boolean play) {
log.trace "Sending " + (play ? "PLAY" : "PAUSE")
return boseKeypress(play ? "PLAY" : "PAUSE")
log.trace "Sending " + (play ? "PLAY" : "PAUSE")
return boseKeypress(play ? "PLAY" : "PAUSE")
}
/**
@@ -617,10 +617,10 @@ def boseSetPlayMode(boolean play) {
* @return command
*/
def boseSetVolume(int level) {
def result = []
int vol = Math.min(100, Math.max(level, 0))
def result = []
int vol = Math.min(100, Math.max(level, 0))
sendEvent(name:"volume", value:"${vol}")
sendEvent(name:"volume", value:"${vol}")
return [bosePOST("/volume", "<volume>${vol}</volume>"), boseGetVolume()]
}
@@ -633,7 +633,7 @@ def boseSetVolume(int level) {
* @return command
*/
def boseSetMute(boolean mute) {
queueCallback('volume', 'cb_boseSetMute', mute ? 'MUTE' : 'UNMUTE')
queueCallback('volume', 'cb_boseSetMute', mute ? 'MUTE' : 'UNMUTE')
return boseGetVolume()
}
@@ -647,14 +647,14 @@ def boseSetMute(boolean mute) {
* @return command
*/
def cb_boseSetMute(xml, mute) {
def result = []
if ((xml.muteenabled.text() == 'false' && mute == 'MUTE') ||
def result = []
if ((xml.muteenabled.text() == 'false' && mute == 'MUTE') ||
(xml.muteenabled.text() == 'true' && mute == 'UNMUTE'))
{
result << boseKeypress("MUTE")
result << boseKeypress("MUTE")
}
log.trace("muteunmute: " + ((mute == "MUTE") ? "unmute" : "mute"))
sendEvent(name:"muteunmute", value:((mute == "MUTE") ? "unmute" : "mute"))
sendEvent(name:"muteunmute", value:((mute == "MUTE") ? "unmute" : "mute"))
return result
}
@@ -664,7 +664,7 @@ def cb_boseSetMute(xml, mute) {
* @return command
*/
def boseGetVolume() {
return boseGET("/volume")
return boseGET("/volume")
}
/**
@@ -674,10 +674,10 @@ def boseGetVolume() {
* @return command
*/
def boseChangeTrack(int direction) {
if (direction < 0) {
return boseKeypress("PREV_TRACK")
if (direction < 0) {
return boseKeypress("PREV_TRACK")
} else if (direction > 0) {
return boseKeypress("NEXT_TRACK")
return boseKeypress("NEXT_TRACK")
}
return []
}
@@ -692,14 +692,14 @@ def boseChangeTrack(int direction) {
* @note If no presets have been loaded, it will first refresh the presets.
*/
def boseSetInput(input) {
log.info "boseSetInput(${input})"
def result = []
log.info "boseSetInput(${input})"
def result = []
if (!state.preset) {
result << boseGetPresets()
result << boseGetPresets()
queueCallback('presets', 'cb_boseSetInput', input)
} else {
result << cb_boseSetInput(null, input)
result << cb_boseSetInput(null, input)
}
return result
}
@@ -720,10 +720,10 @@ def boseSetInput(input) {
* the preset if there is a long delay between the two.
*/
def cb_boseSetInput(xml, input) {
def result = []
def result = []
if (input >= "1" && input <= "6" && state.preset["$input"])
result << bosePOST("/select", state.preset["$input"])
result << bosePOST("/select", state.preset["$input"])
else if (input.toLowerCase() == "aux") {
result << boseKeypress("AUX_INPUT")
}
@@ -731,7 +731,7 @@ def cb_boseSetInput(xml, input) {
// Horrible workaround... but we need to delay
// the update by at least a few seconds...
result << boseRefreshNowPlaying(3000)
return result
return result
}
/**
@@ -746,9 +746,9 @@ def cb_boseSetInput(xml, input) {
* is no discreete call.
*/
def boseSetPowerState(boolean enable) {
log.info "boseSetPowerState(${enable})"
log.info "boseSetPowerState(${enable})"
queueCallback('nowPlaying', "cb_boseSetPowerState", enable ? "POWERON" : "POWEROFF")
return boseRefreshNowPlaying()
return boseRefreshNowPlaying()
}
/**
@@ -761,13 +761,13 @@ def boseSetPowerState(boolean enable) {
* @return command
*/
def cb_boseSetPowerState(xml, state) {
def result = []
if ( (xml.attributes()['source'] == "STANDBY" && state == "POWERON") ||
def result = []
if ( (xml.attributes()['source'] == "STANDBY" && state == "POWERON") ||
(xml.attributes()['source'] != "STANDBY" && state == "POWEROFF") )
{
result << boseKeypress("POWER")
result << boseKeypress("POWER")
if (state == "POWERON") {
result << boseRefreshNowPlaying()
result << boseRefreshNowPlaying()
queueCallback('nowPlaying', "cb_boseConfirmPowerOn", 5)
}
}
@@ -786,9 +786,9 @@ def cb_boseSetPowerState(xml, state) {
* @return command
*/
def cb_boseConfirmPowerOn(xml, tries) {
def result = []
def result = []
log.warn "boseConfirmPowerOn() attempt #" + tries
if (xml.attributes()['source'] == "STANDBY" && tries > 0) {
if (xml.attributes()['source'] == "STANDBY" && tries > 0) {
result << boseRefreshNowPlaying()
queueCallback('nowPlaying', "cb_boseConfirmPowerOn", tries-1)
}
@@ -803,10 +803,10 @@ def cb_boseConfirmPowerOn(xml, tries) {
* @return command
*/
def boseRefreshNowPlaying(delay=0) {
if (delay > 0) {
return ["delay ${delay}", boseGET("/now_playing")]
if (delay > 0) {
return ["delay ${delay}", boseGET("/now_playing")]
}
return boseGET("/now_playing")
return boseGET("/now_playing")
}
/**
@@ -815,7 +815,7 @@ def boseRefreshNowPlaying(delay=0) {
* @return command
*/
def boseGetPresets() {
return boseGET("/presets")
return boseGET("/presets")
}
/**
@@ -864,10 +864,10 @@ def bosePOST(String path, String data, String address=null) {
* @param param Parameters for function (optional)
*/
def queueCallback(String root, String func, param=null) {
if (!state.pending)
state.pending = [:]
if (!state.pending)
state.pending = [:]
if (!state.pending[root])
state.pending[root] = []
state.pending[root] = []
state.pending[root] << ["$func":"$param"]
}
@@ -879,10 +879,10 @@ def queueCallback(String root, String func, param=null) {
* the same loop.
*/
def prepareCallbacks() {
if (!state.pending)
return
if (!state.pending)
return
if (!state.ready)
state.ready = [:]
state.ready = [:]
state.ready << state.pending
state.pending = [:]
}
@@ -901,10 +901,10 @@ def prepareCallbacks() {
* @return list of commands
*/
def processCallbacks(xml) {
def result = []
def result = []
if (!state.ready)
return result
return result
if (state.ready[xml.name()]) {
state.ready[xml.name()].each { callback ->
@@ -935,13 +935,13 @@ def processCallbacks(xml) {
* @param newstate (see above for types)
*/
def boseSetZone(String newstate) {
log.debug "boseSetZone($newstate)"
state.zone = newstate
log.debug "boseSetZone($newstate)"
state.zone = newstate
// Refresh our state
if (newstate) {
// Refresh our state
if (newstate) {
sendEvent(name:"everywhere", value:"leave")
} else {
} else {
sendEvent(name:"everywhere", value:"join")
}
}
@@ -954,7 +954,7 @@ def boseSetZone(String newstate) {
* @return state
*/
def boseGetZone() {
return state.zone
return state.zone
}
/**
@@ -967,7 +967,7 @@ def boseGetZone() {
* @param devID The DeviceID
*/
def boseSetDeviceID(String devID) {
state.deviceID = devID
state.deviceID = devID
}
/**
@@ -976,7 +976,7 @@ def boseSetDeviceID(String devID) {
* @return deviceID
*/
def boseGetDeviceID() {
return state.deviceID
return state.deviceID
}
/**
@@ -985,5 +985,5 @@ def boseGetDeviceID() {
* @return IP address
*/
def getDeviceIP() {
return parent.resolveDNI2Address(device.deviceNetworkId)
return parent.resolveDNI2Address(device.deviceNetworkId)
}
@@ -1,7 +1,7 @@
/**
* Bose SoundTouch (Connect)
*
* Copyright 2015 Henric Andersson
* Copyright 2015 SmartThings
*
* Licensed under 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:
@@ -16,7 +16,7 @@
definition(
name: "Bose SoundTouch (Connect)",
namespace: "smartthings",
author: "Henric.Andersson@smartthings.com",
author: "SmartThings",
description: "Control your Bose SoundTouch speakers",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
@@ -25,7 +25,7 @@
)
preferences {
page(name:"deviceDiscovery", title:"Device Setup", content:"deviceDiscovery", refreshTimeout:5)
page(name:"deviceDiscovery", title:"Device Setup", content:"deviceDiscovery", refreshTimeout:5)
}
/**
@@ -36,7 +36,7 @@ preferences {
* @todo This + getUSNQualifier should be one and should use regular expressions
*/
def getDeviceType() {
return "urn:schemas-upnp-org:device:MediaRenderer:1" // Bose
return "urn:schemas-upnp-org:device:MediaRenderer:1" // Bose
}
/**
@@ -46,7 +46,7 @@ def getDeviceType() {
* @return Additional qualifier OR null if not needed
*/
def getUSNQualifier() {
return "uuid:BO5EBO5E-F00D-F00D-FEED-"
return "uuid:BO5EBO5E-F00D-F00D-FEED-"
}
/**
@@ -56,7 +56,7 @@ def getUSNQualifier() {
* @return name
*/
def getDeviceName() {
return "Bose SoundTouch"
return "Bose SoundTouch"
}
/**
@@ -65,7 +65,7 @@ def getDeviceName() {
* @return namespace
*/
def getNameSpace() {
return "smartthings"
return "smartthings"
}
/**
@@ -77,48 +77,48 @@ def getNameSpace() {
*/
def deviceDiscovery()
{
if(canInstallLabs())
{
def refreshInterval = 3 // Number of seconds between refresh
int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
state.deviceRefreshCount = deviceRefreshCount + refreshInterval
if(canInstallLabs())
{
def refreshInterval = 3 // Number of seconds between refresh
int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
state.deviceRefreshCount = deviceRefreshCount + refreshInterval
def devices = getSelectableDevice()
def numFound = devices.size() ?: 0
def devices = getSelectableDevice()
def numFound = devices.size() ?: 0
// Make sure we get location updates (contains LAN data such as SSDP results, etc)
subscribeNetworkEvents()
//device discovery request every 15s
if((deviceRefreshCount % 15) == 0) {
discoverDevices()
}
//device discovery request every 15s
if((deviceRefreshCount % 15) == 0) {
discoverDevices()
}
// Verify request every 3 seconds except on discoveries
if(((deviceRefreshCount % 3) == 0) && ((deviceRefreshCount % 15) != 0)) {
verifyDevices()
}
// Verify request every 3 seconds except on discoveries
if(((deviceRefreshCount % 3) == 0) && ((deviceRefreshCount % 15) != 0)) {
verifyDevices()
}
log.trace "Discovered devices: ${devices}"
log.trace "Discovered devices: ${devices}"
return dynamicPage(name:"deviceDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
section("Please wait while we discover your ${getDeviceName()}. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
input "selecteddevice", "enum", required:false, title:"Select ${getDeviceName()} (${numFound} found)", multiple:true, options:devices
}
}
}
else
{
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
return dynamicPage(name:"deviceDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
section("Please wait while we discover your ${getDeviceName()}. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
input "selecteddevice", "enum", required:false, title:"Select ${getDeviceName()} (${numFound} found)", multiple:true, options:devices
}
}
}
else
{
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
return dynamicPage(name:"deviceDiscovery", title:"Upgrade needed!", nextPage:"", install:true, uninstall: true) {
section("Upgrade") {
paragraph "$upgradeNeeded"
}
}
}
return dynamicPage(name:"deviceDiscovery", title:"Upgrade needed!", nextPage:"", install:true, uninstall: true) {
section("Upgrade") {
paragraph "$upgradeNeeded"
}
}
}
}
/**
@@ -126,17 +126,17 @@ To update your Hub, access Location Settings in the Main Menu (tap the gear next
* pressed "Install".
*/
def installed() {
log.trace "Installed with settings: ${settings}"
initialize()
log.trace "Installed with settings: ${settings}"
initialize()
}
/**
* Called by SmartThings Cloud when app has been updated
*/
def updated() {
log.trace "Updated with settings: ${settings}"
unsubscribe()
initialize()
log.trace "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
/**
@@ -157,13 +157,13 @@ def uninstalled() {
* for changes (new address, port, etc...)
*/
def initialize() {
log.trace "initialize()"
state.subscribe = false
if (selecteddevice) {
addDevice()
log.trace "initialize()"
state.subscribe = false
if (selecteddevice) {
addDevice()
refreshDevices()
subscribeNetworkEvents(true)
}
}
}
/**
@@ -172,33 +172,33 @@ def initialize() {
* Uses selecteddevice defined in the deviceDiscovery() page
*/
def addDevice(){
def devices = getVerifiedDevices()
def devlist
def devices = getVerifiedDevices()
def devlist
log.trace "Adding childs"
// If only one device is selected, we don't get a list (when using simulator)
// If only one device is selected, we don't get a list (when using simulator)
if (!(selecteddevice instanceof List)) {
devlist = [selecteddevice]
} else {
devlist = selecteddevice
devlist = selecteddevice
}
log.trace "These are being installed: ${devlist}"
devlist.each { dni ->
def d = getChildDevice(dni)
if(!d) {
def newDevice = devices.find { (it.value.mac) == dni }
devlist.each { dni ->
def d = getChildDevice(dni)
if(!d) {
def newDevice = devices.find { (it.value.mac) == dni }
def deviceName = newDevice?.value.name
if (!deviceName)
deviceName = getDeviceName() + "[${newDevice?.value.name}]"
d = addChildDevice(getNameSpace(), getDeviceName(), dni, newDevice?.value.hub, [label:"${deviceName}"])
d = addChildDevice(getNameSpace(), getDeviceName(), dni, newDevice?.value.hub, [label:"${deviceName}"])
d.boseSetDeviceID(newDevice.value.deviceID)
log.trace "Created ${d.displayName} with id $dni"
} else {
log.trace "${d.displayName} with id $dni already exists"
}
}
log.trace "Created ${d.displayName} with id $dni"
} else {
log.trace "${d.displayName} with id $dni already exists"
}
}
}
/**
@@ -208,9 +208,9 @@ def addDevice(){
* @return address or null
*/
def resolveDNI2Address(dni) {
def device = getVerifiedDevices().find { (it.value.mac) == dni }
def device = getVerifiedDevices().find { (it.value.mac) == dni }
if (device) {
return convertHexToIP(device.value.networkAddress)
return convertHexToIP(device.value.networkAddress)
}
return null
}
@@ -222,7 +222,7 @@ def resolveDNI2Address(dni) {
* @return A list of maps with POST data
*/
def boseZoneJoin(child) {
log = child.log // So we can debug this function
log = child.log // So we can debug this function
def results = []
def result = [:]
@@ -231,21 +231,21 @@ def boseZoneJoin(child) {
def server = getChildDevices().find{ it.boseGetZone() == "server" }
if (server) {
log.debug "boseJoinZone() We have a server already, so lets add the new speaker"
log.debug "boseJoinZone() We have a server already, so lets add the new speaker"
child.boseSetZone("client")
result['endpoint'] = "/setZone"
result['host'] = server.getDeviceIP() + ":8090"
result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">"
getChildDevices().each{ it ->
log.trace "child: " + child
log.trace "child: " + child
log.trace "zone : " + it.boseGetZone()
if (it.boseGetZone() || it.boseGetDeviceID() == child.boseGetDeviceID())
result['body'] = result['body'] + "<member ipaddress=\"${it.getDeviceIP()}\">${it.boseGetDeviceID()}</member>"
if (it.boseGetZone() || it.boseGetDeviceID() == child.boseGetDeviceID())
result['body'] = result['body'] + "<member ipaddress=\"${it.getDeviceIP()}\">${it.boseGetDeviceID()}</member>"
}
result['body'] = result['body'] + '</zone>'
} else {
log.debug "boseJoinZone() No server, add it!"
log.debug "boseJoinZone() No server, add it!"
result['endpoint'] = "/setZone"
result['host'] = child.getDeviceIP() + ":8090"
result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">"
@@ -258,11 +258,11 @@ def boseZoneJoin(child) {
}
def boseZoneReset() {
getChildDevices().each{ it.boseSetZone(null) }
getChildDevices().each{ it.boseSetZone(null) }
}
def boseZoneHasMaster() {
return getChildDevices().find{ it.boseGetZone() == "server" } != null
return getChildDevices().find{ it.boseGetZone() == "server" } != null
}
/**
@@ -272,7 +272,7 @@ def boseZoneHasMaster() {
* @return a list of maps with POST data
*/
def boseZoneLeave(child) {
log = child.log // So we can debug this function
log = child.log // So we can debug this function
def results = []
def result = [:]
@@ -284,7 +284,7 @@ def boseZoneLeave(child) {
def server = getChildDevices().find{ it.boseGetZone() == "server" }
if (server && server.boseGetDeviceID() != child.boseGetDeviceID()) {
log.debug "boseLeaveZone() We have a server, so tell him we're leaving"
log.debug "boseLeaveZone() We have a server, so tell him we're leaving"
result['endpoint'] = "/removeZoneSlave"
result['host'] = server.getDeviceIP() + ":8090"
result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">"
@@ -292,24 +292,24 @@ def boseZoneLeave(child) {
result['body'] = result['body'] + '</zone>'
results << result
} else {
log.debug "boseLeaveZone() No server, then...uhm, we probably were it!"
log.debug "boseLeaveZone() No server, then...uhm, we probably were it!"
// Dismantle the entire thing, first send this to master
result['endpoint'] = "/removeZoneSlave"
result['host'] = child.getDeviceIP() + ":8090"
result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">"
getChildDevices().each{ dev ->
if (dev.boseGetZone() || dev.boseGetDeviceID() == child.boseGetDeviceID())
result['body'] = result['body'] + "<member ipaddress=\"${dev.getDeviceIP()}\">${dev.boseGetDeviceID()}</member>"
if (dev.boseGetZone() || dev.boseGetDeviceID() == child.boseGetDeviceID())
result['body'] = result['body'] + "<member ipaddress=\"${dev.getDeviceIP()}\">${dev.boseGetDeviceID()}</member>"
}
result['body'] = result['body'] + '</zone>'
results << result
// Also issue this to each individual client
getChildDevices().each{ dev ->
if (dev.boseGetZone() && dev.boseGetDeviceID() != child.boseGetDeviceID()) {
log.trace "Additional device: " + dev
if (dev.boseGetZone() && dev.boseGetDeviceID() != child.boseGetDeviceID()) {
log.trace "Additional device: " + dev
result['host'] = dev.getDeviceIP() + ":8090"
results << result
results << result
}
}
}
@@ -323,10 +323,10 @@ def boseZoneLeave(child) {
* @return mapping of root-node <-> parser function
*/
def getParsers() {
[
[
"root" : "parseDESC",
"info" : "parseINFO"
]
]
}
/**
@@ -337,25 +337,25 @@ def getParsers() {
* @param evt Holds event information
*/
def onLocation(evt) {
// Convert the event into something we can use
// Convert the event into something we can use
def lanEvent = parseLanMessage(evt.description, true)
lanEvent << ["hub":evt?.hubId]
// Determine what we need to do...
if (lanEvent?.ssdpTerm?.contains(getDeviceType()) &&
// Determine what we need to do...
if (lanEvent?.ssdpTerm?.contains(getDeviceType()) &&
(getUSNQualifier() == null ||
lanEvent?.ssdpUSN?.contains(getUSNQualifier())
)
)
{
parseSSDP(lanEvent)
}
else if (
lanEvent.headers && lanEvent.body &&
lanEvent.headers."content-type".contains("xml")
parseSSDP(lanEvent)
}
else if (
lanEvent.headers && lanEvent.body &&
lanEvent.headers."content-type".contains("xml")
)
{
def parsers = getParsers()
def parsers = getParsers()
def xmlData = new XmlSlurper().parseText(lanEvent.body)
// Let each parser take a stab at it
@@ -382,7 +382,7 @@ private def parseDESC(xmlData) {
def devices = getDevices()
def device = devices.find {it?.key?.contains(xmlData?.device?.UDN?.text())}
if (device && !device.value?.verified) {
// Unlike regular DESC, we cannot trust this just yet, parseINFO() decides all
// Unlike regular DESC, we cannot trust this just yet, parseINFO() decides all
device.value << [name:xmlData?.device?.friendlyName?.text(),model:xmlData?.device?.modelName?.text(), serialNumber:xmlData?.device?.serialNum?.text()]
} else {
log.error "parseDESC(): The xml file returned a device that didn't exist"
@@ -398,7 +398,7 @@ private def parseDESC(xmlData) {
* @param xmlData
*/
private def parseINFO(xmlData) {
log.info "parseINFO()"
log.info "parseINFO()"
def devicetype = getDeviceType().toLowerCase()
def deviceID = xmlData.attributes()['deviceID']
@@ -428,7 +428,7 @@ def parseSSDP(lanEvent) {
// update the values
def d = devices."${USN}"
if (d.networkAddress != lanEvent.networkAddress || d.deviceAddress != lanEvent.deviceAddress) {
log.trace "parseSSDP() Updating device location (ip & port)"
log.trace "parseSSDP() Updating device location (ip & port)"
d.networkAddress = lanEvent.networkAddress
d.deviceAddress = lanEvent.deviceAddress
}
@@ -442,14 +442,14 @@ def parseSSDP(lanEvent) {
* @return Map with zero or more devices
*/
Map getSelectableDevice() {
def devices = getVerifiedDevices()
def map = [:]
devices.each {
def value = "${it.value.name}"
def key = it.value.mac
map["${key}"] = value
}
map
def devices = getVerifiedDevices()
def map = [:]
devices.each {
def value = "${it.value.name}"
def key = it.value.mac
map["${key}"] = value
}
map
}
/**
@@ -484,7 +484,7 @@ private subscribeNetworkEvents(force=false) {
*/
private discoverDevices() {
log.trace "discoverDevice() Issuing SSDP request"
sendHubCommand(new physicalgraph.device.HubAction("lan discovery ${getDeviceType()}", physicalgraph.device.Protocol.LAN))
sendHubCommand(new physicalgraph.device.HubAction("lan discovery ${getDeviceType()}", physicalgraph.device.Protocol.LAN))
}
/**
@@ -492,16 +492,16 @@ private discoverDevices() {
* request for each of them (basically calling verifyDevice() per unverified)
*/
private verifyDevices() {
def devices = getDevices().findAll { it?.value?.verified != true }
def devices = getDevices().findAll { it?.value?.verified != true }
devices.each {
verifyDevice(
it?.value?.mac,
convertHexToIP(it?.value?.networkAddress),
devices.each {
verifyDevice(
it?.value?.mac,
convertHexToIP(it?.value?.networkAddress),
convertHexToInt(it?.value?.deviceAddress),
it?.value?.ssdpPath
)
}
}
}
/**
@@ -528,7 +528,7 @@ private verifyDevice(String deviceNetworkId, String ip, int port, String devices
HOST: address,
]]))
} else {
log.warn("verifyDevice() IP address was empty")
log.warn("verifyDevice() IP address was empty")
}
}
@@ -538,7 +538,7 @@ private verifyDevice(String deviceNetworkId, String ip, int port, String devices
* @return array of verified devices
*/
def getVerifiedDevices() {
getDevices().findAll{ it?.value?.verified == true }
getDevices().findAll{ it?.value?.verified == true }
}
/**
@@ -547,7 +547,7 @@ def getVerifiedDevices() {
* @return array of devices
*/
def getDevices() {
state.devices = state.devices ?: [:]
state.devices = state.devices ?: [:]
}
/**
@@ -557,7 +557,7 @@ def getDevices() {
* @return An integer
*/
private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
Integer.parseInt(hex,16)
}
/**
@@ -567,10 +567,10 @@ private Integer convertHexToInt(hex) {
* @return String containing normal IPv4 dot notation
*/
private String convertHexToIP(hex) {
if (hex)
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
if (hex)
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
else
hex
hex
}
/**
@@ -580,7 +580,7 @@ private String convertHexToIP(hex) {
*/
private Boolean canInstallLabs()
{
return hasAllHubsOver("000.011.00603")
return hasAllHubsOver("000.011.00603")
}
/**
@@ -592,7 +592,7 @@ private Boolean canInstallLabs()
*/
private Boolean hasAllHubsOver(String desiredFirmware)
{
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
}
/**
@@ -602,5 +602,5 @@ private Boolean hasAllHubsOver(String desiredFirmware)
*/
private List getRealHubFirmwareVersions()
{
return location.hubs*.firmwareVersionString.findAll { it }
return location.hubs*.firmwareVersionString.findAll { it }
}