Compare commits

...

21 Commits

Author SHA1 Message Date
Vinay Rao
9d5ab3bfc8 Merge pull request #1303 from SmartThingsCommunity/staging
Rolling down staging hotfix to master
2016-09-27 14:38:13 -07:00
Juan Pablo Risso
218cc43520 DVCSMP-2089 - Harmony - Make HTTP Requests Async (#1299) 2016-09-27 15:43:37 -04:00
Zach Varberg
9ddc020f04 Merge pull request #1248 from varzac/add-send-to-raw-messages
Add explicit send commands for zigbee raw
2016-09-27 09:07:55 -05:00
Vinay Rao
aab3b8d7f8 Merge pull request #1297 from workingmonk/feature/temp_rounding
SSVD-2897 to round celsius and fix rounding on fahrenheit
2016-09-26 14:40:42 -07:00
Vinay Rao
a0ccf35eaa SSVD-2897 to round celsius and fix rounding on fahrenheit 2016-09-26 14:39:07 -07:00
Steve Vlaminck
9fbbaec8f6 Merge pull request #1296 from vlaminck/GWU-completion-percentage-fix
Fix: Cleanup event feed when dimming cycle ends
2016-09-26 14:21:31 -05:00
vlaminck
e4c1824afd Fix: reorder events so the feed makes more sense 2016-09-26 14:18:31 -05:00
vlaminck
797a58cb68 Fix: hide reset events 2016-09-26 14:14:55 -05:00
vlaminck
c428267d63 Fix: stop sending 100 percent from completion 2016-09-26 14:05:03 -05:00
Vinay Rao
02f30cf425 Merge pull request #1295 from SmartThingsCommunity/production
Rolling down production hotfix to staging
2016-09-26 11:50:24 -07:00
Lars Finander
fea802ffce Merge pull request #1294 from larsfinander/DVCSMP-2070_Philips_Hue_unreachable_devices_staging
DVCSMP-2070 Philips Hue: No commands sent if light is unreachable
2016-09-26 12:06:57 -06:00
Lars Finander
6400d26f4a DVCSMP-2070 Philips Hue: No commands sent if light is unreachable
-PROB-1384
2016-09-26 11:59:48 -06:00
Lars Finander
5e3aaa3270 Merge pull request #1293 from larsfinander/DVCSMP-2081_Philips_Hue_650k_exceptions_staging
DVCSMP-2081 Philips Hue: Bridge is throwing 650k exceptions a day
2016-09-26 11:52:12 -06:00
Lars Finander
f5c3997679 DVCSMP-2081 Philips Hue: Bridge is throwing 650k exceptions a day 2016-09-26 10:21:03 -06:00
Lars Finander
30993aa218 Merge pull request #1284 from larsfinander/SSVD-2798_philips_hue_discovery_bridge_staging
SSVD-2798 Philips Hue: Bridge keeps getting unchecked during discovery
2016-09-22 12:11:15 -06:00
Lars Finander
2f8ed277ff SSVD-2798 Philips Hue: Bridge keeps getting unchecked during discovery 2016-09-22 12:07:09 -06:00
Zach Varberg
230541a145 Add explicit send commands for zigbee raw
Previously a few places depended on the dev-conn behavior of
automatically appending a send command after a raw zigbee command.  We
intend to deprecate that behavior and so we are adding explicit sends
where they were missing.
2016-09-22 11:08:30 -05:00
Lars Finander
8c4f7edc83 Merge pull request #1276 from larsfinander/DVCSMP-2057_Philips_Hue_Correct_incorrect_bridge_mac_production
INC-6888 Philips Hue: Correct incorrect bridge mac
2016-09-21 13:11:12 -06:00
Lars Finander
4f188581df INC-6888 Philips Hue: Correct incorrect bridge mac 2016-09-21 11:14:11 -06:00
Vinay Rao
0b7bb40474 Merge pull request #1274 from SmartThingsCommunity/master
Rolling up master for next week deploy
2016-09-20 12:05:49 -07:00
Vinay Rao
e373b6f92e Merge pull request #1272 from SmartThingsCommunity/staging
Rolling up staging to production for deployment
2016-09-20 11:53:36 -07:00
9 changed files with 295 additions and 205 deletions

View File

@@ -87,16 +87,27 @@ def beep() {
up to this long from the time you send the message to the time you hear a sound. up to this long from the time you send the message to the time you hear a sound.
*/ */
// Used source endpoint of 0x02 because we are using smartthings manufacturer specific cluster.
[ [
"raw 0xFC05 {15 0A 11 00 00 15 01}", "raw 0xFC05 {15 0A 11 00 00 15 01}",
"delay 200",
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
"delay 7000", "delay 7000",
"raw 0xFC05 {15 0A 11 00 00 15 01}", "raw 0xFC05 {15 0A 11 00 00 15 01}",
"delay 200",
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
"delay 7000", "delay 7000",
"raw 0xFC05 {15 0A 11 00 00 15 01}", "raw 0xFC05 {15 0A 11 00 00 15 01}",
"delay 200",
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
"delay 7000", "delay 7000",
"raw 0xFC05 {15 0A 11 00 00 15 01}", "raw 0xFC05 {15 0A 11 00 00 15 01}",
"delay 200",
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
"delay 7000", "delay 7000",
"raw 0xFC05 {15 0A 11 00 00 15 01}" "raw 0xFC05 {15 0A 11 00 00 15 01}",
"delay 200",
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
] ]
} }

View File

@@ -105,11 +105,21 @@ def parseDescriptionAsMap(description) {
// Commands to device // Commands to device
def on() { def on() {
'zcl on-off on' [
'zcl on-off on',
'delay 200',
"send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}",
'delay 500'
]
} }
def off() { def off() {
'zcl on-off off' [
'zcl on-off off',
'delay 200',
"send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}",
'delay 500'
]
} }
def setLevel(value) { def setLevel(value) {

View File

@@ -47,9 +47,21 @@ def parse(String description) {
// Commands to device // Commands to device
def on() { def on() {
'zcl on-off on' [
'zcl on-off on',
'delay 200',
"send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}",
'delay 500'
]
} }
def off() { def off() {
'zcl on-off off' [
'zcl on-off off',
'delay 200',
"send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}",
'delay 500'
]
} }

View File

@@ -180,9 +180,9 @@ private Map parseIasMessage(String description) {
def getTemperature(value) { def getTemperature(value) {
def celsius = Integer.parseInt(value, 16).shortValue() / 100 def celsius = Integer.parseInt(value, 16).shortValue() / 100
if(getTemperatureScale() == "C"){ if(getTemperatureScale() == "C"){
return celsius return Math.round(celsius)
} else { } else {
return celsiusToFahrenheit(celsius) as Integer return Math.round(celsiusToFahrenheit(celsius))
} }
} }

View File

@@ -194,9 +194,9 @@ private Map parseIasMessage(String description) {
def getTemperature(value) { def getTemperature(value) {
def celsius = Integer.parseInt(value, 16).shortValue() / 100 def celsius = Integer.parseInt(value, 16).shortValue() / 100
if(getTemperatureScale() == "C"){ if(getTemperatureScale() == "C"){
return celsius return Math.round(celsius)
} else { } else {
return celsiusToFahrenheit(celsius) as Integer return Math.round(celsiusToFahrenheit(celsius))
} }
} }

View File

@@ -261,9 +261,9 @@ def updated() {
def getTemperature(value) { def getTemperature(value) {
def celsius = Integer.parseInt(value, 16).shortValue() / 100 def celsius = Integer.parseInt(value, 16).shortValue() / 100
if(getTemperatureScale() == "C"){ if(getTemperatureScale() == "C"){
return celsius return Math.round(celsius)
} else { } else {
return celsiusToFahrenheit(celsius) as Integer return Math.round(celsiusToFahrenheit(celsius))
} }
} }

View File

@@ -454,17 +454,23 @@ def sendStopEvent(source) {
eventData.value += "cancelled" eventData.value += "cancelled"
} }
// send 100% completion event
sendTimeRemainingEvent(100)
// send a non-displayed 0% completion to reset tiles
sendTimeRemainingEvent(0, false)
// send sessionStatus event last so the event feed is ordered properly
sendControllerEvent(eventData) sendControllerEvent(eventData)
sendTimeRemainingEvent(0)
} }
def sendTimeRemainingEvent(percentComplete) { def sendTimeRemainingEvent(percentComplete, displayed = true) {
log.trace "sendTimeRemainingEvent(${percentComplete})" log.trace "sendTimeRemainingEvent(${percentComplete})"
def percentCompleteEventData = [ def percentCompleteEventData = [
name: "percentComplete", name: "percentComplete",
value: percentComplete as int, value: percentComplete as int,
displayed: true, displayed: displayed,
isStateChange: true isStateChange: true
] ]
sendControllerEvent(percentCompleteEventData) sendControllerEvent(percentCompleteEventData)
@@ -474,7 +480,7 @@ def sendTimeRemainingEvent(percentComplete) {
def timeRemainingEventData = [ def timeRemainingEventData = [
name: "timeRemaining", name: "timeRemaining",
value: displayableTime(timeRemaining), value: displayableTime(timeRemaining),
displayed: true, displayed: displayed,
isStateChange: true isStateChange: true
] ]
sendControllerEvent(timeRemainingEventData) sendControllerEvent(timeRemainingEventData)
@@ -608,8 +614,6 @@ private completion() {
handleCompletionMessaging() handleCompletionMessaging()
handleCompletionModesAndPhrases() handleCompletionModesAndPhrases()
sendTimeRemainingEvent(100)
} }
private handleCompletionSwitches() { private handleCompletionSwitches() {

View File

@@ -83,7 +83,7 @@ def bridgeDiscovery(params=[:])
return dynamicPage(name:"bridgeDiscovery", title:"Discovery Started!", nextPage:"bridgeBtnPush", refreshInterval:refreshInterval, uninstall: true) { return dynamicPage(name:"bridgeDiscovery", title:"Discovery Started!", nextPage:"bridgeBtnPush", refreshInterval:refreshInterval, uninstall: true) {
section("Please wait while we discover your Hue Bridge. 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 Hue Bridge. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
input "selectedHue", "enum", required:false, title:"Select Hue Bridge (${numFound} found)", multiple:false, options:options input "selectedHue", "enum", required:false, title:"Select Hue Bridge (${numFound} found)", multiple:false, options:options, submitOnChange: true
} }
} }
} }
@@ -333,9 +333,9 @@ def bulbListHandler(hub, data = "") {
def bridge = null def bridge = null
if (selectedHue) { if (selectedHue) {
bridge = getChildDevice(selectedHue) bridge = getChildDevice(selectedHue)
bridge?.sendEvent(name: "bulbList", value: hub, data: bulbs, isStateChange: true, displayed: false)
} }
bridge.sendEvent(name: "bulbList", value: hub, data: bulbs, isStateChange: true, displayed: false) msg = "${bulbs.size()} bulbs found. ${bulbs}"
msg = "${bulbs.size()} bulbs found. ${bulbs}"
return msg return msg
} }
@@ -490,24 +490,25 @@ def ssdpBridgeHandler(evt) {
def host = ip + ":" + port def host = ip + ":" + port
log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host." log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host."
def dstate = bridges."${parsedEvent.ssdpUSN.toString()}" def dstate = bridges."${parsedEvent.ssdpUSN.toString()}"
def dni = "${parsedEvent.mac}" def dniReceived = "${parsedEvent.mac}"
def d = getChildDevice(dni) def currentDni = dstate.mac
def d = getChildDevice(dniReceived)
def networkAddress = null def networkAddress = null
if (!d) { if (!d) {
childDevices.each { // There might be a mismatch between bridge DNI and the actual bridge mac address, correct that
if (it.getDeviceDataByName("mac")) { log.debug "Bridge with $dniReceived not found"
def newDNI = "${it.getDeviceDataByName("mac")}" def bridge = childDevices.find { it.deviceNetworkId == currentDni }
d = it if (bridge != null) {
if (newDNI != it.deviceNetworkId) { log.warn "Bridge is set to ${bridge.deviceNetworkId}, updating to $dniReceived"
def oldDNI = it.deviceNetworkId bridge.setDeviceNetworkId("${dniReceived}")
log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}" dstate.mac = dniReceived
it.setDeviceNetworkId("${newDNI}") // Check to see if selectedHue is a valid bridge, otherwise update it
if (oldDNI == selectedHue) { def isSelectedValid = bridges?.find {it.value?.mac == selectedHue}
app.updateSetting("selectedHue", newDNI) if (isSelectedValid == null) {
} log.warn "Correcting selectedHue in state"
doDeviceSync() app.updateSetting("selectedHue", dniReceived)
}
} }
doDeviceSync()
} }
} else { } else {
updateBridgeStatus(d) updateBridgeStatus(d)
@@ -525,6 +526,18 @@ def ssdpBridgeHandler(evt) {
d.sendEvent(name:"networkAddress", value: host) d.sendEvent(name:"networkAddress", value: host)
d.updateDataValue("networkAddress", host) d.updateDataValue("networkAddress", host)
} }
if (dstate.mac != dniReceived) {
log.warn "Correcting bridge mac address in state"
dstate.mac = dniReceived
}
if (selectedHue != dniReceived) {
// Check to see if selectedHue is a valid bridge, otherwise update it
def isSelectedValid = bridges?.find {it.value?.mac == selectedHue}
if (isSelectedValid == null) {
log.warn "Correcting selectedHue in state"
app.updateSetting("selectedHue", dniReceived)
}
}
} }
} }
} }
@@ -950,6 +963,14 @@ private handleCommandResponse(body) {
* @return empty array * @return empty array
*/ */
private handlePoll(body) { private handlePoll(body) {
// Used to track "unreachable" time
// Device is considered "offline" if it has been in the "unreachable" state for
// 11 minutes (e.g. two poll intervals)
// Note, Hue Bridge marks devices as "unreachable" often even when they accept commands
Calendar time11 = Calendar.getInstance()
time11.add(Calendar.MINUTE, -11)
Calendar currentTime = Calendar.getInstance()
def bulbs = getChildDevices() def bulbs = getChildDevices()
for (bulb in body) { for (bulb in body) {
def device = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"} def device = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
@@ -959,7 +980,10 @@ private handlePoll(body) {
// light just came back online, notify device watch // light just came back online, notify device watch
def lastActivity = now() def lastActivity = now()
device.sendEvent(name: "deviceWatch-status", value: "ONLINE", description: "Last Activity is on ${new Date((long) lastActivity)}", displayed: false, isStateChange: true) device.sendEvent(name: "deviceWatch-status", value: "ONLINE", description: "Last Activity is on ${new Date((long) lastActivity)}", displayed: false, isStateChange: true)
log.debug "$device is Online"
} }
// Mark light as "online"
state.bulbs[bulb.key]?.unreachableSince = null
state.bulbs[bulb.key]?.online = true state.bulbs[bulb.key]?.online = true
// If user just executed commands, then do not send events to avoid confusing the turning on/off state // If user just executed commands, then do not send events to avoid confusing the turning on/off state
@@ -969,9 +993,18 @@ private handlePoll(body) {
sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode) sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode)
} }
} else { } else {
state.bulbs[bulb.key]?.online = false if (state.bulbs[bulb.key]?.unreachableSince == null) {
log.warn "$device is not reachable by Hue bridge" // Store the first time where device was reported as "unreachable"
device.sendEvent(name: "DeviceWatch-DeviceOffline", value: "offline", displayed: false, isStateChange: true) state.bulbs[bulb.key]?.unreachableSince = currentTime.getTimeInMillis()
} else if (state.bulbs[bulb.key]?.online) {
// Check if device was "unreachable" for more than 11 minutes and mark "offline" if necessary
if (state.bulbs[bulb.key]?.unreachableSince < time11.getTimeInMillis()) {
log.warn "$device went Offline"
state.bulbs[bulb.key]?.online = false
device.sendEvent(name: "DeviceWatch-DeviceOffline", value: "offline", displayed: false, isStateChange: true)
}
}
log.warn "$device may not reachable by Hue bridge"
} }
} }
} }
@@ -1006,9 +1039,6 @@ def hubVerification(bodytext) {
def on(childDevice) { def on(childDevice) {
log.debug "Executing 'on'" log.debug "Executing 'on'"
def id = getId(childDevice) def id = getId(childDevice)
if (!isOnline(id)) {
return "Bulb is unreachable"
}
updateInProgress() updateInProgress()
createSwitchEvent(childDevice, "on") createSwitchEvent(childDevice, "on")
put("lights/$id/state", [on: true]) put("lights/$id/state", [on: true])
@@ -1018,9 +1048,6 @@ def on(childDevice) {
def off(childDevice) { def off(childDevice) {
log.debug "Executing 'off'" log.debug "Executing 'off'"
def id = getId(childDevice) def id = getId(childDevice)
if (!isOnline(id)) {
return "Bulb is unreachable"
}
updateInProgress() updateInProgress()
createSwitchEvent(childDevice, "off") createSwitchEvent(childDevice, "off")
put("lights/$id/state", [on: false]) put("lights/$id/state", [on: false])
@@ -1030,9 +1057,6 @@ def off(childDevice) {
def setLevel(childDevice, percent) { def setLevel(childDevice, percent) {
log.debug "Executing 'setLevel'" log.debug "Executing 'setLevel'"
def id = getId(childDevice) def id = getId(childDevice)
if (!isOnline(id)) {
return "Bulb is unreachable"
}
updateInProgress() updateInProgress()
// 1 - 254 // 1 - 254
def level def level
@@ -1057,10 +1081,6 @@ def setLevel(childDevice, percent) {
def setSaturation(childDevice, percent) { def setSaturation(childDevice, percent) {
log.debug "Executing 'setSaturation($percent)'" log.debug "Executing 'setSaturation($percent)'"
def id = getId(childDevice) def id = getId(childDevice)
if (!isOnline(id)) {
return "Bulb is unreachable"
}
updateInProgress() updateInProgress()
// 0 - 254 // 0 - 254
def level = Math.min(Math.round(percent * 254 / 100), 254) def level = Math.min(Math.round(percent * 254 / 100), 254)
@@ -1073,9 +1093,6 @@ def setSaturation(childDevice, percent) {
def setHue(childDevice, percent) { def setHue(childDevice, percent) {
log.debug "Executing 'setHue($percent)'" log.debug "Executing 'setHue($percent)'"
def id = getId(childDevice) def id = getId(childDevice)
if (!isOnline(id)) {
return "Bulb is unreachable"
}
updateInProgress() updateInProgress()
// 0 - 65535 // 0 - 65535
def level = Math.min(Math.round(percent * 65535 / 100), 65535) def level = Math.min(Math.round(percent * 65535 / 100), 65535)
@@ -1088,9 +1105,6 @@ def setHue(childDevice, percent) {
def setColorTemperature(childDevice, huesettings) { def setColorTemperature(childDevice, huesettings) {
log.debug "Executing 'setColorTemperature($huesettings)'" log.debug "Executing 'setColorTemperature($huesettings)'"
def id = getId(childDevice) def id = getId(childDevice)
if (!isOnline(id)) {
return "Bulb is unreachable"
}
updateInProgress() updateInProgress()
// 153 (6500K) to 500 (2000K) // 153 (6500K) to 500 (2000K)
def ct = hueSettings == 6500 ? 153 : Math.round(1000000/huesettings) def ct = hueSettings == 6500 ? 153 : Math.round(1000000/huesettings)
@@ -1102,9 +1116,6 @@ def setColorTemperature(childDevice, huesettings) {
def setColor(childDevice, huesettings) { def setColor(childDevice, huesettings) {
log.debug "Executing 'setColor($huesettings)'" log.debug "Executing 'setColor($huesettings)'"
def id = getId(childDevice) def id = getId(childDevice)
if (!isOnline(id)) {
return "Bulb is unreachable"
}
updateInProgress() updateInProgress()
def value = [:] def value = [:]
@@ -1120,7 +1131,7 @@ def setColor(childDevice, huesettings) {
value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535) value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
if (huesettings.saturation != null) if (huesettings.saturation != null)
value.sat = Math.min(Math.round(huesettings.saturation * 254 / 100), 254) value.sat = Math.min(Math.round(huesettings.saturation * 254 / 100), 254)
} else if (huesettings.hex != null && false) { } else if (huesettings.hex != null) {
// For now ignore model to get a consistent color if same color is set across multiple devices // For now ignore model to get a consistent color if same color is set across multiple devices
// def model = state.bulbs[getId(childDevice)]?.modelid // def model = state.bulbs[getId(childDevice)]?.modelid
// value.xy = calculateXY(huesettings.hex, model) // value.xy = calculateXY(huesettings.hex, model)
@@ -1224,7 +1235,7 @@ private getBridgeIP() {
if (d) { if (d) {
if (d.getDeviceDataByName("networkAddress")) if (d.getDeviceDataByName("networkAddress"))
host = d.getDeviceDataByName("networkAddress") host = d.getDeviceDataByName("networkAddress")
else else
host = d.latestState('networkAddress').stringValue host = d.latestState('networkAddress').stringValue
} }
if (host == null || host == "") { if (host == null || host == "") {
@@ -1663,7 +1674,7 @@ private boolean checkPointInLampsReach(p, colorPoints) {
} }
/** /**
* Converts an RGB color in hex to HSV. * Converts an RGB color in hex to HSV/HSB.
* Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space. * Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space.
* *
* @param colorStr color value in hex (#ff03d3) * @param colorStr color value in hex (#ff03d3)
@@ -1673,32 +1684,32 @@ private boolean checkPointInLampsReach(p, colorPoints) {
def hexToHsv(colorStr){ def hexToHsv(colorStr){
def r = Integer.valueOf( colorStr.substring( 1, 3 ), 16 ) / 255 def r = Integer.valueOf( colorStr.substring( 1, 3 ), 16 ) / 255
def g = Integer.valueOf( colorStr.substring( 3, 5 ), 16 ) / 255 def g = Integer.valueOf( colorStr.substring( 3, 5 ), 16 ) / 255
def b = Integer.valueOf( colorStr.substring( 5, 7 ), 16 ) / 255; def b = Integer.valueOf( colorStr.substring( 5, 7 ), 16 ) / 255
def max = Math.max(Math.max(r, g), b) def max = Math.max(Math.max(r, g), b)
def min = Math.min(Math.min(r, g), b) def min = Math.min(Math.min(r, g), b)
def h, s, v = max; def h, s, v = max
def d = max - min; def d = max - min
s = max == 0 ? 0 : d / max; s = max == 0 ? 0 : d / max
if(max == min){ if(max == min){
h = 0; h = 0
}else{ }else{
switch(max){ switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break; case r: h = (g - b) / d + (g < b ? 6 : 0); break
case g: h = (b - r) / d + 2; break; case g: h = (b - r) / d + 2; break
case b: h = (r - g) / d + 4; break; case b: h = (r - g) / d + 4; break
} }
h /= 6; h /= 6;
} }
return [(h * 100).round(), (s * 100).round(), (v * 100).round()]; return [Math.round(h * 100), Math.round(s * 100), Math.round(v * 100)]
} }
/** /**
* Converts HSV color to RGB in hex. * Converts HSV/HSB color to RGB in hex.
* Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space. * Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space.
* *
* @param hue hue 0-100 * @param hue hue 0-100
@@ -1713,11 +1724,11 @@ def hsvToHex(hue, sat, value = 100){
def s = sat / 100 def s = sat / 100
def v = value / 100 def v = value / 100
def i = Math.floor(h * 6); def i = Math.floor(h * 6)
def f = h * 6 - i; def f = h * 6 - i
def p = v * (1 - s); def p = v * (1 - s)
def q = v * (1 - f * s); def q = v * (1 - f * s)
def t = v * (1 - (1 - f) * s); def t = v * (1 - (1 - f) * s)
switch (i % 6) { switch (i % 6) {
case 0: case 0:
@@ -1753,9 +1764,9 @@ def hsvToHex(hue, sat, value = 100){
} }
// Converting float components to int components. // Converting float components to int components.
def r1 = String.format("%02X", (int) (r * 255.0f)); def r1 = String.format("%02X", (int) (r * 255.0f))
def g1 = String.format("%02X", (int) (g * 255.0f)); def g1 = String.format("%02X", (int) (g * 255.0f))
def b1 = String.format("%02X", (int) (b * 255.0f)); def b1 = String.format("%02X", (int) (b * 255.0f))
return "#$r1$g1$b1" return "#$r1$g1$b1"
} }

View File

@@ -34,6 +34,7 @@
* locks | lock | lock, unlock | locked, unlocked * locks | lock | lock, unlock | locked, unlocked
* ---------------------+-------------------+-----------------------------+------------------------------------ * ---------------------+-------------------+-----------------------------+------------------------------------
*/ */
include 'asynchttp_v1'
definition( definition(
name: "Logitech Harmony (Connect)", name: "Logitech Harmony (Connect)",
@@ -109,26 +110,28 @@ def authPage() {
//device discovery request every 5 //25 seconds //device discovery request every 5 //25 seconds
int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
state.deviceRefreshCount = deviceRefreshCount + 1 state.deviceRefreshCount = deviceRefreshCount + 1
def refreshInterval = 3 def refreshInterval = 5
def huboptions = state.HarmonyHubs ?: [] def huboptions = state.HarmonyHubs ?: []
def actoptions = state.HarmonyActivities ?: [] def actoptions = state.HarmonyActivities ?: []
def numFoundHub = huboptions.size() ?: 0 def numFoundHub = huboptions.size() ?: 0
def numFoundAct = actoptions.size() ?: 0 def numFoundAct = actoptions.size() ?: 0
if((deviceRefreshCount % 5) == 0) { if((deviceRefreshCount % 5) == 0) {
discoverDevices() discoverDevices()
} }
return dynamicPage(name:"Credentials", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { return dynamicPage(name:"Credentials", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
section("Please wait while we discover your Harmony Hubs and Activities. 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 Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, options:huboptions input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, submitOnChange: true, options:huboptions
} }
// Virtual activity flag // Virtual activity flag
if (numFoundHub > 0 && numFoundAct > 0 && true) if (numFoundHub > 0 && numFoundAct > 0 && true)
section("You can also add activities as virtual switches for other convenient integrations") { section("You can also add activities as virtual switches for other convenient integrations") {
input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, options:actoptions input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, submitOnChange: true, options:actoptions
} }
if (state.resethub) if (state.resethub)
section("Connection to the hub timed out. Please restart the hub and try again.") {} section("Connection to the hub timed out. Please restart the hub and try again.") {}
} }
} }
@@ -380,8 +383,6 @@ def discovery() {
log.debug "valid Token" log.debug "valid Token"
state.Harmonydevices = response.data state.Harmonydevices = response.data
state.resethub = false state.resethub = false
getActivityList()
poll()
} else { } else {
log.debug "Error: $response.status" log.debug "Error: $response.status"
} }
@@ -430,142 +431,182 @@ def addDevice() {
} }
def activity(dni,mode) { def activity(dni,mode) {
def Params = [auth: state.HarmonyAccessToken] def tokenParam = [auth: state.HarmonyAccessToken]
def msg = "Command failed" def url
def url = ''
if (dni == "all") { if (dni == "all") {
url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}" url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(tokenParam)}"
} else { } else {
def aux = dni.split('-') def aux = dni.split('-')
def hubId = aux[1] def hubId = aux[1]
if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){ if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(Params)}" url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(tokenParam)}"
} else { } else {
def activityId = aux[2] def activityId = aux[2]
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(Params)}" url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(tokenParam)}"
} }
} }
try { def params = [
httpPostJson(uri: url) { response -> uri: url,
if (response.data.code == 200 || dni == "all") { contentType: 'application/json'
msg = "Command sent succesfully" ]
state.aux = 0 asynchttp_v1.post('activityResponse', params)
} else { return "Command Sent"
msg = "Command failed. Error: $response.data.code" }
}
} def activityResponse(response, data) {
} catch (groovyx.net.http.HttpResponseException ex) { if (response.hasError()) {
log.error ex log.error "Logitech Harmony - response has error: $response.errorMessage"
if (state.aux == 0) { if (response.status == 401) { // token is expired
state.aux = 1 state.remove("HarmonyAccessToken")
activity(dni,mode) log.warn "Logitech Harmony - Access token has expired"
} else {
msg = ex
state.aux = 0
}
} catch(Exception ex) {
msg = ex
} }
runIn(10, "poll", [overwrite: true]) } else {
return msg def ResponseValues
try {
// json response already parsed into JSONElement object
ResponseValues = response.json
} catch (e) {
log.error "Logitech Harmony - error parsing json from response: $e"
}
if (ResponseValues) {
if (ResponseValues.code == 200) {
log.trace "Command sent succesfully"
poll()
} else {
log.trace "Command failed. Error: $response.data.code"
}
} else {
log.debug "Logitech Harmony - did not get json results from response body: $response.data"
}
}
} }
def poll() { def poll() {
// GET THE LIST OF ACTIVITIES // GET THE LIST OF ACTIVITIES
if (state.HarmonyAccessToken) { if (state.HarmonyAccessToken) {
getActivityList() getActivityList()
def Params = [auth: state.HarmonyAccessToken] def tokenParam = [auth: state.HarmonyAccessToken]
def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}" def params = [
try { uri: "https://home.myharmony.com/cloudapi/state?${toQueryString(tokenParam)}",
httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> headers: ["Accept": "application/json"],
def map = [:] contentType: 'application/json'
response.data.hubs.each { ]
if (it.value.message == "OK") { asynchttp_v1.get('pollResponse', params)
map["${it.key}"] = "${it.value.response.data.currentAvActivity},${it.value.response.data.activityStatus}" } else {
def hub = getChildDevice("harmony-${it.key}") log.warn "Logitech Harmony - Access token has expired"
if (hub) { }
if (it.value.response.data.currentAvActivity == "-1") { }
hub.sendEvent(name: "currentActivity", value: "--", descriptionText: "There isn't any activity running", display: false)
} else { def pollResponse(response, data) {
def currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key) if (response.hasError()) {
hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false) log.error "Logitech Harmony - response has error: $response.errorMessage"
} if (response.status == 401) { // token is expired
} state.remove("HarmonyAccessToken")
} else { log.warn "Logitech Harmony - Access token has expired"
log.trace it.value.message }
} } else {
} def ResponseValues
def activities = getChildDevices() try {
def activitynotrunning = true // json response already parsed into JSONElement object
activities.each { activity -> ResponseValues = response.json
def act = activity.deviceNetworkId.split('-') } catch (e) {
if (act.size() > 2) { log.error "Logitech Harmony - error parsing json from response: $e"
def aux = map.find { it.key == act[1] } }
if (aux) { if (ResponseValues) {
def aux2 = aux.value.split(',') def map = [:]
def childDevice = getChildDevice(activity.deviceNetworkId) ResponseValues.hubs.each {
if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) { if (it.value.message == "OK") {
childDevice?.sendEvent(name: "switch", value: "on") map["${it.key}"] = "${it.value.response.data.currentAvActivity},${it.value.response.data.activityStatus}"
if (aux2[1] == "1") def hub = getChildDevice("harmony-${it.key}")
runIn(5, "poll", [overwrite: true]) if (hub) {
} else { if (it.value.response.data.currentAvActivity == "-1") {
childDevice?.sendEvent(name: "switch", value: "off") hub.sendEvent(name: "currentActivity", value: "--", descriptionText: "There isn't any activity running", display: false)
if (aux2[1] == "3") } else {
runIn(5, "poll", [overwrite: true]) def currentActivity = getChildDevice("harmony-${it.key}-${it.value.response.data.currentAvActivity}").device.displayName
} hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false)
} }
}
} else {
log.trace "Logitech Harmony - error response: $it.value.message"
}
}
def activities = getChildDevices()
def activitynotrunning = true
activities.each { activity ->
def act = activity.deviceNetworkId.split('-')
if (act.size() > 2) {
def aux = map.find { it.key == act[1] }
if (aux) {
def aux2 = aux.value.split(',')
def childDevice = getChildDevice(activity.deviceNetworkId)
if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) {
childDevice?.sendEvent(name: "switch", value: "on")
if (aux2[1] == "1")
runIn(5, "poll", [overwrite: true])
} else {
childDevice?.sendEvent(name: "switch", value: "off")
if (aux2[1] == "3")
runIn(5, "poll", [overwrite: true])
} }
} }
return "Poll completed $map - $state.hubs"
} }
} catch (groovyx.net.http.HttpResponseException e) {
if (e.statusCode == 401) { // token is expired
state.remove("HarmonyAccessToken")
log.warn "Harmony Access token has expired"
}
} catch (java.net.SocketTimeoutException e) {
log.warn "Connection to the hub timed out. Please restart the hub and try again."
state.resethub = true
} catch (e) {
log.info "Logitech Harmony - Error: $e"
} }
} else {
log.debug "Logitech Harmony - did not get json results from response body: $response.data"
}
}
}
def getActivityList() {
if (state.HarmonyAccessToken) {
def tokenParam = [auth: state.HarmonyAccessToken]
def params = [
uri: "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(tokenParam)}",
headers: ["Accept": "application/json"],
contentType: 'application/json'
]
asynchttp_v1.get('activityListResponse', params)
} else {
log.warn "Logitech Harmony - Access token has expired"
} }
} }
def activityListResponse(response, data) {
def getActivityList() { if (response.hasError()) {
// GET ACTIVITY'S NAME log.error "Logitech Harmony - response has error: $response.errorMessage"
if (state.HarmonyAccessToken) { if (response.status == 401) { // token is expired
def Params = [auth: state.HarmonyAccessToken] state.remove("HarmonyAccessToken")
def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}" log.warn "Logitech Harmony - Access token has expired"
try { }
httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> } else {
response.data.hubs.each { def ResponseValues
def hub = getChildDevice("harmony-${it.key}") try {
if (hub) { // json response already parsed into JSONElement object
def hubname = getHubName("${it.key}") ResponseValues = response.json
def activities = [] } catch (e) {
def aux = it.value.response.data.activities.size() log.error "Logitech Harmony - error parsing json from response: $e"
if (aux >= 1) { }
activities = it.value.response.data.activities.collect { if (ResponseValues) {
[id: it.key, name: it.value['name'], type: it.value['type']] ResponseValues.hubs.each {
} def hub = getChildDevice("harmony-${it.key}")
activities += [id: "off", name: "Activity OFF", type: "0"] if (hub) {
log.trace activities def hubname = getHubName("${it.key}")
} def activities = []
hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false) def aux = it.value.response?.data.activities.size()
} if (aux >= 1) {
} activities = it.value.response.data.activities.collect {
} [id: it.key, name: it.value['name'], type: it.value['type']]
} catch (groovyx.net.http.HttpResponseException e) { }
log.trace e activities += [id: "off", name: "Activity OFF", type: "0"]
} catch (java.net.SocketTimeoutException e) { log.trace activities
log.trace e }
} catch(Exception e) { hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false)
log.trace e }
} }
} } else {
return activity log.debug "Logitech Harmony - did not get json results from response body: $response.data"
}
}
} }
def getActivityName(activity,hubId) { def getActivityName(activity,hubId) {
@@ -746,7 +787,7 @@ def addSubscription() {
def attribute = data.attributeName def attribute = data.attributeName
def callbackUrl = data.callbackUrl def callbackUrl = data.callbackUrl
log.debug "addSubscription, params: ${params}, request: ${data}" log.debug "Logitech Harmony - addSubscription, params: ${params}, request: ${data}"
if (!attribute) { if (!attribute) {
render status: 400, data: '{"msg": "attributeName is required"}' render status: 400, data: '{"msg": "attributeName is required"}'
} else { } else {
@@ -808,6 +849,7 @@ def deviceHandler(evt) {
def deviceInfo = state[evt.deviceId] def deviceInfo = state[evt.deviceId]
if (state.harmonyHubs) { if (state.harmonyHubs) {
state.harmonyHubs.each { harmonyHub -> state.harmonyHubs.each { harmonyHub ->
log.trace "Logitech Harmony - Sending data to $harmonyHub.name"
sendToHarmony(evt, harmonyHub.callbackUrl) sendToHarmony(evt, harmonyHub.callbackUrl)
} }
} else if (deviceInfo) { } else if (deviceInfo) {