From 6123fbeea58dea5738f5fbd01e254204c52d985f Mon Sep 17 00:00:00 2001 From: Duncan McKee Date: Mon, 22 Feb 2016 13:52:56 -0500 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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;