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

View File

@@ -1,7 +1,7 @@
/** /**
* Bose SoundTouch * 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 * 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: * in compliance with the License. You may obtain a copy of the License at:
@@ -13,54 +13,54 @@
* for the specific language governing permissions and limitations under the License. * for the specific language governing permissions and limitations under the License.
* *
*/ */
// Needed to be able to serialize the XmlSlurper data back to XML // Needed to be able to serialize the XmlSlurper data back to XML
import groovy.xml.XmlUtil import groovy.xml.XmlUtil
// for the UI // for the UI
metadata { metadata {
definition (name: "Bose SoundTouch", namespace: "smartthings", author: "Henric.Andersson@smartthings.com") { definition (name: "Bose SoundTouch", namespace: "smartthings", author: "SmartThings") {
/** /**
* List our capabilties. Doing so adds predefined command(s) which * List our capabilties. Doing so adds predefined command(s) which
* belong to the capability. * belong to the capability.
*/ */
capability "Switch" capability "Switch"
capability "Refresh" capability "Refresh"
capability "Music Player" capability "Music Player"
capability "Polling" capability "Polling"
/** /**
* Define all commands, ie, if you have a custom action not * Define all commands, ie, if you have a custom action not
* covered by a capability, you NEED to define it here or * covered by a capability, you NEED to define it here or
* the call will not be made. * the call will not be made.
* *
* To call a capability function, just prefix it with the name * To call a capability function, just prefix it with the name
* of the capability, for example, refresh would be "refresh.refresh" * of the capability, for example, refresh would be "refresh.refresh"
*/ */
command "preset1" command "preset1"
command "preset2" command "preset2"
command "preset3" command "preset3"
command "preset4" command "preset4"
command "preset5" command "preset5"
command "preset6" command "preset6"
command "aux" command "aux"
command "everywhereJoin"
command "everywhereLeave"
}
/** command "everywhereJoin"
command "everywhereLeave"
}
/**
* Define the various tiles and the states that they can be in. * Define the various tiles and the states that they can be in.
* The 2nd parameter defines an event which the tile listens to, * The 2nd parameter defines an event which the tile listens to,
* if received, it tries to map it to a state. * if received, it tries to map it to a state.
* *
* You can also use ${currentValue} for the value of the event * You can also use ${currentValue} for the value of the event
* or ${name} for the name of the event. Just make SURE to use * or ${name} for the name of the event. Just make SURE to use
* single quotes, otherwise it will only be interpreted at time of * single quotes, otherwise it will only be interpreted at time of
* launch, instead of every time the event triggers. * launch, instead of every time the event triggers.
*/ */
valueTile("nowplaying", "device.nowplaying", width: 2, height: 1, decoration:"flat") { valueTile("nowplaying", "device.nowplaying", width: 2, height: 1, decoration:"flat") {
state "nowplaying", label:'${currentValue}', action:"refresh.refresh" state "nowplaying", label:'${currentValue}', action:"refresh.refresh"
} }
standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) {
@@ -88,18 +88,18 @@ metadata {
valueTile("aux", "device.switch", decoration: "flat", canChangeIcon: false) { valueTile("aux", "device.switch", decoration: "flat", canChangeIcon: false) {
state "default", label:'Auxillary\nInput', action:"aux" state "default", label:'Auxillary\nInput', action:"aux"
} }
standardTile("refresh", "device.nowplaying", decoration: "flat", canChangeIcon: false) { standardTile("refresh", "device.nowplaying", decoration: "flat", canChangeIcon: false) {
state "default", label:'', action:"refresh", icon:"st.secondary.refresh" state "default", label:'', action:"refresh", icon:"st.secondary.refresh"
} }
controlTile("volume", "device.volume", "slider", height:1, width:3, range:"(0..100)") { 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") { standardTile("playpause", "device.playpause", decoration: "flat") {
state "pause", label:'', icon:'st.sonos.play-btn', action:'music Player.play' 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 "play", label:'', icon:'st.sonos.pause-btn', action:'music Player.pause'
} }
standardTile("prev", "device.switch", decoration: "flat", canChangeIcon: false) { standardTile("prev", "device.switch", decoration: "flat", canChangeIcon: false) {
@@ -110,28 +110,28 @@ metadata {
} }
valueTile("everywhere", "device.everywhere", width:2, height:1, decoration:"flat") { valueTile("everywhere", "device.everywhere", width:2, height:1, decoration:"flat") {
state "join", label:"Join\nEverywhere", action:"everywhereJoin" state "join", label:"Join\nEverywhere", action:"everywhereJoin"
state "leave", label:"Leave\nEverywhere", action:"everywhereLeave" state "leave", label:"Leave\nEverywhere", action:"everywhereLeave"
// Final state is used if the device is in a state where joining is not possible // Final state is used if the device is in a state where joining is not possible
state "unavailable", label:"Not Available" state "unavailable", label:"Not Available"
} }
// Defines which tile to show in the overview // Defines which tile to show in the overview
main "switch" main "switch"
// Defines which tile(s) to show when user opens the detailed view // Defines which tile(s) to show when user opens the detailed view
details ([ details ([
"nowplaying", "refresh", // Row 1 (112) "nowplaying", "refresh", // Row 1 (112)
"prev", "playpause", "next", // Row 2 (123) "prev", "playpause", "next", // Row 2 (123)
"volume", // Row 3 (111) "volume", // Row 3 (111)
"1", "2", "3", // Row 4 (123) "1", "2", "3", // Row 4 (123)
"4", "5", "6", // Row 5 (123) "4", "5", "6", // Row 5 (123)
"aux", "everywhere"]) // Row 6 (122) "aux", "everywhere"]) // Row 6 (122)
} }
/************************************************************************** /**************************************************************************
* The following section simply maps the actions as defined in * The following section simply maps the actions as defined in
* the metadata into onAction() calls. * the metadata into onAction() calls.
* *
* This is preferred since some actions can be dealt with more * This is preferred since some actions can be dealt with more
* efficiently this way. Also keeps all user interaction code in * efficiently this way. Also keeps all user interaction code in
@@ -164,7 +164,7 @@ def everywhereLeave() { onAction("eleave") }
/** /**
* Main point of interaction with things. * Main point of interaction with things.
* This function is called by SmartThings Cloud with the resulting data from * This function is called by SmartThings Cloud with the resulting data from
* any action (see HubAction()). * any action (see HubAction()).
* *
* Conversely, to execute any actions, you need to return them as a single * Conversely, to execute any actions, you need to return them as a single
* item or a list (flattened). * item or a list (flattened).
@@ -176,38 +176,38 @@ def everywhereLeave() { onAction("eleave") }
def parse(String event) { def parse(String event) {
def data = parseLanMessage(event) def data = parseLanMessage(event)
def actions = [] def actions = []
// List of permanent root node handlers // List of permanent root node handlers
def handlers = [ def handlers = [
"nowPlaying" : "boseParseNowPlaying", "nowPlaying" : "boseParseNowPlaying",
"volume" : "boseParseVolume", "volume" : "boseParseVolume",
"presets" : "boseParsePresets", "presets" : "boseParsePresets",
"zone" : "boseParseEverywhere", "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")) 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() prepareCallbacks()
def xml = new XmlSlurper().parseText(data.body) def xml = new XmlSlurper().parseText(data.body)
// Let each parser take a stab at it // Let each parser take a stab at it
handlers.each { node,func -> handlers.each { node,func ->
if (xml.name() == node) if (xml.name() == node)
actions << "$func"(xml) actions << "$func"(xml)
} }
// If we have callbacks waiting for this... // If we have callbacks waiting for this...
actions << processCallbacks(xml) actions << processCallbacks(xml)
// Be nice and helpful
if (actions.size() == 0) {
log.warn "parse(): Unhandled data = " + lan
return null
}
// Issue new actions // Be nice and helpful
if (actions.size() == 0) {
log.warn "parse(): Unhandled data = " + lan
return null
}
// Issue new actions
return actions.flatten() return actions.flatten()
} }
@@ -231,21 +231,21 @@ def installed() {
def onAction(String user, data=null) { def onAction(String user, data=null) {
log.info "onAction(${user})" 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) state.address = parent.resolveDNI2Address(device.deviceNetworkId)
// Process action // Process action
def actions = null def actions = null
switch (user) { switch (user) {
case "on": case "on":
actions = boseSetPowerState(true) actions = boseSetPowerState(true)
break break
case "off": case "off":
boseSetNowPlaying(null, "STANDBY") boseSetNowPlaying(null, "STANDBY")
actions = boseSetPowerState(false) actions = boseSetPowerState(false)
break break
case "volume": case "volume":
actions = boseSetVolume(data) actions = boseSetVolume(data)
break break
case "aux": case "aux":
boseSetNowPlaying(null, "AUX") boseSetNowPlaying(null, "AUX")
@@ -257,43 +257,43 @@ def onAction(String user, data=null) {
case "4": case "4":
case "5": case "5":
case "6": case "6":
actions = boseSetInput(user) actions = boseSetInput(user)
break break
case "refresh": case "refresh":
boseSetNowPlaying(null, "REFRESH") boseSetNowPlaying(null, "REFRESH")
actions = [boseRefreshNowPlaying(), boseGetPresets(), boseGetVolume(), boseGetEverywhereState()] actions = [boseRefreshNowPlaying(), boseGetPresets(), boseGetVolume(), boseGetEverywhereState()]
break break
case "play": case "play":
actions = [boseSetPlayMode(true), boseRefreshNowPlaying()] actions = [boseSetPlayMode(true), boseRefreshNowPlaying()]
break break
case "pause": case "pause":
actions = [boseSetPlayMode(false), boseRefreshNowPlaying()] actions = [boseSetPlayMode(false), boseRefreshNowPlaying()]
break break
case "previous": case "previous":
actions = [boseChangeTrack(-1), boseRefreshNowPlaying()] actions = [boseChangeTrack(-1), boseRefreshNowPlaying()]
break break
case "next": case "next":
actions = [boseChangeTrack(1), boseRefreshNowPlaying()] actions = [boseChangeTrack(1), boseRefreshNowPlaying()]
break break
case "mute": case "mute":
actions = boseSetMute(true) actions = boseSetMute(true)
break break
case "unmute": case "unmute":
actions = boseSetMute(false) actions = boseSetMute(false)
break break
case "ejoin": case "ejoin":
actions = boseZoneJoin() actions = boseZoneJoin()
break break
case "eleave": case "eleave":
actions = boseZoneLeave() actions = boseZoneLeave()
break break
default: default:
log.error "Unhandled action: " + user log.error "Unhandled action: " + user
} }
// Make sure we don't have nested lists // Make sure we don't have nested lists
if (actions instanceof List) if (actions instanceof List)
return actions.flatten() return actions.flatten()
return actions return actions
} }
@@ -309,16 +309,16 @@ def poll() {
* Joins this speaker into the everywhere zone * Joins this speaker into the everywhere zone
*/ */
def boseZoneJoin() { def boseZoneJoin() {
def results = [] def results = []
def posts = parent.boseZoneJoin(this) def posts = parent.boseZoneJoin(this)
for (post in posts) { for (post in posts) {
if (post['endpoint']) if (post['endpoint'])
results << bosePOST(post['endpoint'], post['body'], post['host']) results << bosePOST(post['endpoint'], post['body'], post['host'])
} }
sendEvent(name:"everywhere", value:"leave") sendEvent(name:"everywhere", value:"leave")
results << boseRefreshNowPlaying() results << boseRefreshNowPlaying()
return results return results
} }
@@ -326,15 +326,15 @@ def boseZoneJoin() {
* Removes this speaker from the everywhere zone * Removes this speaker from the everywhere zone
*/ */
def boseZoneLeave() { def boseZoneLeave() {
def results = [] def results = []
def posts = parent.boseZoneLeave(this) def posts = parent.boseZoneLeave(this)
for (post in posts) { for (post in posts) {
if (post['endpoint']) if (post['endpoint'])
results << bosePOST(post['endpoint'], post['body'], post['host']) results << bosePOST(post['endpoint'], post['body'], post['host'])
} }
sendEvent(name:"everywhere", value:"join") sendEvent(name:"everywhere", value:"join")
results << boseRefreshNowPlaying() results << boseRefreshNowPlaying()
return results return results
} }
@@ -346,7 +346,7 @@ def boseZoneLeave() {
* cause the zone to collapse (for example, AUX) * cause the zone to collapse (for example, AUX)
*/ */
def boseZoneReset() { def boseZoneReset() {
parent.boseZoneReset() parent.boseZoneReset()
} }
/** /**
@@ -359,13 +359,13 @@ def boseZoneReset() {
* @return command * @return command
*/ */
def boseParseNowPlaying(xmlData) { 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)) { if (boseSetNowPlaying(xmlData)) {
result << boseRefreshNowPlaying() result << boseRefreshNowPlaying()
} }
return result return result
} }
@@ -376,11 +376,11 @@ def boseParseNowPlaying(xmlData) {
* @return command * @return command
*/ */
def boseParseVolume(xmlData) { def boseParseVolume(xmlData) {
def result = [] def result = []
sendEvent(name:"volume", value:xmlData.actualvolume.text()) sendEvent(name:"volume", value:xmlData.actualvolume.text())
sendEvent(name:"mute", value:(Boolean.toBoolean(xmlData.muteenabled.text()) ? "unmuted" : "muted")) sendEvent(name:"mute", value:(Boolean.toBoolean(xmlData.muteenabled.text()) ? "unmuted" : "muted"))
return result return result
} }
@@ -390,7 +390,7 @@ def boseParseVolume(xmlData) {
* @param xmlData * @param xmlData
*/ */
def boseParseEverywhere(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,28 +400,28 @@ def boseParseEverywhere(xmlData) {
* @return command * @return command
*/ */
def boseParsePresets(xmlData) { def boseParsePresets(xmlData) {
def result = [] def result = []
state.preset = [:] state.preset = [:]
def missing = ["1", "2", "3", "4", "5", "6"] def missing = ["1", "2", "3", "4", "5", "6"]
for (preset in xmlData.preset) { for (preset in xmlData.preset) {
def id = preset.attributes()['id'] def id = preset.attributes()['id']
def name = preset.ContentItem.itemName[0].text().replaceAll(~/ +/, "\n") def name = preset.ContentItem.itemName[0].text().replaceAll(~/ +/, "\n")
if (name == "##TRANS_SONGS##") if (name == "##TRANS_SONGS##")
name = "Local\nPlaylist" name = "Local\nPlaylist"
sendEvent(name:"station${id}", value:name) sendEvent(name:"station${id}", value:name)
missing = missing.findAll { it -> it != id } missing = missing.findAll { it -> it != id }
// Store the presets into the state for recall later // Store the presets into the state for recall later
state.preset["$id"] = XmlUtil.serialize(preset.ContentItem) state.preset["$id"] = XmlUtil.serialize(preset.ContentItem)
} }
for (id in missing) { for (id in missing) {
state.preset["$id"] = null state.preset["$id"] = null
sendEvent(name:"station${id}", value:"Preset $id\n\nNot set") sendEvent(name:"station${id}", value:"Preset $id\n\nNot set")
} }
return result return result
} }
@@ -435,25 +435,25 @@ def boseParsePresets(xmlData) {
* @return true if it would prefer a refresh soon * @return true if it would prefer a refresh soon
*/ */
def boseSetNowPlaying(xmlData, override=null) { def boseSetNowPlaying(xmlData, override=null) {
def needrefresh = false def needrefresh = false
def nowplaying = null def nowplaying = null
if (xmlData && xmlData.playStatus) { if (xmlData && xmlData.playStatus) {
switch(xmlData.playStatus) { switch(xmlData.playStatus) {
case "BUFFERING_STATE": case "BUFFERING_STATE":
nowplaying = "Please wait\nBuffering..." nowplaying = "Please wait\nBuffering..."
needrefresh = true needrefresh = true
break break
case "PLAY_STATE": case "PLAY_STATE":
sendEvent(name:"playpause", value:"play") sendEvent(name:"playpause", value:"play")
break break
case "PAUSE_STATE": case "PAUSE_STATE":
case "STOP_STATE": case "STOP_STATE":
sendEvent(name:"playpause", value:"pause") sendEvent(name:"playpause", value:"pause")
break break
} }
} }
// If the previous section didn't handle this, take another stab at it // If the previous section didn't handle this, take another stab at it
if (!nowplaying) { if (!nowplaying) {
nowplaying = "" nowplaying = ""
@@ -480,21 +480,21 @@ def boseSetNowPlaying(xmlData, override=null) {
if (xmlData.ContentItem.itemName[0]) if (xmlData.ContentItem.itemName[0])
nowplaying += "[${xmlData.ContentItem.itemName[0].text()}]\n\n" nowplaying += "[${xmlData.ContentItem.itemName[0].text()}]\n\n"
case "STORED_MUSIC": case "STORED_MUSIC":
nowplaying += "${xmlData.track.text()}" nowplaying += "${xmlData.track.text()}"
if (xmlData.artist) if (xmlData.artist)
nowplaying += "\nby\n${xmlData.artist.text()}" nowplaying += "\nby\n${xmlData.artist.text()}"
if (xmlData.album) if (xmlData.album)
nowplaying += "\n\n(${xmlData.album.text()})" nowplaying += "\n\n(${xmlData.album.text()})"
break break
default: default:
if (xmlData != null) if (xmlData != null)
nowplaying = "${xmlData.ContentItem.itemName[0].text()}" nowplaying = "${xmlData.ContentItem.itemName[0].text()}"
else else
nowplaying = "Unknown" 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) {
if (xmlData.attributes()['source'] == "STANDBY") { if (xmlData.attributes()['source'] == "STANDBY") {
log.trace "nowPlaying reports standby: " + XmlUtil.serialize(xmlData) log.trace "nowPlaying reports standby: " + XmlUtil.serialize(xmlData)
@@ -502,22 +502,22 @@ def boseSetNowPlaying(xmlData, override=null) {
} else { } else {
sendEvent(name:"switch", value:"on") sendEvent(name:"switch", value:"on")
} }
boseSetPlayerAttributes(xmlData) boseSetPlayerAttributes(xmlData)
} }
// Do not allow a standby device or AUX to be master // Do not allow a standby device or AUX to be master
if (!parent.boseZoneHasMaster() && (override ? override : xmlData.attributes()['source']) == "STANDBY") 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") else if ((override ? override : xmlData.attributes()['source']) == "AUX")
sendEvent(name:"everywhere", value:"unavailable") sendEvent(name:"everywhere", value:"unavailable")
else if (boseGetZone()) { else if (boseGetZone()) {
log.info "We're in the zone: " + boseGetZone() log.info "We're in the zone: " + boseGetZone()
sendEvent(name:"everywhere", value:"leave") sendEvent(name:"everywhere", value:"leave")
} else } else
sendEvent(name:"everywhere", value:"join") sendEvent(name:"everywhere", value:"join")
sendEvent(name:"nowplaying", value:nowplaying)
sendEvent(name:"nowplaying", value:nowplaying)
return needrefresh return needrefresh
} }
@@ -531,18 +531,18 @@ def boseSetPlayerAttributes(xmlData) {
def trackText = "" def trackText = ""
def trackDesc = "" def trackDesc = ""
def trackData = [:] def trackData = [:]
switch (xmlData.attributes()['source']) { switch (xmlData.attributes()['source']) {
case "STANDBY": case "STANDBY":
trackData["station"] = trackText = trackDesc = "Standby" trackData["station"] = trackText = trackDesc = "Standby"
break break
case "AUX": case "AUX":
trackData["station"] = trackText = trackDesc = "Auxiliary Input" trackData["station"] = trackText = trackDesc = "Auxiliary Input"
break break
case "AIRPLAY": case "AIRPLAY":
trackData["station"] = trackText = trackDesc = "Air Play" trackData["station"] = trackText = trackDesc = "Air Play"
break break
case "SPOTIFY": case "SPOTIFY":
case "DEEZER": case "DEEZER":
case "PANDORA": case "PANDORA":
case "IHEART": case "IHEART":
@@ -550,25 +550,25 @@ def boseSetPlayerAttributes(xmlData) {
trackText = trackDesc = "${xmlData.track.text()}" trackText = trackDesc = "${xmlData.track.text()}"
trackData["name"] = xmlData.track.text() trackData["name"] = xmlData.track.text()
if (xmlData.artist) { if (xmlData.artist) {
trackText += " by ${xmlData.artist.text()}" trackText += " by ${xmlData.artist.text()}"
trackDesc += " - ${xmlData.artist.text()}" trackDesc += " - ${xmlData.artist.text()}"
trackData["artist"] = xmlData.artist.text() trackData["artist"] = xmlData.artist.text()
} }
if (xmlData.album) { if (xmlData.album) {
trackText += " (${xmlData.album.text()})" trackText += " (${xmlData.album.text()})"
trackData["album"] = xmlData.album.text() trackData["album"] = xmlData.album.text()
} }
break break
case "INTERNET_RADIO": case "INTERNET_RADIO":
trackDesc = xmlData.stationName.text() trackDesc = xmlData.stationName.text()
trackText = xmlData.stationName.text() + ": " + xmlData.description.text() trackText = xmlData.stationName.text() + ": " + xmlData.description.text()
trackData["station"] = xmlData.stationName.text() trackData["station"] = xmlData.stationName.text()
break break
default: default:
trackText = trackDesc = xmlData.ContentItem.itemName[0].text() 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 * @return command
*/ */
def boseGetEverywhereState() { def boseGetEverywhereState() {
return boseGET("/getZone") return boseGET("/getZone")
} }
/** /**
@@ -591,9 +591,9 @@ def boseGetEverywhereState() {
* the second key info. * the second key info.
*/ */
def boseKeypress(key) { 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>" def release = "<key state=\"release\" sender=\"Gabbo\">${key}</key>"
return [bosePOST("/key", press), bosePOST("/key", release)] return [bosePOST("/key", press), bosePOST("/key", release)]
} }
@@ -605,23 +605,23 @@ def boseKeypress(key) {
* @return command * @return command
*/ */
def boseSetPlayMode(boolean play) { def boseSetPlayMode(boolean play) {
log.trace "Sending " + (play ? "PLAY" : "PAUSE") log.trace "Sending " + (play ? "PLAY" : "PAUSE")
return boseKeypress(play ? "PLAY" : "PAUSE") return boseKeypress(play ? "PLAY" : "PAUSE")
} }
/** /**
* Sets the volume in a deterministic way. * Sets the volume in a deterministic way.
* *
* @param New volume level, ranging from 0 to 100 * @param New volume level, ranging from 0 to 100
* *
* @return command * @return command
*/ */
def boseSetVolume(int level) { def boseSetVolume(int level) {
def result = [] def result = []
int vol = Math.min(100, Math.max(level, 0)) 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()] return [bosePOST("/volume", "<volume>${vol}</volume>"), boseGetVolume()]
} }
@@ -633,28 +633,28 @@ def boseSetVolume(int level) {
* @return command * @return command
*/ */
def boseSetMute(boolean mute) { def boseSetMute(boolean mute) {
queueCallback('volume', 'cb_boseSetMute', mute ? 'MUTE' : 'UNMUTE') queueCallback('volume', 'cb_boseSetMute', mute ? 'MUTE' : 'UNMUTE')
return boseGetVolume() return boseGetVolume()
} }
/** /**
* Callback for boseSetMute(), checks current state and changes it * Callback for boseSetMute(), checks current state and changes it
* if it doesn't match the requested state. * if it doesn't match the requested state.
* *
* @param xml The volume XML data * @param xml The volume XML data
* @param mute The new state of mute * @param mute The new state of mute
* *
* @return command * @return command
*/ */
def cb_boseSetMute(xml, mute) { def cb_boseSetMute(xml, mute) {
def result = [] def result = []
if ((xml.muteenabled.text() == 'false' && mute == 'MUTE') || if ((xml.muteenabled.text() == 'false' && mute == 'MUTE') ||
(xml.muteenabled.text() == 'true' && mute == 'UNMUTE')) (xml.muteenabled.text() == 'true' && mute == 'UNMUTE'))
{ {
result << boseKeypress("MUTE") result << boseKeypress("MUTE")
} }
log.trace("muteunmute: " + ((mute == "MUTE") ? "unmute" : "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 return result
} }
@@ -664,7 +664,7 @@ def cb_boseSetMute(xml, mute) {
* @return command * @return command
*/ */
def boseGetVolume() { def boseGetVolume() {
return boseGET("/volume") return boseGET("/volume")
} }
/** /**
@@ -674,10 +674,10 @@ def boseGetVolume() {
* @return command * @return command
*/ */
def boseChangeTrack(int direction) { def boseChangeTrack(int direction) {
if (direction < 0) { if (direction < 0) {
return boseKeypress("PREV_TRACK") return boseKeypress("PREV_TRACK")
} else if (direction > 0) { } else if (direction > 0) {
return boseKeypress("NEXT_TRACK") return boseKeypress("NEXT_TRACK")
} }
return [] return []
} }
@@ -692,14 +692,14 @@ def boseChangeTrack(int direction) {
* @note If no presets have been loaded, it will first refresh the presets. * @note If no presets have been loaded, it will first refresh the presets.
*/ */
def boseSetInput(input) { def boseSetInput(input) {
log.info "boseSetInput(${input})" log.info "boseSetInput(${input})"
def result = [] def result = []
if (!state.preset) { if (!state.preset) {
result << boseGetPresets() result << boseGetPresets()
queueCallback('presets', 'cb_boseSetInput', input) queueCallback('presets', 'cb_boseSetInput', input)
} else { } else {
result << cb_boseSetInput(null, input) result << cb_boseSetInput(null, input)
} }
return result return result
} }
@@ -720,10 +720,10 @@ def boseSetInput(input) {
* the preset if there is a long delay between the two. * the preset if there is a long delay between the two.
*/ */
def cb_boseSetInput(xml, input) { def cb_boseSetInput(xml, input) {
def result = [] def result = []
if (input >= "1" && input <= "6" && state.preset["$input"]) 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") { else if (input.toLowerCase() == "aux") {
result << boseKeypress("AUX_INPUT") result << boseKeypress("AUX_INPUT")
} }
@@ -731,7 +731,7 @@ def cb_boseSetInput(xml, input) {
// Horrible workaround... but we need to delay // Horrible workaround... but we need to delay
// the update by at least a few seconds... // the update by at least a few seconds...
result << boseRefreshNowPlaying(3000) result << boseRefreshNowPlaying(3000)
return result return result
} }
/** /**
@@ -746,9 +746,9 @@ def cb_boseSetInput(xml, input) {
* is no discreete call. * is no discreete call.
*/ */
def boseSetPowerState(boolean enable) { def boseSetPowerState(boolean enable) {
log.info "boseSetPowerState(${enable})" log.info "boseSetPowerState(${enable})"
queueCallback('nowPlaying', "cb_boseSetPowerState", enable ? "POWERON" : "POWEROFF") queueCallback('nowPlaying', "cb_boseSetPowerState", enable ? "POWERON" : "POWEROFF")
return boseRefreshNowPlaying() return boseRefreshNowPlaying()
} }
/** /**
@@ -761,13 +761,13 @@ def boseSetPowerState(boolean enable) {
* @return command * @return command
*/ */
def cb_boseSetPowerState(xml, state) { def cb_boseSetPowerState(xml, state) {
def result = [] def result = []
if ( (xml.attributes()['source'] == "STANDBY" && state == "POWERON") || if ( (xml.attributes()['source'] == "STANDBY" && state == "POWERON") ||
(xml.attributes()['source'] != "STANDBY" && state == "POWEROFF") ) (xml.attributes()['source'] != "STANDBY" && state == "POWEROFF") )
{ {
result << boseKeypress("POWER") result << boseKeypress("POWER")
if (state == "POWERON") { if (state == "POWERON") {
result << boseRefreshNowPlaying() result << boseRefreshNowPlaying()
queueCallback('nowPlaying', "cb_boseConfirmPowerOn", 5) queueCallback('nowPlaying', "cb_boseConfirmPowerOn", 5)
} }
} }
@@ -786,9 +786,9 @@ def cb_boseSetPowerState(xml, state) {
* @return command * @return command
*/ */
def cb_boseConfirmPowerOn(xml, tries) { def cb_boseConfirmPowerOn(xml, tries) {
def result = [] def result = []
log.warn "boseConfirmPowerOn() attempt #" + tries log.warn "boseConfirmPowerOn() attempt #" + tries
if (xml.attributes()['source'] == "STANDBY" && tries > 0) { if (xml.attributes()['source'] == "STANDBY" && tries > 0) {
result << boseRefreshNowPlaying() result << boseRefreshNowPlaying()
queueCallback('nowPlaying', "cb_boseConfirmPowerOn", tries-1) queueCallback('nowPlaying', "cb_boseConfirmPowerOn", tries-1)
} }
@@ -803,19 +803,19 @@ def cb_boseConfirmPowerOn(xml, tries) {
* @return command * @return command
*/ */
def boseRefreshNowPlaying(delay=0) { def boseRefreshNowPlaying(delay=0) {
if (delay > 0) { if (delay > 0) {
return ["delay ${delay}", boseGET("/now_playing")] return ["delay ${delay}", boseGET("/now_playing")]
} }
return boseGET("/now_playing") return boseGET("/now_playing")
} }
/** /**
* Requests the list of presets * Requests the list of presets
* *
* @return command * @return command
*/ */
def boseGetPresets() { 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) * @param param Parameters for function (optional)
*/ */
def queueCallback(String root, String func, param=null) { def queueCallback(String root, String func, param=null) {
if (!state.pending) if (!state.pending)
state.pending = [:] state.pending = [:]
if (!state.pending[root]) if (!state.pending[root])
state.pending[root] = [] state.pending[root] = []
state.pending[root] << ["$func":"$param"] state.pending[root] << ["$func":"$param"]
} }
@@ -879,16 +879,16 @@ def queueCallback(String root, String func, param=null) {
* the same loop. * the same loop.
*/ */
def prepareCallbacks() { def prepareCallbacks() {
if (!state.pending) if (!state.pending)
return return
if (!state.ready) if (!state.ready)
state.ready = [:] state.ready = [:]
state.ready << state.pending state.ready << state.pending
state.pending = [:] state.pending = [:]
} }
/** /**
* Executes any ready callback for a specific root node * Executes any ready callback for a specific root node
* with associated parameter and then clears that entry. * with associated parameter and then clears that entry.
* *
* If a callback returns data, it's added to a list of * If a callback returns data, it's added to a list of
@@ -901,14 +901,14 @@ def prepareCallbacks() {
* @return list of commands * @return list of commands
*/ */
def processCallbacks(xml) { def processCallbacks(xml) {
def result = [] def result = []
if (!state.ready) if (!state.ready)
return result return result
if (state.ready[xml.name()]) { if (state.ready[xml.name()]) {
state.ready[xml.name()].each { callback -> state.ready[xml.name()].each { callback ->
callback.each { func, param -> callback.each { func, param ->
if (func != "func") { if (func != "func") {
if (param) if (param)
result << "$func"(xml, param) result << "$func"(xml, param)
@@ -923,25 +923,25 @@ def processCallbacks(xml) {
} }
/** /**
* State managament for the Play Everywhere zone. * State managament for the Play Everywhere zone.
* This is typically called from the parent. * This is typically called from the parent.
* *
* A device is either: * A device is either:
* *
* null = Not participating * null = Not participating
* server = running the show * server = running the show
* client = under the control of the server * client = under the control of the server
* *
* @param newstate (see above for types) * @param newstate (see above for types)
*/ */
def boseSetZone(String newstate) { def boseSetZone(String newstate) {
log.debug "boseSetZone($newstate)" log.debug "boseSetZone($newstate)"
state.zone = newstate state.zone = newstate
// Refresh our state // Refresh our state
if (newstate) { if (newstate) {
sendEvent(name:"everywhere", value:"leave") sendEvent(name:"everywhere", value:"leave")
} else { } else {
sendEvent(name:"everywhere", value:"join") sendEvent(name:"everywhere", value:"join")
} }
} }
@@ -954,7 +954,7 @@ def boseSetZone(String newstate) {
* @return state * @return state
*/ */
def boseGetZone() { def boseGetZone() {
return state.zone return state.zone
} }
/** /**
@@ -967,7 +967,7 @@ def boseGetZone() {
* @param devID The DeviceID * @param devID The DeviceID
*/ */
def boseSetDeviceID(String devID) { def boseSetDeviceID(String devID) {
state.deviceID = devID state.deviceID = devID
} }
/** /**
@@ -976,7 +976,7 @@ def boseSetDeviceID(String devID) {
* @return deviceID * @return deviceID
*/ */
def boseGetDeviceID() { def boseGetDeviceID() {
return state.deviceID return state.deviceID
} }
/** /**
@@ -985,5 +985,5 @@ def boseGetDeviceID() {
* @return IP address * @return IP address
*/ */
def getDeviceIP() { def getDeviceIP() {
return parent.resolveDNI2Address(device.deviceNetworkId) return parent.resolveDNI2Address(device.deviceNetworkId)
} }

View File

@@ -1,7 +1,7 @@
/** /**
* Bose SoundTouch (Connect) * 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 * 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: * in compliance with the License. You may obtain a copy of the License at:
@@ -16,7 +16,7 @@
definition( definition(
name: "Bose SoundTouch (Connect)", name: "Bose SoundTouch (Connect)",
namespace: "smartthings", namespace: "smartthings",
author: "Henric.Andersson@smartthings.com", author: "SmartThings",
description: "Control your Bose SoundTouch speakers", description: "Control your Bose SoundTouch speakers",
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
@@ -25,7 +25,7 @@
) )
preferences { 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 * @todo This + getUSNQualifier should be one and should use regular expressions
*/ */
def getDeviceType() { 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 * @return Additional qualifier OR null if not needed
*/ */
def getUSNQualifier() { def getUSNQualifier() {
return "uuid:BO5EBO5E-F00D-F00D-FEED-" return "uuid:BO5EBO5E-F00D-F00D-FEED-"
} }
/** /**
@@ -56,7 +56,7 @@ def getUSNQualifier() {
* @return name * @return name
*/ */
def getDeviceName() { def getDeviceName() {
return "Bose SoundTouch" return "Bose SoundTouch"
} }
/** /**
@@ -65,7 +65,7 @@ def getDeviceName() {
* @return namespace * @return namespace
*/ */
def getNameSpace() { def getNameSpace() {
return "smartthings" return "smartthings"
} }
/** /**
@@ -77,48 +77,48 @@ def getNameSpace() {
*/ */
def deviceDiscovery() def deviceDiscovery()
{ {
if(canInstallLabs()) if(canInstallLabs())
{ {
def refreshInterval = 3 // Number of seconds between refresh def refreshInterval = 3 // Number of seconds between refresh
int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
state.deviceRefreshCount = deviceRefreshCount + refreshInterval 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) // Make sure we get location updates (contains LAN data such as SSDP results, etc)
subscribeNetworkEvents() subscribeNetworkEvents()
//device discovery request every 15s //device discovery request every 15s
if((deviceRefreshCount % 15) == 0) { if((deviceRefreshCount % 15) == 0) {
discoverDevices() discoverDevices()
} }
// Verify request every 3 seconds except on discoveries // Verify request every 3 seconds except on discoveries
if(((deviceRefreshCount % 3) == 0) && ((deviceRefreshCount % 15) != 0)) { if(((deviceRefreshCount % 3) == 0) && ((deviceRefreshCount % 15) != 0)) {
verifyDevices() 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) { 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.") { 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 input "selecteddevice", "enum", required:false, title:"Select ${getDeviceName()} (${numFound} found)", multiple:true, options:devices
} }
} }
} }
else else
{ {
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. 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".""" 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) { return dynamicPage(name:"deviceDiscovery", title:"Upgrade needed!", nextPage:"", install:true, uninstall: true) {
section("Upgrade") { section("Upgrade") {
paragraph "$upgradeNeeded" paragraph "$upgradeNeeded"
} }
} }
} }
} }
/** /**
@@ -126,17 +126,17 @@ To update your Hub, access Location Settings in the Main Menu (tap the gear next
* pressed "Install". * pressed "Install".
*/ */
def installed() { def installed() {
log.trace "Installed with settings: ${settings}" log.trace "Installed with settings: ${settings}"
initialize() initialize()
} }
/** /**
* Called by SmartThings Cloud when app has been updated * Called by SmartThings Cloud when app has been updated
*/ */
def updated() { def updated() {
log.trace "Updated with settings: ${settings}" log.trace "Updated with settings: ${settings}"
unsubscribe() unsubscribe()
initialize() initialize()
} }
/** /**
@@ -157,13 +157,13 @@ def uninstalled() {
* for changes (new address, port, etc...) * for changes (new address, port, etc...)
*/ */
def initialize() { def initialize() {
log.trace "initialize()" log.trace "initialize()"
state.subscribe = false state.subscribe = false
if (selecteddevice) { if (selecteddevice) {
addDevice() addDevice()
refreshDevices() refreshDevices()
subscribeNetworkEvents(true) subscribeNetworkEvents(true)
} }
} }
/** /**
@@ -172,33 +172,33 @@ def initialize() {
* Uses selecteddevice defined in the deviceDiscovery() page * Uses selecteddevice defined in the deviceDiscovery() page
*/ */
def addDevice(){ def addDevice(){
def devices = getVerifiedDevices() def devices = getVerifiedDevices()
def devlist def devlist
log.trace "Adding childs" 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)) { if (!(selecteddevice instanceof List)) {
devlist = [selecteddevice] devlist = [selecteddevice]
} else { } else {
devlist = selecteddevice devlist = selecteddevice
} }
log.trace "These are being installed: ${devlist}" log.trace "These are being installed: ${devlist}"
devlist.each { dni -> devlist.each { dni ->
def d = getChildDevice(dni) def d = getChildDevice(dni)
if(!d) { if(!d) {
def newDevice = devices.find { (it.value.mac) == dni } def newDevice = devices.find { (it.value.mac) == dni }
def deviceName = newDevice?.value.name def deviceName = newDevice?.value.name
if (!deviceName) if (!deviceName)
deviceName = getDeviceName() + "[${newDevice?.value.name}]" 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) d.boseSetDeviceID(newDevice.value.deviceID)
log.trace "Created ${d.displayName} with id $dni" log.trace "Created ${d.displayName} with id $dni"
} else { } else {
log.trace "${d.displayName} with id $dni already exists" log.trace "${d.displayName} with id $dni already exists"
} }
} }
} }
/** /**
@@ -208,9 +208,9 @@ def addDevice(){
* @return address or null * @return address or null
*/ */
def resolveDNI2Address(dni) { def resolveDNI2Address(dni) {
def device = getVerifiedDevices().find { (it.value.mac) == dni } def device = getVerifiedDevices().find { (it.value.mac) == dni }
if (device) { if (device) {
return convertHexToIP(device.value.networkAddress) return convertHexToIP(device.value.networkAddress)
} }
return null return null
} }
@@ -219,33 +219,33 @@ def resolveDNI2Address(dni) {
* Joins a child to the "Play Everywhere" zone * Joins a child to the "Play Everywhere" zone
* *
* @param child The speaker joining the zone * @param child The speaker joining the zone
* @return A list of maps with POST data * @return A list of maps with POST data
*/ */
def boseZoneJoin(child) { def boseZoneJoin(child) {
log = child.log // So we can debug this function log = child.log // So we can debug this function
def results = [] def results = []
def result = [:] def result = [:]
// Find the master (if any) // Find the master (if any)
def server = getChildDevices().find{ it.boseGetZone() == "server" } def server = getChildDevices().find{ it.boseGetZone() == "server" }
if (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") child.boseSetZone("client")
result['endpoint'] = "/setZone" result['endpoint'] = "/setZone"
result['host'] = server.getDeviceIP() + ":8090" result['host'] = server.getDeviceIP() + ":8090"
result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">" result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">"
getChildDevices().each{ it -> getChildDevices().each{ it ->
log.trace "child: " + child log.trace "child: " + child
log.trace "zone : " + it.boseGetZone() log.trace "zone : " + it.boseGetZone()
if (it.boseGetZone() || it.boseGetDeviceID() == child.boseGetDeviceID()) if (it.boseGetZone() || it.boseGetDeviceID() == child.boseGetDeviceID())
result['body'] = result['body'] + "<member ipaddress=\"${it.getDeviceIP()}\">${it.boseGetDeviceID()}</member>" result['body'] = result['body'] + "<member ipaddress=\"${it.getDeviceIP()}\">${it.boseGetDeviceID()}</member>"
} }
result['body'] = result['body'] + '</zone>' result['body'] = result['body'] + '</zone>'
} else { } else {
log.debug "boseJoinZone() No server, add it!" log.debug "boseJoinZone() No server, add it!"
result['endpoint'] = "/setZone" result['endpoint'] = "/setZone"
result['host'] = child.getDeviceIP() + ":8090" result['host'] = child.getDeviceIP() + ":8090"
result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">" result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">"
@@ -258,11 +258,11 @@ def boseZoneJoin(child) {
} }
def boseZoneReset() { def boseZoneReset() {
getChildDevices().each{ it.boseSetZone(null) } getChildDevices().each{ it.boseSetZone(null) }
} }
def boseZoneHasMaster() { def boseZoneHasMaster() {
return getChildDevices().find{ it.boseGetZone() == "server" } != null return getChildDevices().find{ it.boseGetZone() == "server" } != null
} }
/** /**
@@ -272,19 +272,19 @@ def boseZoneHasMaster() {
* @return a list of maps with POST data * @return a list of maps with POST data
*/ */
def boseZoneLeave(child) { def boseZoneLeave(child) {
log = child.log // So we can debug this function log = child.log // So we can debug this function
def results = [] def results = []
def result = [:] def result = [:]
// First, tag us as a non-member // First, tag us as a non-member
child.boseSetZone(null) child.boseSetZone(null)
// Find the master (if any) // Find the master (if any)
def server = getChildDevices().find{ it.boseGetZone() == "server" } def server = getChildDevices().find{ it.boseGetZone() == "server" }
if (server && server.boseGetDeviceID() != child.boseGetDeviceID()) { 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['endpoint'] = "/removeZoneSlave"
result['host'] = server.getDeviceIP() + ":8090" result['host'] = server.getDeviceIP() + ":8090"
result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">" result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">"
@@ -292,28 +292,28 @@ def boseZoneLeave(child) {
result['body'] = result['body'] + '</zone>' result['body'] = result['body'] + '</zone>'
results << result results << result
} else { } 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 // Dismantle the entire thing, first send this to master
result['endpoint'] = "/removeZoneSlave" result['endpoint'] = "/removeZoneSlave"
result['host'] = child.getDeviceIP() + ":8090" result['host'] = child.getDeviceIP() + ":8090"
result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">" result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">"
getChildDevices().each{ dev -> getChildDevices().each{ dev ->
if (dev.boseGetZone() || dev.boseGetDeviceID() == child.boseGetDeviceID()) if (dev.boseGetZone() || dev.boseGetDeviceID() == child.boseGetDeviceID())
result['body'] = result['body'] + "<member ipaddress=\"${dev.getDeviceIP()}\">${dev.boseGetDeviceID()}</member>" result['body'] = result['body'] + "<member ipaddress=\"${dev.getDeviceIP()}\">${dev.boseGetDeviceID()}</member>"
} }
result['body'] = result['body'] + '</zone>' result['body'] = result['body'] + '</zone>'
results << result results << result
// Also issue this to each individual client // Also issue this to each individual client
getChildDevices().each{ dev -> getChildDevices().each{ dev ->
if (dev.boseGetZone() && dev.boseGetDeviceID() != child.boseGetDeviceID()) { if (dev.boseGetZone() && dev.boseGetDeviceID() != child.boseGetDeviceID()) {
log.trace "Additional device: " + dev log.trace "Additional device: " + dev
result['host'] = dev.getDeviceIP() + ":8090" result['host'] = dev.getDeviceIP() + ":8090"
results << result results << result
} }
} }
} }
return results return results
} }
@@ -323,10 +323,10 @@ def boseZoneLeave(child) {
* @return mapping of root-node <-> parser function * @return mapping of root-node <-> parser function
*/ */
def getParsers() { def getParsers() {
[ [
"root" : "parseDESC", "root" : "parseDESC",
"info" : "parseINFO" "info" : "parseINFO"
] ]
} }
/** /**
@@ -337,27 +337,27 @@ def getParsers() {
* @param evt Holds event information * @param evt Holds event information
*/ */
def onLocation(evt) { 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) def lanEvent = parseLanMessage(evt.description, true)
lanEvent << ["hub":evt?.hubId] lanEvent << ["hub":evt?.hubId]
// Determine what we need to do... // Determine what we need to do...
if (lanEvent?.ssdpTerm?.contains(getDeviceType()) && if (lanEvent?.ssdpTerm?.contains(getDeviceType()) &&
(getUSNQualifier() == null || (getUSNQualifier() == null ||
lanEvent?.ssdpUSN?.contains(getUSNQualifier()) lanEvent?.ssdpUSN?.contains(getUSNQualifier())
) )
) )
{ {
parseSSDP(lanEvent) parseSSDP(lanEvent)
} }
else if ( else if (
lanEvent.headers && lanEvent.body && lanEvent.headers && lanEvent.body &&
lanEvent.headers."content-type".contains("xml") lanEvent.headers."content-type".contains("xml")
) )
{ {
def parsers = getParsers() def parsers = getParsers()
def xmlData = new XmlSlurper().parseText(lanEvent.body) def xmlData = new XmlSlurper().parseText(lanEvent.body)
// Let each parser take a stab at it // Let each parser take a stab at it
parsers.each { node,func -> parsers.each { node,func ->
if (xmlData.name() == node) if (xmlData.name() == node)
@@ -369,20 +369,20 @@ def onLocation(evt) {
/** /**
* Handles SSDP description file. * Handles SSDP description file.
* *
* @param xmlData * @param xmlData
*/ */
private def parseDESC(xmlData) { private def parseDESC(xmlData) {
log.info "parseDESC()" log.info "parseDESC()"
def devicetype = getDeviceType().toLowerCase() def devicetype = getDeviceType().toLowerCase()
def devicetxml = body.device.deviceType.text().toLowerCase() def devicetxml = body.device.deviceType.text().toLowerCase()
// Make sure it's the type we want // Make sure it's the type we want
if (devicetxml == devicetype) { if (devicetxml == devicetype) {
def devices = getDevices() def devices = getDevices()
def device = devices.find {it?.key?.contains(xmlData?.device?.UDN?.text())} def device = devices.find {it?.key?.contains(xmlData?.device?.UDN?.text())}
if (device && !device.value?.verified) { 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()] device.value << [name:xmlData?.device?.friendlyName?.text(),model:xmlData?.device?.modelName?.text(), serialNumber:xmlData?.device?.serialNum?.text()]
} else { } else {
log.error "parseDESC(): The xml file returned a device that didn't exist" log.error "parseDESC(): The xml file returned a device that didn't exist"
@@ -398,9 +398,9 @@ private def parseDESC(xmlData) {
* @param xmlData * @param xmlData
*/ */
private def parseINFO(xmlData) { private def parseINFO(xmlData) {
log.info "parseINFO()" log.info "parseINFO()"
def devicetype = getDeviceType().toLowerCase() def devicetype = getDeviceType().toLowerCase()
def deviceID = xmlData.attributes()['deviceID'] def deviceID = xmlData.attributes()['deviceID']
def device = getDevices().find {it?.key?.contains(deviceID)} def device = getDevices().find {it?.key?.contains(deviceID)}
if (device && !device.value?.verified) { if (device && !device.value?.verified) {
@@ -420,15 +420,15 @@ def parseSSDP(lanEvent) {
def USN = lanEvent.ssdpUSN.toString() def USN = lanEvent.ssdpUSN.toString()
def devices = getDevices() def devices = getDevices()
if (!(devices."${USN}")) { if (!(devices."${USN}")) {
//device does not exist //device does not exist
log.trace "parseSDDP() Adding Device \"${USN}\" to known list" log.trace "parseSDDP() Adding Device \"${USN}\" to known list"
devices << ["${USN}":lanEvent] devices << ["${USN}":lanEvent]
} else { } else {
// update the values // update the values
def d = devices."${USN}" def d = devices."${USN}"
if (d.networkAddress != lanEvent.networkAddress || d.deviceAddress != lanEvent.deviceAddress) { 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.networkAddress = lanEvent.networkAddress
d.deviceAddress = lanEvent.deviceAddress d.deviceAddress = lanEvent.deviceAddress
} }
@@ -442,14 +442,14 @@ def parseSSDP(lanEvent) {
* @return Map with zero or more devices * @return Map with zero or more devices
*/ */
Map getSelectableDevice() { Map getSelectableDevice() {
def devices = getVerifiedDevices() def devices = getVerifiedDevices()
def map = [:] def map = [:]
devices.each { devices.each {
def value = "${it.value.name}" def value = "${it.value.name}"
def key = it.value.mac def key = it.value.mac
map["${key}"] = value map["${key}"] = value
} }
map map
} }
/** /**
@@ -470,7 +470,7 @@ private refreshDevices() {
private subscribeNetworkEvents(force=false) { private subscribeNetworkEvents(force=false) {
if (force) { if (force) {
unsubscribe() unsubscribe()
state.subscribe = false state.subscribe = false
} }
if(!state.subscribe) { if(!state.subscribe) {
@@ -484,7 +484,7 @@ private subscribeNetworkEvents(force=false) {
*/ */
private discoverDevices() { private discoverDevices() {
log.trace "discoverDevice() Issuing SSDP request" 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) * request for each of them (basically calling verifyDevice() per unverified)
*/ */
private verifyDevices() { private verifyDevices() {
def devices = getDevices().findAll { it?.value?.verified != true } def devices = getDevices().findAll { it?.value?.verified != true }
devices.each { devices.each {
verifyDevice( verifyDevice(
it?.value?.mac, it?.value?.mac,
convertHexToIP(it?.value?.networkAddress), convertHexToIP(it?.value?.networkAddress),
convertHexToInt(it?.value?.deviceAddress), convertHexToInt(it?.value?.deviceAddress),
it?.value?.ssdpPath it?.value?.ssdpPath
) )
} }
} }
/** /**
@@ -509,7 +509,7 @@ private verifyDevices() {
* holds information such as the actual mac to use in certain scenarios. * holds information such as the actual mac to use in certain scenarios.
* *
* Without this mac (henceforth referred to as deviceID), we can't do multi-speaker * Without this mac (henceforth referred to as deviceID), we can't do multi-speaker
* functions. * functions.
* *
* @param deviceNetworkId The DNI of the device * @param deviceNetworkId The DNI of the device
* @param ip The address of the device on the network (not the same as DNI) * @param ip The address of the device on the network (not the same as DNI)
@@ -528,7 +528,7 @@ private verifyDevice(String deviceNetworkId, String ip, int port, String devices
HOST: address, HOST: address,
]])) ]]))
} else { } 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 * @return array of verified devices
*/ */
def getVerifiedDevices() { def getVerifiedDevices() {
getDevices().findAll{ it?.value?.verified == true } getDevices().findAll{ it?.value?.verified == true }
} }
/** /**
@@ -547,7 +547,7 @@ def getVerifiedDevices() {
* @return array of devices * @return array of devices
*/ */
def getDevices() { def getDevices() {
state.devices = state.devices ?: [:] state.devices = state.devices ?: [:]
} }
/** /**
@@ -557,7 +557,7 @@ def getDevices() {
* @return An integer * @return An integer
*/ */
private Integer convertHexToInt(hex) { 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 * @return String containing normal IPv4 dot notation
*/ */
private String convertHexToIP(hex) { private String convertHexToIP(hex) {
if (hex) if (hex)
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
else else
hex hex
} }
/** /**
@@ -580,7 +580,7 @@ private String convertHexToIP(hex) {
*/ */
private Boolean canInstallLabs() 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) 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() private List getRealHubFirmwareVersions()
{ {
return location.hubs*.firmwareVersionString.findAll { it } return location.hubs*.firmwareVersionString.findAll { it }
} }