From 6123fbeea58dea5738f5fbd01e254204c52d985f Mon Sep 17 00:00:00 2001 From: Duncan McKee Date: Mon, 22 Feb 2016 13:52:56 -0500 Subject: [PATCH 01/11] DEVFRWK-78 Fix First Alert smoke alarm check-in handling --- .../zwave-smoke-alarm.groovy | 77 +++++++++++-------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy b/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy index 6f1f1ab..aa426e4 100644 --- a/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy +++ b/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy @@ -61,37 +61,44 @@ def parse(String description) { zwaveEvent(cmd, results) } } - // log.debug "\"$description\" parsed to ${results.inspect()}" + log.debug "'$description' parsed to ${results.inspect()}" return results } def createSmokeOrCOEvents(name, results) { def text = null - if (name == "smoke") { - text = "$device.displayName smoke was detected!" - // these are displayed:false because the composite event is the one we want to see in the app - results << createEvent(name: "smoke", value: "detected", descriptionText: text, displayed: false) - } else if (name == "carbonMonoxide") { - text = "$device.displayName carbon monoxide was detected!" - results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false) - } else if (name == "tested") { - text = "$device.displayName was tested" - results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false) - results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false) - } else if (name == "smokeClear") { - text = "$device.displayName smoke is clear" - results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) - name = "clear" - } else if (name == "carbonMonoxideClear") { - text = "$device.displayName carbon monoxide is clear" - results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false) - name = "clear" - } else if (name == "testClear") { - text = "$device.displayName smoke is clear" - results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) - results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false) - name = "clear" + switch (name) { + case "smoke": + text = "$device.displayName smoke was detected!" + // these are displayed:false because the composite event is the one we want to see in the app + results << createEvent(name: "smoke", value: "detected", descriptionText: text, displayed: false) + break + case "carbonMonoxide": + text = "$device.displayName carbon monoxide was detected!" + results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false) + break + case "tested": + text = "$device.displayName was tested" + results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false) + results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false) + break + case "smokeClear": + text = "$device.displayName smoke is clear" + results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) + name = "clear" + break + case "carbonMonoxideClear": + text = "$device.displayName carbon monoxide is clear" + results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false) + name = "clear" + break + case "testClear": + text = "$device.displayName test cleared" + results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) + results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false) + name = "clear" + break } // This composite event is used for updating the tile results << createEvent(name: "alarmState", value: name, descriptionText: text) @@ -117,8 +124,10 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) { createSmokeOrCOEvents(cmd.alarmLevel ? "tested" : "testClear", results) break case 13: // sent every hour -- not sure what this means, just a wake up notification? - if (cmd.alarmLevel != 255) { - results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.alarmLevel", displayed: true) + if (cmd.alarmLevel == 255) { + results << createEvent(descriptionText: "$device.displayName checked in", isStateChange: false) + } else { + results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.alarmLevel", isStateChange:true, displayed:false) } // Clear smoke in case they pulled batteries and we missed the clear msg @@ -127,9 +136,8 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) { } // Check battery if we don't have a recent battery event - def prevBattery = device.currentState("battery") - if (!prevBattery || (new Date().time - prevBattery.date.time)/60000 >= 60 * 53) { - results << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format()) + if (!state.lastbatt || (now() - state.lastbatt) >= 48*60*60*1000) { + results << response(zwave.batteryV1.batteryGet()) } break default: @@ -158,12 +166,17 @@ def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd, } def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd, results) { - results << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) results << createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) + if (!state.lastbatt || (now() - state.lastbatt) >= 56*60*60*1000) { + results << response(zwave.batteryV1.batteryGet(), "delay 2000", zwave.wakeUpV1.wakeUpNoMoreInformation()) + } else { + results << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + } } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd, results) { - def map = [ name: "battery", unit: "%" ] + def map = [ name: "battery", unit: "%", isStateChange: true ] + state.lastbatt = now() if (cmd.batteryLevel == 0xFF) { map.value = 1 map.descriptionText = "$device.displayName battery is low!" From e61be4ff9c755ed3378a47d6648cbd2b8c22d9d1 Mon Sep 17 00:00:00 2001 From: Duncan McKee Date: Mon, 22 Feb 2016 14:55:58 -0500 Subject: [PATCH 02/11] DVCSMP-1438: Fix Z-Wave Motion handling of Notification Report --- .../zwave-motion-sensor.groovy | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/devicetypes/smartthings/zwave-motion-sensor.src/zwave-motion-sensor.groovy b/devicetypes/smartthings/zwave-motion-sensor.src/zwave-motion-sensor.groovy index b46f9c6..dae2c60 100644 --- a/devicetypes/smartthings/zwave-motion-sensor.src/zwave-motion-sensor.groovy +++ b/devicetypes/smartthings/zwave-motion-sensor.src/zwave-motion-sensor.groovy @@ -57,7 +57,7 @@ def parse(String description) { return result } -def sensorValueEvent(Short value) { +def sensorValueEvent(value) { if (value) { createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion") } else { @@ -94,24 +94,24 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm { def result = [] if (cmd.notificationType == 0x07) { - if (cmd.event == 0x01 || cmd.event == 0x02) { + if (cmd.v1AlarmType == 0x07) { // special case for nonstandard messages from Monoprice ensors + result << sensorValueEvent(cmd.v1AlarmLevel) + } else if (cmd.event == 0x01 || cmd.event == 0x02 || cmd.event == 0x07 || cmd.event == 0x08) { result << sensorValueEvent(1) + } else if (cmd.event == 0x00) { + result << sensorValueEvent(0) } else if (cmd.event == 0x03) { - result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true) - result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId)) - if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName covering was removed", isStateChange: true) + result << response(zwave.batteryV1.batteryGet()) } else if (cmd.event == 0x05 || cmd.event == 0x06) { result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true) - } else if (cmd.event == 0x07) { - if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) - result << sensorValueEvent(1) } } else if (cmd.notificationType) { def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}" - result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, displayed: false) + result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, isStateChange: true, displayed: false) } else { def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive" - result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false) + result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, isStateChange: true, displayed: false) } result } From df421a51acabbbc142ce944152751f6eb6f02e44 Mon Sep 17 00:00:00 2001 From: juano2310 Date: Mon, 21 Mar 2016 14:38:24 -0400 Subject: [PATCH 03/11] Added hubVerification() If the hub exist as a thing parsing the response from description.xml was lost. Now it is sent back to the parent were the device can be marked as verified if the modelName starts with "Philips hue bridge" --- .../hue-bridge.src/hue-bridge.groovy | 14 ++++++-------- .../hue-connect.src/hue-connect.groovy | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy b/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy index 7d63107..9537fef 100644 --- a/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy +++ b/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy @@ -17,16 +17,13 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"rich-control"){ - tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + tileAttribute ("", key: "PRIMARY_CONTROL") { attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200" - } + } tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") { attributeState "default", label:'SN: ${currentValue}' - } - } - standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) { - state "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#FFFFFF" - } + } + } valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) { state "default", label:'SN: ${currentValue}' } @@ -34,7 +31,7 @@ metadata { state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false } - main (["icon"]) + main (["rich-control"]) details(["rich-control", "networkAddress"]) } } @@ -75,6 +72,7 @@ def parse(description) { } else if (contentType?.contains("xml")) { log.debug "HUE BRIDGE ALREADY PRESENT" + parent.hubVerification(device.hub.id, msg.body) } } } diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 5b908d8..c76e105 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -68,7 +68,7 @@ def bridgeDiscovery(params=[:]) } //setup.xml request every 3 seconds except on discoveries - if(((bridgeRefreshCount % 1) == 0) && ((bridgeRefreshCount % 5) != 0)) { + if(((bridgeRefreshCount % 3) == 0) && ((bridgeRefreshCount % 5) != 0)) { verifyHueBridges() } @@ -175,6 +175,7 @@ private discoverHueBulbs() { } private verifyHueBridge(String deviceNetworkId, String host) { + log.trace "Verify Hue Bridge $deviceNetworkId" sendHubCommand(new physicalgraph.device.HubAction([ method: "GET", path: "/description.xml", @@ -602,6 +603,20 @@ def parse(childDevice, description) { } } +def hubVerification(bodytext) { + log.trace "Bridge sent back description.xml for verification" + def body = new XmlSlurper().parseText(bodytext) + if (body?.device?.modelName?.text().startsWith("Philips hue bridge")) { + def bridges = getHueBridges() + def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())} + if (bridge) { + bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true] + } else { + log.error "/description.xml returned a bridge that didn't exist" + } + } +} + def on(childDevice) { log.debug "Executing 'on'" put("lights/${getId(childDevice)}/state", [on: true]) From 38d0ca617032d3560326c960764f2d21f307a339 Mon Sep 17 00:00:00 2001 From: tslagle13 Date: Tue, 22 Mar 2016 13:00:49 -0700 Subject: [PATCH 04/11] Update vacation-lighting-director.groovy * Moved scheduling over to Cron and added time as a trigger. * Cleaned up formatting and some typos. * Updated license. * Made people option optional * Added sttement to unschedule on mode change if people option is not selected --- .../vacation-lighting-director.groovy | 290 +++++++++++------- 1 file changed, 180 insertions(+), 110 deletions(-) diff --git a/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy b/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy index f71c126..8ea150e 100644 --- a/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy +++ b/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy @@ -1,11 +1,17 @@ /** * Vacation Lighting Director * - * Version 2.4 - Added information paragraphs + * Version 2.5 - Moved scheduling over to Cron and added time as a trigger. + * Cleaned up formatting and some typos. + * Updated license. + * Made people option optional + * Added sttement to unschedule on mode change if people option is not selected + * + * Version 2.4 - Added information paragraphs * * Source code can be found here: https://github.com/tslagle13/SmartThings/blob/master/smartapps/tslagle13/vacation-lighting-director.groovy * - * Copyright 2015 Tim Slagle + * Copyright 2016 Tim Slagle * * 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: @@ -51,8 +57,7 @@ def pageSetup() { return dynamicPage(pageProperties) { section(""){ paragraph "This app can be used to make your home seem occupied anytime you are away from your home. " + - "Please use each othe the sections below to setup the different preferences to your liking. " + - "I recommend this app be used with at least two away modes. An example would be 'Away Day' 'and Away Night'. " + "Please use each of the the sections below to setup the different preferences to your liking. " } section("Setup Menu") { href "Setup", title: "Setup", description: "", state:greyedOut() @@ -70,7 +75,7 @@ def Setup() { def newMode = [ name: "newMode", type: "mode", - title: "Which?", + title: "Modes", multiple: true, required: true ] @@ -96,14 +101,6 @@ def Setup() { required: true, ] - def people = [ - name: "people", - type: "capability.presenceSensor", - title: "If these people are home do not change light status", - required: true, - multiple: true - ] - def pageName = "Setup" def pageProperties = [ @@ -116,10 +113,11 @@ def Setup() { section(""){ paragraph "In this section you need to setup the deatils of how you want your lighting to be affected while " + - paragraph "you are away. All of these settings are required in order for the simulator to run correctly." + "you are away. All of these settings are required in order for the simulator to run correctly." } - section("Which mode change triggers the simulator? (This app will only run in selected mode(s))") { - input newMode + section("Simulator Triggers") { + input newMode + href "timeIntervalInput", title: "Times", description: timeIntervalLabel(), refreshAfterSelection:true } section("Light switches to turn on/off") { input switches @@ -130,9 +128,6 @@ def Setup() { section("Number of active lights at any given time") { input number_of_active_lights } - section("People") { - input people - } } } @@ -162,18 +157,29 @@ def Settings() { title: "Settings", nextPage: "pageSetup" ] + + def people = [ + name: "people", + type: "capability.presenceSensor", + title: "If these people are home do not change light status", + required: false, + multiple: true + ] return dynamicPage(pageProperties) { section(""){ paragraph "In this section you can restrict how your simulator runs. For instance you can restrict on which days it will run " + - paragraph "as well as a delay for the simulator to start after it is in the correct mode. Delaying the simulator helps with false starts based on a incorrect mode change." + "as well as a delay for the simulator to start after it is in the correct mode. Delaying the simulator helps with false starts based on a incorrect mode change." } section("Delay to start simulator") { input falseAlarmThreshold } + section("People") { + paragraph "Not using this setting may cause some lights to remain on when you arrive home" + input people + } section("More options") { - href "timeIntervalInput", title: "Only during a certain time", description: getTimeLabel(starting, ending), state: greyedOutTime(starting, ending), refreshAfterSelection:true input days } } @@ -181,9 +187,24 @@ def Settings() { page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) { section { - input "starting", "time", title: "Starting", required: false - input "ending", "time", title: "Ending", required: false + input "startTimeType", "enum", title: "Starting at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true + if (startTimeType in ["sunrise","sunset"]) { + input "startTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false + } + else { + input "starting", "time", title: "Start time", required: false + } } + section { + input "endTimeType", "enum", title: "Ending at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true + if (endTimeType in ["sunrise","sunset"]) { + input "endTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false + } + else { + input "ending", "time", title: "End time", required: false + } + } + } def installed() { @@ -201,10 +222,13 @@ def initialize(){ if (newMode != null) { subscribe(location, modeChangeHandler) } + if (starting != null) { + schedule(starting, modeChangeHandler) + } + log.debug "Installed with settings: ${settings}" } def modeChangeHandler(evt) { - log.debug "Mode change to: ${evt.value}" def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 2 * 60 runIn(delay, scheduleCheck) } @@ -212,48 +236,54 @@ def modeChangeHandler(evt) { //Main logic to pick a random set of lights from the large set of lights to turn on and then turn the rest off def scheduleCheck(evt) { -if(allOk){ -log.debug("Running") - // turn off all the switches - switches.off() - - // grab a random switch - def random = new Random() - def inactive_switches = switches - for (int i = 0 ; i < number_of_active_lights ; i++) { - // if there are no inactive switches to turn on then let's break - if (inactive_switches.size() == 0){ - break + if(allOk){ + log.debug("Running") + // turn off all the switches + switches.off() + + // grab a random switch + def random = new Random() + def inactive_switches = switches + for (int i = 0 ; i < number_of_active_lights ; i++) { + // if there are no inactive switches to turn on then let's break + if (inactive_switches.size() == 0){ + break + } + + // grab a random switch and turn it on + def random_int = random.nextInt(inactive_switches.size()) + inactive_switches[random_int].on() + + // then remove that switch from the pool off switches that can be turned on + inactive_switches.remove(random_int) + } + + // re-run again when the frequency demands it + schedule("0 0/${frequency_minutes} * 1/1 * ? *", scheduleCheck) } - - // grab a random switch and turn it on - def random_int = random.nextInt(inactive_switches.size()) - inactive_switches[random_int].on() - - // then remove that switch from the pool off switches that can be turned on - inactive_switches.remove(random_int) - } - - // re-run again when the frequency demands it - runIn(frequency_minutes * 60, scheduleCheck) -} -//Check to see if mode is ok but not time/day. If mode is still ok, check again after frequency period. -else if (modeOk) { - log.debug("mode OK. Running again") - runIn(frequency_minutes * 60, scheduleCheck) - switches.off() -} -//if none is ok turn off frequency check and turn off lights. -else if(people){ - //don't turn off lights if anyone is home - if(someoneIsHome()){ - log.debug("Stopping Check for Light") + //Check to see if mode is ok but not time/day. If mode is still ok, check again after frequency period. + else if (modeOk) { + log.debug("mode OK. Running again") + switches.off() + } + //if none is ok turn off frequency check and turn off lights. + else { + if(people){ + //don't turn off lights if anyone is home + if(someoneIsHome()){ + log.debug("Stopping Check for Light") + unschedule() + } + else{ + log.debug("Stopping Check for Light and turning off all lights") + switches.off() + unschedule() + } } - else{ - log.debug("Stopping Check for Light and turning off all lights") - switches.off() + else if (!modeOk) { + unschedule() + } } -} } @@ -286,26 +316,6 @@ private getDaysOk() { result } -private getTimeOk() { - def result = true - if (starting && ending) { - def currTime = now() - def start = timeToday(starting).time - def stop = timeToday(ending).time - result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start - } - - else if (starting){ - result = currTime >= start - } - else if (ending){ - result = currTime <= stop - } - - log.trace "timeOk = $result" - result -} - private getHomeIsEmpty() { def result = true @@ -330,25 +340,59 @@ private getSomeoneIsHome() { return result } - -//gets the label for time restriction. Label phrasing changes depending on if there is both start and stop times or just one start/stop time. -def getTimeLabel(starting, ending){ - - def timeLabel = "Tap to set" - - if(starting && ending){ - timeLabel = "Between" + " " + hhmm(starting) + " " + "and" + " " + hhmm(ending) - } - else if (starting) { - timeLabel = "Start at" + " " + hhmm(starting) - } - else if(ending){ - timeLabel = "End at" + hhmm(ending) - } - timeLabel +private getTimeOk() { + def result = true + def start = timeWindowStart() + def stop = timeWindowStop() + if (start && stop && location.timeZone) { + result = timeOfDayIsBetween(start, stop, new Date(), location.timeZone) + } + log.trace "timeOk = $result" + result +} + +private timeWindowStart() { + def result = null + if (startTimeType == "sunrise") { + result = location.currentState("sunriseTime")?.dateValue + if (result && startTimeOffset) { + result = new Date(result.time + Math.round(startTimeOffset * 60000)) + } + } + else if (startTimeType == "sunset") { + result = location.currentState("sunsetTime")?.dateValue + if (result && startTimeOffset) { + result = new Date(result.time + Math.round(startTimeOffset * 60000)) + } + } + else if (starting && location.timeZone) { + result = timeToday(starting, location.timeZone) + } + log.trace "timeWindowStart = ${result}" + result +} + +private timeWindowStop() { + def result = null + if (endTimeType == "sunrise") { + result = location.currentState("sunriseTime")?.dateValue + if (result && endTimeOffset) { + result = new Date(result.time + Math.round(endTimeOffset * 60000)) + } + } + else if (endTimeType == "sunset") { + result = location.currentState("sunsetTime")?.dateValue + if (result && endTimeOffset) { + result = new Date(result.time + Math.round(endTimeOffset * 60000)) + } + } + else if (ending && location.timeZone) { + result = timeToday(ending, location.timeZone) + } + log.trace "timeWindowStop = ${result}" + result } -//fomrats time to readable format for time label private hhmm(time, fmt = "h:mm a") { def t = timeToday(time, location.timeZone) @@ -357,6 +401,41 @@ private hhmm(time, fmt = "h:mm a") f.format(t) } +private timeIntervalLabel() { + def start = "" + switch (startTimeType) { + case "time": + if (ending) { + start += hhmm(starting) + } + break + case "sunrise": + case "sunset": + start += startTimeType[0].toUpperCase() + startTimeType[1..-1] + if (startTimeOffset) { + start += startTimeOffset > 0 ? "+${startTimeOffset} min" : "${startTimeOffset} min" + } + break + } + + def finish = "" + switch (endTimeType) { + case "time": + if (ending) { + finish += hhmm(ending) + } + break + case "sunrise": + case "sunset": + finish += endTimeType[0].toUpperCase() + endTimeType[1..-1] + if (endTimeOffset) { + finish += endTimeOffset > 0 ? "+${endTimeOffset} min" : "${endTimeOffset} min" + } + break + } + start && finish ? "${start} to ${finish}" : "" +} + //sets complete/not complete for the setup section on the main dynamic page def greyedOut(){ def result = "" @@ -369,16 +448,7 @@ def greyedOut(){ //sets complete/not complete for the settings section on the main dynamic page def greyedOutSettings(){ def result = "" - if (starting || ending || days || falseAlarmThreshold) { - result = "complete" - } - result -} - -//sets complete/not complete for time restriction section in settings -def greyedOutTime(starting, ending){ - def result = "" - if (starting || ending) { + if (people || days || falseAlarmThreshold ) { result = "complete" } result From 281fc939acc5af4aa778342c34e1d2a0088f504e Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Wed, 23 Mar 2016 00:27:23 -0700 Subject: [PATCH 05/11] refactoring code to get it inline with the new zigbee apis --- .../zigbee-switch-power.groovy | 19 ++++++------------- .../zigbee-switch.src/zigbee-switch.groovy | 13 +++---------- ...zigbee-white-color-temperature-bulb.groovy | 13 +++---------- 3 files changed, 12 insertions(+), 33 deletions(-) diff --git a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy index 430b66c..a5eb0ac 100644 --- a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy +++ b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy @@ -51,22 +51,15 @@ metadata { // Parse incoming device messages to generate events def parse(String description) { log.debug "description is $description" - - def resultMap = zigbee.getKnownDescription(description) - if (resultMap) { - log.info resultMap - if (resultMap.type == "update") { - log.info "$device updates: ${resultMap.value}" - } - else if (resultMap.type == "power") { + def event = zigbee.getEvent(description) + if (event) { + if (event.name == "power") { def powerValue - if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power - powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration - sendEvent(name: "power", value: powerValue) - } + powerValue = (event.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration + sendEvent(name: "power", value: powerValue) } else { - sendEvent(name: resultMap.type, value: resultMap.value) + sendEvent(event) } } else { diff --git a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy index d861b95..4440054 100644 --- a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy +++ b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy @@ -53,16 +53,9 @@ metadata { // Parse incoming device messages to generate events def parse(String description) { log.debug "description is $description" - - def resultMap = zigbee.getKnownDescription(description) - if (resultMap) { - log.info resultMap - if (resultMap.type == "update") { - log.info "$device updates: ${resultMap.value}" - } - else { - sendEvent(name: resultMap.type, value: resultMap.value) - } + def event = zigbee.getEvent(description) + if (event) { + sendEvent(event) } else { log.warn "DID NOT PARSE MESSAGE for description : $description" diff --git a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy index b8ad984..50f270d 100644 --- a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy +++ b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy @@ -73,16 +73,9 @@ metadata { // Parse incoming device messages to generate events def parse(String description) { log.debug "description is $description" - - def finalResult = zigbee.getKnownDescription(description) - if (finalResult) { - log.info finalResult - if (finalResult.type == "update") { - log.info "$device updates: ${finalResult.value}" - } - else { - sendEvent(name: finalResult.type, value: finalResult.value) - } + def event = zigbee.getEvent(description) + if (event) { + sendEvent(event) } else { log.warn "DID NOT PARSE MESSAGE for description : $description" From c7147205783853df0c76ed9ebc396068a483630f Mon Sep 17 00:00:00 2001 From: Tom Manley Date: Wed, 23 Mar 2016 09:51:02 -0500 Subject: [PATCH 06/11] Update zigbee-lock, zigbee-dimmer and zigbee-dimmer-power to use ZB lib API https://smartthings.atlassian.net/browse/DPROT-20 --- .../zigbee-dimmer-power.groovy | 18 ++--- .../zigbee-dimmer.src/zigbee-dimmer.groovy | 12 +--- .../zigbee-lock.src/zigbee-lock.groovy | 68 ++++++++----------- 3 files changed, 37 insertions(+), 61 deletions(-) diff --git a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy index 14cac15..af8c227 100644 --- a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy +++ b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy @@ -56,21 +56,17 @@ metadata { def parse(String description) { log.debug "description is $description" - def resultMap = zigbee.getKnownDescription(description) - if (resultMap) { - log.info resultMap - if (resultMap.type == "update") { - log.info "$device updates: ${resultMap.value}" - } - else if (resultMap.type == "power") { - def powerValue + def event = zigbee.getEvent(description) + if (event) { + log.info event + if (event.name == "power") { if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power - powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration - sendEvent(name: "power", value: powerValue) + event.value = (event.value as Integer) / 10 //TODO: The divisor value needs to be set as part of configuration + sendEvent(event) } } else { - sendEvent(name: resultMap.type, value: resultMap.value) + sendEvent(event) } } else { diff --git a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy index 770639c..72809f5 100644 --- a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy +++ b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy @@ -51,15 +51,9 @@ metadata { def parse(String description) { log.debug "description is $description" - def resultMap = zigbee.getKnownDescription(description) - if (resultMap) { - log.info resultMap - if (resultMap.type == "update") { - log.info "$device updates: ${resultMap.value}" - } - else { - sendEvent(name: resultMap.type, value: resultMap.value) - } + def event = zigbee.getEvent(description) + if (event) { + sendEvent(event) } else { log.warn "DID NOT PARSE MESSAGE for description : $description" diff --git a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy index 39e78d2..2c6bcfc 100644 --- a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy +++ b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy @@ -83,32 +83,19 @@ def uninstalled() { } def configure() { - /* def cmds = - zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}", - "${TYPE_ENUM8}", 0, 3600, "{01}") + - zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}", - "${TYPE_U8}", 600, 21600, "{01}") - */ - def zigbeeId = device.zigbeeId - def cmds = - [ - "zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_DOORLOCK} {$zigbeeId} {}", "delay 200", - "zcl global send-me-a-report ${CLUSTER_DOORLOCK} ${DOORLOCK_ATTR_LOCKSTATE} ${TYPE_ENUM8} 0 3600 {01}", "delay 200", - "send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200", - - "zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_POWER} {$zigbeeId} {}", "delay 200", - "zcl global send-me-a-report ${CLUSTER_POWER} ${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING} ${TYPE_U8} 600 21600 {01}", "delay 200", - "send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200", - ] + zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE, + TYPE_ENUM8, 0, 3600, null) + + zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, + TYPE_U8, 600, 21600, 0x01) log.info "configure() --- cmds: $cmds" return cmds + refresh() // send refresh cmds as part of config } def refresh() { def cmds = - zigbee.refreshData("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}") + - zigbee.refreshData("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}") + zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE) + + zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) log.info "refresh() --- cmds: $cmds" return cmds } @@ -121,34 +108,27 @@ def parse(String description) { map = parseReportAttributeMessage(description) } - log.debug "parse() --- Parse returned $map" def result = map ? createEvent(map) : null + log.debug "parse() --- returned: $result" return result } // Lock capability commands def lock() { - //def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}") - //log.info "lock() -- cmds: $cmds" - //return cmds - "st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_LOCK_DOOR} {}" + def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_LOCK_DOOR) + log.info "lock() -- cmds: $cmds" + return cmds } def unlock() { - //def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}") - //log.info "unlock() -- cmds: $cmds" - //return cmds - "st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_UNLOCK_DOOR} {}" + def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_UNLOCK_DOOR) + log.info "unlock() -- cmds: $cmds" + return cmds } // Private methods private Map parseReportAttributeMessage(String description) { - log.trace "parseReportAttributeMessage() --- description: $description" - Map descMap = zigbee.parseDescriptionAsMap(description) - - log.debug "parseReportAttributeMessage() --- descMap: $descMap" - Map resultMap = [:] if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) { resultMap.name = "battery" @@ -156,18 +136,24 @@ private Map parseReportAttributeMessage(String description) { if (device.getDataValue("manufacturer") == "Yale") { //Handling issue with Yale locks incorrect battery reporting resultMap.value = Integer.parseInt(descMap.value, 16) } - log.info "parseReportAttributeMessage() --- battery: ${resultMap.value}" } else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) { def value = Integer.parseInt(descMap.value, 16) + def linkText = getLinkText(device) resultMap.name = "lock" - resultMap.putAll([0:["value":"unknown", - "descriptionText":"Not fully locked"], - 1:["value":"locked"], - 2:["value":"unlocked"]].get(value, - ["value":"unknown", - "descriptionText":"Unknown lock state"])) - log.info "parseReportAttributeMessage() --- lock: ${resultMap.value}" + if (value == 0) { + resultMap.value = "unknown" + resultMap.descriptionText = "${linkText} is not fully locked" + } else if (value == 1) { + resultMap.value = "locked" + resultMap.descriptionText = "${linkText} is locked" + } else if (value == 2) { + resultMap.value = "unlocked" + resultMap.descriptionText = "${linkText} is unlocked" + } else { + resultMap.value = "unknown" + resultMap.descriptionText = "${linkText} is in unknown lock state" + } } else { log.debug "parseReportAttributeMessage() --- ignoring attribute" From ffd0dd154582a56cb9616a0de7c885df695b6526 Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Wed, 23 Mar 2016 12:25:40 -0700 Subject: [PATCH 07/11] DVCSMP-1615 Hue setColor throws exceptions when missing parameters -DVCSMP-1637 Color picker set the light on but status not updated on the detail page -DVCSMP-1638 Temperature control selection set the light on but status not updated on the detail page -Added a lot of error checking for input parameters -Fixed some data parsing exceptions --- .../smartthings/hue-bulb.src/hue-bulb.groovy | 99 ++++++++++++++----- .../hue-lux-bulb.src/hue-lux-bulb.groovy | 8 +- .../hue-connect.src/hue-connect.groovy | 70 +++++++------ 3 files changed, 123 insertions(+), 54 deletions(-) diff --git a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy index a5d41ef..99b529b 100644 --- a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy +++ b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy @@ -103,62 +103,104 @@ void nextLevel() { } void setLevel(percent) { - log.debug "Executing 'setLevel'" - parent.setLevel(this, percent) - sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%") + log.debug "Executing 'setLevel'" + if (verifyPercent(percent)) { + parent.setLevel(this, percent) + sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%") + sendEvent(name: "switch", value: "on") + } } void setSaturation(percent) { - log.debug "Executing 'setSaturation'" - parent.setSaturation(this, percent) - sendEvent(name: "saturation", value: percent, displayed: false) + log.debug "Executing 'setSaturation'" + if (verifyPercent(percent)) { + parent.setSaturation(this, percent) + sendEvent(name: "saturation", value: percent, displayed: false) + } } void setHue(percent) { - log.debug "Executing 'setHue'" - parent.setHue(this, percent) - sendEvent(name: "hue", value: percent, displayed: false) + log.debug "Executing 'setHue'" + if (verifyPercent(percent)) { + parent.setHue(this, percent) + sendEvent(name: "hue", value: percent, displayed: false) + } } void setColor(value) { - log.debug "setColor: ${value}, $this" - parent.setColor(this, value) - if (value.hue) { sendEvent(name: "hue", value: value.hue, displayed: false)} - if (value.saturation) { sendEvent(name: "saturation", value: value.saturation, displayed: false)} - if (value.hex) { sendEvent(name: "color", value: value.hex)} - if (value.level) { sendEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%")} - sendEvent(name: "switch", value: "on") + log.debug "setColor: ${value}, $this" + def events = [] + def validValues = [:] + + if (verifyPercent(value.hue)) { + events << createEvent(name: "hue", value: value.hue, displayed: false) + validValues.hue = value.hue + } + if (verifyPercent(value.saturation)) { + events << createEvent(name: "saturation", value: value.saturation, displayed: false) + validValues.saturation = value.saturation + } + if (value.hex != null) { + if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) { + events << createEvent(name: "color", value: value.hex) + validValues.hex = value.hex + } else { + log.warn "$value.hex is not a valid color" + } + } + if (verifyPercent(value.level)) { + events << createEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%") + validValues.level = value.level + } + if (value.switch == "off" || (value.level != null && value.level <= 0)) { + events << createEvent(name: "switch", value: "off") + validValues.switch = "off" + } else { + events << createEvent(name: "switch", value: "on") + validValues.switch = "on" + } + if (!events.isEmpty()) { + parent.setColor(this, validValues) + } + events.each { + sendEvent(it) + } } void reset() { - log.debug "Executing 'reset'" + log.debug "Executing 'reset'" def value = [level:100, saturation:56, hue:23] setAdjustedColor(value) - parent.poll() + parent.poll() } void setAdjustedColor(value) { - if (value) { + if (value) { log.trace "setAdjustedColor: ${value}" def adjusted = value + [:] adjusted.hue = adjustOutgoingHue(value.hue) // Needed because color picker always sends 100 adjusted.level = null setColor(adjusted) + } else { + log.warn "Invalid color input" } } void setColorTemperature(value) { - if (value) { + if (value) { log.trace "setColorTemperature: ${value}k" parent.setColorTemperature(this, value) sendEvent(name: "colorTemperature", value: value) - } + sendEvent(name: "switch", value: "on") + } else { + log.warn "Invalid color temperature" + } } void refresh() { - log.debug "Executing 'refresh'" - parent.manualRefresh() + log.debug "Executing 'refresh'" + parent.manualRefresh() } def adjustOutgoingHue(percent) { @@ -177,3 +219,14 @@ def adjustOutgoingHue(percent) { log.info "percent: $percent, adjusted: $adjusted" adjusted } + +def verifyPercent(percent) { + if (percent == null) + return false + else if (percent >= 0 && percent <= 100) { + return true + } else { + log.warn "$percent is not 0-100" + return false + } +} \ No newline at end of file diff --git a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy index 18affa4..408c2e2 100644 --- a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy +++ b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy @@ -79,8 +79,12 @@ void off() { void setLevel(percent) { log.debug "Executing 'setLevel'" - parent.setLevel(this, percent) - sendEvent(name: "level", value: percent) + if (percent != null && percent >= 0 && percent <= 100) { + parent.setLevel(this, percent) + sendEvent(name: "level", value: percent) + } else { + log.warn "$percent is not 0-100" + } } void refresh() { diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index c76e105..1a8829a 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -657,39 +657,53 @@ def setColorTemperature(childDevice, huesettings) { } def setColor(childDevice, huesettings) { - log.debug "Executing 'setColor($huesettings)'" + log.debug "Executing 'setColor($huesettings)'" + + def value = [:] def hue = null def sat = null def xy = null - if (huesettings.hex) { - xy = getHextoXY(huesettings.hex) - } else if (huesettings.hue && huesettings.saturation) { - hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535) - sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255) + + if (huesettings.hex != null) { + value.xy = getHextoXY(huesettings.hex) + } else { + if (huesettings.hue != null) + value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535) + if (huesettings.saturation != null) + value.sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255) } - def alert = huesettings.alert ? huesettings.alert : "none" - def transition = huesettings.transition ? huesettings.transition : 4 + + // Default behavior is to turn light on + value.on = true - def value = [xy: xy, sat: sat, hue: hue, alert: alert, transitiontime: transition, on: true] + if (huesettings.level != null) { + if (huesettings.level <= 0) + value.on = false + else if (huesettings.level == 1) + value.bri = 1 + else + value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255) + } + value.alert = huesettings.alert ? huesettings.alert : "none" + value.transition = huesettings.transition ? huesettings.transition : 4 - if (huesettings.level != null) { - if (huesettings.level == 1) value.bri = 1 else value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255) - value.on = value.bri > 0 - } + // Make sure to turn off light if requested + if (huesettings.switch == "off") + value.on = false - log.debug "sending command $value" - put("lights/${getId(childDevice)}/state", value) + log.debug "sending command $value" + put("lights/${getId(childDevice)}/state", value) + return "Color set to $value" } def nextLevel(childDevice) { - def level = device.latestValue("level") as Integer ?: 0 - if (level < 100) { - level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer - } - else { - level = 25 - } - setLevel(childDevice,level) + def level = device.latestValue("level") as Integer ?: 0 + if (level < 100) { + level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer + } else { + level = 25 + } + setLevel(childDevice,level) } private getId(childDevice) { @@ -788,16 +802,14 @@ private getHextoXY(String colorStr) { // Make green more vivid if (normalizedToOne[1] > 0.04045) { - green = (float) Math.pow((normalizedToOne[1] + 0.055) - / (1.0 + 0.055), 2.4); + green = (float) Math.pow((normalizedToOne[1] + 0.055) / (1.0 + 0.055), 2.4); } else { green = (float) (normalizedToOne[1] / 12.92); } // Make blue more vivid if (normalizedToOne[2] > 0.04045) { - blue = (float) Math.pow((normalizedToOne[2] + 0.055) - / (1.0 + 0.055), 2.4); + blue = (float) Math.pow((normalizedToOne[2] + 0.055) / (1.0 + 0.055), 2.4); } else { blue = (float) (normalizedToOne[2] / 12.92); } @@ -806,8 +818,8 @@ private getHextoXY(String colorStr) { float Y = (float) (red * 0.234327 + green * 0.743075 + blue * 0.022598); float Z = (float) (red * 0.0000000 + green * 0.053077 + blue * 1.035763); - float x = X / (X + Y + Z); - float y = Y / (X + Y + Z); + float x = (X != 0 ? X / (X + Y + Z) : 0); + float y = (Y != 0 ? Y / (X + Y + Z) : 0); double[] xy = new double[2]; xy[0] = x; From 9374290d64155836f7540c6d0de8b778d8089512 Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Thu, 24 Mar 2016 13:12:23 -0700 Subject: [PATCH 08/11] Revert "Revert "DVCSMP-1615 & DEVC-372"" --- .../smartthings/hue-bulb.src/hue-bulb.groovy | 56 +++++--------- .../hue-lux-bulb.src/hue-lux-bulb.groovy | 9 +-- .../hue-connect.src/hue-connect.groovy | 73 +++++++++++++++++-- 3 files changed, 83 insertions(+), 55 deletions(-) diff --git a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy index f49ec95..a5d41ef 100644 --- a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy +++ b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy @@ -3,6 +3,7 @@ * * Author: SmartThings */ + // for the UI metadata { // Automatically generated. Make future change here. @@ -27,10 +28,10 @@ metadata { tiles (scale: 2){ multiAttributeTile(name:"rich-control", type: "lighting", width: 6, height: 4, canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { - attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" - attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" - attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" - attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn" } tileAttribute ("device.level", key: "SLIDER_CONTROL") { attributeState "level", action:"switch level.setLevel", range:"(0..100)" @@ -43,16 +44,10 @@ metadata { } } - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" - state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" - state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" - } - controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") { state "colorTemperature", action:"color temperature.setColorTemperature" } + valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "colorTemperature", label: '${currentValue} K' } @@ -60,29 +55,12 @@ metadata { standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single" } - standardTile("refresh", "device.switch", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { + + standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { - state "level", action:"switch level.setLevel" - } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { - state "level", label: 'Level ${currentValue}%' - } - controlTile("saturationSliderControl", "device.saturation", "slider", height: 1, width: 2, inactiveLabel: false) { - state "saturation", action:"color control.setSaturation" - } - valueTile("saturation", "device.saturation", inactiveLabel: false, decoration: "flat") { - state "saturation", label: 'Sat ${currentValue} ' - } - controlTile("hueSliderControl", "device.hue", "slider", height: 1, width: 2, inactiveLabel: false) { - state "hue", action:"color control.setHue" - } - valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") { - state "hue", label: 'Hue ${currentValue} ' - } - main(["switch"]) + main(["rich-control"]) details(["rich-control", "colorTempSliderControl", "colorTemp", "reset", "refresh"]) } } @@ -127,34 +105,34 @@ void nextLevel() { void setLevel(percent) { log.debug "Executing 'setLevel'" parent.setLevel(this, percent) - sendEvent(name: "level", value: percent) + sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%") } void setSaturation(percent) { log.debug "Executing 'setSaturation'" parent.setSaturation(this, percent) - sendEvent(name: "saturation", value: percent) + sendEvent(name: "saturation", value: percent, displayed: false) } void setHue(percent) { log.debug "Executing 'setHue'" parent.setHue(this, percent) - sendEvent(name: "hue", value: percent) + sendEvent(name: "hue", value: percent, displayed: false) } void setColor(value) { log.debug "setColor: ${value}, $this" parent.setColor(this, value) - if (value.hue) { sendEvent(name: "hue", value: value.hue)} - if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)} + if (value.hue) { sendEvent(name: "hue", value: value.hue, displayed: false)} + if (value.saturation) { sendEvent(name: "saturation", value: value.saturation, displayed: false)} if (value.hex) { sendEvent(name: "color", value: value.hex)} - if (value.level) { sendEvent(name: "level", value: value.level)} - if (value.switch) { sendEvent(name: "switch", value: value.switch)} + if (value.level) { sendEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%")} + sendEvent(name: "switch", value: "on") } void reset() { log.debug "Executing 'reset'" - def value = [level:100, hex:"#90C638", saturation:56, hue:23] + def value = [level:100, saturation:56, hue:23] setAdjustedColor(value) parent.poll() } diff --git a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy index 49e7f2f..18affa4 100644 --- a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy +++ b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy @@ -36,13 +36,6 @@ metadata { } } - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" - state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" - state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" - } - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { state "level", action:"switch level.setLevel" } @@ -51,7 +44,7 @@ metadata { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } - main(["switch"]) + main(["rich-control"]) details(["rich-control", "refresh"]) } } diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 93453ac..5b908d8 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -24,7 +24,7 @@ definition( category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png", - //singleInstance: true + singleInstance: true ) preferences { @@ -643,21 +643,25 @@ def setColorTemperature(childDevice, huesettings) { def setColor(childDevice, huesettings) { log.debug "Executing 'setColor($huesettings)'" - def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535) - def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255) + def hue = null + def sat = null + def xy = null + if (huesettings.hex) { + xy = getHextoXY(huesettings.hex) + } else if (huesettings.hue && huesettings.saturation) { + hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535) + sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255) + } def alert = huesettings.alert ? huesettings.alert : "none" def transition = huesettings.transition ? huesettings.transition : 4 - def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition] + def value = [xy: xy, sat: sat, hue: hue, alert: alert, transitiontime: transition, on: true] + if (huesettings.level != null) { if (huesettings.level == 1) value.bri = 1 else value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255) value.on = value.bri > 0 } - if (huesettings.switch) { - value.on = huesettings.switch == "on" - } - log.debug "sending command $value" put("lights/${getId(childDevice)}/state", value) } @@ -743,6 +747,59 @@ private getBridgeIP() { return host } +private getHextoXY(String colorStr) { + // For the hue bulb the corners of the triangle are: + // -Red: 0.675, 0.322 + // -Green: 0.4091, 0.518 + // -Blue: 0.167, 0.04 + + def cred = Integer.valueOf( colorStr.substring( 1, 3 ), 16 ) + def cgreen = Integer.valueOf( colorStr.substring( 3, 5 ), 16 ) + def cblue = Integer.valueOf( colorStr.substring( 5, 7 ), 16 ) + + double[] normalizedToOne = new double[3]; + normalizedToOne[0] = (cred / 255); + normalizedToOne[1] = (cgreen / 255); + normalizedToOne[2] = (cblue / 255); + float red, green, blue; + + // Make red more vivid + if (normalizedToOne[0] > 0.04045) { + red = (float) Math.pow( + (normalizedToOne[0] + 0.055) / (1.0 + 0.055), 2.4); + } else { + red = (float) (normalizedToOne[0] / 12.92); + } + + // Make green more vivid + if (normalizedToOne[1] > 0.04045) { + green = (float) Math.pow((normalizedToOne[1] + 0.055) + / (1.0 + 0.055), 2.4); + } else { + green = (float) (normalizedToOne[1] / 12.92); + } + + // Make blue more vivid + if (normalizedToOne[2] > 0.04045) { + blue = (float) Math.pow((normalizedToOne[2] + 0.055) + / (1.0 + 0.055), 2.4); + } else { + blue = (float) (normalizedToOne[2] / 12.92); + } + + float X = (float) (red * 0.649926 + green * 0.103455 + blue * 0.197109); + float Y = (float) (red * 0.234327 + green * 0.743075 + blue * 0.022598); + float Z = (float) (red * 0.0000000 + green * 0.053077 + blue * 1.035763); + + float x = X / (X + Y + Z); + float y = Y / (X + Y + Z); + + double[] xy = new double[2]; + xy[0] = x; + xy[1] = y; + return xy; +} + private Integer convertHexToInt(hex) { Integer.parseInt(hex,16) } From a8628b734308626021806ac0f2f07ef2f6c7e7ac Mon Sep 17 00:00:00 2001 From: tslagle13 Date: Tue, 29 Mar 2016 17:12:28 -0700 Subject: [PATCH 09/11] Make timeIntervalInput dynamic page Forgot to make timeIntervalInput a dynamic page so it can update view on selection. --- .../vacation-lighting-director.groovy | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy b/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy index 8ea150e..517d5ee 100644 --- a/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy +++ b/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy @@ -40,6 +40,7 @@ preferences { page name:"pageSetup" page name:"Setup" page name:"Settings" + page name: "timeIntervalInput" } @@ -185,7 +186,8 @@ def Settings() { } } -page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) { +def timeIntervalInput() { + dynamicPage(name: "timeIntervalInput") { section { input "startTimeType", "enum", title: "Starting at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true if (startTimeType in ["sunrise","sunset"]) { @@ -204,9 +206,10 @@ page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfte input "ending", "time", title: "End time", required: false } } - + } } + def installed() { initialize() } From 1cd5c68e6835c85a19503fe966f87236bb5cba13 Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Wed, 30 Mar 2016 13:58:53 -0700 Subject: [PATCH 10/11] DVCSMP-1667 Authentication needs to be updated for Philips Hue -Philips updaded their API and current ST implementation will stop working when next Hue firmware is released without this change --- smartapps/smartthings/hue-connect.src/hue-connect.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 5b908d8..c58ae6a 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -161,7 +161,7 @@ private sendDeveloperReq() { headers: [ HOST: host ], - body: [devicetype: "$token-0", username: "$token-0"]], "${selectedHue}")) + body: [devicetype: "$token-0"]], "${selectedHue}")) } private discoverHueBulbs() { From d419fb860636accb0666467dbaa9e50898b4eb0e Mon Sep 17 00:00:00 2001 From: sabersd Date: Thu, 31 Mar 2016 09:28:25 -0500 Subject: [PATCH 11/11] INTL-309 translating tiles --- .../smartpower-outlet.src/i18n/messages.properties | 4 ++++ .../smartpower-outlet.src/smartpower-outlet.groovy | 8 ++++---- .../i18n/messages.properties | 4 ++++ .../smartsense-multi-sensor.groovy | 12 ++++++------ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/devicetypes/smartthings/smartpower-outlet.src/i18n/messages.properties b/devicetypes/smartthings/smartpower-outlet.src/i18n/messages.properties index 49ce523..e423e19 100644 --- a/devicetypes/smartthings/smartpower-outlet.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartpower-outlet.src/i18n/messages.properties @@ -28,4 +28,8 @@ '''{{ device.displayName }} is On'''.ko={{ device.displayName }}켜졌습니다. '''{{ device.displayName }} is Off'''.ko={{ device.displayName }}꺼졌습니다. '''{{ device.displayName }} power is {{ value }} Watts'''.ko={{ device.displayName }} 전원은 {{ value }}와트입니다 +'''On'''.ko=켜기 +'''Off'''.ko=끄기 +'''Turning On'''.ko=켜기 +'''Turning Off'''.ko=끄기 #============================================================================== diff --git a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy index 0dea33e..176bcca 100644 --- a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy +++ b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy @@ -65,10 +65,10 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { - attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff" - attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" - attributeState "turningOn", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff" - attributeState "turningOff", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" + attributeState "on", label: 'On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff" + attributeState "off", label: 'Off', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" + attributeState "turningOn", label: 'Turning On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff" + attributeState "turningOff", label: 'Turning Off', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" } tileAttribute ("power", key: "SECONDARY_CONTROL") { attributeState "power", label:'${currentValue} W' diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties index 0f97e6c..be9cc7f 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties @@ -43,3 +43,7 @@ '''{{ device.displayName }} battery was {{ value }}%'''.ko={{ device.displayName }}남아있는 배터리는 {{ value }}%입니다. '''Updating device to garage sensor'''.ko=기기-차고 센서 업데이트 중 '''Updating device to open/close sensor'''.ko=기기-열림/닫힘 센서 업데이트 중 +'''Inactive'''.ko=비활성 +'''Active'''.ko=활성 +'''Open'''.ko=열다 +'''Closed'''.ko=닫은 \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy index ab26143..21d31b0 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy @@ -83,19 +83,19 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){ tileAttribute ("device.status", key: "PRIMARY_CONTROL") { - attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e" - attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821" + attributeState "open", label:'Open', icon:"st.contact.contact.open", backgroundColor:"#ffa81e" + attributeState "closed", label:'Closed', icon:"st.contact.contact.closed", backgroundColor:"#79b821" attributeState "garage-open", label:'Open', icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e" attributeState "garage-closed", label:'Closed', icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821" } } standardTile("contact", "device.contact", width: 2, height: 2) { - state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") - state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + state("open", label:'Open', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + state("closed", label:'Closed', icon:"st.contact.contact.closed", backgroundColor:"#79b821") } standardTile("acceleration", "device.acceleration", width: 2, height: 2) { - state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") - state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + state("active", label:'Active', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") + state("inactive", label:'Inactive', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") } valueTile("temperature", "device.temperature", width: 2, height: 2) { state("temperature", label:'${currentValue}°',