mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-09 13:21:53 +00:00
Compare commits
3 Commits
MSA-899-1
...
simulated-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16d207f805 | ||
|
|
8a9d420d0c | ||
|
|
353493bd34 |
@@ -1,104 +0,0 @@
|
||||
/**
|
||||
* EnerTalk Energy Meter
|
||||
*
|
||||
* Copyright 2015 hyeon seok yang
|
||||
*
|
||||
* 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:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "EnerTalk Energy Meter", namespace: "Encored Technologies", author: "hyeon seok yang") {
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles(scale:2) {
|
||||
valueTile("view", "device.view", decoration: "flat") {
|
||||
state "view", label:' ${currentValue} kWh'
|
||||
}
|
||||
valueTile("month", "device.month", width: 6, height : 3, decoration: "flat") {
|
||||
state "month", label:' ${currentValue}'
|
||||
}
|
||||
valueTile("real", "device.real", width: 2, height : 2, decoration: "flat") {
|
||||
state "real", label:' ${currentValue}'
|
||||
}
|
||||
valueTile("tier", "device.tier", width: 2, height : 2, decoration: "flat") {
|
||||
state "tier", label:' ${currentValue}'
|
||||
}
|
||||
valueTile("plan", "device.plan", width: 2, height : 2, decoration: "flat") {
|
||||
state "plan", label:' ${currentValue}'
|
||||
}
|
||||
|
||||
htmlTile(name:"deepLink", action:"linkApp", whitelist:["code.jquery.com",
|
||||
"ajax.googleapis.com",
|
||||
"fonts.googleapis.com",
|
||||
"code.highcharts.com",
|
||||
"enertalk-card.encoredtech.com",
|
||||
"s3-ap-northeast-1.amazonaws.com",
|
||||
"s3.amazonaws.com",
|
||||
"ui-hub.encoredtech.com",
|
||||
"enertalk-auth.encoredtech.com",
|
||||
"api.encoredtech.com",
|
||||
"cdnjs.cloudflare.com",
|
||||
"encoredtech.com",
|
||||
"itunes.apple.com"], width:2, height:2){}
|
||||
|
||||
main (["view"])
|
||||
details (["month", "real", "tier", "plan", "deepLink"])
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
|
||||
path("/linkApp") {action: [ GET: "getLinkedApp" ]}
|
||||
}
|
||||
|
||||
def getLinkedApp() {
|
||||
def lang = clientLocale?.language
|
||||
if ("${lang}" == "ko") {
|
||||
lang = "<p style=\'margin-left:15vw; color: #aeaeb0;\'>기기 설정</p>"
|
||||
} else {
|
||||
lang = "<p style=\'margin-left:5vw; color: #aeaeb0;\'>Setup Device</p>"
|
||||
}
|
||||
renderHTML() {
|
||||
head {
|
||||
"""
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width, height=device-height">
|
||||
<style>
|
||||
#레이어_1 { margin-left : 17vw; width : 50vw; height : 50vw;}
|
||||
.st0{fill:#B5B6BB;}
|
||||
</style>
|
||||
"""
|
||||
}
|
||||
body {
|
||||
"""
|
||||
<div id="container">
|
||||
<a id="st-deep-link" href="#">
|
||||
<svg version="1.1" id="레이어_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve"><path class="st0" d="M20,0C9,0,0,9,0,20C0,30.5,8,39,18.2,40l3.8-4.8l-3.9-4.8c-4.9-0.9-8.6-5.2-8.6-10.4c0-5.8,4.7-10.5,10.5-10.5
|
||||
S30.5,14.2,30.5,20c0,5.1-3.7,9.4-8.5,10.3l3.7,4.5L21.8,40C32,39.1,40,30.5,40,20C40,9,31,0,20,0z"/></svg>
|
||||
</a>
|
||||
${lang}
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
|
||||
<script>
|
||||
var ua = navigator.userAgent.toLowerCase();
|
||||
var isAndroid = ua.indexOf("android") > -1;
|
||||
if(!isAndroid) {
|
||||
\$("#st-deep-link").attr("href", "https://itunes.apple.com/kr/app/enertalk-for-home/id1024660780?mt=8");
|
||||
} else {
|
||||
\$("#st-deep-link").attr("href", "market://details?id=com.ionicframework.enertalkhome874425");
|
||||
}
|
||||
</script>
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,627 +0,0 @@
|
||||
/**
|
||||
* Spruce Controller - Pre Release V2 10/11/2015
|
||||
*
|
||||
* Copyright 2015 Plaid Systems
|
||||
*
|
||||
* Author: NC
|
||||
* Date: 2015-11
|
||||
*
|
||||
* 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:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
-----------V3 updates-11-2015------------
|
||||
-Start program button updated to signal schedule check in Scheduler
|
||||
11/17 alarm "0" -> 0 (ln 305)
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Spruce Controller", namespace: "plaidsystems", author: "NCauffman") {
|
||||
capability "Switch"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Actuator"
|
||||
capability "Valve"
|
||||
|
||||
attribute "switch", "string"
|
||||
attribute "switch1", "string"
|
||||
attribute "switch2", "string"
|
||||
attribute "switch8", "string"
|
||||
attribute "switch5", "string"
|
||||
attribute "switch3", "string"
|
||||
attribute "switch4", "string"
|
||||
attribute "switch6", "string"
|
||||
attribute "switch7", "string"
|
||||
attribute "switch9", "string"
|
||||
attribute "switch10", "string"
|
||||
attribute "switch11", "string"
|
||||
attribute "switch12", "string"
|
||||
attribute "switch13", "string"
|
||||
attribute "switch14", "string"
|
||||
attribute "switch15", "string"
|
||||
attribute "switch16", "string"
|
||||
attribute "status", "string"
|
||||
|
||||
command "programOn"
|
||||
command "programOff"
|
||||
command "on"
|
||||
command "off"
|
||||
command "z1on"
|
||||
command "z1off"
|
||||
command "z2on"
|
||||
command "z2off"
|
||||
command "z3on"
|
||||
command "z3off"
|
||||
command "z4on"
|
||||
command "z4off"
|
||||
command "z5on"
|
||||
command "z5off"
|
||||
command "z6on"
|
||||
command "z6off"
|
||||
command "z7on"
|
||||
command "z7off"
|
||||
command "z8on"
|
||||
command "z8off"
|
||||
command "z9on"
|
||||
command "z9off"
|
||||
command "z10on"
|
||||
command "z10off"
|
||||
command "z11on"
|
||||
command "z11off"
|
||||
command "z12on"
|
||||
command "z12off"
|
||||
command "z13on"
|
||||
command "z13off"
|
||||
command "z14on"
|
||||
command "z14off"
|
||||
command "z15on"
|
||||
command "z15off"
|
||||
command "z16on"
|
||||
command "z16off"
|
||||
command "offtime"
|
||||
|
||||
command "refresh"
|
||||
command "rain"
|
||||
command "manual"
|
||||
command "setDisplay"
|
||||
|
||||
command "settingsMap"
|
||||
command "writeTime"
|
||||
command "writeType"
|
||||
command "notify"
|
||||
command "updated"
|
||||
|
||||
fingerprint endpointId: "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18", profileId: "0104", deviceId: "0002", deviceVersion: "00", inClusters: "0000,0003,0004,0005,0006,000F", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZ16-01"
|
||||
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
// status messages
|
||||
|
||||
// reply messages
|
||||
|
||||
}
|
||||
preferences {
|
||||
input description: "Press Configure button after making changes to these preferences", displayDuringSetup: true, type: "paragraph", element: "paragraph", title: ""
|
||||
input "RainEnable", "bool", title: "Rain Sensor Attached?", required: false, displayDuringSetup: true
|
||||
input "ManualTime", "number", title: "Automatic shutoff time when a zone is turned on manually?", required: false, displayDuringSetup: true
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles {
|
||||
|
||||
standardTile("status", "device.status") {
|
||||
state "schedule", label: 'Schedule Set', icon: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_225_t.png"
|
||||
state "finished", label: 'Spruce Finished', icon: "st.Outdoor.outdoor5", backgroundColor: "#46c2e8"
|
||||
state "raintoday", label: 'Rain Today', icon: "st.custom.wuk.nt_chancerain"
|
||||
state "rainy", label: 'Previous Rain', icon: "st.custom.wuk.nt_chancerain"
|
||||
state "raintom", label: 'Rain Tomorrow', icon: "st.custom.wuk.nt_chancerain"
|
||||
state "donewweek", label: 'Spruce Finished', icon: "st.Outdoor.outdoor5", backgroundColor: "#52c435"
|
||||
state "skipping", label: 'Skip Today', icon: "st.Outdoor.outdoor20", backgroundColor: "#36cfe3"
|
||||
state "moisture", label: '', icon: "st.Weather.weather2", backgroundColor: "#36cfe3"
|
||||
state "pause", label: 'PAUSE', icon: "st.contact.contact.open", backgroundColor: "#f2a51f"
|
||||
state "active", label: 'Active', icon: "st.Outdoor.outdoor12", backgroundColor: "#3DC72E"
|
||||
state "season", label: 'Seasonal Adjustment', icon: "st.Outdoor.outdoor17", backgroundColor: "#ffb900"
|
||||
state "disable", label: 'Disabled', icon: "st.secondary.off", backgroundColor: "#888888"
|
||||
state "warning", label: '', icon: "st.categories.damageAndDanger", backgroundColor: "#ffff7f"
|
||||
state "alarm", label: 'Alarm', icon: "st.categories.damageAndDanger", backgroundColor: "#f9240c"
|
||||
}
|
||||
standardTile("switch", "device.switch") {
|
||||
//state "programOff", label: 'Start Program', action: "programOn", icon: "st.sonos.play-icon", backgroundColor: "#a9a9a9"
|
||||
state "off", label: 'Start Program', action: "programOn", icon: "st.sonos.play-icon", backgroundColor: "#a9a9a9"
|
||||
state "programOn", label: 'Initialize Program', action: "programOff", icon: "st.contact.contact.open", backgroundColor: "#f6e10e"
|
||||
state "on", label: 'Program Running', action: "off", icon: "st.Outdoor.outdoor12", backgroundColor: "#3DC72E"
|
||||
}
|
||||
standardTile("rainsensor", "device.rainsensor") {
|
||||
state "rainSensrooff", label: 'Rain Sensor Clear', icon: "st.Weather.weather14", backgroundColor: "#a9a9a9"
|
||||
state "rainSensoron", label: 'Rain Detected', icon: "st.Weather.weather10", backgroundColor: "#f6e10e"
|
||||
}
|
||||
standardTile("switch1", "device.switch1") {
|
||||
state "z1off", label: '1', action: "z1on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z1on", label: '1', action: "z1off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("switch2", "device.switch2") {
|
||||
state "z2off", label: '2', action: "z2on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z2on", label: '2', action: "z2off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("switch3", "device.switch3", inactiveLabel: false) {
|
||||
state "z3off", label: '3', action: "z3on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z3on", label: '3', action: "z3off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("switch4", "device.switch4", inactiveLabel: false) {
|
||||
state "z4off", label: '4', action: "z4on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z4on", label: '4', action: "z4off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("switch5", "device.switch5", inactiveLabel: false) {
|
||||
state "z5off", label: '5', action: "z5on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z5on", label: '5', action: "z5off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("switch6", "device.switch6", inactiveLabel: false) {
|
||||
state "z6off", label: '6', action: "z6on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z6on", label: '6', action: "z6off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("switch7", "device.switch7", inactiveLabel: false) {
|
||||
state "z7off", label: '7', action: "z7on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z7on", label: '7', action: "z7off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("switch8", "device.switch8", inactiveLabel: false) {
|
||||
state "z8off", label: '8', action: "z8on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z8on", label: '8', action: "z8off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("switch9", "device.switch9", inactiveLabel: false) {
|
||||
state "z9off", label: '9', action: "z9on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z9on", label: '9', action: "z9off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("switch10", "device.switch10", inactiveLabel: false) {
|
||||
state "z10off", label: '10', action: "z10on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z10on", label: '10', action: "z10off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("switch11", "device.switch11", inactiveLabel: false) {
|
||||
state "z11off", label: '11', action: "z11on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z11on", label: '11', action: "z11off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("switch12", "device.switch12", inactiveLabel: false) {
|
||||
state "z12off", label: '12', action: "z12on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z12on", label: '12', action: "z12off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("switch13", "device.switch13", inactiveLabel: false) {
|
||||
state "z13off", label: '13', action: "z13on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z13on", label: '13', action: "z13off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("switch14", "device.switch14", inactiveLabel: false) {
|
||||
state "z14off", label: '14', action: "z14on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z14on", label: '14', action: "z14off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("switch15", "device.switch15", inactiveLabel: false) {
|
||||
state "z15off", label: '15', action: "z15on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z15on", label: '15', action: "z15off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("switch16", "device.switch16", inactiveLabel: false) {
|
||||
state "z16off", label: '16', action: "z16on", icon: "st.valves.water.closed", backgroundColor: "#ffffff"
|
||||
state "z16on", label: '16', action: "z16off", icon: "st.valves.water.open", backgroundColor: "#46c2e8"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", action: "refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||
}
|
||||
|
||||
main (["status"])
|
||||
details(["status","rainsensor","switch","switch1","switch2","switch3","switch4","switch5","switch6","switch7","switch8","switch9","switch10","switch11","switch12","switch13","switch14","switch15","switch16","refresh","configure"])
|
||||
}
|
||||
}
|
||||
|
||||
def programOn(){
|
||||
sendEvent(name: "switch", value: "programOn", descriptionText: "Program turned on")
|
||||
}
|
||||
|
||||
def programOff(){
|
||||
sendEvent(name: "switch", value: "off", descriptionText: "Program turned off")
|
||||
off()
|
||||
}
|
||||
|
||||
def updated(){
|
||||
log.debug "updated"
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
//log.debug "Parse description $description"
|
||||
def result = null
|
||||
def map = [:]
|
||||
if (description?.startsWith("read attr -")) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
//log.debug "Desc Map: $descMap"
|
||||
//using 000F cluster instead of 0006 (switch) because ST does not differentiate between EPs and processes all as switch
|
||||
if (descMap.cluster == "000F" && descMap.attrId == "0055") {
|
||||
log.debug "Zone"
|
||||
map = getZone(descMap)
|
||||
}
|
||||
else if (descMap.cluster == "0009" && descMap.attrId == "0000") {
|
||||
log.debug "Alarm"
|
||||
map = getAlarm(descMap)
|
||||
}
|
||||
}
|
||||
|
||||
if (map) {
|
||||
result = createEvent(map)
|
||||
}
|
||||
log.debug "Parse returned $map $result"
|
||||
return result
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
|
||||
def getZone(descMap){
|
||||
def map = [:]
|
||||
|
||||
def EP = Integer.parseInt(descMap.endpoint.trim(), 16)
|
||||
|
||||
String onoff
|
||||
if(descMap.value == "00"){
|
||||
onoff = "off"
|
||||
}
|
||||
else onoff = "on"
|
||||
|
||||
if (EP == 1){
|
||||
map.name = "switch"
|
||||
map.value = onoff
|
||||
map.descriptionText = "${device.displayName} turned sprinkler program $onoff"
|
||||
}
|
||||
|
||||
else if (EP == 18) {
|
||||
map.name = "rainsensor"
|
||||
map.value = "rainSensor" + onoff
|
||||
map.descriptionText = "${device.displayName} rain sensor is $onoff"
|
||||
}
|
||||
else {
|
||||
EP -= 1
|
||||
map.name = "switch" + EP
|
||||
map.value = "z" + EP + onoff
|
||||
map.descriptionText = "${device.displayName} turned Zone $EP $onoff"
|
||||
}
|
||||
|
||||
map.isStateChange = true
|
||||
map.displayed = true
|
||||
return map
|
||||
}
|
||||
|
||||
def getAlarm(descMap){
|
||||
def map = [:]
|
||||
map.name = "status"
|
||||
def alarmID = Integer.parseInt(descMap.value.trim(), 16)
|
||||
log.debug "${alarmID}"
|
||||
if(alarmID <= 0) map.descriptionText = "${device.displayName} has rebooted, no other alarms"
|
||||
else map.descriptionText = "${device.displayName} rebooted, reported error on zone ${alarmID - 1}, please check zone is working correctly"
|
||||
map.value = "alarm"
|
||||
map.isStateChange = true
|
||||
map.displayed = true
|
||||
return map
|
||||
}
|
||||
|
||||
//status notify and change status
|
||||
def notify(value, text){
|
||||
sendEvent(name:"status", value:"$value", descriptionText:"$text", isStateChange: true, display: false)
|
||||
|
||||
}
|
||||
|
||||
//prefrences - rain sensor, manual time
|
||||
def rain() {
|
||||
log.debug "Rain $RainEnable"
|
||||
if (RainEnable) "st wattr 0x${device.deviceNetworkId} 18 0x0F 0x51 0x10 {01}"
|
||||
else "st wattr 0x${device.deviceNetworkId} 18 0x0F 0x51 0x10 {00}"
|
||||
}
|
||||
def manual(){
|
||||
log.debug "Time $ManualTime"
|
||||
def mTime = 10
|
||||
if (ManualTime) mTime = ManualTime
|
||||
def manualTime = hex(mTime)
|
||||
"st wattr 0x${device.deviceNetworkId} 1 6 0x4002 0x21 {00${manualTime}}"
|
||||
|
||||
}
|
||||
|
||||
//write switch time settings map
|
||||
def settingsMap(WriteTimes, attrType){
|
||||
log.debug WriteTimes
|
||||
|
||||
def i = 1
|
||||
def runTime
|
||||
def sendCmds = []
|
||||
while(i <= 17){
|
||||
|
||||
if (WriteTimes."${i}"){
|
||||
runTime = hex(Integer.parseInt(WriteTimes."${i}"))
|
||||
log.debug "${i} : $runTime"
|
||||
|
||||
if (attrType == 4001) sendCmds.push("st wattr 0x${device.deviceNetworkId} ${i} 0x06 0x4001 0x21 {00${runTime}}")
|
||||
else sendCmds.push("st wattr 0x${device.deviceNetworkId} ${i} 0x06 0x4002 0x21 {00${runTime}}")
|
||||
sendCmds.push("delay 500")
|
||||
}
|
||||
i++
|
||||
}
|
||||
return sendCmds
|
||||
}
|
||||
|
||||
//send switch time
|
||||
def writeType(wEP, cycle){
|
||||
log.debug "wt ${wEP} ${cycle}"
|
||||
"st wattr 0x${device.deviceNetworkId} ${wEP} 0x06 0x4001 0x21 {00" + hex(cycle) + "}"
|
||||
}
|
||||
//send switch off time
|
||||
def writeTime(wEP, runTime){
|
||||
"st wattr 0x${device.deviceNetworkId} ${wEP} 0x06 0x4002 0x21 {00" + hex(runTime) + "}"
|
||||
}
|
||||
|
||||
//set reporting and binding
|
||||
def configure() {
|
||||
|
||||
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
log.debug "Confuguring Reporting and Bindings ${device.deviceNetworkId} ${device.zigbeeId}"
|
||||
sendEvent(name: 'configuration',value: 100, descriptionText: "Configuration initialized")
|
||||
|
||||
def configCmds = [
|
||||
//program on/off
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x09 {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
//zones 1-8
|
||||
"zdo bind 0x${device.deviceNetworkId} 2 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 3 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 4 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 5 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 6 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 7 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 8 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 9 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
//zones 9-16
|
||||
"zdo bind 0x${device.deviceNetworkId} 10 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 11 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 12 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 13 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 14 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 15 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 16 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 17 1 0x0F {${device.zigbeeId}} {}", "delay 1000",
|
||||
//rain sensor
|
||||
"zdo bind 0x${device.deviceNetworkId} 18 1 0x0F {${device.zigbeeId}} {}",
|
||||
|
||||
"zcl global send-me-a-report 6 0 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 2", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 3", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 4", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 5", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 6", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 7", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 8", "delay 500",
|
||||
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 9", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 10", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 11", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 12", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 13", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 14", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 15", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 16", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 17", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 18", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x09 0x00 0x21 1 0 {00}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||
]
|
||||
return configCmds + rain() + manual()
|
||||
}
|
||||
|
||||
|
||||
|
||||
private hex(value) {
|
||||
new BigInteger(Math.round(value).toString()).toString(16)
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
|
||||
log.debug "refresh"
|
||||
def refreshCmds = [
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x0F 0x55", "delay 500",
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 2 0x0F 0x55", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 3 0x0F 0x55", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 4 0x0F 0x55", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 5 0x0F 0x55", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 6 0x0F 0x55", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 7 0x0F 0x55", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 8 0x0F 0x55", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 9 0x0F 0x55", "delay 500",
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 10 0x0F 0x55", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 11 0x0F 0x55", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 12 0x0F 0x55", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 13 0x0F 0x55", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 14 0x0F 0x55", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 15 0x0F 0x55", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 16 0x0F 0x55", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 17 0x0F 0x55", "delay 500",
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 18 0x0F 0x51","delay 500",
|
||||
|
||||
]
|
||||
return refreshCmds + rain() + manual()
|
||||
}
|
||||
|
||||
// Commands to device
|
||||
//zones on - 8
|
||||
def on() {
|
||||
//sendEvent(name:"status", value:"active", descriptionText:"Program Running", isStateChange: true, display: false)
|
||||
log.debug "on"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
|
||||
}
|
||||
def off() {
|
||||
log.debug "off"
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
|
||||
}
|
||||
def z1on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 2 6 1 {}"
|
||||
}
|
||||
def z1off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 2 6 0 {}"
|
||||
}
|
||||
def z2on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 3 6 1 {}"
|
||||
}
|
||||
def z2off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 3 6 0 {}"
|
||||
}
|
||||
def z3on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 4 6 1 {}"
|
||||
}
|
||||
def z3off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 4 6 0 {}"
|
||||
}
|
||||
def z4on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 5 6 1 {}"
|
||||
}
|
||||
def z4off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 5 6 0 {}"
|
||||
}
|
||||
def z5on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 6 6 1 {}"
|
||||
}
|
||||
def z5off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 6 6 0 {}"
|
||||
}
|
||||
def z6on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 7 6 1 {}"
|
||||
}
|
||||
def z6off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 7 6 0 {}"
|
||||
}
|
||||
def z7on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 8 6 1 {}"
|
||||
}
|
||||
def z7off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 8 6 0 {}"
|
||||
}
|
||||
def z8on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 9 6 1 {}"
|
||||
}
|
||||
def z8off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 9 6 0 {}"
|
||||
}
|
||||
|
||||
//zones 9 - 16
|
||||
def z9on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 10 6 1 {}"
|
||||
}
|
||||
def z9off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 10 6 0 {}"
|
||||
}
|
||||
def z10on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 11 6 1 {}"
|
||||
}
|
||||
def z10off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 11 6 0 {}"
|
||||
}
|
||||
def z11on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 12 6 1 {}"
|
||||
}
|
||||
def z11off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 12 6 0 {}"
|
||||
}
|
||||
def z12on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 13 6 1 {}"
|
||||
}
|
||||
def z12off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 13 6 0 {}"
|
||||
}
|
||||
def z13on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 14 6 1 {}"
|
||||
}
|
||||
def z13off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 14 6 0 {}"
|
||||
}
|
||||
def z14on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 15 6 1 {}"
|
||||
}
|
||||
def z14off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 15 6 0 {}"
|
||||
}
|
||||
def z15on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 16 6 1 {}"
|
||||
}
|
||||
def z15off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 16 6 0 {}"
|
||||
}
|
||||
def z16on() {
|
||||
"st cmd 0x${device.deviceNetworkId} 17 6 1 {}"
|
||||
}
|
||||
def z16off() {
|
||||
"st cmd 0x${device.deviceNetworkId} 17 6 0 {}"
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
/**
|
||||
* Spruce Sensor -Pre-release V2 10/8/2015
|
||||
*
|
||||
* Copyright 2014 Plaid Systems
|
||||
*
|
||||
* 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:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
-------10/20/2015 Updates--------
|
||||
-Fix/add battery reporting interval to update
|
||||
-remove polling and/or refresh(?)
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Spruce Sensor", namespace: "plaidsystems", author: "NCauffman") {
|
||||
|
||||
capability "Configuration"
|
||||
capability "Battery"
|
||||
capability "Relative Humidity Measurement"
|
||||
capability "Temperature Measurement"
|
||||
capability "Sensor"
|
||||
//capability "Polling"
|
||||
|
||||
attribute "maxHum", "string"
|
||||
attribute "minHum", "string"
|
||||
|
||||
command "resetHumidity"
|
||||
command "refresh"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0402,0405", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-01"
|
||||
}
|
||||
|
||||
preferences {
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph", title: ""
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
input "interval", "number", title: "Measurement Interval 1-120 minutes (default: 10 minutes)", description: "Set how often you would like to check soil moisture in minutes", range: "1..120", defaultValue: 10, displayDuringSetup: false
|
||||
input "resetMinMax", "bool", title: "Reset Humidity min and max", required: false, displayDuringSetup: false
|
||||
}
|
||||
|
||||
tiles {
|
||||
valueTile("temperature", "device.temperature", canChangeIcon: false, canChangeBackground: false) {
|
||||
state "temperature", label:'${currentValue}°',
|
||||
backgroundColors:[
|
||||
[value: 31, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
}
|
||||
valueTile("humidity", "device.humidity", width: 2, height: 2, canChangeIcon: false, canChangeBackground: true) {
|
||||
state "humidity", label:'${currentValue}%', unit:"",
|
||||
backgroundColors:[
|
||||
[value: 0, color: "#635C0C"],
|
||||
[value: 16, color: "#EBEB21"],
|
||||
[value: 22, color: "#C7DE6A"],
|
||||
[value: 42, color: "#9AD290"],
|
||||
[value: 64, color: "#44B621"],
|
||||
[value: 80, color: "#3D79D9"],
|
||||
[value: 96, color: "#0A50C2"]
|
||||
]
|
||||
}
|
||||
|
||||
valueTile("maxHum", "device.maxHum", canChangeIcon: false, canChangeBackground: false) {
|
||||
state "maxHum", label:'High ${currentValue}%', unit:"",
|
||||
backgroundColors:[
|
||||
[value: 0, color: "#635C0C"],
|
||||
[value: 16, color: "#EBEB21"],
|
||||
[value: 22, color: "#C7DE6A"],
|
||||
[value: 42, color: "#9AD290"],
|
||||
[value: 64, color: "#44B621"],
|
||||
[value: 80, color: "#3D79D9"],
|
||||
[value: 96, color: "#0A50C2"]
|
||||
]
|
||||
}
|
||||
valueTile("minHum", "device.minHum", canChangeIcon: false, canChangeBackground: false) {
|
||||
state "minHum", label:'Low ${currentValue}%', unit:"",
|
||||
backgroundColors:[
|
||||
[value: 0, color: "#635C0C"],
|
||||
[value: 16, color: "#EBEB21"],
|
||||
[value: 22, color: "#C7DE6A"],
|
||||
[value: 42, color: "#9AD290"],
|
||||
[value: 64, color: "#44B621"],
|
||||
[value: 80, color: "#3D79D9"],
|
||||
[value: 96, color: "#0A50C2"]
|
||||
]
|
||||
}
|
||||
|
||||
valueTile("battery", "device.battery", decoration: "flat", canChangeIcon: false, canChangeBackground: false) {
|
||||
state "battery", label:'${currentValue}% battery'
|
||||
}
|
||||
|
||||
main (["humidity"])
|
||||
details(["humidity","maxHum","minHum","temperature","battery"])
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
log.debug "Parse description $description config: ${device.latestValue('configuration')} interval: $interval"
|
||||
|
||||
Map map = [:]
|
||||
|
||||
if (description?.startsWith('catchall:')) {
|
||||
map = parseCatchAllMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('read attr -')) {
|
||||
map = parseReportAttributeMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('temperature: ') || description?.startsWith('humidity: ')) {
|
||||
map = parseCustomMessage(description)
|
||||
}
|
||||
def result = map ? createEvent(map) : null
|
||||
|
||||
//check in configuration change
|
||||
if (!device.latestValue('configuration')) result = poll()
|
||||
if (device.latestValue('configuration').toInteger() != interval && interval != null) {
|
||||
result = poll()
|
||||
}
|
||||
log.debug "result: $result"
|
||||
return result
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def linkText = getLinkText(device)
|
||||
//log.debug "Catchall"
|
||||
def descMap = zigbee.parse(description)
|
||||
|
||||
//check humidity configuration is complete
|
||||
if (descMap.command == 0x07 && descMap.clusterId == 0x0405){
|
||||
def configInterval = 10
|
||||
if (interval != null) configInterval = interval
|
||||
sendEvent(name: 'configuration',value: configInterval, descriptionText: "Configuration Successful")
|
||||
//setConfig()
|
||||
log.debug "config complete"
|
||||
//return resultMap = [name: 'configuration', value: configInterval, descriptionText: "Settings configured successfully"]
|
||||
}
|
||||
else if (descMap.command == 0x0001){
|
||||
def hexString = "${hex(descMap.data[5])}" + "${hex(descMap.data[4])}"
|
||||
def intString = Integer.parseInt(hexString, 16)
|
||||
//log.debug "command: $descMap.command clusterid: $descMap.clusterId $hexString $intString"
|
||||
|
||||
if (descMap.clusterId == 0x0402){
|
||||
def value = getTemperature(hexString)
|
||||
resultMap = getTemperatureResult(value)
|
||||
}
|
||||
else if (descMap.clusterId == 0x0405){
|
||||
def value = Math.round(new BigDecimal(intString / 100)).toString()
|
||||
resultMap = getHumidityResult(value)
|
||||
|
||||
}
|
||||
else return null
|
||||
}
|
||||
else return null
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
log.debug "Desc Map: $descMap"
|
||||
log.debug "Report Attributes"
|
||||
|
||||
Map resultMap = [:]
|
||||
if (descMap.cluster == "0001" && descMap.attrId == "0000") {
|
||||
resultMap = getBatteryResult(descMap.value)
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
|
||||
log.debug "parseCustom"
|
||||
if (description?.startsWith('temperature: ')) {
|
||||
def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
|
||||
resultMap = getTemperatureResult(value)
|
||||
}
|
||||
else if (description?.startsWith('humidity: ')) {
|
||||
def pct = (description - "humidity: " - "%").trim()
|
||||
if (pct.isNumber()) {
|
||||
def value = Math.round(new BigDecimal(pct)).toString()
|
||||
resultMap = getHumidityResult(value)
|
||||
} else {
|
||||
log.error "invalid humidity: ${pct}"
|
||||
}
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map getHumidityResult(value) {
|
||||
def linkText = getLinkText(device)
|
||||
def maxHumValue = 0
|
||||
def minHumValue = 0
|
||||
if (device.currentValue("maxHum") != null) maxHumValue = device.currentValue("maxHum").toInteger()
|
||||
if (device.currentValue("minHum") != null) minHumValue = device.currentValue("minHum").toInteger()
|
||||
log.debug "Humidity max: ${maxHumValue} min: ${minHumValue}"
|
||||
def compare = value.toInteger()
|
||||
|
||||
if (compare > maxHumValue) {
|
||||
sendEvent(name: 'maxHum', value: value, unit: '%', descriptionText: "${linkText} soil moisture high is ${value}%")
|
||||
}
|
||||
else if (((compare < minHumValue) || (minHumValue <= 2)) && (compare != 0)) {
|
||||
sendEvent(name: 'minHum', value: value, unit: '%', descriptionText: "${linkText} soil moisture low is ${value}%")
|
||||
}
|
||||
|
||||
return [
|
||||
name: 'humidity',
|
||||
value: value,
|
||||
unit: '%',
|
||||
descriptionText: "${linkText} soil moisture is ${value}%"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
def getTemperature(value) {
|
||||
def celsius = (Integer.parseInt(value, 16).shortValue()/100)
|
||||
//log.debug "Report Temp $value : $celsius C"
|
||||
if(getTemperatureScale() == "C"){
|
||||
return celsius
|
||||
} else {
|
||||
return celsiusToFahrenheit(celsius) as Integer
|
||||
}
|
||||
}
|
||||
|
||||
private Map getTemperatureResult(value) {
|
||||
log.debug "Temperature: $value"
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
if (tempOffset) {
|
||||
def offset = tempOffset as int
|
||||
def v = value as int
|
||||
value = v + offset
|
||||
}
|
||||
def descriptionText = "${linkText} is ${value}°${temperatureScale}"
|
||||
return [
|
||||
name: 'temperature',
|
||||
value: value,
|
||||
descriptionText: descriptionText
|
||||
]
|
||||
}
|
||||
|
||||
private Map getBatteryResult(value) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def result = [
|
||||
name: 'battery'
|
||||
]
|
||||
|
||||
def min = 2500
|
||||
def percent = ((Integer.parseInt(value, 16) - min) / 5)
|
||||
percent = Math.max(0, Math.min(percent, 100.0))
|
||||
result.value = Math.round(percent)
|
||||
|
||||
def descriptionText
|
||||
if (percent < 10) result.descriptionText = "${linkText} battery is getting low $percent %."
|
||||
else result.descriptionText = "${linkText} battery is ${result.value}%"
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
def resetHumidity(){
|
||||
def linkText = getLinkText(device)
|
||||
def minHumValue = 0
|
||||
def maxHumValue = 0
|
||||
sendEvent(name: 'minHum', value: minHumValue, unit: '%', descriptionText: "${linkText} min soil moisture reset to ${minHumValue}%")
|
||||
sendEvent(name: 'maxHum', value: maxHumValue, unit: '%', descriptionText: "${linkText} max soil moisture reset to ${maxHumValue}%")
|
||||
}
|
||||
|
||||
def setConfig(){
|
||||
def configInterval = 100
|
||||
if (interval != null) configInterval = interval
|
||||
sendEvent(name: 'configuration',value: configInterval, descriptionText: "Configuration initialized")
|
||||
}
|
||||
|
||||
//when device preferences are changed
|
||||
def updated(){
|
||||
log.debug "device updated"
|
||||
if (!device.latestValue('configuration')) configure()
|
||||
else{
|
||||
if (resetMinMax == true) resetHumidity()
|
||||
if (device.latestValue('configuration').toInteger() != interval && interval != null){
|
||||
sendEvent(name: 'configuration',value: 0, descriptionText: "Settings changed and will update at next report. Measure interval set to ${interval} mins")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//poll
|
||||
def poll() {
|
||||
log.debug "poll called"
|
||||
List cmds = []
|
||||
if (!device.latestValue('configuration')) cmds += configure()
|
||||
else if (device.latestValue('configuration').toInteger() != interval && interval != null) {
|
||||
cmds += intervalUpdate()
|
||||
}
|
||||
//cmds += refresh()
|
||||
log.debug "commands $cmds"
|
||||
return cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||
}
|
||||
|
||||
//update intervals
|
||||
def intervalUpdate(){
|
||||
log.debug "intervalUpdate"
|
||||
def minReport = 10
|
||||
def maxReport = 610
|
||||
if (interval != null) {
|
||||
minReport = interval
|
||||
maxReport = interval * 61
|
||||
}
|
||||
[
|
||||
"zcl global send-me-a-report 0x405 0x0000 0x21 $minReport $maxReport {6400}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
"zcl global send-me-a-report 1 0x0000 0x21 0x0C 0 {0500}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "refresh"
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x405 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0"
|
||||
]
|
||||
}
|
||||
|
||||
//configure
|
||||
def configure() {
|
||||
//set minReport = measurement in minutes
|
||||
def minReport = 10
|
||||
def maxReport = 610
|
||||
|
||||
//String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
//log.debug "zigbeeid ${device.zigbeeId} deviceId ${device.deviceNetworkId}"
|
||||
if (!device.zigbeeId) sendEvent(name: 'configuration',value: 0, descriptionText: "Device Zigbee Id not found, remove and attempt to rejoin device")
|
||||
else sendEvent(name: 'configuration',value: 100, descriptionText: "Configuration initialized")
|
||||
//log.debug "Configuring Reporting and Bindings. min: $minReport max: $maxReport "
|
||||
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x405 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 1000",
|
||||
|
||||
//temperature
|
||||
"zcl global send-me-a-report 0x402 0x0000 0x29 1 0 {3200}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
//min = soil measure interval
|
||||
"zcl global send-me-a-report 0x405 0x0000 0x21 $minReport $maxReport {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
//min = battery measure interval 1 = 1 hour
|
||||
"zcl global send-me-a-report 1 0x0000 0x21 0x0C 0 {0500}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||
] + refresh()
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
new BigInteger(Math.round(value).toString()).toString(16)
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Arrival Sensor HA", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Tone"
|
||||
capability "Actuator"
|
||||
capability "Presence Sensor"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Configuration"
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,000F,0020", outClusters: "0003,0019",
|
||||
manufacturer: "SmartThings", model: "tagv4", deviceJoinName: "Arrival Sensor"
|
||||
}
|
||||
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.png"
|
||||
])
|
||||
}
|
||||
section {
|
||||
input "checkInterval", "enum", title: "Presence timeout (minutes)",
|
||||
defaultValue:"2", options: ["2", "3", "5"], displayDuringSetup: false
|
||||
}
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) {
|
||||
state "present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0"
|
||||
state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("beep", "device.beep", decoration: "flat") {
|
||||
state "beep", label:'', action:"tone.beep", icon:"st.secondary.beep", backgroundColor:"#ffffff"
|
||||
}
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
main "presence"
|
||||
details(["presence", "beep", "battery"])
|
||||
}
|
||||
}
|
||||
|
||||
def updated() {
|
||||
startTimer()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
def cmds = zigbee.configureReporting(0x0001, 0x0020, 0x20, 20, 20, 0x01)
|
||||
log.debug "configure -- cmds: ${cmds}"
|
||||
return cmds
|
||||
}
|
||||
|
||||
def beep() {
|
||||
log.debug "Sending Identify command to beep the sensor for 5 seconds"
|
||||
return zigbee.command(0x0003, 0x00, "0500")
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
state.lastCheckin = now()
|
||||
handlePresenceEvent(true)
|
||||
|
||||
if (description?.startsWith('read attr -')) {
|
||||
handleReportAttributeMessage(description)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
private handleReportAttributeMessage(String description) {
|
||||
def descMap = zigbee.parseDescriptionAsMap(description)
|
||||
|
||||
if (descMap.clusterInt == 0x0001 && descMap.attrInt == 0x0020) {
|
||||
handleBatteryEvent(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create battery event from reported battery voltage.
|
||||
*
|
||||
* @param volts Battery voltage in .1V increments
|
||||
*/
|
||||
private handleBatteryEvent(volts) {
|
||||
if (volts == 0 || volts == 255) {
|
||||
log.debug "Ignoring invalid value for voltage (${volts/10}V)"
|
||||
}
|
||||
else {
|
||||
def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
|
||||
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
|
||||
def minVolts = 15
|
||||
def maxVolts = 28
|
||||
|
||||
if (volts < minVolts)
|
||||
volts = minVolts
|
||||
else if (volts > maxVolts)
|
||||
volts = maxVolts
|
||||
def pct = batteryMap[volts]
|
||||
if (pct != null) {
|
||||
def linkText = getLinkText(device)
|
||||
def eventMap = [
|
||||
name: 'battery',
|
||||
value: pct,
|
||||
descriptionText: "${linkText} battery was ${pct}%"
|
||||
]
|
||||
log.debug "Creating battery event for voltage=${volts/10}V: ${eventMap}"
|
||||
sendEvent(eventMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handlePresenceEvent(present) {
|
||||
def wasPresent = device.currentState("presence")?.value == "present"
|
||||
if (!wasPresent && present) {
|
||||
log.debug "Sensor is present"
|
||||
startTimer()
|
||||
} else if (!present) {
|
||||
log.debug "Sensor is not present"
|
||||
stopTimer()
|
||||
}
|
||||
def linkText = getLinkText(device)
|
||||
def eventMap = [
|
||||
name: "presence",
|
||||
value: present ? "present" : "not present",
|
||||
linkText: linkText,
|
||||
descriptionText: "${linkText} has ${present ? 'arrived' : 'left'}",
|
||||
]
|
||||
log.debug "Creating presence event: ${eventMap}"
|
||||
sendEvent(eventMap)
|
||||
}
|
||||
|
||||
private startTimer() {
|
||||
log.debug "Scheduling periodic timer"
|
||||
schedule("0 * * * * ?", checkPresenceCallback)
|
||||
}
|
||||
|
||||
private stopTimer() {
|
||||
log.debug "Stopping periodic timer"
|
||||
unschedule()
|
||||
}
|
||||
|
||||
def checkPresenceCallback() {
|
||||
def timeSinceLastCheckin = (now() - state.lastCheckin) / 1000
|
||||
def theCheckInterval = (checkInterval ? checkInterval as int : 2) * 60
|
||||
log.debug "Sensor checked in ${timeSinceLastCheckin} seconds ago"
|
||||
if (timeSinceLastCheckin >= theCheckInterval) {
|
||||
handlePresenceEvent(false)
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,10 @@ metadata {
|
||||
capability "Polling"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles {
|
||||
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||
state("temperature", label:'${currentValue}°', unit:"F",
|
||||
@@ -52,12 +56,16 @@ metadata {
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "refresh called"
|
||||
log.debug "refresh..."
|
||||
poll()
|
||||
}
|
||||
|
||||
void poll() {
|
||||
log.debug "Executing 'poll' using parent SmartApp"
|
||||
parent.pollChild(this)
|
||||
|
||||
parent.pollChildren(this)
|
||||
}
|
||||
|
||||
//generate custom mobile activity feeds event
|
||||
def generateActivityFeedsEvent(notificationMessage) {
|
||||
sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true)
|
||||
}
|
||||
|
||||
@@ -19,49 +19,34 @@ metadata {
|
||||
definition (name: "Ecobee Thermostat", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Thermostat"
|
||||
capability "Temperature Measurement"
|
||||
capability "Polling"
|
||||
capability "Sensor"
|
||||
capability "Refresh"
|
||||
capability "Relative Humidity Measurement"
|
||||
capability "Refresh"
|
||||
|
||||
command "generateEvent"
|
||||
command "raiseSetpoint"
|
||||
command "lowerSetpoint"
|
||||
command "resumeProgram"
|
||||
command "switchMode"
|
||||
command "switchFanMode"
|
||||
command "generateEvent"
|
||||
command "raiseSetpoint"
|
||||
command "lowerSetpoint"
|
||||
command "resumeProgram"
|
||||
command "switchMode"
|
||||
|
||||
attribute "thermostatSetpoint","number"
|
||||
attribute "thermostatStatus","string"
|
||||
attribute "maxHeatingSetpoint", "number"
|
||||
attribute "minHeatingSetpoint", "number"
|
||||
attribute "maxCoolingSetpoint", "number"
|
||||
attribute "minCoolingSetpoint", "number"
|
||||
attribute "deviceTemperatureUnit", "number"
|
||||
attribute "thermostatSetpoint","number"
|
||||
attribute "thermostatStatus","string"
|
||||
}
|
||||
|
||||
tiles {
|
||||
simulator { }
|
||||
|
||||
tiles {
|
||||
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||
state("temperature", label:'${currentValue}°', unit:"F",
|
||||
backgroundColors:[
|
||||
// Celsius
|
||||
[value: 0, color: "#153591"],
|
||||
[value: 7, color: "#1e9cbb"],
|
||||
[value: 15, color: "#90d2a7"],
|
||||
[value: 23, color: "#44b621"],
|
||||
[value: 28, color: "#f1d801"],
|
||||
[value: 35, color: "#d04e00"],
|
||||
[value: 37, color: "#bc2323"],
|
||||
// Fahrenheit
|
||||
[value: 40, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
backgroundColors:[
|
||||
[value: 31, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
)
|
||||
}
|
||||
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||
@@ -73,15 +58,16 @@ metadata {
|
||||
state "updating", label:"Working", icon: "st.secondary.secondary"
|
||||
}
|
||||
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
||||
state "auto", action:"switchFanMode", nextState: "updating", icon: "st.thermostat.fan-auto"
|
||||
state "on", action:"switchFanMode", nextState: "updating", icon: "st.thermostat.fan-on"
|
||||
state "updating", label:"Working", icon: "st.secondary.secondary"
|
||||
state "auto", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "on"
|
||||
state "on", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "off"
|
||||
state "off", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "circulate"
|
||||
state "circulate", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "auto"
|
||||
}
|
||||
standardTile("upButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
state "setpoint", action:"raiseSetpoint", icon:"st.thermostat.thermostat-up"
|
||||
}
|
||||
valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") {
|
||||
state "thermostatSetpoint", label:'${currentValue}'
|
||||
state "thermostatSetpoint", label:'${currentValue}°'
|
||||
}
|
||||
valueTile("currentStatus", "device.thermostatStatus", height: 1, width: 2, decoration: "flat") {
|
||||
state "thermostatStatus", label:'${currentValue}', backgroundColor:"#ffffff"
|
||||
@@ -105,14 +91,11 @@ metadata {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("resumeProgram", "device.resumeProgram", inactiveLabel: false, decoration: "flat") {
|
||||
state "resume", action:"resumeProgram", nextState: "updating", label:'Resume', icon:"st.samsung.da.oven_ic_send"
|
||||
state "resume", action:"resumeProgram", nextState: "updating", label:'Resume Schedule', icon:"st.samsung.da.oven_ic_send"
|
||||
state "updating", label:"Working", icon: "st.secondary.secondary"
|
||||
}
|
||||
valueTile("humidity", "device.humidity", decoration: "flat") {
|
||||
state "humidity", label:'${currentValue}%'
|
||||
}
|
||||
main "temperature"
|
||||
details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "fanMode","humidity", "resumeProgram", "refresh"])
|
||||
details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"])
|
||||
}
|
||||
|
||||
preferences {
|
||||
@@ -124,6 +107,8 @@ metadata {
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
// TODO: handle '' attribute
|
||||
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
@@ -148,27 +133,16 @@ def generateEvent(Map results) {
|
||||
def isChange = false
|
||||
def isDisplayed = true
|
||||
def event = [name: name, linkText: linkText, descriptionText: getThermostatDescriptionText(name, value, linkText),
|
||||
handlerName: name]
|
||||
handlerName: name]
|
||||
|
||||
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint" ) {
|
||||
def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F
|
||||
sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue
|
||||
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
|
||||
def sendValue = value? convertTemperatureIfNeeded(value.toDouble(), "F", 1): value //API return temperature value in F
|
||||
isChange = isTemperatureStateChange(device, name, value.toString())
|
||||
isDisplayed = isChange
|
||||
event << [value: sendValue, isStateChange: isChange, displayed: isDisplayed]
|
||||
} else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") {
|
||||
def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F
|
||||
sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue
|
||||
event << [value: sendValue, displayed: false]
|
||||
} else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){
|
||||
} else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){
|
||||
isChange = isStateChange(device, name, value.toString())
|
||||
event << [value: value.toString(), isStateChange: isChange, displayed: false]
|
||||
} else if (name=="thermostatFanMode"){
|
||||
isChange = isStateChange(device, name, value.toString())
|
||||
event << [value: value.toString(), isStateChange: isChange, displayed: false]
|
||||
} else if (name=="humidity") {
|
||||
isChange = isStateChange(device, name, value.toString())
|
||||
event << [value: value.toString(), isStateChange: isChange, displayed: false, unit: "%"]
|
||||
} else {
|
||||
isChange = isStateChange(device, name, value.toString())
|
||||
isDisplayed = isChange
|
||||
@@ -184,19 +158,13 @@ def generateEvent(Map results) {
|
||||
//return descriptionText to be shown on mobile activity feed
|
||||
private getThermostatDescriptionText(name, value, linkText) {
|
||||
if(name == "temperature") {
|
||||
def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F
|
||||
sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue
|
||||
return "$linkText temperature is $sendValue ${location.temperatureScale}"
|
||||
return "$linkText temperature is $value°F"
|
||||
|
||||
} else if(name == "heatingSetpoint") {
|
||||
def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F
|
||||
sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue
|
||||
return "heating setpoint is $sendValue ${location.temperatureScale}"
|
||||
return "heating setpoint is $value°F"
|
||||
|
||||
} else if(name == "coolingSetpoint"){
|
||||
def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F
|
||||
sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue
|
||||
return "cooling setpoint is $sendValue ${location.temperatureScale}"
|
||||
return "cooling setpoint is $value°F"
|
||||
|
||||
} else if (name == "thermostatMode") {
|
||||
return "thermostat mode is ${value}"
|
||||
@@ -204,26 +172,26 @@ private getThermostatDescriptionText(name, value, linkText) {
|
||||
} else if (name == "thermostatFanMode") {
|
||||
return "thermostat fan mode is ${value}"
|
||||
|
||||
} else if (name == "humidity") {
|
||||
return "humidity is ${value} %"
|
||||
} else {
|
||||
return "${name} = ${value}"
|
||||
}
|
||||
}
|
||||
|
||||
void setHeatingSetpoint(setpoint) {
|
||||
log.debug "***heating setpoint $setpoint"
|
||||
setHeatingSetpoint(setpoint.toDouble())
|
||||
}
|
||||
|
||||
void setHeatingSetpoint(Double setpoint) {
|
||||
// def mode = device.currentValue("thermostatMode")
|
||||
def heatingSetpoint = setpoint
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toDouble()
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint")
|
||||
def minHeatingSetpoint = device.currentValue("minHeatingSetpoint")
|
||||
|
||||
//enforce limits of heatingSetpoint
|
||||
if (heatingSetpoint > maxHeatingSetpoint) {
|
||||
heatingSetpoint = maxHeatingSetpoint
|
||||
} else if (heatingSetpoint < minHeatingSetpoint) {
|
||||
heatingSetpoint = minHeatingSetpoint
|
||||
if (heatingSetpoint > 79) {
|
||||
heatingSetpoint = 79
|
||||
} else if (heatingSetpoint < 45) {
|
||||
heatingSetpoint = 45
|
||||
}
|
||||
|
||||
//enforce limits of heatingSetpoint vs coolingSetpoint
|
||||
@@ -233,34 +201,32 @@ void setHeatingSetpoint(setpoint) {
|
||||
|
||||
log.debug "Sending setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
|
||||
|
||||
def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint
|
||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
|
||||
|
||||
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||
if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
|
||||
if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) {
|
||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
||||
log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
} else {
|
||||
log.error "Error setHeatingSetpoint(setpoint)"
|
||||
log.error "Error setHeatingSetpoint(setpoint)" //This error is handled by the connect app
|
||||
}
|
||||
}
|
||||
|
||||
void setCoolingSetpoint(setpoint) {
|
||||
log.debug "***cooling setpoint $setpoint"
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||
setCoolingSetpoint(setpoint.toDouble())
|
||||
}
|
||||
|
||||
void setCoolingSetpoint(Double setpoint) {
|
||||
// def mode = device.currentValue("thermostatMode")
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toDouble()
|
||||
def coolingSetpoint = setpoint
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint")
|
||||
def minCoolingSetpoint = device.currentValue("minCoolingSetpoint")
|
||||
|
||||
|
||||
if (coolingSetpoint > maxCoolingSetpoint) {
|
||||
coolingSetpoint = maxCoolingSetpoint
|
||||
} else if (coolingSetpoint < minCoolingSetpoint) {
|
||||
coolingSetpoint = minCoolingSetpoint
|
||||
if (coolingSetpoint > 92) {
|
||||
coolingSetpoint = 92
|
||||
} else if (coolingSetpoint < 65) {
|
||||
coolingSetpoint = 65
|
||||
}
|
||||
|
||||
//enforce limits of heatingSetpoint vs coolingSetpoint
|
||||
@@ -270,18 +236,15 @@ void setCoolingSetpoint(setpoint) {
|
||||
|
||||
log.debug "Sending setCoolingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
|
||||
|
||||
def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint
|
||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
|
||||
|
||||
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||
if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
|
||||
if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) {
|
||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
||||
log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}"
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
} else {
|
||||
log.error "Error setCoolingSetpoint(setpoint)"
|
||||
log.error "Error setCoolingSetpoint(setpoint)" //This error is handled by the connect app
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,7 +278,7 @@ def modes() {
|
||||
}
|
||||
|
||||
def fanModes() {
|
||||
["on", "auto"]
|
||||
["off", "on", "auto", "circulate"]
|
||||
}
|
||||
|
||||
def switchMode() {
|
||||
@@ -344,15 +307,17 @@ def switchFanMode() {
|
||||
def returnCommand
|
||||
|
||||
switch (currentFanMode) {
|
||||
case "on":
|
||||
returnCommand = switchToFanMode("auto")
|
||||
case "fanAuto":
|
||||
returnCommand = switchToFanMode("fanOn")
|
||||
break
|
||||
case "auto":
|
||||
returnCommand = switchToFanMode("on")
|
||||
case "fanOn":
|
||||
returnCommand = switchToFanMode("fanCirculate")
|
||||
break
|
||||
case "fanCirculate":
|
||||
returnCommand = switchToFanMode("fanAuto")
|
||||
break
|
||||
|
||||
}
|
||||
if(!currentFanMode) { returnCommand = switchToFanMode("auto") }
|
||||
if(!currentFanMode) { returnCommand = switchToFanMode("fanOn") }
|
||||
returnCommand
|
||||
}
|
||||
|
||||
@@ -361,20 +326,25 @@ def switchToFanMode(nextMode) {
|
||||
log.debug "switching to fan mode: $nextMode"
|
||||
def returnCommand
|
||||
|
||||
if(nextMode == "auto") {
|
||||
if(!fanModes.contains("auto")) {
|
||||
if(nextMode == "fanAuto") {
|
||||
if(!fanModes.contains("fanAuto")) {
|
||||
returnCommand = fanAuto()
|
||||
} else {
|
||||
returnCommand = switchToFanMode("on")
|
||||
returnCommand = switchToFanMode("fanOn")
|
||||
}
|
||||
} else if(nextMode == "on") {
|
||||
if(!fanModes.contains("on")) {
|
||||
} else if(nextMode == "fanOn") {
|
||||
if(!fanModes.contains("fanOn")) {
|
||||
returnCommand = fanOn()
|
||||
} else {
|
||||
returnCommand = switchToFanMode("auto")
|
||||
returnCommand = switchToFanMode("fanCirculate")
|
||||
}
|
||||
} else if(nextMode == "fanCirculate") {
|
||||
if(!fanModes.contains("fanCirculate")) {
|
||||
returnCommand = fanCirculate()
|
||||
} else {
|
||||
returnCommand = switchToFanMode("fanAuto")
|
||||
}
|
||||
}
|
||||
|
||||
returnCommand
|
||||
}
|
||||
|
||||
@@ -384,10 +354,13 @@ def getDataByName(String name) {
|
||||
|
||||
def setThermostatMode(String value) {
|
||||
log.debug "setThermostatMode({$value})"
|
||||
|
||||
}
|
||||
|
||||
def setThermostatFanMode(String value) {
|
||||
|
||||
log.debug "setThermostatFanMode({$value})"
|
||||
|
||||
}
|
||||
|
||||
def generateModeEvent(mode) {
|
||||
@@ -430,10 +403,6 @@ def heat() {
|
||||
generateStatusEvent()
|
||||
}
|
||||
|
||||
def emergencyHeat() {
|
||||
auxHeatOnly()
|
||||
}
|
||||
|
||||
def auxHeatOnly() {
|
||||
log.debug "auxHeatOnly"
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
@@ -478,44 +447,26 @@ def auto() {
|
||||
|
||||
def fanOn() {
|
||||
log.debug "fanOn"
|
||||
String fanMode = "on"
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
// parent.setFanMode (this,"on")
|
||||
|
||||
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||
|
||||
def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint
|
||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
|
||||
|
||||
if (parent.setFanMode(this, heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) {
|
||||
generateFanModeEvent(fanMode)
|
||||
} else {
|
||||
log.debug "Error setting new mode."
|
||||
def currentFanMode = device.currentState("thermostatFanMode")?.value
|
||||
generateModeEvent(currentFanMode) // reset the tile back
|
||||
}
|
||||
}
|
||||
|
||||
def fanAuto() {
|
||||
log.debug "fanAuto"
|
||||
String fanMode = "auto"
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
// parent.setFanMode (this,"auto")
|
||||
|
||||
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||
}
|
||||
|
||||
def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint
|
||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
|
||||
def fanCirculate() {
|
||||
log.debug "fanCirculate"
|
||||
// parent.setFanMode (this,"circulate")
|
||||
|
||||
}
|
||||
|
||||
def fanOff() {
|
||||
log.debug "fanOff"
|
||||
// parent.setFanMode (this,"off")
|
||||
|
||||
if (parent.setFanMode(this, heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) {
|
||||
generateFanModeEvent(fanMode)
|
||||
} else {
|
||||
log.debug "Error setting new mode."
|
||||
def currentFanMode = device.currentState("thermostatFanMode")?.value
|
||||
generateModeEvent(currentFanMode) // reset the tile back
|
||||
}
|
||||
}
|
||||
|
||||
def generateSetpointEvent() {
|
||||
@@ -525,42 +476,20 @@ def generateSetpointEvent() {
|
||||
def mode = device.currentValue("thermostatMode")
|
||||
log.debug "Current Mode = ${mode}"
|
||||
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||
log.debug "Heating Setpoint = ${heatingSetpoint}"
|
||||
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||
log.debug "Cooling Setpoint = ${coolingSetpoint}"
|
||||
|
||||
def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint")
|
||||
def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint")
|
||||
def minHeatingSetpoint = device.currentValue("minHeatingSetpoint")
|
||||
def minCoolingSetpoint = device.currentValue("minCoolingSetpoint")
|
||||
|
||||
if(location.temperatureScale == "C")
|
||||
{
|
||||
maxHeatingSetpoint = roundC(maxHeatingSetpoint)
|
||||
maxCoolingSetpoint = roundC(maxCoolingSetpoint)
|
||||
minHeatingSetpoint = roundC(minHeatingSetpoint)
|
||||
minCoolingSetpoint = roundC(minCoolingSetpoint)
|
||||
heatingSetpoint = roundC(heatingSetpoint)
|
||||
coolingSetpoint = roundC(coolingSetpoint)
|
||||
}
|
||||
|
||||
|
||||
sendEvent("name":"maxHeatingSetpoint", "value":maxHeatingSetpoint, "unit":location.temperatureScale)
|
||||
sendEvent("name":"maxCoolingSetpoint", "value":maxCoolingSetpoint, "unit":location.temperatureScale)
|
||||
sendEvent("name":"minHeatingSetpoint", "value":minHeatingSetpoint, "unit":location.temperatureScale)
|
||||
sendEvent("name":"minCoolingSetpoint", "value":minCoolingSetpoint, "unit":location.temperatureScale)
|
||||
|
||||
|
||||
if (mode == "heat") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint )
|
||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString())
|
||||
|
||||
}
|
||||
else if (mode == "cool") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint)
|
||||
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString())
|
||||
|
||||
} else if (mode == "auto") {
|
||||
|
||||
@@ -570,9 +499,9 @@ def generateSetpointEvent() {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":"Off")
|
||||
|
||||
} else if (mode == "auxHeatOnly") {
|
||||
} else if (mode == "emergencyHeat") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint)
|
||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString())
|
||||
|
||||
}
|
||||
|
||||
@@ -581,30 +510,26 @@ def generateSetpointEvent() {
|
||||
void raiseSetpoint() {
|
||||
def mode = device.currentValue("thermostatMode")
|
||||
def targetvalue
|
||||
def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint")
|
||||
def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint")
|
||||
|
||||
|
||||
if (mode == "off" || mode == "auto") {
|
||||
log.warn "this mode: $mode does not allow raiseSetpoint"
|
||||
} else {
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||
def thermostatSetpoint = device.currentValue("thermostatSetpoint")
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||
def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger()
|
||||
log.debug "raiseSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}"
|
||||
|
||||
if (device.latestState('thermostatSetpoint')) {
|
||||
targetvalue = device.latestState('thermostatSetpoint').value
|
||||
targetvalue = location.temperatureScale == "F"? targetvalue.toInteger() : targetvalue.toDouble()
|
||||
targetvalue = device.latestState('thermostatSetpoint').value as Integer
|
||||
} else {
|
||||
targetvalue = 0
|
||||
}
|
||||
targetvalue = location.temperatureScale == "F"? targetvalue + 1 : targetvalue + 0.5
|
||||
targetvalue = targetvalue + 1
|
||||
|
||||
if ((mode == "heat" || mode == "auxHeatOnly") && targetvalue > maxHeatingSetpoint) {
|
||||
targetvalue = maxHeatingSetpoint
|
||||
} else if (mode == "cool" && targetvalue > maxCoolingSetpoint) {
|
||||
targetvalue = maxCoolingSetpoint
|
||||
if (mode == "heat" && targetvalue > 79) {
|
||||
targetvalue = 79
|
||||
} else if (mode == "cool" && targetvalue > 92) {
|
||||
targetvalue = 92
|
||||
}
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false)
|
||||
@@ -618,29 +543,25 @@ void raiseSetpoint() {
|
||||
void lowerSetpoint() {
|
||||
def mode = device.currentValue("thermostatMode")
|
||||
def targetvalue
|
||||
def minHeatingSetpoint = device.currentValue("minHeatingSetpoint")
|
||||
def minCoolingSetpoint = device.currentValue("minCoolingSetpoint")
|
||||
|
||||
|
||||
if (mode == "off" || mode == "auto") {
|
||||
log.warn "this mode: $mode does not allow lowerSetpoint"
|
||||
} else {
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||
def thermostatSetpoint = device.currentValue("thermostatSetpoint")
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||
def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger()
|
||||
log.debug "lowerSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}"
|
||||
if (device.latestState('thermostatSetpoint')) {
|
||||
targetvalue = device.latestState('thermostatSetpoint').value
|
||||
targetvalue = location.temperatureScale == "F"? targetvalue.toInteger() : targetvalue.toDouble()
|
||||
targetvalue = device.latestState('thermostatSetpoint').value as Integer
|
||||
} else {
|
||||
targetvalue = 0
|
||||
}
|
||||
targetvalue = location.temperatureScale == "F"? targetvalue - 1 : targetvalue - 0.5
|
||||
targetvalue = targetvalue - 1
|
||||
|
||||
if ((mode == "heat" || mode == "auxHeatOnly") && targetvalue < minHeatingSetpoint) {
|
||||
targetvalue = minHeatingSetpoint
|
||||
} else if (mode == "cool" && targetvalue < minCoolingSetpoint) {
|
||||
targetvalue = minCoolingSetpoint
|
||||
if (mode == "heat" && targetvalue.toInteger() < 45) {
|
||||
targetvalue = 45
|
||||
} else if (mode == "cool" && targetvalue.toInteger() < 65) {
|
||||
targetvalue = 65
|
||||
}
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false)
|
||||
@@ -654,15 +575,15 @@ void lowerSetpoint() {
|
||||
void alterSetpoint(temp) {
|
||||
|
||||
def mode = device.currentValue("thermostatMode")
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
|
||||
def targetHeatingSetpoint
|
||||
def targetCoolingSetpoint
|
||||
|
||||
//step1: check thermostatMode, enforce limits before sending request to cloud
|
||||
if (mode == "heat" || mode == "auxHeatOnly"){
|
||||
if (mode == "heat"){
|
||||
if (temp.value > coolingSetpoint){
|
||||
targetHeatingSetpoint = temp.value
|
||||
targetCoolingSetpoint = temp.value
|
||||
@@ -681,22 +602,19 @@ void alterSetpoint(temp) {
|
||||
}
|
||||
}
|
||||
|
||||
log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to $targetHeatingSetpoint " +
|
||||
"coolingSetpoint to $targetCoolingSetpoint with holdType : ${holdType}"
|
||||
log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to ${targetHeatingSetpoint} " +
|
||||
"coolingSetpoint to ${targetCoolingSetpoint} with holdType : ${holdType}"
|
||||
|
||||
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||
|
||||
def coolingValue = location.temperatureScale == "C"? convertCtoF(targetCoolingSetpoint) : targetCoolingSetpoint
|
||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(targetHeatingSetpoint) : targetHeatingSetpoint
|
||||
|
||||
if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
|
||||
sendEvent("name": "thermostatSetpoint", "value": temp.value, displayed: false)
|
||||
//step2: call parent.setHold to send http request to 3rd party cloud
|
||||
if (parent.setHold(this, targetHeatingSetpoint, targetCoolingSetpoint, deviceId, sendHoldType)) {
|
||||
sendEvent("name": "thermostatSetpoint", "value": temp.value.toString(), displayed: false)
|
||||
sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint)
|
||||
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint)
|
||||
log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}"
|
||||
} else {
|
||||
log.error "Error alterSetpoint()"
|
||||
if (mode == "heat" || mode == "auxHeatOnly"){
|
||||
if (mode == "heat"){
|
||||
sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false)
|
||||
} else if (mode == "cool") {
|
||||
sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false)
|
||||
@@ -708,9 +626,9 @@ void alterSetpoint(temp) {
|
||||
def generateStatusEvent() {
|
||||
|
||||
def mode = device.currentValue("thermostatMode")
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||
def temperature = device.currentValue("temperature")
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||
def temperature = device.currentValue("temperature").toInteger()
|
||||
|
||||
def statusText
|
||||
|
||||
@@ -725,14 +643,14 @@ def generateStatusEvent() {
|
||||
if (temperature >= heatingSetpoint)
|
||||
statusText = "Right Now: Idle"
|
||||
else
|
||||
statusText = "Heating to ${heatingSetpoint} ${location.temperatureScale}"
|
||||
statusText = "Heating to ${heatingSetpoint}° F"
|
||||
|
||||
} else if (mode == "cool") {
|
||||
|
||||
if (temperature <= coolingSetpoint)
|
||||
statusText = "Right Now: Idle"
|
||||
else
|
||||
statusText = "Cooling to ${coolingSetpoint} ${location.temperatureScale}"
|
||||
statusText = "Cooling to ${coolingSetpoint}° F"
|
||||
|
||||
} else if (mode == "auto") {
|
||||
|
||||
@@ -742,7 +660,7 @@ def generateStatusEvent() {
|
||||
|
||||
statusText = "Right Now: Off"
|
||||
|
||||
} else if (mode == "auxHeatOnly") {
|
||||
} else if (mode == "emergencyHeat") {
|
||||
|
||||
statusText = "Emergency Heat"
|
||||
|
||||
@@ -755,18 +673,7 @@ def generateStatusEvent() {
|
||||
sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true)
|
||||
}
|
||||
|
||||
//generate custom mobile activity feeds event
|
||||
def generateActivityFeedsEvent(notificationMessage) {
|
||||
sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true)
|
||||
}
|
||||
|
||||
def roundC (tempC) {
|
||||
return (Math.round(tempC.toDouble() * 2))/2
|
||||
}
|
||||
|
||||
def convertFtoC (tempF) {
|
||||
return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2)
|
||||
}
|
||||
|
||||
def convertCtoF (tempC) {
|
||||
return (Math.round(tempC * (9/5)) + 32).toInteger()
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Gentle Wake Up Controller", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Switch"
|
||||
capability "Timed Session"
|
||||
|
||||
attribute "percentComplete", "number"
|
||||
|
||||
command "setPercentComplete", ["number"]
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
|
||||
multiAttributeTile(name: "richTile", type:"generic", width:6, height:4) {
|
||||
tileAttribute("sessionStatus", key: "PRIMARY_CONTROL") {
|
||||
attributeState "cancelled", action: "timed session.start", icon: "http://f.cl.ly/items/322n181j2K3f281r2s0A/playbutton.png", backgroundColor: "#ffffff", nextState: "running"
|
||||
attributeState "stopped", action: "timed session.start", icon: "http://f.cl.ly/items/322n181j2K3f281r2s0A/playbutton.png", backgroundColor: "#ffffff", nextState: "cancelled"
|
||||
attributeState "running", action: "timed session.stop", icon: "http://f.cl.ly/items/0B3y3p2V3X2l3P3y3W09/stopbutton.png", backgroundColor: "#79b821", nextState: "cancelled"
|
||||
}
|
||||
tileAttribute("timeRemaining", key: "SECONDARY_CONTROL") {
|
||||
attributeState "timeRemaining", label:'${currentValue} remaining'
|
||||
}
|
||||
tileAttribute("percentComplete", key: "SLIDER_CONTROL") {
|
||||
attributeState "percentComplete", action: "timed session.setTimeRemaining"
|
||||
}
|
||||
}
|
||||
|
||||
// start/stop
|
||||
standardTile("sessionStatusTile", "sessionStatus", width: 1, height: 1, canChangeIcon: true) {
|
||||
state "cancelled", label: "Stopped", action: "timed session.start", backgroundColor: "#ffffff", icon: "http://f.cl.ly/items/1J1g0H2P0S1G1f2O1s1s/icon.png"
|
||||
state "stopped", label: "Stopped", action: "timed session.start", backgroundColor: "#ffffff", icon: "http://f.cl.ly/items/1J1g0H2P0S1G1f2O1s1s/icon.png"
|
||||
state "running", label: "Running", action: "timed session.stop", backgroundColor: "#79b821", icon: "http://f.cl.ly/items/1J1g0H2P0S1G1f2O1s1s/icon.png"
|
||||
}
|
||||
|
||||
// duration
|
||||
valueTile("timeRemainingTile", "timeRemaining", decoration: "flat", width: 2) {
|
||||
state "timeRemaining", label:'${currentValue} left'
|
||||
}
|
||||
controlTile("percentCompleteTile", "percentComplete", "slider", height: 1, width: 3) {
|
||||
state "percentComplete", action: "timed session.setTimeRemaining"
|
||||
}
|
||||
|
||||
main "sessionStatusTile"
|
||||
details "richTile"
|
||||
// details(["richTile", "sessionStatusTile", "timeRemainingTile", "percentCompleteTile"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
// TODO: handle 'switch' attribute
|
||||
// TODO: handle 'level' attribute
|
||||
// TODO: handle 'sessionStatus' attribute
|
||||
// TODO: handle 'timeRemaining' attribute
|
||||
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def on() {
|
||||
log.debug "Executing 'on'"
|
||||
startDimming()
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "Executing 'off'"
|
||||
stopDimming()
|
||||
}
|
||||
|
||||
def setTimeRemaining(percentComplete) {
|
||||
log.debug "Executing 'setTimeRemaining' to ${percentComplete}% complete"
|
||||
parent.jumpTo(percentComplete)
|
||||
}
|
||||
|
||||
def start() {
|
||||
log.debug "Executing 'start'"
|
||||
startDimming()
|
||||
}
|
||||
|
||||
def stop() {
|
||||
log.debug "Executing 'stop'"
|
||||
stopDimming()
|
||||
}
|
||||
|
||||
def pause() {
|
||||
log.debug "Executing 'pause'"
|
||||
// TODO: handle 'pause' command
|
||||
}
|
||||
|
||||
def cancel() {
|
||||
log.debug "Executing 'cancel'"
|
||||
stopDimming()
|
||||
}
|
||||
|
||||
def startDimming() {
|
||||
log.trace "startDimming"
|
||||
log.debug "parent: ${parent}"
|
||||
parent.start("controller")
|
||||
}
|
||||
|
||||
def stopDimming() {
|
||||
log.trace "stopDimming"
|
||||
log.debug "parent: ${parent}"
|
||||
parent.stop("controller")
|
||||
}
|
||||
|
||||
def controllerEvent(eventData) {
|
||||
log.trace "controllerEvent"
|
||||
sendEvent(eventData)
|
||||
}
|
||||
@@ -80,12 +80,19 @@ def parse(String description) {
|
||||
if (cmd) {
|
||||
result = zwaveEvent(cmd)
|
||||
}
|
||||
log.debug "Parsed ${description.inspect()} to ${result.inspect()}"
|
||||
// log.debug "Parsed ${description.inspect()} to ${result.inspect()}"
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.multiinstancev1.MultiInstanceCmdEncap cmd) {
|
||||
def encapsulated = cmd.encapsulatedCommand([0x31: 1, 0x84: 2, 0x60: 1, 0x85: 1, 0x70: 1])
|
||||
def encapsulated = null
|
||||
if (cmd.respondsTo("encapsulatedCommand")) {
|
||||
encapsulated = cmd.encapsulatedCommand()
|
||||
} else {
|
||||
def hex1 = { n -> String.format("%02X", n) }
|
||||
def sorry = "command: ${hex1(cmd.commandClass)}${hex1(cmd.command)}, payload: " + cmd.parameter.collect{ hex1(it) }.join(" ")
|
||||
encapsulated = zwave.parse(sorry, [0x31: 1, 0x84: 2, 0x60: 1, 0x85: 1, 0x70: 1])
|
||||
}
|
||||
return encapsulated ? zwaveEvent(encapsulated) : null
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling
|
||||
that issue by using state variables
|
||||
*/
|
||||
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
|
||||
|
||||
metadata {
|
||||
definition (name: "OSRAM LIGHTIFY LED Flexible Strip RGBW", namespace: "smartthings", author: "SmartThings") {
|
||||
@@ -24,8 +23,8 @@ metadata {
|
||||
command "setAdjustedColor"
|
||||
|
||||
|
||||
//fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW"
|
||||
//fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW"
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata {
|
||||
capability "Switch"
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0006, 0004, 0003, 0000, 0005", outClusters: "0019", manufacturer: "Compacta International, Ltd", model: "ZBMPlug15", deviceJoinName: "SmartPower Outlet V1"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0006", outClusters: "0019"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -20,8 +20,8 @@ metadata {
|
||||
capability "Refresh"
|
||||
capability "Temperature Measurement"
|
||||
capability "Water Sensor"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S", deviceJoinName: "Water Leak Sensor"
|
||||
@@ -29,9 +29,9 @@ metadata {
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu", deviceJoinName: "Water Leak Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "moisturev4", deviceJoinName: "Water Leak Sensor"
|
||||
}
|
||||
|
||||
|
||||
simulator {
|
||||
|
||||
|
||||
}
|
||||
|
||||
preferences {
|
||||
@@ -47,7 +47,7 @@ metadata {
|
||||
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.water", key: "PRIMARY_CONTROL") {
|
||||
@@ -78,7 +78,7 @@ metadata {
|
||||
details(["water", "temperature", "battery", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def parse(String description) {
|
||||
log.debug "description: $description"
|
||||
|
||||
@@ -92,59 +92,59 @@ def parse(String description) {
|
||||
else if (description?.startsWith('temperature: ')) {
|
||||
map = parseCustomMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('zone status')) {
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
|
||||
else if (description?.startsWith('zone status')) {
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
|
||||
log.debug "Parse returned $map"
|
||||
def result = map ? createEvent(map) : null
|
||||
|
||||
if (description?.startsWith('enroll request')) {
|
||||
List cmds = enrollResponse()
|
||||
log.debug "enroll response: ${cmds}"
|
||||
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||
}
|
||||
return result
|
||||
|
||||
if (description?.startsWith('enroll request')) {
|
||||
List cmds = enrollResponse()
|
||||
log.debug "enroll response: ${cmds}"
|
||||
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
break
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
case 0x0402:
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
}
|
||||
|
||||
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
log.debug "Desc Map: $descMap"
|
||||
|
||||
|
||||
Map resultMap = [:]
|
||||
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||
def value = getTemperature(descMap.value)
|
||||
@@ -153,10 +153,10 @@ private Map parseReportAttributeMessage(String description) {
|
||||
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
if (description?.startsWith('temperature: ')) {
|
||||
@@ -167,42 +167,42 @@ private Map parseCustomMessage(String description) {
|
||||
}
|
||||
|
||||
private Map parseIasMessage(String description) {
|
||||
List parsedMsg = description.split(' ')
|
||||
String msgCode = parsedMsg[2]
|
||||
List parsedMsg = description.split(' ')
|
||||
String msgCode = parsedMsg[2]
|
||||
|
||||
Map resultMap = [:]
|
||||
switch(msgCode) {
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
resultMap = getMoistureResult('dry')
|
||||
break
|
||||
|
||||
Map resultMap = [:]
|
||||
switch(msgCode) {
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
resultMap = getMoistureResult('dry')
|
||||
break
|
||||
case '0x0021': // Open/Motion/Wet
|
||||
resultMap = getMoistureResult('wet')
|
||||
break
|
||||
|
||||
case '0x0021': // Open/Motion/Wet
|
||||
resultMap = getMoistureResult('wet')
|
||||
break
|
||||
case '0x0022': // Tamper Alarm
|
||||
break
|
||||
|
||||
case '0x0022': // Tamper Alarm
|
||||
break
|
||||
case '0x0023': // Battery Alarm
|
||||
break
|
||||
|
||||
case '0x0023': // Battery Alarm
|
||||
break
|
||||
case '0x0024': // Supervision Report
|
||||
log.debug 'dry with tamper alarm'
|
||||
resultMap = getMoistureResult('dry')
|
||||
break
|
||||
|
||||
case '0x0024': // Supervision Report
|
||||
log.debug 'dry with tamper alarm'
|
||||
resultMap = getMoistureResult('dry')
|
||||
break
|
||||
case '0x0025': // Restore Report
|
||||
log.debug 'water with tamper alarm'
|
||||
resultMap = getMoistureResult('wet')
|
||||
break
|
||||
|
||||
case '0x0025': // Restore Report
|
||||
log.debug 'water with tamper alarm'
|
||||
resultMap = getMoistureResult('wet')
|
||||
break
|
||||
case '0x0026': // Trouble/Failure
|
||||
break
|
||||
|
||||
case '0x0026': // Trouble/Failure
|
||||
break
|
||||
|
||||
case '0x0028': // Test Mode
|
||||
break
|
||||
}
|
||||
return resultMap
|
||||
case '0x0028': // Test Mode
|
||||
break
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
def getTemperature(value) {
|
||||
@@ -215,47 +215,24 @@ def getTemperature(value) {
|
||||
}
|
||||
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug "Battery rawValue = ${rawValue}"
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
]
|
||||
|
||||
|
||||
def result = [
|
||||
name: 'battery'
|
||||
]
|
||||
|
||||
def volts = rawValue / 10
|
||||
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
def descriptionText
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
else {
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
else {
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
volts = rawValue // For the batteryMap to work the key needs to be an int
|
||||
def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
|
||||
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
|
||||
def minVolts = 15
|
||||
def maxVolts = 28
|
||||
|
||||
if (volts < minVolts)
|
||||
volts = minVolts
|
||||
else if (volts > maxVolts)
|
||||
volts = maxVolts
|
||||
def pct = batteryMap[volts]
|
||||
if (pct != null) {
|
||||
result.value = pct
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
}
|
||||
else {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
}
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -290,7 +267,7 @@ private Map getMoistureResult(value) {
|
||||
def refresh() {
|
||||
log.debug "Refreshing Temperature and Battery"
|
||||
def refreshCmds = [
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200"
|
||||
]
|
||||
|
||||
@@ -300,32 +277,32 @@ def refresh() {
|
||||
def configure() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
log.debug "Sending enroll response"
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
@@ -337,19 +314,19 @@ private hex(value) {
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ metadata {
|
||||
capability "Water Sensor"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Temperature Measurement"
|
||||
|
||||
fingerprint deviceId: "0x2001", inClusters: "0x30,0x9C,0x9D,0x85,0x80,0x72,0x31,0x84,0x86"
|
||||
fingerprint deviceId: "0x2101", inClusters: "0x71,0x70,0x85,0x80,0x72,0x31,0x84,0x86"
|
||||
@@ -40,29 +39,17 @@ metadata {
|
||||
attributeState "wet", label: "Wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0"
|
||||
}
|
||||
}
|
||||
standardTile("temperatureState", "device.temperature", width: 2, height: 2) {
|
||||
standardTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||
state "normal", icon:"st.alarm.temperature.normal", backgroundColor:"#ffffff"
|
||||
state "freezing", icon:"st.alarm.temperature.freeze", backgroundColor:"#53a7c0"
|
||||
state "overheated", icon:"st.alarm.temperature.overheat", backgroundColor:"#F80000"
|
||||
}
|
||||
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||
state("temperature", label:'${currentValue}°',
|
||||
backgroundColors:[
|
||||
[value: 31, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
)
|
||||
}
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
main (["water", "temperatureState"])
|
||||
details(["water", "temperatureState", "temperature", "battery"])
|
||||
|
||||
main (["water", "temperature"])
|
||||
details(["water", "temperature", "battery"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +115,7 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd)
|
||||
map.descriptionText = "${device.displayName} is ${map.value}"
|
||||
}
|
||||
if(cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_HEAT) {
|
||||
map.name = "temperatureState"
|
||||
map.name = "temperature"
|
||||
if(cmd.zwaveAlarmEvent == 1) { map.value = "overheated"}
|
||||
if(cmd.zwaveAlarmEvent == 2) { map.value = "overheated"}
|
||||
if(cmd.zwaveAlarmEvent == 3) { map.value = "changing temperature rapidly"}
|
||||
@@ -142,30 +129,17 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd)
|
||||
map
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd)
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd)
|
||||
{
|
||||
def map = [:]
|
||||
if(cmd.sensorType == 1) {
|
||||
map.name = "temperature"
|
||||
if(cmd.scale == 0) {
|
||||
map.value = getTemperature(cmd.scaledSensorValue)
|
||||
} else {
|
||||
map.value = cmd.scaledSensorValue
|
||||
}
|
||||
map.unit = location.temperatureScale
|
||||
}
|
||||
map.name = "water"
|
||||
map.value = cmd.value ? "wet" : "dry"
|
||||
map.descriptionText = "${device.displayName} is ${map.value}"
|
||||
map
|
||||
}
|
||||
|
||||
def getTemperature(value) {
|
||||
if(location.temperatureScale == "C"){
|
||||
return value
|
||||
} else {
|
||||
return Math.round(celsiusToFahrenheit(value))
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd)
|
||||
{
|
||||
log.debug "COMMAND CLASS: $cmd"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,18 +19,17 @@ metadata {
|
||||
capability "Motion Sensor"
|
||||
capability "Configuration"
|
||||
capability "Battery"
|
||||
capability "Temperature Measurement"
|
||||
capability "Temperature Measurement"
|
||||
capability "Refresh"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326-L", deviceJoinName: "Iris Motion Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv4", deviceJoinName: "Motion Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
|
||||
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv4", deviceJoinName: "Motion Sensor"
|
||||
}
|
||||
|
||||
simulator {
|
||||
@@ -86,7 +85,7 @@ metadata {
|
||||
|
||||
def parse(String description) {
|
||||
log.debug "description: $description"
|
||||
|
||||
|
||||
Map map = [:]
|
||||
if (description?.startsWith('catchall:')) {
|
||||
map = parseCatchAllMessage(description)
|
||||
@@ -97,55 +96,55 @@ def parse(String description) {
|
||||
else if (description?.startsWith('temperature: ')) {
|
||||
map = parseCustomMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('zone status')) {
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
|
||||
else if (description?.startsWith('zone status')) {
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
|
||||
log.debug "Parse returned $map"
|
||||
def result = map ? createEvent(map) : null
|
||||
|
||||
if (description?.startsWith('enroll request')) {
|
||||
List cmds = enrollResponse()
|
||||
log.debug "enroll response: ${cmds}"
|
||||
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||
}
|
||||
return result
|
||||
|
||||
if (description?.startsWith('enroll request')) {
|
||||
List cmds = enrollResponse()
|
||||
log.debug "enroll response: ${cmds}"
|
||||
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
break
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
case 0x0402:
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
|
||||
case 0x0406:
|
||||
log.debug 'motion'
|
||||
resultMap.name = 'motion'
|
||||
break
|
||||
}
|
||||
}
|
||||
log.debug 'motion'
|
||||
resultMap.name = 'motion'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
}
|
||||
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
@@ -154,7 +153,7 @@ private Map parseReportAttributeMessage(String description) {
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
log.debug "Desc Map: $descMap"
|
||||
|
||||
|
||||
Map resultMap = [:]
|
||||
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||
def value = getTemperature(descMap.value)
|
||||
@@ -163,14 +162,14 @@ private Map parseReportAttributeMessage(String description) {
|
||||
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
|
||||
def value = descMap.value.endsWith("01") ? "active" : "inactive"
|
||||
resultMap = getMotionResult(value)
|
||||
}
|
||||
|
||||
else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
|
||||
def value = descMap.value.endsWith("01") ? "active" : "inactive"
|
||||
resultMap = getMotionResult(value)
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
if (description?.startsWith('temperature: ')) {
|
||||
@@ -181,44 +180,44 @@ private Map parseCustomMessage(String description) {
|
||||
}
|
||||
|
||||
private Map parseIasMessage(String description) {
|
||||
List parsedMsg = description.split(' ')
|
||||
String msgCode = parsedMsg[2]
|
||||
List parsedMsg = description.split(' ')
|
||||
String msgCode = parsedMsg[2]
|
||||
|
||||
Map resultMap = [:]
|
||||
switch(msgCode) {
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
resultMap = getMotionResult('inactive')
|
||||
break
|
||||
|
||||
Map resultMap = [:]
|
||||
switch(msgCode) {
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
resultMap = getMotionResult('inactive')
|
||||
break
|
||||
case '0x0021': // Open/Motion/Wet
|
||||
resultMap = getMotionResult('active')
|
||||
break
|
||||
|
||||
case '0x0021': // Open/Motion/Wet
|
||||
resultMap = getMotionResult('active')
|
||||
break
|
||||
case '0x0022': // Tamper Alarm
|
||||
log.debug 'motion with tamper alarm'
|
||||
resultMap = getMotionResult('active')
|
||||
break
|
||||
|
||||
case '0x0022': // Tamper Alarm
|
||||
log.debug 'motion with tamper alarm'
|
||||
resultMap = getMotionResult('active')
|
||||
break
|
||||
case '0x0023': // Battery Alarm
|
||||
break
|
||||
|
||||
case '0x0023': // Battery Alarm
|
||||
break
|
||||
case '0x0024': // Supervision Report
|
||||
log.debug 'no motion with tamper alarm'
|
||||
resultMap = getMotionResult('inactive')
|
||||
break
|
||||
|
||||
case '0x0024': // Supervision Report
|
||||
log.debug 'no motion with tamper alarm'
|
||||
resultMap = getMotionResult('inactive')
|
||||
break
|
||||
case '0x0025': // Restore Report
|
||||
break
|
||||
|
||||
case '0x0025': // Restore Report
|
||||
break
|
||||
case '0x0026': // Trouble/Failure
|
||||
log.debug 'motion with failure alarm'
|
||||
resultMap = getMotionResult('active')
|
||||
break
|
||||
|
||||
case '0x0026': // Trouble/Failure
|
||||
log.debug 'motion with failure alarm'
|
||||
resultMap = getMotionResult('active')
|
||||
break
|
||||
|
||||
case '0x0028': // Test Mode
|
||||
break
|
||||
}
|
||||
return resultMap
|
||||
case '0x0028': // Test Mode
|
||||
break
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
def getTemperature(value) {
|
||||
@@ -231,46 +230,30 @@ def getTemperature(value) {
|
||||
}
|
||||
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug "Battery rawValue = ${rawValue}"
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
log.debug rawValue
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
]
|
||||
|
||||
def volts = rawValue / 10
|
||||
def descriptionText
|
||||
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
if (rawValue == 0) {}
|
||||
else {
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
else {
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
volts = rawValue // For the batteryMap to work the key needs to be an int
|
||||
def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
|
||||
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
|
||||
def minVolts = 15
|
||||
def maxVolts = 28
|
||||
|
||||
if (volts < minVolts)
|
||||
volts = minVolts
|
||||
else if (volts > maxVolts)
|
||||
volts = maxVolts
|
||||
def pct = batteryMap[volts]
|
||||
if (pct != null) {
|
||||
result.value = pct
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
}
|
||||
else {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
else if (volts > 0){
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,19 +338,19 @@ private hex(value) {
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
@@ -14,28 +14,28 @@
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Three Axis"
|
||||
metadata {
|
||||
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Three Axis"
|
||||
capability "Battery"
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Contact Sensor"
|
||||
capability "Acceleration Sensor"
|
||||
capability "Refresh"
|
||||
capability "Temperature Measurement"
|
||||
|
||||
|
||||
command "enrollResponse"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500,FC02", outClusters: "0019", manufacturer: "SmartThings", model: "multiv4", deviceJoinName: "Multipurpose Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500,FC02", outClusters: "0019", manufacturer: "SmartThings", model: "multiv4", deviceJoinName: "Multipurpose Sensor"
|
||||
|
||||
attribute "status", "string"
|
||||
}
|
||||
}
|
||||
|
||||
simulator {
|
||||
simulator {
|
||||
status "open": "zone report :: type: 19 value: 0031"
|
||||
status "closed": "zone report :: type: 19 value: 0030"
|
||||
|
||||
@@ -52,7 +52,7 @@ metadata {
|
||||
status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0"
|
||||
status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000"
|
||||
}
|
||||
preferences {
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg",
|
||||
@@ -62,13 +62,13 @@ metadata {
|
||||
])
|
||||
}
|
||||
section {
|
||||
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
}
|
||||
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
}
|
||||
section {
|
||||
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
|
||||
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){
|
||||
@@ -106,9 +106,9 @@ metadata {
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
|
||||
main(["status", "acceleration", "temperature"])
|
||||
@@ -121,61 +121,61 @@ def parse(String description) {
|
||||
if (description?.startsWith('catchall:')) {
|
||||
map = parseCatchAllMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('temperature: ')) {
|
||||
else if (description?.startsWith('temperature: ')) {
|
||||
map = parseCustomMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('zone status')) {
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
|
||||
def result = map ? createEvent(map) : null
|
||||
def result = map ? createEvent(map) : null
|
||||
|
||||
if (description?.startsWith('enroll request')) {
|
||||
List cmds = enrollResponse()
|
||||
log.debug "enroll response: ${cmds}"
|
||||
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||
}
|
||||
else if (description?.startsWith('read attr -')) {
|
||||
result = parseReportAttributeMessage(description).each { createEvent(it) }
|
||||
}
|
||||
else if (description?.startsWith('read attr -')) {
|
||||
result = parseReportAttributeMessage(description).each { createEvent(it) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug cluster
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
break
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug cluster
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
break
|
||||
|
||||
case 0xFC02:
|
||||
log.debug 'ACCELERATION'
|
||||
break
|
||||
case 0xFC02:
|
||||
log.debug 'ACCELERATION'
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
log.debug 'TEMP'
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
case 0x0402:
|
||||
log.debug 'TEMP'
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
}
|
||||
|
||||
private List parseReportAttributeMessage(String description) {
|
||||
@@ -202,12 +202,10 @@ private List parseReportAttributeMessage(String description) {
|
||||
result << parseAxis(threeAxisAttributes)
|
||||
descMap.value = descMap.value[-2..-1]
|
||||
}
|
||||
result << getAccelerationResult(descMap.value)
|
||||
result << getAccelerationResult(descMap.value)
|
||||
}
|
||||
else if (descMap.cluster == "FC02" && descMap.attrId == "0012" && descMap.value.size() == 24) {
|
||||
// The size is checked to ensure the attribute report contains X, Y and Z values
|
||||
// If all three axis are not included then the attribute report is ignored
|
||||
result << parseAxis(descMap.value)
|
||||
else if (descMap.cluster == "FC02" && descMap.attrId == "0012") {
|
||||
result << parseAxis(descMap.value)
|
||||
}
|
||||
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||
result << getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||
@@ -231,43 +229,43 @@ private Map parseIasMessage(String description) {
|
||||
|
||||
Map resultMap = [:]
|
||||
switch(msgCode) {
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
if (garageSensor != "Yes"){
|
||||
resultMap = getContactResult('closed')
|
||||
}
|
||||
break
|
||||
break
|
||||
|
||||
case '0x0021': // Open/Motion/Wet
|
||||
case '0x0021': // Open/Motion/Wet
|
||||
if (garageSensor != "Yes"){
|
||||
resultMap = getContactResult('open')
|
||||
}
|
||||
break
|
||||
break
|
||||
|
||||
case '0x0022': // Tamper Alarm
|
||||
break
|
||||
case '0x0022': // Tamper Alarm
|
||||
break
|
||||
|
||||
case '0x0023': // Battery Alarm
|
||||
break
|
||||
case '0x0023': // Battery Alarm
|
||||
break
|
||||
|
||||
case '0x0024': // Supervision Report
|
||||
case '0x0024': // Supervision Report
|
||||
if (garageSensor != "Yes"){
|
||||
resultMap = getContactResult('closed')
|
||||
}
|
||||
break
|
||||
break
|
||||
|
||||
case '0x0025': // Restore Report
|
||||
case '0x0025': // Restore Report
|
||||
if (garageSensor != "Yes"){
|
||||
resultMap = getContactResult('open')
|
||||
}
|
||||
break
|
||||
break
|
||||
|
||||
case '0x0026': // Trouble/Failure
|
||||
break
|
||||
case '0x0026': // Trouble/Failure
|
||||
break
|
||||
|
||||
case '0x0028': // Test Mode
|
||||
break
|
||||
}
|
||||
return resultMap
|
||||
case '0x0028': // Test Mode
|
||||
break
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
def updated() {
|
||||
@@ -302,171 +300,134 @@ def getTemperature(value) {
|
||||
}
|
||||
}
|
||||
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug "Battery rawValue = ${rawValue}"
|
||||
def linkText = getLinkText(device)
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug "Battery"
|
||||
log.debug rawValue
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def result = [
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
]
|
||||
value: '--'
|
||||
]
|
||||
|
||||
def volts = rawValue / 10
|
||||
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
else {
|
||||
if (volts > 3.5) {
|
||||
def volts = rawValue / 10
|
||||
def descriptionText
|
||||
|
||||
if (rawValue == 255) {}
|
||||
else {
|
||||
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
else {
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
volts = rawValue // For the batteryMap to work the key needs to be an int
|
||||
def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
|
||||
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
|
||||
def minVolts = 15
|
||||
def maxVolts = 28
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}}
|
||||
|
||||
if (volts < minVolts)
|
||||
volts = minVolts
|
||||
else if (volts > maxVolts)
|
||||
volts = maxVolts
|
||||
def pct = batteryMap[volts]
|
||||
if (pct != null) {
|
||||
result.value = pct
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
}
|
||||
else {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private Map getTemperatureResult(value) {
|
||||
log.debug "Temperature"
|
||||
def linkText = getLinkText(device)
|
||||
if (tempOffset) {
|
||||
def offset = tempOffset as int
|
||||
def v = value as int
|
||||
value = v + offset
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private Map getTemperatureResult(value) {
|
||||
log.debug "Temperature"
|
||||
def linkText = getLinkText(device)
|
||||
if (tempOffset) {
|
||||
def offset = tempOffset as int
|
||||
def v = value as int
|
||||
value = v + offset
|
||||
}
|
||||
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
||||
return [
|
||||
name: 'temperature',
|
||||
value: value,
|
||||
descriptionText: descriptionText
|
||||
]
|
||||
}
|
||||
|
||||
private Map getContactResult(value) {
|
||||
log.debug "Contact"
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
|
||||
sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed:false)
|
||||
sendEvent(name: 'status', value: value, descriptionText: descriptionText)
|
||||
}
|
||||
|
||||
private getAccelerationResult(numValue) {
|
||||
log.debug "Acceleration"
|
||||
def name = "acceleration"
|
||||
def value = numValue.endsWith("1") ? "active" : "inactive"
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "$linkText was $value"
|
||||
def isStateChange = isStateChange(device, name, value)
|
||||
[
|
||||
name: name,
|
||||
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
||||
return [
|
||||
name: 'temperature',
|
||||
value: value,
|
||||
descriptionText: descriptionText,
|
||||
isStateChange: isStateChange
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Refreshing Values "
|
||||
|
||||
def refreshCmds = []
|
||||
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
log.debug "Refreshing Values for manufacturer: SmartThings "
|
||||
refreshCmds = refreshCmds + [
|
||||
/* These values of Motion Threshold Multiplier(01) and Motion Threshold (7602)
|
||||
seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer.
|
||||
Separating these out in a separate if-else because I do not want to touch Centralite part
|
||||
as of now.
|
||||
*/
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global write 0xFC02 0 0x20 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global write 0xFC02 2 0x21 {7602}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||
]
|
||||
} else {
|
||||
refreshCmds = refreshCmds + [
|
||||
/* sensitivity - default value (8) */
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||
descriptionText: descriptionText
|
||||
]
|
||||
}
|
||||
|
||||
//Common refresh commands
|
||||
refreshCmds = refreshCmds + [
|
||||
private Map getContactResult(value) {
|
||||
log.debug "Contact"
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
|
||||
sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed:false)
|
||||
sendEvent(name: 'status', value: value, descriptionText: descriptionText)
|
||||
}
|
||||
|
||||
private getAccelerationResult(numValue) {
|
||||
log.debug "Acceleration"
|
||||
def name = "acceleration"
|
||||
def value = numValue.endsWith("1") ? "active" : "inactive"
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "$linkText was $value"
|
||||
def isStateChange = isStateChange(device, name, value)
|
||||
[
|
||||
name: name,
|
||||
value: value,
|
||||
descriptionText: descriptionText,
|
||||
isStateChange: isStateChange
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Refreshing Values "
|
||||
def refreshCmds = [
|
||||
|
||||
/* sensitivity - default value (8) */
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global read 0xFC02 0x0010",
|
||||
"send 0x${device.deviceNetworkId} 1 1","delay 400"
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global read 0xFC02 0x0010",
|
||||
"send 0x${device.deviceNetworkId} 1 1","delay 400"
|
||||
]
|
||||
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting"
|
||||
def configure() {
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting"
|
||||
|
||||
def configCmds = [
|
||||
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", "delay 200", //checkin time 6 hrs
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}", "delay 200",
|
||||
"zcl mfg-code ${manufacturerCode}",
|
||||
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}", "delay 200",
|
||||
"zcl mfg-code ${manufacturerCode}",
|
||||
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}", "delay 200",
|
||||
"zcl mfg-code ${manufacturerCode}",
|
||||
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}", "delay 200",
|
||||
"zcl mfg-code ${manufacturerCode}",
|
||||
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
]
|
||||
|
||||
return configCmds + refresh()
|
||||
]
|
||||
|
||||
return configCmds + refresh()
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
@@ -481,43 +442,44 @@ def enrollResponse() {
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
private Map parseAxis(String description) {
|
||||
def z = hexToSignedInt(description[0..3])
|
||||
def y = hexToSignedInt(description[10..13])
|
||||
def x = hexToSignedInt(description[20..23])
|
||||
def xyzResults = [x: x, y: y, z: z]
|
||||
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
// This mapping matches the current behavior of the Device Handler for the Centralite sensors
|
||||
xyzResults.x = z
|
||||
xyzResults.y = y
|
||||
xyzResults.z = -x
|
||||
} else {
|
||||
// The axises reported by the Device Handler differ from the axises reported by the sensor
|
||||
// This may change in the future
|
||||
xyzResults.x = z
|
||||
xyzResults.y = x
|
||||
xyzResults.z = y
|
||||
}
|
||||
|
||||
log.debug "parseAxis -- ${xyzResults}"
|
||||
|
||||
if (garageSensor == "Yes")
|
||||
garageEvent(xyzResults.z)
|
||||
log.debug "parseAxis"
|
||||
def xyzResults = [x: 0, y: 0, z: 0]
|
||||
def parts = description.split("2900")
|
||||
parts[0] = "12" + parts[0]
|
||||
parts.each { part ->
|
||||
part = part.trim()
|
||||
if (part.startsWith("12")) {
|
||||
def unsignedX = hexToInt(part.split("12")[1].trim())
|
||||
def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX
|
||||
xyzResults.x = signedX
|
||||
log.debug "X Part: ${signedX}"
|
||||
}
|
||||
else if (part.startsWith("13")) {
|
||||
def unsignedY = hexToInt(part.split("13")[1].trim())
|
||||
def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY
|
||||
xyzResults.y = signedY
|
||||
log.debug "Y Part: ${signedY}"
|
||||
}
|
||||
else if (part.startsWith("14")) {
|
||||
def unsignedZ = hexToInt(part.split("14")[1].trim())
|
||||
def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ
|
||||
xyzResults.z = signedZ
|
||||
log.debug "Z Part: ${signedZ}"
|
||||
if (garageSensor == "Yes")
|
||||
garageEvent(signedZ)
|
||||
}
|
||||
}
|
||||
|
||||
getXyzResult(xyzResults, description)
|
||||
}
|
||||
|
||||
private hexToSignedInt(hexVal) {
|
||||
def unsignedVal = hexToInt(hexVal)
|
||||
unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal
|
||||
}
|
||||
|
||||
def garageEvent(zValue) {
|
||||
def absValue = zValue.abs()
|
||||
def contactValue = null
|
||||
@@ -590,3 +552,4 @@ private byte[] reverseArray(byte[] array) {
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
|
||||
@@ -16,18 +16,17 @@
|
||||
|
||||
metadata {
|
||||
definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Battery"
|
||||
capability "Battery"
|
||||
capability "Configuration"
|
||||
capability "Contact Sensor"
|
||||
capability "Contact Sensor"
|
||||
capability "Refresh"
|
||||
capability "Temperature Measurement"
|
||||
|
||||
command "enrollResponse"
|
||||
command "enrollResponse"
|
||||
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300-S"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300"
|
||||
fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3320-L", deviceJoinName: "Iris Contact Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300"
|
||||
}
|
||||
|
||||
simulator {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Simulated Media Player", namespace: "smartthings/testing", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Switch"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Music Player"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name: "mediaMulti", type:"mediaPlayer", width:6, height:4) {
|
||||
tileAttribute("device.status", key: "PRIMARY_CONTROL") {
|
||||
attributeState("paused", label:"Paused",)
|
||||
attributeState("playing", label:"Playing")
|
||||
attributeState("stopped", label:"Stopped")
|
||||
}
|
||||
tileAttribute("device.status", key: "MEDIA_STATUS") {
|
||||
attributeState("paused", label:"Paused", action:"music Player.play", nextState: "playing")
|
||||
attributeState("playing", label:"Playing", action:"music Player.pause", nextState: "paused")
|
||||
attributeState("stopped", label:"Stopped", action:"music Player.play", nextState: "playing")
|
||||
}
|
||||
tileAttribute("device.status", key: "PREVIOUS_TRACK") {
|
||||
attributeState("default", action:"music Player.previousTrack")
|
||||
}
|
||||
tileAttribute("device.status", key: "NEXT_TRACK") {
|
||||
attributeState("default", action:"music Player.nextTrack")
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState("level", action:"music Player.setLevel")
|
||||
}
|
||||
tileAttribute ("device.mute", key: "MEDIA_MUTED") {
|
||||
attributeState("unmuted", action:"music Player.mute", nextState: "muted")
|
||||
attributeState("muted", action:"music Player.unmute", nextState: "unmuted")
|
||||
}
|
||||
tileAttribute("device.trackDescription", key: "MARQUEE") {
|
||||
attributeState("default", label:"${currentValue}")
|
||||
}
|
||||
}
|
||||
|
||||
main "mediaMulti"
|
||||
details(["mediaMulti"])
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
state.tracks = [
|
||||
"Gangnam Style (강남스타일)\nPSY\nPsy 6 (Six Rules), Part 1",
|
||||
"Careless Whisper\nWham!\nMake It Big",
|
||||
"Never Gonna Give You Up\nRick Astley\nWhenever You Need Somebody",
|
||||
"Shake It Off\nTaylor Swift\n1989",
|
||||
"Ironic\nAlanis Morissette\nJagged Little Pill",
|
||||
"Hotline Bling\nDrake\nHotline Bling - Single"
|
||||
]
|
||||
state.currentTrack = 0
|
||||
|
||||
sendEvent(name: "level", value: 72)
|
||||
sendEvent(name: "mute", value: "unmuted")
|
||||
sendEvent(name: "status", value: "stopped")
|
||||
}
|
||||
|
||||
def parse(description) {
|
||||
// No parsing will happen with this simulated device.
|
||||
}
|
||||
|
||||
def play() {
|
||||
sendEvent(name: "status", value: "playing")
|
||||
sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack])
|
||||
}
|
||||
|
||||
def pause() {
|
||||
sendEvent(name: "status", value: "paused")
|
||||
sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack])
|
||||
}
|
||||
|
||||
def stop() {
|
||||
sendEvent(name: "status", value: "stopped")
|
||||
}
|
||||
|
||||
def previousTrack() {
|
||||
state.currentTrack = state.currentTrack - 1
|
||||
if (state.currentTrack < 0)
|
||||
state.currentTrack = state.tracks.size()-1
|
||||
|
||||
sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack])
|
||||
}
|
||||
|
||||
def nextTrack() {
|
||||
state.currentTrack = state.currentTrack + 1
|
||||
if (state.currentTrack == state.tracks.size())
|
||||
state.currentTrack = 0
|
||||
|
||||
sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack])
|
||||
}
|
||||
|
||||
def mute() {
|
||||
sendEvent(name: "mute", value: "muted")
|
||||
}
|
||||
|
||||
def unmute() {
|
||||
sendEvent(name: "mute", value: "unmuted")
|
||||
}
|
||||
|
||||
def setLevel(level) {
|
||||
sendEvent(name: "level", value: level)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
* Copyright 2014 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
@@ -15,7 +15,6 @@ metadata {
|
||||
// Automatically generated. Make future change here.
|
||||
definition (name: "Simulated Thermostat", namespace: "smartthings/testing", author: "SmartThings") {
|
||||
capability "Thermostat"
|
||||
capability "Relative Humidity Measurement"
|
||||
|
||||
command "tempUp"
|
||||
command "tempDown"
|
||||
@@ -23,40 +22,11 @@ metadata {
|
||||
command "heatDown"
|
||||
command "coolUp"
|
||||
command "coolDown"
|
||||
command "setTemperature", ["number"]
|
||||
command "setTemperature", ["number"]
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"thermostatMulti", type:"thermostat", width:6, height:4) {
|
||||
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
|
||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
||||
}
|
||||
tileAttribute("device.temperature", key: "VALUE_CONTROL") {
|
||||
attributeState("default", action: "setTemperature")
|
||||
}
|
||||
tileAttribute("device.humidity", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'${currentValue}%', unit:"%")
|
||||
}
|
||||
tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") {
|
||||
attributeState("idle", backgroundColor:"#44b621")
|
||||
attributeState("heating", backgroundColor:"#ffa81e")
|
||||
attributeState("cooling", backgroundColor:"#269bd2")
|
||||
}
|
||||
tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") {
|
||||
attributeState("off", label:'${name}')
|
||||
attributeState("heat", label:'${name}')
|
||||
attributeState("cool", label:'${name}')
|
||||
attributeState("auto", label:'${name}')
|
||||
}
|
||||
tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
|
||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
||||
}
|
||||
tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") {
|
||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
||||
}
|
||||
}
|
||||
|
||||
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||
tiles {
|
||||
valueTile("temperature", "device.temperature", width: 1, height: 1) {
|
||||
state("temperature", label:'${currentValue}', unit:"dF",
|
||||
backgroundColors:[
|
||||
[value: 31, color: "#153591"],
|
||||
@@ -69,51 +39,51 @@ metadata {
|
||||
]
|
||||
)
|
||||
}
|
||||
standardTile("tempDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("tempDown", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'down', action:"tempDown"
|
||||
}
|
||||
standardTile("tempUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("tempUp", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'up', action:"tempUp"
|
||||
}
|
||||
|
||||
valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
state "heat", label:'${currentValue} heat', unit: "F", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("heatDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("heatDown", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'down', action:"heatDown"
|
||||
}
|
||||
standardTile("heatUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("heatUp", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'up', action:"heatUp"
|
||||
}
|
||||
|
||||
valueTile("coolingSetpoint", "device.coolingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
state "cool", label:'${currentValue} cool', unit:"F", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("coolDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("coolDown", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'down', action:"coolDown"
|
||||
}
|
||||
standardTile("coolUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("coolUp", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'up', action:"coolUp"
|
||||
}
|
||||
|
||||
standardTile("mode", "device.thermostatMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||
state "off", label:'${name}', action:"thermostat.heat", backgroundColor:"#ffffff"
|
||||
state "heat", label:'${name}', action:"thermostat.cool", backgroundColor:"#ffa81e"
|
||||
state "cool", label:'${name}', action:"thermostat.auto", backgroundColor:"#269bd2"
|
||||
state "auto", label:'${name}', action:"thermostat.off", backgroundColor:"#79b821"
|
||||
}
|
||||
standardTile("fanMode", "device.thermostatFanMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
||||
state "fanAuto", label:'${name}', action:"thermostat.fanOn", backgroundColor:"#ffffff"
|
||||
state "fanOn", label:'${name}', action:"thermostat.fanCirculate", backgroundColor:"#ffffff"
|
||||
state "fanCirculate", label:'${name}', action:"thermostat.fanAuto", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("operatingState", "device.thermostatOperatingState", width: 2, height: 2) {
|
||||
standardTile("operatingState", "device.thermostatOperatingState") {
|
||||
state "idle", label:'${name}', backgroundColor:"#ffffff"
|
||||
state "heating", label:'${name}', backgroundColor:"#ffa81e"
|
||||
state "cooling", label:'${name}', backgroundColor:"#269bd2"
|
||||
}
|
||||
|
||||
main("thermostatMulti")
|
||||
main("temperature","operatingState")
|
||||
details([
|
||||
"temperature","tempDown","tempUp",
|
||||
"mode", "fanMode", "operatingState",
|
||||
@@ -131,7 +101,6 @@ def installed() {
|
||||
sendEvent(name: "thermostatMode", value: "off")
|
||||
sendEvent(name: "thermostatFanMode", value: "fanAuto")
|
||||
sendEvent(name: "thermostatOperatingState", value: "idle")
|
||||
sendEvent(name: "humidity", value: 53, unit: "%")
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
|
||||
@@ -29,7 +29,7 @@ metadata {
|
||||
|
||||
tiles {
|
||||
|
||||
valueTile("power", "device.power", canChangeIcon: true) {
|
||||
valueTile("power", "device.power") {
|
||||
state "power", label: '${currentValue} W'
|
||||
}
|
||||
|
||||
|
||||
@@ -273,7 +273,7 @@ User-Agent: CyberGarage-HTTP/1.0
|
||||
def poll() {
|
||||
log.debug "Executing 'poll'"
|
||||
if (device.currentValue("currentIP") != "Offline")
|
||||
runIn(30, setOffline)
|
||||
runIn(10, setOffline)
|
||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||
Content-Length: 277
|
||||
|
||||
@@ -77,8 +77,9 @@ def parse(String description) {
|
||||
def result = []
|
||||
def bodyString = msg.body
|
||||
if (bodyString) {
|
||||
unschedule("setOffline")
|
||||
unschedule("setOffline")
|
||||
def body = new XmlSlurper().parseText(bodyString)
|
||||
|
||||
if (body?.property?.TimeSyncRequest?.text()) {
|
||||
log.trace "Got TimeSyncRequest"
|
||||
result << timeSyncResponse()
|
||||
@@ -133,7 +134,7 @@ def refresh() {
|
||||
def getStatus() {
|
||||
log.debug "Executing WeMo Motion 'getStatus'"
|
||||
if (device.currentValue("currentIP") != "Offline")
|
||||
runIn(30, setOffline)
|
||||
runIn(10, setOffline)
|
||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||
Content-Length: 277
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
command "subscribe"
|
||||
command "resubscribe"
|
||||
command "unsubscribe"
|
||||
command "setOffline"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
@@ -208,7 +207,7 @@ def subscribe(ip, port) {
|
||||
def existingIp = getDataValue("ip")
|
||||
def existingPort = getDataValue("port")
|
||||
if (ip && ip != existingIp) {
|
||||
log.debug "Updating ip from $existingIp to $ip"
|
||||
log.debug "Updating ip from $existingIp to $ip"
|
||||
updateDataValue("ip", ip)
|
||||
def ipvalue = convertHexToIP(getDataValue("ip"))
|
||||
sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}")
|
||||
@@ -276,7 +275,7 @@ def setOffline() {
|
||||
def poll() {
|
||||
log.debug "Executing 'poll'"
|
||||
if (device.currentValue("currentIP") != "Offline")
|
||||
runIn(30, setOffline)
|
||||
runIn(10, setOffline)
|
||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||
Content-Length: 277
|
||||
@@ -291,4 +290,4 @@ User-Agent: CyberGarage-HTTP/1.0
|
||||
</u:GetBinaryState>
|
||||
</s:Body>
|
||||
</s:Envelope>""", physicalgraph.device.Protocol.LAN)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2016-01-19
|
||||
*
|
||||
* This DTH should serve as the generic DTH to handle RGBW ZigBee HA devices
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZigBee RGBW Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Color Control"
|
||||
capability "Color Temperature"
|
||||
capability "Configuration"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY LED FLEXIBLE STRIP RGBW"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY LED FLEXIBLE STRIP RGBW"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 RGBW", deviceJoinName: "OSRAM LIGHTIFY LED A19 RGBW"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR RGBW", deviceJoinName: "OSRAM LIGHTIFY LED BR30 RGBW"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT RGBW", deviceJoinName: "OSRAM LIGHTIFY LED RT 5/6 RGBW"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
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.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"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||
attributeState "color", action:"color control.setColor"
|
||||
}
|
||||
}
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "colorTemperature", label: '${currentValue} K'
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
//Globals
|
||||
private getATTRIBUTE_HUE() { 0x0000 }
|
||||
private getATTRIBUTE_SATURATION() { 0x0001 }
|
||||
private getHUE_COMMAND() { 0x00 }
|
||||
private getSATURATION_COMMAND() { 0x03 }
|
||||
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||
private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 }
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def finalResult = zigbee.getEvent(description)
|
||||
if (finalResult) {
|
||||
log.debug finalResult
|
||||
sendEvent(finalResult)
|
||||
}
|
||||
else {
|
||||
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
|
||||
log.trace "zigbeeMap : $zigbeeMap"
|
||||
|
||||
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
||||
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 360)
|
||||
sendEvent(name: "hue", value: hueValue, displayed:false)
|
||||
}
|
||||
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "saturation", value: saturationValue, displayed:false)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.info "DID NOT PARSE MESSAGE for description : $description"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbee.off()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.readAttribute(0x0006, 0x00) + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(0x0300, 0x00) + zigbee.readAttribute(0x0300, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(0x0300, ATTRIBUTE_HUE) + zigbee.readAttribute(0x0300, ATTRIBUTE_SATURATION) + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.readAttribute(0x0006, 0x00) + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
|
||||
def setColorTemperature(value) {
|
||||
zigbee.setColorTemperature(value)
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
def setColor(value){
|
||||
log.trace "setColor($value)"
|
||||
zigbee.on() + setHue(value.hue) + "delay 300" + setSaturation(value.saturation)
|
||||
}
|
||||
|
||||
def setHue(value) {
|
||||
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||
}
|
||||
|
||||
def setSaturation(value) {
|
||||
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") //payload-> sat value, transition time
|
||||
}
|
||||
@@ -25,7 +25,6 @@ metadata {
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B05", outClusters: "0003, 000A, 0019", manufacturer: "Jasco Products", model: "45853", deviceJoinName: "GE ZigBee Plug-In Switch"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45856", deviceJoinName: "GE ZigBee In-Wall Switch"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 000F, 0B04", outClusters: "0019", manufacturer: "SmartThings", model: "outletv4", deviceJoinName: "Outlet"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
|
||||
@@ -275,7 +275,6 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) {
|
||||
case 32:
|
||||
map = [ name: "codeChanged", value: "all", descriptionText: "$device.displayName: all user codes deleted", isStateChange: true ]
|
||||
allCodesDeleted()
|
||||
break
|
||||
case 33:
|
||||
map = [ name: "codeReport", value: cmd.alarmLevel, data: [ code: "" ], isStateChange: true ]
|
||||
map.descriptionText = "$device.displayName code $cmd.alarmLevel was deleted"
|
||||
@@ -342,14 +341,14 @@ def zwaveEvent(UserCodeReport cmd) {
|
||||
map = [ name: "codeReport", value: cmd.userIdentifier, data: [ code: code ] ]
|
||||
map.descriptionText = "$device.displayName code $cmd.userIdentifier is set"
|
||||
map.displayed = (cmd.userIdentifier != state.requestCode && cmd.userIdentifier != state.pollCode)
|
||||
map.isStateChange = true
|
||||
map.isStateChange = (code != decrypt(state[name]))
|
||||
}
|
||||
result << createEvent(map)
|
||||
} else {
|
||||
map = [ name: "codeReport", value: cmd.userIdentifier, data: [ code: "" ] ]
|
||||
if (state.blankcodes && state["reset$name"]) { // we deleted this code so we can tell that our new code gets set
|
||||
map.descriptionText = "$device.displayName code $cmd.userIdentifier was reset"
|
||||
map.displayed = map.isStateChange = true
|
||||
map.displayed = map.isStateChange = false
|
||||
result << createEvent(map)
|
||||
state["set$name"] = state["reset$name"]
|
||||
result << response(setCode(cmd.userIdentifier, state["reset$name"]))
|
||||
@@ -361,7 +360,7 @@ def zwaveEvent(UserCodeReport cmd) {
|
||||
map.descriptionText = "$device.displayName code $cmd.userIdentifier is not set"
|
||||
}
|
||||
map.displayed = (cmd.userIdentifier != state.requestCode && cmd.userIdentifier != state.pollCode)
|
||||
map.isStateChange = true
|
||||
map.isStateChange = state[name] as Boolean
|
||||
result << createEvent(map)
|
||||
}
|
||||
code = ""
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
metadata {
|
||||
definition (name: "Timevalve Smart", namespace: "timevalve.gaslock.t-08", author: "ruinnel") {
|
||||
capability "Valve"
|
||||
capability "Refresh"
|
||||
capability "Battery"
|
||||
capability "Temperature Measurement"
|
||||
|
||||
command "setRemaining"
|
||||
command "setTimeout"
|
||||
command "setTimeout10"
|
||||
command "setTimeout20"
|
||||
command "setTimeout30"
|
||||
command "setTimeout40"
|
||||
|
||||
command "remainingLevel"
|
||||
|
||||
attribute "remaining", "number"
|
||||
attribute "remainingText", "String"
|
||||
attribute "timeout", "number"
|
||||
|
||||
//raw desc : 0 0 0x1006 0 0 0 7 0x5E 0x86 0x72 0x5A 0x73 0x98 0x80
|
||||
//fingerprint deviceId:"0x1006", inClusters:"0x5E, 0x86, 0x72, 0x5A, 0x73, 0x98, 0x80"
|
||||
}
|
||||
|
||||
tiles (scale: 2) {
|
||||
multiAttributeTile(name:"statusTile", type:"generic", width:6, height:4) {
|
||||
tileAttribute("device.contact", key: "PRIMARY_CONTROL") {
|
||||
attributeState "open", label: '${name}', action: "close", icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
||||
attributeState "closed", label:'${name}', action: "", icon:"st.contact.contact.closed", backgroundColor:"#79b821"
|
||||
}
|
||||
tileAttribute("device.remainingText", key: "SECONDARY_CONTROL") {
|
||||
attributeState "open", label: '${currentValue}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
||||
attributeState "closed", label:'', icon:"st.contact.contact.closed", backgroundColor:"#79b821"
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("refreshTile", "command.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
controlTile("remainingSliderTile", "device.remaining", "slider", inactiveLabel: false, range:"(0..590)", height: 2, width: 4) {
|
||||
state "level", action:"remainingLevel"
|
||||
}
|
||||
valueTile("setRemaining", "device.remainingText", inactiveLabel: false, decoration: "flat", height: 2, width: 2){
|
||||
state "remainingText", label:'${currentValue}\nRemaining'//, action: "setRemaining"//, icon: "st.Office.office6"
|
||||
}
|
||||
|
||||
standardTile("setTimeout10", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'10Min', action: "setTimeout10", icon:"st.Health & Wellness.health7", defaultState: true
|
||||
state "10", label:'10Min', action: "setTimeout10", icon:"st.Office.office13"
|
||||
}
|
||||
standardTile("setTimeout20", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'20Min', action: "setTimeout20", icon:"st.Health & Wellness.health7", defaultState: true
|
||||
state "20", label:'20Min', action: "setTimeout20", icon:"st.Office.office13"
|
||||
}
|
||||
standardTile("setTimeout30", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'30Min', action: "setTimeout30", icon:"st.Health & Wellness.health7", defaultState: true
|
||||
state "30", label:'30Min', action: "setTimeout30", icon:"st.Office.office13"
|
||||
}
|
||||
standardTile("setTimeout40", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'40Min', action: "setTimeout40", icon:"st.Health & Wellness.health7", defaultState: true
|
||||
state "40", label:'40Min', action: "setTimeout40", icon:"st.Office.office13"
|
||||
}
|
||||
|
||||
valueTile("batteryTile", "device.battery", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
main (["statusTile"])
|
||||
// details (["statusTile", "remainingSliderTile", "setRemaining", "setTimeout10", "setTimeout20", "batteryTile", "refreshTile", "setTimeout30", "setTimeout40"])
|
||||
// details (["statusTile", "batteryTile", "setRemaining", "refreshTile"])
|
||||
details (["statusTile", "batteryTile", "refreshTile"])
|
||||
}
|
||||
}
|
||||
|
||||
def parse(description) {
|
||||
// log.debug "parse - " + description
|
||||
def result = null
|
||||
if (description.startsWith("Err 106")) {
|
||||
state.sec = 0
|
||||
result = createEvent(descriptionText: description, isStateChange: true)
|
||||
} else if (description != "updated") {
|
||||
def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x71: 1, 0x98: 1])
|
||||
if (cmd) {
|
||||
log.debug "parsed cmd = " + cmd
|
||||
result = zwaveEvent(cmd)
|
||||
//log.debug("'$description' parsed to $result")
|
||||
} else {
|
||||
log.debug("Couldn't zwave.parse '$description'")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 복호화 후 zwaveEvent() 호출
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||
//log.debug "SecurityMessageEncapsulation - " + cmd
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1, 0x70: 1, 0x71: 1, 0x98: 1])
|
||||
if (encapsulatedCommand) {
|
||||
state.sec = 1
|
||||
log.debug "encapsulatedCommand = " + encapsulatedCommand
|
||||
zwaveEvent(encapsulatedCommand)
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
|
||||
//log.debug "switch status - " + cmd.value
|
||||
createEvent(name:"contact", value: cmd.value ? "open" : "closed")
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||
def map = [ name: "battery", unit: "%" ]
|
||||
if (cmd.batteryLevel == 0xFF) { // Special value for low battery alert
|
||||
map.value = 1
|
||||
map.descriptionText = "${device.displayName} has a low battery"
|
||||
map.isStateChange = true
|
||||
} else {
|
||||
map.value = cmd.batteryLevel
|
||||
}
|
||||
|
||||
log.debug "battery - ${map.value}${map.unit}"
|
||||
// Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler
|
||||
state.lastbatt = new Date().time
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
//log.debug "zwaveEvent - ${device.displayName}: ${cmd}"
|
||||
createEvent(descriptionText: "${device.displayName}: ${cmd}")
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
|
||||
def result = []
|
||||
log.info "zwave.configurationV1.configurationGet - " + cmd
|
||||
def array = cmd.configurationValue
|
||||
def value = ( (array[0] * 0x1000000) + (array[1] * 0x10000) + (array[2] * 0x100) + array[3] ).intdiv(60)
|
||||
if (device.currentValue("contact") == "open") {
|
||||
value = ( (array[0] * 0x1000000) + (array[1] * 0x10000) + (array[2] * 0x100) + array[3] ).intdiv(60)
|
||||
} else {
|
||||
value = 0
|
||||
}
|
||||
|
||||
if (device.currentValue('contact') == 'open') {
|
||||
def hour = value.intdiv(60);
|
||||
def min = (value % 60).toString().padLeft(2, '0');
|
||||
def text = "${hour}:${min}M"
|
||||
|
||||
log.info "remain - " + text
|
||||
result.add( createEvent(name: "remaining", value: value, displayed: false, isStateChange: true) )
|
||||
result.add( createEvent(name: "remainingText", value: text, displayed: false, isStateChange: true) )
|
||||
} else {
|
||||
result.add( createEvent(name: "timeout", value: value, displayed: false, isStateChange: true) )
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
|
||||
def type = cmd.notificationType
|
||||
if (type == cmd.NOTIFICATION_TYPE_HEAT) {
|
||||
log.info "NotificationReport - ${type}"
|
||||
createEvent(name: "temperature", value: 999, unit: "C", descriptionText: "${device.displayName} is over heat!", displayed: true, isStateChange: true)
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.alarmv1.AlarmReport cmd) {
|
||||
def type = cmd.alarmType
|
||||
def level = cmd.alarmLevel
|
||||
|
||||
log.info "AlarmReport - type : ${type}, level : ${level}"
|
||||
def msg = "${device.displayName} is over heat!"
|
||||
def result = createEvent(name: "temperature", value: 999, unit: "C", descriptionText: msg, displayed: true, isStateChange: true)
|
||||
if (sendPushMessage) {
|
||||
sendPushMessage(msg)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// remote open not allow
|
||||
def open() {}
|
||||
|
||||
def close() {
|
||||
// log.debug 'cmd - close()'
|
||||
commands([
|
||||
zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00),
|
||||
zwave.switchBinaryV1.switchBinaryGet()
|
||||
])
|
||||
}
|
||||
|
||||
def setTimeout10() { setTimeout(10) }
|
||||
def setTimeout20() { setTimeout(20) }
|
||||
def setTimeout30() { setTimeout(30) }
|
||||
def setTimeout40() { setTimeout(40) }
|
||||
|
||||
|
||||
def setTimeout(value) {
|
||||
// log.debug "setDefaultTime($value)"
|
||||
commands([
|
||||
zwave.configurationV1.configurationSet(parameterNumber: 0x01, size: 4, scaledConfigurationValue: value * 60),
|
||||
zwave.configurationV1.configurationGet(parameterNumber: 0x01)
|
||||
]);
|
||||
}
|
||||
|
||||
def remainingLevel(value) {
|
||||
// log.debug "remainingLevel($value)"
|
||||
def hour = value.intdiv(60);
|
||||
def min = (value % 60).toString().padLeft(2, '0');
|
||||
def text = "${hour}:${min}M"
|
||||
sendEvent(name: "remaining", value: value, displayed: false, isStateChange: true)
|
||||
sendEvent(name: "remainingText", value: text, displayed: false, isStateChange: true)
|
||||
}
|
||||
|
||||
def setRemaining() {
|
||||
def remaining = device.currentValue("remaining")
|
||||
// log.debug "setConfiguration() - remaining : $remaining"
|
||||
commands([
|
||||
zwave.configurationV1.configurationSet(parameterNumber: 0x03, size: 4, scaledConfigurationValue: remaining * 60),
|
||||
zwave.configurationV1.configurationGet(parameterNumber: 0x03)
|
||||
]);
|
||||
}
|
||||
|
||||
private command(physicalgraph.zwave.Command cmd) {
|
||||
if (state.sec != 0 && !(cmd instanceof physicalgraph.zwave.commands.batteryv1.BatteryGet)) {
|
||||
log.debug "cmd = " + cmd + ", encapsulation"
|
||||
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||
} else {
|
||||
log.debug "cmd = " + cmd + ", plain"
|
||||
cmd.format()
|
||||
}
|
||||
}
|
||||
|
||||
private commands(commands, delay=200) {
|
||||
delayBetween(commands.collect{ command(it) }, delay)
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
// log.debug 'cmd - refresh()'
|
||||
commands([
|
||||
zwave.batteryV1.batteryGet(),
|
||||
zwave.switchBinaryV1.switchBinaryGet(),
|
||||
zwave.configurationV1.configurationGet(parameterNumber: 0x01),
|
||||
zwave.configurationV1.configurationGet(parameterNumber: 0x03)
|
||||
], 400)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.3 KiB |
@@ -1,189 +0,0 @@
|
||||
/**
|
||||
* Vinli Home Beta
|
||||
*
|
||||
* Copyright 2015 Daniel
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Vinli Home Connect",
|
||||
namespace: "com.vinli.smartthings",
|
||||
author: "Daniel",
|
||||
description: "Allows Vinli users to connect their car to SmartThings",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://d3azp77rte0gip.cloudfront.net/smartapps/baeb2e5d-ebd0-49fe-a4ec-e92417ae20bb/images/vinli_oauth_60.png",
|
||||
iconX2Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/baeb2e5d-ebd0-49fe-a4ec-e92417ae20bb/images/vinli_oauth_120.png",
|
||||
iconX3Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/baeb2e5d-ebd0-49fe-a4ec-e92417ae20bb/images/vinli_oauth_120.png",
|
||||
oauth: true)
|
||||
|
||||
preferences {
|
||||
section ("Allow external service to control these things...") {
|
||||
input "switches", "capability.switch", multiple: true, required: true
|
||||
input "locks", "capability.lock", multiple: true, required: true
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
|
||||
path("/devices") {
|
||||
action: [
|
||||
GET: "listAllDevices"
|
||||
]
|
||||
}
|
||||
|
||||
path("/switches") {
|
||||
action: [
|
||||
GET: "listSwitches"
|
||||
]
|
||||
}
|
||||
path("/switches/:command") {
|
||||
action: [
|
||||
PUT: "updateSwitches"
|
||||
]
|
||||
}
|
||||
path("/switches/:id/:command") {
|
||||
action: [
|
||||
PUT: "updateSwitch"
|
||||
]
|
||||
}
|
||||
path("/locks/:command") {
|
||||
action: [
|
||||
PUT: "updateLocks"
|
||||
]
|
||||
}
|
||||
path("/locks/:id/:command") {
|
||||
action: [
|
||||
PUT: "updateLock"
|
||||
]
|
||||
}
|
||||
|
||||
path("/devices/:id/:command") {
|
||||
action: [
|
||||
PUT: "commandDevice"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// returns a list of all devices
|
||||
def listAllDevices() {
|
||||
def resp = []
|
||||
switches.each {
|
||||
resp << [name: it.name, label: it.label, value: it.currentValue("switch"), type: "switch", id: it.id, hub: it.hub.name]
|
||||
}
|
||||
|
||||
locks.each {
|
||||
resp << [name: it.name, label: it.label, value: it.currentValue("lock"), type: "lock", id: it.id, hub: it.hub.name]
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// returns a list like
|
||||
// [[name: "kitchen lamp", value: "off"], [name: "bathroom", value: "on"]]
|
||||
def listSwitches() {
|
||||
def resp = []
|
||||
switches.each {
|
||||
resp << [name: it.displayName, value: it.currentValue("switch"), type: "switch", id: it.id]
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
void updateLocks() {
|
||||
// use the built-in request object to get the command parameter
|
||||
def command = params.command
|
||||
|
||||
if (command) {
|
||||
|
||||
// check that the switch supports the specified command
|
||||
// If not, return an error using httpError, providing a HTTP status code.
|
||||
locks.each {
|
||||
if (!it.hasCommand(command)) {
|
||||
httpError(501, "$command is not a valid command for all switches specified")
|
||||
}
|
||||
}
|
||||
|
||||
// all switches have the comand
|
||||
// execute the command on all switches
|
||||
// (note we can do this on the array - the command will be invoked on every element
|
||||
locks."$command"()
|
||||
}
|
||||
}
|
||||
|
||||
void updateLock() {
|
||||
def command = params.command
|
||||
|
||||
locks.each {
|
||||
if (!it.hasCommand(command)) {
|
||||
httpError(400, "$command is not a valid command for all lock specified")
|
||||
}
|
||||
|
||||
if (it.id == params.id) {
|
||||
it."$command"()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateSwitch() {
|
||||
def command = params.command
|
||||
|
||||
switches.each {
|
||||
if (!it.hasCommand(command)) {
|
||||
httpError(400, "$command is not a valid command for all switches specified")
|
||||
}
|
||||
|
||||
if (it.id == params.id) {
|
||||
it."$command"()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void commandDevice() {
|
||||
def command = params.command
|
||||
def devices = []
|
||||
|
||||
switches.each {
|
||||
devices << it
|
||||
}
|
||||
|
||||
locks.each {
|
||||
devices << it
|
||||
}
|
||||
|
||||
devices.each {
|
||||
if (it.id == params.id) {
|
||||
if (!it.hasCommand(command)) {
|
||||
httpError(400, "$command is not a valid command for specified device")
|
||||
}
|
||||
it."$command"()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateSwitches() {
|
||||
// use the built-in request object to get the command parameter
|
||||
def command = params.command
|
||||
|
||||
if (command) {
|
||||
|
||||
// check that the switch supports the specified command
|
||||
// If not, return an error using httpError, providing a HTTP status code.
|
||||
switches.each {
|
||||
if (!it.hasCommand(command)) {
|
||||
httpError(400, "$command is not a valid command for all switches specified")
|
||||
}
|
||||
}
|
||||
|
||||
// all switches have the comand
|
||||
// execute the command on all switches
|
||||
// (note we can do this on the array - the command will be invoked on every element
|
||||
switches."$command"()
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Initial State Event Streamer
|
||||
*
|
||||
* Copyright 2016 David Sulpy
|
||||
* Copyright 2015 David Sulpy
|
||||
*
|
||||
* 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:
|
||||
@@ -77,62 +77,6 @@ mappings {
|
||||
}
|
||||
}
|
||||
|
||||
def getAccessKey() {
|
||||
log.trace "get access key"
|
||||
if (atomicState.accessKey == null) {
|
||||
httpError(404, "Access Key Not Found")
|
||||
} else {
|
||||
[
|
||||
accessKey: atomicState.accessKey
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def getBucketKey() {
|
||||
log.trace "get bucket key"
|
||||
if (atomicState.bucketKey == null) {
|
||||
httpError(404, "Bucket key Not Found")
|
||||
} else {
|
||||
[
|
||||
bucketKey: atomicState.bucketKey,
|
||||
bucketName: atomicState.bucketName
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def setBucketKey() {
|
||||
log.trace "set bucket key"
|
||||
def newBucketKey = request.JSON?.bucketKey
|
||||
def newBucketName = request.JSON?.bucketName
|
||||
|
||||
log.debug "bucket name: $newBucketName"
|
||||
log.debug "bucket key: $newBucketKey"
|
||||
|
||||
if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
|
||||
atomicState.bucketKey = "$newBucketKey"
|
||||
atomicState.bucketName = "$newBucketName"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
|
||||
tryCreateBucket()
|
||||
}
|
||||
|
||||
def setAccessKey() {
|
||||
log.trace "set access key"
|
||||
def newAccessKey = request.JSON?.accessKey
|
||||
def newGrokerSubdomain = request.JSON?.grokerSubdomain
|
||||
|
||||
if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
|
||||
atomicState.grokerSubdomain = "$newGrokerSubdomain"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
|
||||
if (newAccessKey && newAccessKey != atomicState.accessKey) {
|
||||
atomicState.accessKey = "$newAccessKey"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
}
|
||||
|
||||
def subscribeToEvents() {
|
||||
if (accelerometers != null) {
|
||||
subscribe(accelerometers, "acceleration", genericHandler)
|
||||
@@ -225,27 +169,85 @@ def subscribeToEvents() {
|
||||
}
|
||||
}
|
||||
|
||||
def getAccessKey() {
|
||||
log.trace "get access key"
|
||||
if (atomicState.accessKey == null) {
|
||||
httpError(404, "Access Key Not Found")
|
||||
} else {
|
||||
[
|
||||
accessKey: atomicState.accessKey
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def getBucketKey() {
|
||||
log.trace "get bucket key"
|
||||
if (atomicState.bucketKey == null) {
|
||||
httpError(404, "Bucket key Not Found")
|
||||
} else {
|
||||
[
|
||||
bucketKey: atomicState.bucketKey,
|
||||
bucketName: atomicState.bucketName
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def setBucketKey() {
|
||||
log.trace "set bucket key"
|
||||
def newBucketKey = request.JSON?.bucketKey
|
||||
def newBucketName = request.JSON?.bucketName
|
||||
|
||||
log.debug "bucket name: $newBucketName"
|
||||
log.debug "bucket key: $newBucketKey"
|
||||
|
||||
if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
|
||||
atomicState.bucketKey = "$newBucketKey"
|
||||
atomicState.bucketName = "$newBucketName"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
|
||||
tryCreateBucket()
|
||||
}
|
||||
|
||||
def setAccessKey() {
|
||||
log.trace "set access key"
|
||||
def newAccessKey = request.JSON?.accessKey
|
||||
def newGrokerSubdomain = request.JSON?.grokerSubdomain
|
||||
|
||||
if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
|
||||
atomicState.grokerSubdomain = "$newGrokerSubdomain"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
|
||||
if (newAccessKey && newAccessKey != atomicState.accessKey) {
|
||||
atomicState.accessKey = "$newAccessKey"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
atomicState.version = "1.1.0"
|
||||
|
||||
atomicState.isBucketCreated = false
|
||||
atomicState.grokerSubdomain = "groker"
|
||||
|
||||
atomicState.version = "1.0.18"
|
||||
subscribeToEvents()
|
||||
|
||||
atomicState.isBucketCreated = false
|
||||
atomicState.grokerSubdomain = "groker"
|
||||
atomicState.eventBuffer = []
|
||||
|
||||
runEvery15Minutes(flushBuffer)
|
||||
|
||||
log.debug "installed (version $atomicState.version)"
|
||||
}
|
||||
|
||||
def updated() {
|
||||
atomicState.version = "1.1.0"
|
||||
atomicState.version = "1.0.18"
|
||||
unsubscribe()
|
||||
|
||||
if (atomicState.bucketKey != null && atomicState.accessKey != null) {
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
if (atomicState.eventBuffer == null) {
|
||||
atomicState.eventBuffer = []
|
||||
}
|
||||
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
||||
atomicState.grokerSubdomain = "groker"
|
||||
}
|
||||
@@ -325,17 +327,37 @@ def genericHandler(evt) {
|
||||
eventHandler(key, value)
|
||||
}
|
||||
|
||||
def eventHandler(name, value) {
|
||||
def epoch = now() / 1000
|
||||
|
||||
def event = new JsonSlurper().parseText("{\"key\": \"$name\", \"value\": \"$value\", \"epoch\": \"$epoch\"}")
|
||||
|
||||
tryShipEvents(event)
|
||||
|
||||
log.debug "Shipped Event: " + event
|
||||
// This is a handler function for flushing the event buffer
|
||||
// after a specified amount of time to reduce the load on ST servers
|
||||
def flushBuffer() {
|
||||
def eventBuffer = atomicState.eventBuffer
|
||||
log.trace "About to flush the buffer on schedule"
|
||||
if (eventBuffer != null && eventBuffer.size() > 0) {
|
||||
atomicState.eventBuffer = []
|
||||
tryShipEvents(eventBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
def tryShipEvents(event) {
|
||||
def eventHandler(name, value) {
|
||||
def epoch = now() / 1000
|
||||
def eventBuffer = atomicState.eventBuffer ?: []
|
||||
eventBuffer << [key: "$name", value: "$value", epoch: "$epoch"]
|
||||
|
||||
if (eventBuffer.size() >= 10) {
|
||||
// Clear eventBuffer right away since we've already pulled it off of atomicState to reduce the risk of missing
|
||||
// events. This assumes the grokerSubdomain, accessKey, and bucketKey are set correctly to avoid the eventBuffer
|
||||
// from growing unbounded.
|
||||
atomicState.eventBuffer = []
|
||||
tryShipEvents(eventBuffer)
|
||||
} else {
|
||||
// Make sure we persist the updated eventBuffer with the new event added back to atomicState
|
||||
atomicState.eventBuffer = eventBuffer
|
||||
}
|
||||
log.debug "Event added to buffer: " + eventBuffer
|
||||
}
|
||||
|
||||
// a helper function for shipping the atomicState.eventBuffer to Initial State
|
||||
def tryShipEvents(eventBuffer) {
|
||||
|
||||
def grokerSubdomain = atomicState.grokerSubdomain
|
||||
// can't ship events if there is no grokerSubdomain
|
||||
@@ -358,7 +380,7 @@ def tryShipEvents(event) {
|
||||
"X-IS-AccessKey": "${accessKey}",
|
||||
"Accept-Version": "0.0.2"
|
||||
],
|
||||
body: event
|
||||
body: eventBuffer
|
||||
]
|
||||
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@ preferences{
|
||||
input "lock1", "capability.lock", required: true
|
||||
}
|
||||
section("Select the door contact sensor:") {
|
||||
input "contact", "capability.contactSensor", required: true
|
||||
input "contact", "capability.contactSensor", required: true
|
||||
}
|
||||
section("Automatically lock the door when closed...") {
|
||||
input "minutesLater", "number", title: "Delay (in minutes):", required: true
|
||||
@@ -22,10 +22,9 @@ preferences{
|
||||
input "secondsLater", "number", title: "Delay (in seconds):", required: true
|
||||
}
|
||||
section( "Notifications" ) {
|
||||
input("recipients", "contact", title: "Send notifications to", required: false) {
|
||||
input "phoneNumber", "phone", title: "Warn with text message (optional)", description: "Phone Number", required: false
|
||||
}
|
||||
}
|
||||
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes", "No"]], required: false
|
||||
input "phoneNumber", "phone", title: "Enter phone number to send text notification.", required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed(){
|
||||
@@ -43,73 +42,55 @@ def initialize(){
|
||||
subscribe(lock1, "lock", doorHandler, [filterEvents: false])
|
||||
subscribe(lock1, "unlock", doorHandler, [filterEvents: false])
|
||||
subscribe(contact, "contact.open", doorHandler)
|
||||
subscribe(contact, "contact.closed", doorHandler)
|
||||
subscribe(contact, "contact.closed", doorHandler)
|
||||
}
|
||||
|
||||
def lockDoor(){
|
||||
log.debug "Locking the door."
|
||||
lock1.lock()
|
||||
if(location.contactBookEnabled) {
|
||||
if ( recipients ) {
|
||||
log.debug ( "Sending Push Notification..." )
|
||||
sendNotificationToContacts( "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!", recipients)
|
||||
}
|
||||
}
|
||||
if (phoneNumber) {
|
||||
log.debug("Sending text message...")
|
||||
sendSms( phoneNumber, "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!")
|
||||
}
|
||||
log.debug ( "Sending Push Notification..." )
|
||||
if ( sendPushMessage != "No" ) sendPush( "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!" )
|
||||
log.debug("Sending text message...")
|
||||
if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!" )
|
||||
}
|
||||
|
||||
def unlockDoor(){
|
||||
log.debug "Unlocking the door."
|
||||
lock1.unlock()
|
||||
if(location.contactBookEnabled) {
|
||||
if ( recipients ) {
|
||||
log.debug ( "Sending Push Notification..." )
|
||||
sendNotificationToContacts( "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!", recipients)
|
||||
}
|
||||
}
|
||||
if ( phoneNumber ) {
|
||||
log.debug("Sending text message...")
|
||||
sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!")
|
||||
}
|
||||
log.debug ( "Sending Push Notification..." )
|
||||
if ( sendPushMessage != "No" ) sendPush( "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!" )
|
||||
log.debug("Sending text message...")
|
||||
if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!" )
|
||||
}
|
||||
|
||||
def doorHandler(evt){
|
||||
if ((contact.latestValue("contact") == "open") && (evt.value == "locked")) { // If the door is open and a person locks the door then...
|
||||
//def delay = (secondsLater) // runIn uses seconds
|
||||
runIn( secondsLater, unlockDoor ) // ...schedule (in minutes) to unlock... We don't want the door to be closed while the lock is engaged.
|
||||
def delay = (secondsLater) // runIn uses seconds
|
||||
runIn( delay, unlockDoor ) // ...schedule (in minutes) to unlock... We don't want the door to be closed while the lock is engaged.
|
||||
}
|
||||
else if ((contact.latestValue("contact") == "open") && (evt.value == "unlocked")) { // If the door is open and a person unlocks it then...
|
||||
unschedule( unlockDoor ) // ...we don't need to unlock it later.
|
||||
}
|
||||
}
|
||||
else if ((contact.latestValue("contact") == "closed") && (evt.value == "locked")) { // If the door is closed and a person manually locks it then...
|
||||
unschedule( lockDoor ) // ...we don't need to lock it later.
|
||||
}
|
||||
else if ((contact.latestValue("contact") == "closed") && (evt.value == "unlocked")) { // If the door is closed and a person unlocks it then...
|
||||
//def delay = (minutesLater * 60) // runIn uses seconds
|
||||
runIn( (minutesLater * 60), lockDoor ) // ...schedule (in minutes) to lock.
|
||||
def delay = (minutesLater * 60) // runIn uses seconds
|
||||
runIn( delay, lockDoor ) // ...schedule (in minutes) to lock.
|
||||
}
|
||||
else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "open")) { // If a person opens an unlocked door...
|
||||
unschedule( lockDoor ) // ...we don't need to lock it later.
|
||||
}
|
||||
else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "closed")) { // If a person closes an unlocked door...
|
||||
//def delay = (minutesLater * 60) // runIn uses seconds
|
||||
runIn( (minutesLater * 60), lockDoor ) // ...schedule (in minutes) to lock.
|
||||
}
|
||||
def delay = (minutesLater * 60) // runIn uses seconds
|
||||
runIn( delay, lockDoor ) // ...schedule (in minutes) to lock.
|
||||
}
|
||||
else { //Opening or Closing door when locked (in case you have a handle lock)
|
||||
log.debug "Unlocking the door."
|
||||
lock1.unlock()
|
||||
if(location.contactBookEnabled) {
|
||||
if ( recipients ) {
|
||||
log.debug ( "Sending Push Notification..." )
|
||||
sendNotificationToContacts( "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!", recipients)
|
||||
}
|
||||
}
|
||||
if ( phoneNumber ) {
|
||||
log.debug("Sending text message...")
|
||||
sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!")
|
||||
}
|
||||
}
|
||||
log.debug "Unlocking the door."
|
||||
lock1.unlock()
|
||||
log.debug ( "Sending Push Notification..." )
|
||||
if ( sendPushMessage != "No" ) sendPush( "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!" )
|
||||
log.debug("Sending text message...")
|
||||
if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!" )
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,7 @@ definition(
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png",
|
||||
singleInstance: true
|
||||
singleInstance: true
|
||||
) {
|
||||
appSetting "clientId"
|
||||
}
|
||||
@@ -61,7 +61,7 @@ def authPage() {
|
||||
description = "Click to enter Ecobee Credentials"
|
||||
}
|
||||
|
||||
def redirectUrl = buildRedirectUrl
|
||||
def redirectUrl = buildRedirectUrl //"${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}"
|
||||
log.debug "RedirectUrl = ${redirectUrl}"
|
||||
// get rid of next button until the user is actually auth'd
|
||||
if (!oauthTokenProvided) {
|
||||
@@ -103,7 +103,7 @@ def oauthInitUrl() {
|
||||
scope: "smartRead,smartWrite",
|
||||
client_id: smartThingsClientId,
|
||||
state: atomicState.oauthInitState,
|
||||
redirect_uri: callbackUrl
|
||||
redirect_uri: callbackUrl //"https://graph.api.smartthings.com/oauth/callback"
|
||||
]
|
||||
|
||||
redirect(location: "${apiEndpoint}/authorize?${toQueryString(oauthParams)}")
|
||||
@@ -115,13 +115,14 @@ def callback() {
|
||||
def code = params.code
|
||||
def oauthState = params.state
|
||||
|
||||
//verify oauthState == atomicState.oauthInitState, so the callback corresponds to the authentication request
|
||||
if (oauthState == atomicState.oauthInitState){
|
||||
|
||||
def tokenParams = [
|
||||
grant_type: "authorization_code",
|
||||
code : code,
|
||||
client_id : smartThingsClientId,
|
||||
redirect_uri: callbackUrl
|
||||
redirect_uri: callbackUrl //"https://graph.api.smartthings.com/oauth/callback"
|
||||
]
|
||||
|
||||
def tokenUrl = "https://www.ecobee.com/home/token?${toQueryString(tokenParams)}"
|
||||
@@ -246,26 +247,31 @@ def getEcobeeThermostats() {
|
||||
]
|
||||
|
||||
def stats = [:]
|
||||
try {
|
||||
httpGet(deviceListParams) { resp ->
|
||||
try {
|
||||
httpGet(deviceListParams) { resp ->
|
||||
|
||||
if (resp.status == 200) {
|
||||
resp.data.thermostatList.each { stat ->
|
||||
atomicState.remoteSensors = stat.remoteSensors
|
||||
def dni = [app.id, stat.identifier].join('.')
|
||||
stats[dni] = getThermostatDisplayName(stat)
|
||||
}
|
||||
} else {
|
||||
log.debug "http status: ${resp.status}"
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.trace "Exception polling children: " + e.response.data.status
|
||||
if (e.response.data.status.code == 14) {
|
||||
atomicState.action = "getEcobeeThermostats"
|
||||
log.debug "Refreshing your auth_token!"
|
||||
refreshAuthToken()
|
||||
if (resp.status == 200) {
|
||||
resp.data.thermostatList.each { stat ->
|
||||
atomicState.remoteSensors = stat.remoteSensors
|
||||
def dni = [app.id, stat.identifier].join('.')
|
||||
stats[dni] = getThermostatDisplayName(stat)
|
||||
}
|
||||
} else {
|
||||
log.debug "http status: ${resp.status}"
|
||||
//refresh the auth token
|
||||
if (resp.status == 500 && resp.data.status.code == 14) {
|
||||
log.debug "Storing the failed action to try later"
|
||||
atomicState.action = "getEcobeeThermostats"
|
||||
log.debug "Refreshing your auth_token!"
|
||||
refreshAuthToken()
|
||||
} else {
|
||||
log.error "Authentication error, invalid authentication method, lack of credentials, etc."
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug "___exception getEcobeeThermostats(): " + e
|
||||
refreshAuthToken()
|
||||
}
|
||||
atomicState.thermostats = stats
|
||||
return stats
|
||||
@@ -311,7 +317,7 @@ def initialize() {
|
||||
def devices = thermostats.collect { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
d = addChildDevice(app.namespace, getChildName(), dni, null, ["label":"${atomicState.thermostats[dni]}" ?: "Ecobee Thermostat"])
|
||||
d = addChildDevice(app.namespace, getChildName(), dni, null, ["label":"Ecobee Thermostat:${atomicState.thermostats[dni]}"])
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
@@ -322,7 +328,7 @@ def initialize() {
|
||||
def sensors = ecobeesensors.collect { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
d = addChildDevice(app.namespace, getSensorChildName(), dni, null, ["label":"${atomicState.sensors[dni]}" ?:"Ecobee Sensor"])
|
||||
d = addChildDevice(app.namespace, getSensorChildName(), dni, null, ["label":"Ecobee Sensor:${atomicState.sensors[dni]}"])
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
@@ -348,17 +354,21 @@ def initialize() {
|
||||
|
||||
atomicState.thermostatData = [:] //reset Map to store thermostat data
|
||||
|
||||
//send activity feeds to tell that device is connected
|
||||
def notificationMessage = "is connected to SmartThings"
|
||||
sendActivityFeeds(notificationMessage)
|
||||
atomicState.timeSendPush = null
|
||||
atomicState.reAttempt = 0
|
||||
//send activity feeds to tell that device is connected
|
||||
def notificationMessage = "is connected to SmartThings"
|
||||
sendActivityFeeds(notificationMessage)
|
||||
state.timeSendPush = null
|
||||
|
||||
pollHandler() //first time polling data data from thermostat
|
||||
|
||||
//automatically update devices status every 5 mins
|
||||
runEvery5Minutes("poll")
|
||||
|
||||
//since access_token expires every 2 hours
|
||||
runEvery1Hour("refreshAuthToken")
|
||||
|
||||
atomicState.reAttempt = 0
|
||||
|
||||
}
|
||||
|
||||
def pollHandler() {
|
||||
@@ -379,10 +389,18 @@ def pollHandler() {
|
||||
def pollChildren(child = null) {
|
||||
def thermostatIdsString = getChildDeviceIdsString()
|
||||
log.debug "polling children: $thermostatIdsString"
|
||||
def data = ""
|
||||
|
||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeExtendedRuntime":"true","includeSettings":"true","includeRuntime":"true","includeSensors":true}}'
|
||||
def result = false
|
||||
// // TODO: test this:
|
||||
//
|
||||
// def jsonRequestBody = toJson([
|
||||
// selection:[
|
||||
// selectionType: "thermostats",
|
||||
// selectionMatch: getChildDeviceIdsString(),
|
||||
// includeRuntime: true
|
||||
// ]
|
||||
// ])
|
||||
|
||||
def pollParams = [
|
||||
uri: apiEndpoint,
|
||||
@@ -393,6 +411,11 @@ def pollChildren(child = null) {
|
||||
|
||||
try{
|
||||
httpGet(pollParams) { resp ->
|
||||
|
||||
// if (resp.data) {
|
||||
// debugEventFromParent(child, "pollChildren(child) >> resp.status = ${resp.status}, resp.data = ${resp.data}")
|
||||
// }
|
||||
|
||||
if(resp.status == 200) {
|
||||
log.debug "poll results returned resp.data ${resp.data}"
|
||||
atomicState.remoteSensors = resp.data.thermostatList.remoteSensors
|
||||
@@ -403,57 +426,45 @@ def pollChildren(child = null) {
|
||||
|
||||
log.debug "updating dni $dni"
|
||||
|
||||
data = [
|
||||
def data = [
|
||||
coolMode: (stat.settings.coolStages > 0),
|
||||
heatMode: (stat.settings.heatStages > 0),
|
||||
deviceTemperatureUnit: stat.settings.useCelsius,
|
||||
minHeatingSetpoint: (stat.settings.heatRangeLow / 10),
|
||||
maxHeatingSetpoint: (stat.settings.heatRangeHigh / 10),
|
||||
minCoolingSetpoint: (stat.settings.coolRangeLow / 10),
|
||||
maxCoolingSetpoint: (stat.settings.coolRangeHigh / 10),
|
||||
autoMode: stat.settings.autoHeatCoolFeatureEnabled,
|
||||
auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler),
|
||||
temperature: (stat.runtime.actualTemperature / 10),
|
||||
temperature: stat.runtime.actualTemperature / 10,
|
||||
heatingSetpoint: stat.runtime.desiredHeat / 10,
|
||||
coolingSetpoint: stat.runtime.desiredCool / 10,
|
||||
thermostatMode: stat.settings.hvacMode,
|
||||
humidity: stat.runtime.actualHumidity,
|
||||
thermostatFanMode: stat.runtime.desiredFanMode
|
||||
thermostatMode: stat.settings.hvacMode
|
||||
]
|
||||
|
||||
if (location.temperatureScale == "F")
|
||||
{
|
||||
data["temperature"] = data["temperature"] ? Math.round(data["temperature"].toDouble()) : data["temperature"]
|
||||
data["heatingSetpoint"] = data["heatingSetpoint"] ? Math.round(data["heatingSetpoint"].toDouble()) : data["heatingSetpoint"]
|
||||
data["coolingSetpoint"] = data["coolingSetpoint"] ? Math.round(data["coolingSetpoint"].toDouble()) : data["coolingSetpoint"]
|
||||
data["minHeatingSetpoint"] = data["minHeatingSetpoint"] ? Math.round(data["minHeatingSetpoint"].toDouble()) : data["minHeatingSetpoint"]
|
||||
data["maxHeatingSetpoint"] = data["maxHeatingSetpoint"] ? Math.round(data["maxHeatingSetpoint"].toDouble()) : data["maxHeatingSetpoint"]
|
||||
data["minCoolingSetpoint"] = data["minCoolingSetpoint"] ? Math.round(data["minCoolingSetpoint"].toDouble()) : data["minCoolingSetpoint"]
|
||||
data["maxCoolingSetpoint"] = data["maxCoolingSetpoint"] ? Math.round(data["maxCoolingSetpoint"].toDouble()) : data["maxCoolingSetpoint"]
|
||||
|
||||
}
|
||||
|
||||
if (data?.deviceTemperatureUnit == false && location.temperatureScale == "F") {
|
||||
data["deviceTemperatureUnit"] = "F"
|
||||
|
||||
} else {
|
||||
data["deviceTemperatureUnit"] = "C"
|
||||
}
|
||||
data["temperature"] = data["temperature"] ? data["temperature"].toDouble().toInteger() : data["temperature"]
|
||||
data["heatingSetpoint"] = data["heatingSetpoint"] ? data["heatingSetpoint"].toDouble().toInteger() : data["heatingSetpoint"]
|
||||
data["coolingSetpoint"] = data["coolingSetpoint"] ? data["coolingSetpoint"].toDouble().toInteger() : data["coolingSetpoint"]
|
||||
// debugEventFromParent(child, "Event Data = ${data}")
|
||||
|
||||
collector[dni] = [data:data]
|
||||
return collector
|
||||
}
|
||||
result = true
|
||||
log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}"
|
||||
} else {
|
||||
log.error "polling children & got http status ${resp.status}"
|
||||
|
||||
//refresh the auth token
|
||||
if (resp.status == 500 && resp.data.status.code == 14) {
|
||||
log.debug "Storing the failed action to try later"
|
||||
atomicState.action = "pollChildren";
|
||||
log.debug "Refreshing your auth_token!"
|
||||
refreshAuthToken()
|
||||
}
|
||||
else {
|
||||
log.error "Authentication error, invalid authentication method, lack of credentials, etc."
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.trace "Exception polling children: " + e.response.data.status
|
||||
if (e.response.data.status.code == 14) {
|
||||
atomicState.action = "pollChildren"
|
||||
log.debug "Refreshing your auth_token!"
|
||||
refreshAuthToken()
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug "___exception polling children: " + e
|
||||
// debugEventFromParent(child, "___exception polling children: " + e)
|
||||
refreshAuthToken()
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -465,14 +476,18 @@ def pollChild(child){
|
||||
if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){
|
||||
if(atomicState.thermostats[child.device.deviceNetworkId] != null) {
|
||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||
// debugEventFromParent(child, "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}") //TODO comment
|
||||
log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}"
|
||||
child.generateEvent(tData.data) //parse received message from parent
|
||||
// return tData.data
|
||||
} else if(atomicState.thermostats[child.device.deviceNetworkId] == null) {
|
||||
// debugEventFromParent(child, "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling") //TODO comment
|
||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}"
|
||||
return null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// debugEventFromParent(child, "ERROR: pollChildren(child) for ${child.device.deviceNetworkId} after polling") //TODO comment
|
||||
log.info "ERROR: pollChildren(child) for ${child.device.deviceNetworkId} after polling"
|
||||
return null
|
||||
}
|
||||
@@ -498,6 +513,9 @@ def availableModes(child) {
|
||||
{
|
||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
||||
|
||||
// TODO: flag device as in error state
|
||||
// child.errorState = true
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -524,6 +542,8 @@ def currentMode(child) {
|
||||
if(!tData) {
|
||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
||||
|
||||
// TODO: flag device as in error state
|
||||
// child.errorState = true
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -541,12 +561,8 @@ def updateSensorData() {
|
||||
def occupancy = ""
|
||||
it.capability.each {
|
||||
if (it.type == "temperature") {
|
||||
if (location.temperatureScale == "F") {
|
||||
temperature = Math.round(it.value.toDouble() / 10)
|
||||
} else {
|
||||
temperature = convertFtoC(it.value.toDouble() / 10)
|
||||
}
|
||||
|
||||
temperature = it.value as Double
|
||||
temperature = (temperature / 10).toInteger()
|
||||
} else if (it.type == "occupancy") {
|
||||
if(it.value == "true")
|
||||
occupancy = "active"
|
||||
@@ -559,6 +575,7 @@ def updateSensorData() {
|
||||
if(d) {
|
||||
d.sendEvent(name:"temperature", value: temperature)
|
||||
d.sendEvent(name:"motion", value: occupancy)
|
||||
// debugEventFromParent(d, "temperature : ${temperature}, motion:${occupancy}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -578,65 +595,68 @@ def toQueryString(Map m) {
|
||||
}
|
||||
|
||||
private refreshAuthToken() {
|
||||
log.debug "refreshing auth token"
|
||||
log.debug "refreshing auth token"
|
||||
|
||||
if(!atomicState.refreshToken) {
|
||||
log.warn "Can not refresh OAuth token since there is no refreshToken stored"
|
||||
} else {
|
||||
if(!atomicState.refreshToken) {
|
||||
log.warn "Can not refresh OAuth token since there is no refreshToken stored"
|
||||
} else {
|
||||
|
||||
def refreshParams = [
|
||||
method: 'POST',
|
||||
uri : apiEndpoint,
|
||||
path : "/token",
|
||||
query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId],
|
||||
]
|
||||
def refreshParams = [
|
||||
method: 'POST',
|
||||
uri : apiEndpoint,
|
||||
path : "/token",
|
||||
query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId],
|
||||
]
|
||||
|
||||
log.debug refreshParams
|
||||
log.debug refreshParams
|
||||
|
||||
def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials."
|
||||
//changed to httpPost
|
||||
try {
|
||||
def jsonMap
|
||||
httpPost(refreshParams) { resp ->
|
||||
def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials."
|
||||
//changed to httpPost
|
||||
try {
|
||||
def jsonMap
|
||||
httpPost(refreshParams) { resp ->
|
||||
|
||||
if(resp.status == 200) {
|
||||
log.debug "Token refreshed...calling saved RestAction now!"
|
||||
if(resp.status == 200) {
|
||||
log.debug "Token refreshed...calling saved RestAction now!"
|
||||
|
||||
debugEvent("Token refreshed ... calling saved RestAction now!")
|
||||
debugEvent("Token refreshed ... calling saved RestAction now!")
|
||||
|
||||
log.debug resp
|
||||
log.debug resp
|
||||
|
||||
jsonMap = resp.data
|
||||
jsonMap = resp.data
|
||||
|
||||
if(resp.data) {
|
||||
if(resp.data) {
|
||||
|
||||
log.debug resp.data
|
||||
debugEvent("Response = ${resp.data}")
|
||||
log.debug resp.data
|
||||
debugEvent("Response = ${resp.data}")
|
||||
|
||||
atomicState.refreshToken = resp?.data?.refresh_token
|
||||
atomicState.authToken = resp?.data?.access_token
|
||||
atomicState.refreshToken = resp?.data?.refresh_token
|
||||
atomicState.authToken = resp?.data?.access_token
|
||||
|
||||
debugEvent("Refresh Token = ${atomicState.refreshToken}")
|
||||
debugEvent("OAUTH Token = ${atomicState.authToken}")
|
||||
debugEvent("Refresh Token = ${atomicState.refreshToken}")
|
||||
debugEvent("OAUTH Token = ${atomicState.authToken}")
|
||||
|
||||
if(atomicState.action && atomicState.action != "") {
|
||||
log.debug "Executing next action: ${atomicState.action}"
|
||||
if(atomicState.action && atomicState.action != "") {
|
||||
log.debug "Executing next action: ${atomicState.action}"
|
||||
|
||||
"${atomicState.action}"()
|
||||
"${atomicState.action}"()
|
||||
|
||||
atomicState.action = ""
|
||||
}
|
||||
//remove saved action
|
||||
atomicState.action = ""
|
||||
}
|
||||
|
||||
}
|
||||
atomicState.action = ""
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
|
||||
}
|
||||
atomicState.action = ""
|
||||
} else {
|
||||
log.debug "refresh failed ${resp.status} : ${resp.status.code}"
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
|
||||
def reAttemptPeriod = 300 // in sec
|
||||
if (e.statusCode != 401) { // this issue might comes from exceed 20sec app execution, connectivity issue etc.
|
||||
if (e.statusCode != 401) { //this issue might comes from exceed 20sec app execution, connectivity issue etc.
|
||||
runIn(reAttemptPeriod, "refreshAuthToken")
|
||||
} else if (e.statusCode == 401) { // unauthorized
|
||||
} else if (e.statusCode == 401) { //refresh token is expired
|
||||
atomicState.reAttempt = atomicState.reAttempt + 1
|
||||
log.warn "reAttempt refreshAuthToken to try = ${atomicState.reAttempt}"
|
||||
if (atomicState.reAttempt <= 3) {
|
||||
@@ -645,16 +665,20 @@ private refreshAuthToken() {
|
||||
sendPushAndFeeds(notificationMessage)
|
||||
atomicState.reAttempt = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def resumeProgram(child, deviceId) {
|
||||
|
||||
// def thermostatIdsString = getChildDeviceIdsString()
|
||||
// log.debug "resumeProgram children: $thermostatIdsString"
|
||||
|
||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}'
|
||||
//, { "type": "sendMessage", "params": { "text": "Setpoint Updated" } }
|
||||
def result = sendJson(jsonRequestBody)
|
||||
// debugEventFromParent(child, "resumeProgram(child) with result ${result}")
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -662,27 +686,28 @@ def setHold(child, heating, cooling, deviceId, sendHoldType) {
|
||||
|
||||
int h = heating * 10
|
||||
int c = cooling * 10
|
||||
|
||||
// log.debug "setpoints____________ - h: $heating - $h, c: $cooling - $c"
|
||||
// def thermostatIdsString = getChildDeviceIdsString()
|
||||
|
||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+' } } ]}'
|
||||
|
||||
def result = sendJson(child, jsonRequestBody)
|
||||
return result
|
||||
}
|
||||
|
||||
def setFanMode(child, heating, cooling, deviceId, sendHoldType, fanMode) {
|
||||
|
||||
int h = heating * 10
|
||||
int c = cooling * 10
|
||||
|
||||
|
||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+', "fan": '+fanMode+' } } ]}'
|
||||
// def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}, { "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": "indefinite" } } ]}'
|
||||
def result = sendJson(child, jsonRequestBody)
|
||||
// debugEventFromParent(child, "setHold: heating: ${h}, cooling: ${c} with result ${result}")
|
||||
return result
|
||||
}
|
||||
|
||||
def setMode(child, mode, deviceId) {
|
||||
// def thermostatIdsString = getChildDeviceIdsString()
|
||||
// log.debug "setCoolingSetpoint children: $thermostatIdsString"
|
||||
|
||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}'
|
||||
|
||||
// log.debug "Mode Request Body = ${jsonRequestBody}"
|
||||
// debugEvent ("Mode Request Body = ${jsonRequestBody}")
|
||||
|
||||
def result = sendJson(jsonRequestBody)
|
||||
// debugEventFromParent(child, "setMode to ${mode} with result ${result}")
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -699,6 +724,8 @@ def sendJson(child = null, String jsonBody) {
|
||||
try{
|
||||
httpPost(cmdParams) { resp ->
|
||||
|
||||
// debugEventFromParent(child, "sendJson >> resp.status ${resp.status}, resp.data: ${resp.data}")
|
||||
|
||||
if(resp.status == 200) {
|
||||
|
||||
log.debug "updated ${resp.data}"
|
||||
@@ -709,21 +736,30 @@ def sendJson(child = null, String jsonBody) {
|
||||
log.debug "Error return code = ${resp.data.status.code}"
|
||||
debugEvent("Error return code = ${resp.data.status.code}")
|
||||
}
|
||||
} else {
|
||||
log.error "sent Json & got http status ${resp.status} - ${resp.status.code}"
|
||||
debugEvent ("sent Json & got http status ${resp.status} - ${resp.status.code}")
|
||||
|
||||
//refresh the auth token
|
||||
if (resp.status == 500 && resp.status.code == 14) {
|
||||
//log.debug "Storing the failed action to try later"
|
||||
log.debug "Refreshing your auth_token!"
|
||||
debugEvent ("Refreshing OAUTH Token")
|
||||
refreshAuthToken()
|
||||
return false
|
||||
} else {
|
||||
debugEvent ("Authentication error, invalid authentication method, lack of credentials, etc.")
|
||||
log.error "Authentication error, invalid authentication method, lack of credentials, etc."
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.trace "Exception Sending Json: " + e.response.data.status
|
||||
debugEvent ("sent Json & got http status ${e.statusCode} - ${e.response.data.status.code}")
|
||||
if (e.response.data.status.code == 14) {
|
||||
atomicState.action = "pollChildren"
|
||||
log.debug "Refreshing your auth_token!"
|
||||
refreshAuthToken()
|
||||
}
|
||||
else {
|
||||
debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.")
|
||||
log.error "Authentication error, invalid authentication method, lack of credentials, etc."
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug "Exception Sending Json: " + e
|
||||
debugEvent ("Exception Sending JSON: " + e)
|
||||
refreshAuthToken()
|
||||
return false
|
||||
}
|
||||
|
||||
if (returnStatus == 0)
|
||||
return true
|
||||
@@ -758,37 +794,25 @@ def debugEventFromParent(child, message) {
|
||||
|
||||
//send both push notification and mobile activity feeds
|
||||
def sendPushAndFeeds(notificationMessage){
|
||||
log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}"
|
||||
log.warn "sendPushAndFeeds >> atomicState.timeSendPush: ${atomicState.timeSendPush}"
|
||||
if (atomicState.timeSendPush){
|
||||
if (now() - atomicState.timeSendPush > 86400000){ // notification is sent to remind user once a day
|
||||
sendPush("Your Ecobee thermostat " + notificationMessage)
|
||||
sendActivityFeeds(notificationMessage)
|
||||
atomicState.timeSendPush = now()
|
||||
}
|
||||
} else {
|
||||
sendPush("Your Ecobee thermostat " + notificationMessage)
|
||||
sendActivityFeeds(notificationMessage)
|
||||
atomicState.timeSendPush = now()
|
||||
}
|
||||
atomicState.authToken = null
|
||||
log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}"
|
||||
log.warn "sendPushAndFeeds >> atomicState.timeSendPush: ${atomicState.timeSendPush}"
|
||||
if (atomicState.timeSendPush){
|
||||
if (now() - atomicState.timeSendPush > 86400000){ // notification is sent to remind user once a day
|
||||
sendPush("Your Ecobee thermostat " + notificationMessage)
|
||||
sendActivityFeeds(notificationMessage)
|
||||
atomicState.timeSendPush = now()
|
||||
}
|
||||
} else {
|
||||
sendPush("Your Ecobee thermostat " + notificationMessage)
|
||||
sendActivityFeeds(notificationMessage)
|
||||
atomicState.timeSendPush = now()
|
||||
}
|
||||
atomicState.authToken = null
|
||||
}
|
||||
|
||||
def sendActivityFeeds(notificationMessage) {
|
||||
def devices = getChildDevices()
|
||||
devices.each { child ->
|
||||
child.generateActivityFeedsEvent(notificationMessage) //parse received message from parent
|
||||
}
|
||||
}
|
||||
|
||||
def roundC (tempC) {
|
||||
return String.format("%.1f", (Math.round(tempC * 2))/2)
|
||||
}
|
||||
|
||||
def convertFtoC (tempF) {
|
||||
return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2)
|
||||
}
|
||||
|
||||
def convertCtoF (tempC) {
|
||||
return (Math.round(tempC * (9/5)) + 32).toInteger()
|
||||
def devices = getChildDevices()
|
||||
devices.each { child ->
|
||||
child.generateActivityFeedsEvent(notificationMessage) //parse received message from parent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
@@ -38,75 +38,37 @@ preferences {
|
||||
page(name: "schedulingPage")
|
||||
page(name: "completionPage")
|
||||
page(name: "numbersPage")
|
||||
page(name: "controllerExplanationPage")
|
||||
}
|
||||
|
||||
def rootPage() {
|
||||
dynamicPage(name: "rootPage", title: "", install: true, uninstall: true) {
|
||||
|
||||
section("What to dim") {
|
||||
section {
|
||||
input(name: "dimmers", type: "capability.switchLevel", title: "Dimmers", description: null, multiple: true, required: true, submitOnChange: true)
|
||||
if (dimmers) {
|
||||
href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete")
|
||||
}
|
||||
}
|
||||
|
||||
if (dimmers) {
|
||||
|
||||
section("Gentle Wake Up Has A Controller") {
|
||||
href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null)
|
||||
section {
|
||||
href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete")
|
||||
}
|
||||
|
||||
section("Rules For Dimming") {
|
||||
href(name: "toSchedulingPage", page: "schedulingPage", title: "Automation", description: schedulingHrefDescription() ?: "Set rules for when to start", state: schedulingHrefDescription() ? "complete" : "")
|
||||
input(name: "manualOverride", type: "enum", options: ["cancel": "Cancel dimming", "jumpTo": "Jump to the end"], title: "When one of the dimmers is manually turned off…", description: "dimming will continue", required: false, multiple: false)
|
||||
href(name: "toCompletionPage", title: "Completion Actions", page: "completionPage", state: completionHrefDescription() ? "complete" : "", description: completionHrefDescription() ?: "Set rules for what to do when dimming completes")
|
||||
section {
|
||||
href(name: "toSchedulingPage", page: "schedulingPage", title: "Rules For Automatically Dimming Your Lights", description: schedulingHrefDescription(), state: schedulingHrefDescription() ? "complete" : "")
|
||||
}
|
||||
|
||||
section {
|
||||
href(name: "toCompletionPage", title: "Completion Actions (Optional)", page: "completionPage", state: completionHrefDescription() ? "complete" : "", description: completionHrefDescription())
|
||||
}
|
||||
|
||||
section {
|
||||
// TODO: fancy label
|
||||
label(title: "Label This SmartApp", required: false, defaultValue: "", description: "Highly recommended", submitOnChange: true)
|
||||
label(title: "Label this SmartApp", required: false, defaultValue: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def controllerExplanationPage() {
|
||||
dynamicPage(name: "controllerExplanationPage", title: "How To Control Gentle Wake Up") {
|
||||
|
||||
section("With other SmartApps", hideable: true, hidden: false) {
|
||||
paragraph "When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!"
|
||||
paragraph "The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!"
|
||||
paragraph "Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up."
|
||||
}
|
||||
|
||||
section("More about the controller", hideable: true, hidden: true) {
|
||||
paragraph "You can find the controller with your other 'Things'. It will look like this."
|
||||
image "http://f.cl.ly/items/2O0v0h41301U14042z3i/GentleWakeUpController-tile-stopped.png"
|
||||
paragraph "You can start and stop Gentle Wake up by tapping the control on the right."
|
||||
image "http://f.cl.ly/items/3W323J3M1b3K0k0V3X3a/GentleWakeUpController-tile-running.png"
|
||||
paragraph "If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls."
|
||||
image "http://f.cl.ly/items/291s3z2I2Q0r2q0x171H/GentleWakeUpController-richTile-stopped.png"
|
||||
paragraph "The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep."
|
||||
image "http://f.cl.ly/items/0F0N2G0S3v1q0L0R3J3Y/GentleWakeUpController-richTile-running.png"
|
||||
paragraph "In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle."
|
||||
paragraph "Of course, you may also tap the middle to start or stop the dimming cycle at any time."
|
||||
}
|
||||
|
||||
section("Starting and stopping the SmartApp itself", hideable: true, hidden: true) {
|
||||
paragraph "Tap the 'play' button on the SmartApp to start or stop dimming."
|
||||
image "http://f.cl.ly/items/0R2u1Z2H30393z2I2V3S/GentleWakeUp-appTouch2.png"
|
||||
}
|
||||
|
||||
section("Turning off devices while dimming", hideable: true, hidden: true) {
|
||||
paragraph "It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option."
|
||||
paragraph "If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings."
|
||||
paragraph "Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop."
|
||||
paragraph "That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def numbersPage() {
|
||||
dynamicPage(name:"numbersPage", title:"") {
|
||||
|
||||
@@ -166,33 +128,24 @@ def endLevelLabel() {
|
||||
return "${endLevel}%"
|
||||
}
|
||||
|
||||
def weekdays() {
|
||||
["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
|
||||
}
|
||||
|
||||
def weekends() {
|
||||
["Saturday", "Sunday"]
|
||||
}
|
||||
|
||||
def schedulingPage() {
|
||||
dynamicPage(name: "schedulingPage", title: "Rules For Automatically Dimming Your Lights") {
|
||||
|
||||
section("Use Other SmartApps!") {
|
||||
href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null)
|
||||
section {
|
||||
input(name: "days", type: "enum", title: "Allow Automatic Dimming On These Days", description: "Every day", required: false, multiple: true, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"])
|
||||
}
|
||||
|
||||
section("Allow Automatic Dimming") {
|
||||
input(name: "days", type: "enum", title: "On These Days", description: "Every day", required: false, multiple: true, options: weekdays() + weekends())
|
||||
}
|
||||
|
||||
section("Start Dimming...") {
|
||||
input(name: "startTime", type: "time", title: "At This Time", description: null, required: false)
|
||||
input(name: "modeStart", title: "When Entering This Mode", type: "mode", required: false, mutliple: false, submitOnChange: true, description: null)
|
||||
section {
|
||||
input(name: "modeStart", title: "Start when entering this mode", type: "mode", required: false, mutliple: false, submitOnChange: true)
|
||||
if (modeStart) {
|
||||
input(name: "modeStop", title: "Stop when leaving '${modeStart}' mode", type: "bool", required: false)
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
input(name: "startTime", type: "time", title: "Start Dimming At This Time", description: null, required: false)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,8 +154,8 @@ def completionPage() {
|
||||
|
||||
section("Switches") {
|
||||
input(name: "completionSwitches", type: "capability.switch", title: "Set these switches", description: null, required: false, multiple: true, submitOnChange: true)
|
||||
if (completionSwitches) {
|
||||
input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], defaultValue: "on")
|
||||
if (completionSwitches || androidClient()) {
|
||||
input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], style: "segmented", defaultValue: "on")
|
||||
input(name: "completionSwitchesLevel", type: "number", title: "Optionally, Set Dimmer Levels To", description: null, required: false, multiple: false, range: "(0..99)")
|
||||
}
|
||||
}
|
||||
@@ -241,16 +194,11 @@ def updated() {
|
||||
log.debug "Updating 'Gentle Wake Up' with settings: ${settings}"
|
||||
unschedule()
|
||||
|
||||
def controller = getController()
|
||||
if (controller) {
|
||||
controller.label = app.label
|
||||
}
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
stop("settingsChange")
|
||||
stop()
|
||||
|
||||
if (startTime) {
|
||||
log.debug "scheduling dimming routine to run at $startTime"
|
||||
@@ -261,27 +209,15 @@ private initialize() {
|
||||
subscribe(app, appHandler)
|
||||
|
||||
subscribe(location, locationHandler)
|
||||
|
||||
if (manualOverride) {
|
||||
subscribe(dimmers, "switch.off", stopDimmersHandler)
|
||||
}
|
||||
|
||||
if (!getAllChildDevices()) {
|
||||
// create controller device and set name to the label used here
|
||||
def dni = "${new Date().getTime()}"
|
||||
log.debug "app.label: ${app.label}"
|
||||
addChildDevice("smartthings", "Gentle Wake Up Controller", dni, null, ["label": app.label])
|
||||
state.controllerDni = dni
|
||||
}
|
||||
}
|
||||
|
||||
def appHandler(evt) {
|
||||
log.debug "appHandler evt: ${evt.value}"
|
||||
if (evt.value == "touch") {
|
||||
if (atomicState.running) {
|
||||
stop("appTouch")
|
||||
stop()
|
||||
} else {
|
||||
start("appTouch")
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,47 +233,26 @@ def locationHandler(evt) {
|
||||
def modeStopIsTrue = (modeStop && modeStop != "false")
|
||||
|
||||
if (isSpecifiedMode && canStartAutomatically()) {
|
||||
start("modeChange")
|
||||
start()
|
||||
} else if (!isSpecifiedMode && modeStopIsTrue) {
|
||||
stop("modeChange")
|
||||
stop()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def stopDimmersHandler(evt) {
|
||||
log.trace "stopDimmersHandler evt: ${evt.value}"
|
||||
def percentComplete = completionPercentage()
|
||||
// Often times, the first thing we do is turn lights on or off so make sure we don't stop as soon as we start
|
||||
if (percentComplete > 2 && percentComplete < 98) {
|
||||
if (manualOverride == "cancel") {
|
||||
log.debug "STOPPING in stopDimmersHandler"
|
||||
stop("manualOverride")
|
||||
} else if (manualOverride == "jumpTo") {
|
||||
def end = dynamicEndLevel()
|
||||
log.debug "Jumping to 99% complete in stopDimmersHandler"
|
||||
jumpTo(99)
|
||||
}
|
||||
|
||||
} else {
|
||||
log.debug "not stopping in stopDimmersHandler"
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// Scheduling
|
||||
// ========================================================
|
||||
|
||||
def scheduledStart() {
|
||||
if (canStartAutomatically()) {
|
||||
start("schedule")
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
public def start(source) {
|
||||
def start() {
|
||||
log.trace "START"
|
||||
|
||||
sendStartEvent(source)
|
||||
|
||||
setLevelsInState()
|
||||
|
||||
atomicState.running = true
|
||||
@@ -348,11 +263,9 @@ public def start(source) {
|
||||
increment()
|
||||
}
|
||||
|
||||
public def stop(source) {
|
||||
def stop() {
|
||||
log.trace "STOP"
|
||||
|
||||
sendStopEvent(source)
|
||||
|
||||
atomicState.running = false
|
||||
atomicState.start = 0
|
||||
|
||||
@@ -369,110 +282,6 @@ private healthCheck() {
|
||||
increment()
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// Controller
|
||||
// ========================================================
|
||||
|
||||
def sendStartEvent(source) {
|
||||
log.trace "sendStartEvent(${source})"
|
||||
def eventData = [
|
||||
name: "sessionStatus",
|
||||
value: "running",
|
||||
descriptionText: "${app.label} has started dimming",
|
||||
displayed: true,
|
||||
linkText: app.label,
|
||||
isStateChange: true
|
||||
]
|
||||
if (source == "modeChange") {
|
||||
eventData.descriptionText += " because of a mode change"
|
||||
} else if (source == "schedule") {
|
||||
eventData.descriptionText += " as scheduled"
|
||||
} else if (source == "appTouch") {
|
||||
eventData.descriptionText += " because you pressed play on the app"
|
||||
} else if (source == "controller") {
|
||||
eventData.descriptionText += " because you pressed play on the controller"
|
||||
}
|
||||
|
||||
sendControllerEvent(eventData)
|
||||
}
|
||||
|
||||
def sendStopEvent(source) {
|
||||
log.trace "sendStopEvent(${source})"
|
||||
def eventData = [
|
||||
name: "sessionStatus",
|
||||
value: "stopped",
|
||||
descriptionText: "${app.label} has stopped dimming",
|
||||
displayed: true,
|
||||
linkText: app.label,
|
||||
isStateChange: true
|
||||
]
|
||||
if (source == "modeChange") {
|
||||
eventData.descriptionText += " because of a mode change"
|
||||
eventData.value += "cancelled"
|
||||
} else if (source == "schedule") {
|
||||
eventData.descriptionText = "${app.label} has finished dimming"
|
||||
} else if (source == "appTouch") {
|
||||
eventData.descriptionText += " because you pressed play on the app"
|
||||
eventData.value += "cancelled"
|
||||
} else if (source == "controller") {
|
||||
eventData.descriptionText += " because you pressed stop on the controller"
|
||||
eventData.value += "cancelled"
|
||||
} else if (source == "settingsChange") {
|
||||
eventData.descriptionText += " because the settings have changed"
|
||||
eventData.value += "cancelled"
|
||||
} else if (source == "manualOverride") {
|
||||
eventData.descriptionText += " because the dimmer was manually turned off"
|
||||
eventData.value += "cancelled"
|
||||
}
|
||||
|
||||
sendControllerEvent(eventData)
|
||||
sendTimeRemainingEvent(0)
|
||||
}
|
||||
|
||||
def sendTimeRemainingEvent(percentComplete) {
|
||||
log.trace "sendTimeRemainingEvent(${percentComplete})"
|
||||
|
||||
def percentCompleteEventData = [
|
||||
name: "percentComplete",
|
||||
value: percentComplete as int,
|
||||
displayed: true,
|
||||
isStateChange: true
|
||||
]
|
||||
sendControllerEvent(percentCompleteEventData)
|
||||
|
||||
def duration = sanitizeInt(duration, 30)
|
||||
def timeRemaining = duration - (duration * (percentComplete / 100))
|
||||
def timeRemainingEventData = [
|
||||
name: "timeRemaining",
|
||||
value: displayableTime(timeRemaining),
|
||||
displayed: true,
|
||||
isStateChange: true
|
||||
]
|
||||
sendControllerEvent(timeRemainingEventData)
|
||||
}
|
||||
|
||||
def sendControllerEvent(eventData) {
|
||||
def controller = getController()
|
||||
if (controller) {
|
||||
controller.controllerEvent(eventData)
|
||||
}
|
||||
}
|
||||
|
||||
def getController() {
|
||||
def dni = state.controllerDni
|
||||
if (!dni) {
|
||||
log.warn "no controller dni"
|
||||
return null
|
||||
}
|
||||
def controller = getChildDevice(dni)
|
||||
if (!controller) {
|
||||
log.warn "no controller"
|
||||
return null
|
||||
}
|
||||
log.debug "controller: ${controller}"
|
||||
return controller
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// Setting levels
|
||||
// ========================================================
|
||||
@@ -540,8 +349,6 @@ def updateDimmers(percentComplete) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
sendTimeRemainingEvent(percentComplete)
|
||||
}
|
||||
|
||||
int dynamicLevel(dimmer, percentComplete) {
|
||||
@@ -570,7 +377,7 @@ private completion() {
|
||||
return
|
||||
}
|
||||
|
||||
stop("schedule")
|
||||
stop()
|
||||
|
||||
handleCompletionSwitches()
|
||||
|
||||
@@ -578,7 +385,6 @@ private completion() {
|
||||
|
||||
handleCompletionModesAndPhrases()
|
||||
|
||||
sendTimeRemainingEvent(100)
|
||||
}
|
||||
|
||||
private handleCompletionSwitches() {
|
||||
@@ -687,65 +493,22 @@ def completionPercentage() {
|
||||
return
|
||||
}
|
||||
|
||||
def now = new Date().getTime()
|
||||
def timeElapsed = now - atomicState.start
|
||||
def totalRunTime = totalRunTimeMillis()
|
||||
def percentComplete = timeElapsed / totalRunTime * 100
|
||||
log.debug "percentComplete: ${percentComplete}"
|
||||
int now = new Date().getTime()
|
||||
int diff = now - atomicState.start
|
||||
int totalRunTime = totalRunTimeMillis()
|
||||
int percentOfRunTime = (diff / totalRunTime) * 100
|
||||
log.debug "percentOfRunTime: ${percentOfRunTime}"
|
||||
|
||||
return percentComplete
|
||||
percentOfRunTime
|
||||
}
|
||||
|
||||
int totalRunTimeMillis() {
|
||||
int minutes = sanitizeInt(duration, 30)
|
||||
convertToMillis(minutes)
|
||||
}
|
||||
|
||||
int convertToMillis(minutes) {
|
||||
def seconds = minutes * 60
|
||||
def millis = seconds * 1000
|
||||
return millis
|
||||
return millis as int
|
||||
}
|
||||
|
||||
def timeRemaining(percentComplete) {
|
||||
def normalizedPercentComplete = percentComplete / 100
|
||||
def duration = sanitizeInt(duration, 30)
|
||||
def timeElapsed = duration * normalizedPercentComplete
|
||||
def timeRemaining = duration - timeElapsed
|
||||
return timeRemaining
|
||||
}
|
||||
|
||||
int millisToEnd(percentComplete) {
|
||||
convertToMillis(timeRemaining(percentComplete))
|
||||
}
|
||||
|
||||
String displayableTime(timeRemaining) {
|
||||
def timeString = "${timeRemaining}"
|
||||
def parts = timeString.split(/\./)
|
||||
if (!parts.size()) {
|
||||
return "0:00"
|
||||
}
|
||||
def minutes = parts[0]
|
||||
if (parts.size() == 1) {
|
||||
return "${minutes}:00"
|
||||
}
|
||||
def fraction = "0.${parts[1]}" as double
|
||||
def seconds = "${60 * fraction as int}".padRight(2, "0")
|
||||
return "${minutes}:${seconds}"
|
||||
}
|
||||
|
||||
def jumpTo(percentComplete) {
|
||||
def millisToEnd = millisToEnd(percentComplete)
|
||||
def endTime = new Date().getTime() + millisToEnd
|
||||
def duration = sanitizeInt(duration, 30)
|
||||
def durationMillis = convertToMillis(duration)
|
||||
def shiftedStart = endTime - durationMillis
|
||||
atomicState.start = shiftedStart
|
||||
updateDimmers(percentComplete)
|
||||
sendTimeRemainingEvent(percentComplete)
|
||||
}
|
||||
|
||||
|
||||
int dynamicEndLevel() {
|
||||
if (usesOldSettings()) {
|
||||
if (direction && direction == "Down") {
|
||||
@@ -910,13 +673,7 @@ def schedulingHrefDescription() {
|
||||
|
||||
def descriptionParts = []
|
||||
if (days) {
|
||||
if (days == weekdays()) {
|
||||
descriptionParts << "On weekdays,"
|
||||
} else if (days == weekends()) {
|
||||
descriptionParts << "On weekends,"
|
||||
} else {
|
||||
descriptionParts << "On ${fancyString(days)},"
|
||||
}
|
||||
descriptionParts << "On ${fancyString(days)},"
|
||||
}
|
||||
|
||||
descriptionParts << "${fancyDeviceString(dimmers)} will start dimming"
|
||||
@@ -1002,15 +759,15 @@ def completionHrefDescription() {
|
||||
|
||||
def numbersPageHrefDescription() {
|
||||
def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}"
|
||||
if (colorize) {
|
||||
def colorDimmers = dimmersWithSetColorCommand()
|
||||
if (colorDimmers == dimmers) {
|
||||
title += " and will gradually change color."
|
||||
} else {
|
||||
title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
|
||||
}
|
||||
}
|
||||
return title
|
||||
if (colorize) {
|
||||
def colorDimmers = dimmersWithSetColorCommand()
|
||||
if (colorDimmers == dimmers) {
|
||||
title += " and will gradually change color."
|
||||
} else {
|
||||
title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
|
||||
}
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
def hueSatToHex(h, s) {
|
||||
|
||||
@@ -35,11 +35,23 @@ preferences {
|
||||
}
|
||||
|
||||
def mainPage() {
|
||||
def bridges = bridgesDiscovered()
|
||||
if (state.username && bridges) {
|
||||
return bulbDiscovery()
|
||||
if(canInstallLabs()) {
|
||||
def bridges = bridgesDiscovered()
|
||||
if (state.username && bridges) {
|
||||
return bulbDiscovery()
|
||||
} else {
|
||||
return bridgeDiscovery()
|
||||
}
|
||||
} else {
|
||||
return bridgeDiscovery()
|
||||
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
|
||||
|
||||
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
|
||||
|
||||
return dynamicPage(name:"bridgeDiscovery", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
|
||||
section("Upgrade") {
|
||||
paragraph "$upgradeNeeded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,8 +258,8 @@ def installed() {
|
||||
|
||||
def updated() {
|
||||
log.trace "Updated with settings: ${settings}"
|
||||
unschedule()
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
initialize()
|
||||
}
|
||||
|
||||
@@ -313,8 +325,6 @@ def addBulbs() {
|
||||
} else {
|
||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||
}
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
d.refresh()
|
||||
} else {
|
||||
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
|
||||
}
|
||||
@@ -322,8 +332,10 @@ def addBulbs() {
|
||||
//backwards compatable
|
||||
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name])
|
||||
d.refresh()
|
||||
}
|
||||
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
d.refresh()
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
|
||||
if (bulbs instanceof java.util.Map) {
|
||||
@@ -753,6 +765,10 @@ private String convertHexToIP(hex) {
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
}
|
||||
|
||||
private Boolean canInstallLabs() {
|
||||
return hasAllHubsOver("000.011.00603")
|
||||
}
|
||||
|
||||
private Boolean hasAllHubsOver(String desiredFirmware) {
|
||||
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
||||
}
|
||||
|
||||
@@ -419,11 +419,9 @@ def addDevice() {
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
def newAction = state.HarmonyActivities.find { it.key == dni }
|
||||
if (newAction) {
|
||||
d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"])
|
||||
log.trace "created ${d.displayName} with id $dni"
|
||||
poll()
|
||||
}
|
||||
d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"])
|
||||
log.trace "created ${d.displayName} with id $dni"
|
||||
poll()
|
||||
} else {
|
||||
log.trace "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
'''Acceleration Detected'''.ko=가속화 감지됨
|
||||
'''Arrival Of'''.ko=도착
|
||||
'''Both Push and SMS?'''.ko=푸시 메시지와 SMS를 모두 사용하시겠습니까?
|
||||
'''Button Pushed'''.ko=버튼이 눌렸습니다
|
||||
'''Contact Closes'''.ko=접점 닫힘
|
||||
'''Contact Opens'''.ko=접점 열림
|
||||
'''Departure Of'''.ko=출발
|
||||
'''Message Text'''.ko=문자 메시지
|
||||
'''Minutes'''.ko=분
|
||||
'''Motion Here'''.ko=동작
|
||||
'''Phone Number (for SMS, optional)'''.ko=휴대전화 번호(문자 메시지 - 옵션)
|
||||
'''Receive notifications when anything happens in your home.'''.ko=집 안에 무슨 일이 일어나면 알림이 전송됩니다.
|
||||
'''Smoke Detected'''.ko=연기가 감지되었습니다
|
||||
'''Switch Turned Off'''.ko=스위치 꺼짐
|
||||
'''Switch Turned On'''.ko=스위치 꺼짐
|
||||
'''Choose one or more, when...'''.ko=다음의 경우 하나 이상 선택
|
||||
'''Yes'''.ko=예
|
||||
'''No'''.ko=아니요
|
||||
'''Send this message (optional, sends standard status message if not specified)'''.ko=이 메시지 전송(선택적, 지정되지 않은 경우 표준 상태 메시지를 보냅니다)
|
||||
'''Via a push notification and/or an SMS message'''.ko=푸시 알림 및/또는 문자 메시지를 통해
|
||||
'''Set for specific mode(s)'''.ko=특정 모드 설정
|
||||
'''Tap to set'''.ko=눌러서 설정
|
||||
'''Minimum time between messages (optional, defaults to every message)'''.ko=메시지작 간 최소 시간(선택 사항, 모든 메시지의 기본 설정)
|
||||
'''If outside the US please make sure to enter the proper country code'''.ko=미국 이외 거주자는 적절한 국가 코드를 입력했는지 확인하십시오
|
||||
'''Water Sensor Wet'''.ko=Water Sensor에서 물이 감지되었습니다
|
||||
'''{{ triggerEvent.linkText }} has arrived at the {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}에 도착했습니다
|
||||
'''{{ triggerEvent.linkText }} has arrived at {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}에 도착했습니다
|
||||
'''{{ triggerEvent.linkText }} has left the {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다
|
||||
'''{{ triggerEvent.linkText }} has left {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다
|
||||
'''Assign a name'''.ko=이름 배정
|
||||
'''Choose Modes'''.ko=모드 선택
|
||||
@@ -20,19 +20,19 @@
|
||||
* 2014-10-03: Added capability.button device picker and button.pushed event subscription. For Doorbell.
|
||||
*/
|
||||
definition(
|
||||
name: "Notify Me When",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Receive notifications when anything happens in your home.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png"
|
||||
name: "Notify Me When",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Receive notifications when anything happens in your home.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Choose one or more, when..."){
|
||||
input "button", "capability.button", title: "Button Pushed", required: false, multiple: true //tw
|
||||
input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
|
||||
input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
|
||||
input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
|
||||
input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
|
||||
input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
|
||||
@@ -47,11 +47,11 @@ preferences {
|
||||
input "messageText", "text", title: "Message Text", required: false
|
||||
}
|
||||
section("Via a push notification and/or an SMS message"){
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false
|
||||
paragraph "If outside the US please make sure to enter the proper country code"
|
||||
input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"]
|
||||
}
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false
|
||||
paragraph "If outside the US please make sure to enter the proper country code"
|
||||
input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"]
|
||||
}
|
||||
}
|
||||
section("Minimum time between messages (optional, defaults to every message)") {
|
||||
input "frequency", "decimal", title: "Minutes", required: false
|
||||
@@ -71,7 +71,7 @@ def updated() {
|
||||
|
||||
def subscribeToEvents() {
|
||||
subscribe(button, "button.pushed", eventHandler) //tw
|
||||
subscribe(contact, "contact.open", eventHandler)
|
||||
subscribe(contact, "contact.open", eventHandler)
|
||||
subscribe(contactClosed, "contact.closed", eventHandler)
|
||||
subscribe(acceleration, "acceleration.active", eventHandler)
|
||||
subscribe(motion, "motion.active", eventHandler)
|
||||
@@ -99,55 +99,49 @@ def eventHandler(evt) {
|
||||
}
|
||||
|
||||
private sendMessage(evt) {
|
||||
String msg = messageText
|
||||
Map options = [:]
|
||||
|
||||
if (!messageText) {
|
||||
msg = defaultText(evt)
|
||||
options = [translatable: true, triggerEvent: evt]
|
||||
}
|
||||
def msg = messageText ?: defaultText(evt)
|
||||
log.debug "$evt.name:$evt.value, pushAndPhone:$pushAndPhone, '$msg'"
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients, options)
|
||||
} else {
|
||||
if (!phone || pushAndPhone != 'No') {
|
||||
log.debug 'sending push'
|
||||
options.method = 'push'
|
||||
//sendPush(msg)
|
||||
}
|
||||
if (phone) {
|
||||
options.phone = phone
|
||||
log.debug 'sending SMS'
|
||||
//sendSms(phone, msg)
|
||||
}
|
||||
sendNotification(msg, options)
|
||||
}
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
|
||||
if (!phone || pushAndPhone != "No") {
|
||||
log.debug "sending push"
|
||||
sendPush(msg)
|
||||
}
|
||||
if (phone) {
|
||||
log.debug "sending SMS"
|
||||
sendSms(phone, msg)
|
||||
}
|
||||
}
|
||||
if (frequency) {
|
||||
state[evt.deviceId] = now()
|
||||
}
|
||||
}
|
||||
|
||||
private defaultText(evt) {
|
||||
if (evt.name == 'presence') {
|
||||
if (evt.value == 'present') {
|
||||
if (evt.name == "presence") {
|
||||
if (evt.value == "present") {
|
||||
if (includeArticle) {
|
||||
'{{ triggerEvent.linkText }} has arrived at the {{ location.name }}'
|
||||
"$evt.linkText has arrived at the $location.name"
|
||||
}
|
||||
else {
|
||||
'{{ triggerEvent.linkText }} has arrived at {{ location.name }}'
|
||||
}
|
||||
} else {
|
||||
if (includeArticle) {
|
||||
'{{ triggerEvent.linkText }} has left the {{ location.name }}'
|
||||
}
|
||||
else {
|
||||
'{{ triggerEvent.linkText }} has left {{ location.name }}'
|
||||
"$evt.linkText has arrived at $location.name"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
'{{ triggerEvent.descriptionText }}'
|
||||
else {
|
||||
if (includeArticle) {
|
||||
"$evt.linkText has left the $location.name"
|
||||
}
|
||||
else {
|
||||
"$evt.linkText has left $location.name"
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
evt.descriptionText
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -316,40 +316,60 @@ private def parseEventMessage(String description) {
|
||||
parts.each { part ->
|
||||
part = part.trim()
|
||||
if (part.startsWith('devicetype:')) {
|
||||
part -= "devicetype:"
|
||||
event.devicetype = part.trim()
|
||||
def valueString = part.split(":")[1].trim()
|
||||
event.devicetype = valueString
|
||||
}
|
||||
else if (part.startsWith('mac:')) {
|
||||
part -= "mac:"
|
||||
event.mac = part.trim()
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
event.mac = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('networkAddress:')) {
|
||||
part -= "networkAddress:"
|
||||
event.ip = part.trim()
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
event.ip = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('deviceAddress:')) {
|
||||
part -= "deviceAddress:"
|
||||
event.port = part.trim()
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
event.port = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpPath:')) {
|
||||
part -= "ssdpPath:"
|
||||
event.ssdpPath = part.trim()
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
event.ssdpPath = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpUSN:')) {
|
||||
part -= "ssdpUSN:"
|
||||
event.ssdpUSN = part.trim()
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
event.ssdpUSN = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpTerm:')) {
|
||||
part -= "ssdpTerm:"
|
||||
event.ssdpTerm = part.trim()
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
event.ssdpTerm = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('headers')) {
|
||||
part -= "headers:"
|
||||
event.headers = part.trim()
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
event.headers = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('body')) {
|
||||
part -= "body:"
|
||||
event.body = part.trim()
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
event.body = valueString
|
||||
}
|
||||
}
|
||||
}
|
||||
event
|
||||
|
||||
@@ -16,14 +16,14 @@
|
||||
* Date: 2013-09-06
|
||||
*/
|
||||
definition(
|
||||
name: "Wemo (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Allows you to integrate your WeMo Switch and Wemo Motion sensor with SmartThings.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png",
|
||||
singleInstance: true
|
||||
name: "Wemo (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Allows you to integrate your WeMo Switch and Wemo Motion sensor with SmartThings.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png",
|
||||
singleInstance: true
|
||||
)
|
||||
|
||||
preferences {
|
||||
@@ -39,7 +39,7 @@ private getFriendlyName(String deviceNetworkId) {
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET /setup.xml HTTP/1.1
|
||||
HOST: ${deviceNetworkId}
|
||||
|
||||
""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}", [callback: "setupHandler"]))
|
||||
""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}"))
|
||||
}
|
||||
|
||||
private verifyDevices() {
|
||||
@@ -52,13 +52,6 @@ private verifyDevices() {
|
||||
}
|
||||
}
|
||||
|
||||
void ssdpSubscribe() {
|
||||
subscribe(location, "ssdpTerm.urn:Belkin:device:insight:1", ssdpSwitchHandler)
|
||||
subscribe(location, "ssdpTerm.urn:Belkin:device:controllee:1", ssdpSwitchHandler)
|
||||
subscribe(location, "ssdpTerm.urn:Belkin:device:sensor:1", ssdpMotionHandler)
|
||||
subscribe(location, "ssdpTerm.urn:Belkin:device:lightswitch:1", ssdpLightSwitchHandler)
|
||||
}
|
||||
|
||||
def firstPage()
|
||||
{
|
||||
if(canInstallLabs())
|
||||
@@ -69,7 +62,7 @@ def firstPage()
|
||||
|
||||
log.debug "REFRESH COUNT :: ${refreshCount}"
|
||||
|
||||
ssdpSubscribe()
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
|
||||
//ssdp request every 25 seconds
|
||||
if((refreshCount % 5) == 0) {
|
||||
@@ -112,7 +105,9 @@ def devicesDiscovered() {
|
||||
def motions = getWemoMotions()
|
||||
def lightSwitches = getWemoLightSwitches()
|
||||
def devices = switches + motions + lightSwitches
|
||||
devices?.collect{ [app.id, it.ssdpUSN].join('.') }
|
||||
def list = []
|
||||
|
||||
list = devices?.collect{ [app.id, it.ssdpUSN].join('.') }
|
||||
}
|
||||
|
||||
def switchesDiscovered() {
|
||||
@@ -180,9 +175,8 @@ def updated() {
|
||||
|
||||
def initialize() {
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
|
||||
ssdpSubscribe()
|
||||
unschedule()
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
|
||||
if (selectedSwitches)
|
||||
addSwitches()
|
||||
@@ -195,7 +189,7 @@ def initialize() {
|
||||
|
||||
runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds
|
||||
runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds
|
||||
runEvery5Minutes("refresh")
|
||||
runEvery5Minutes("refresh")
|
||||
}
|
||||
|
||||
def resubscribe() {
|
||||
@@ -205,7 +199,7 @@ def resubscribe() {
|
||||
|
||||
def refresh() {
|
||||
log.debug "refresh() called"
|
||||
doDeviceSync()
|
||||
doDeviceSync()
|
||||
refreshDevices()
|
||||
}
|
||||
|
||||
@@ -241,14 +235,14 @@ def addSwitches() {
|
||||
if (!d) {
|
||||
log.debug "Creating WeMo Switch with dni: ${selectedSwitch.value.mac}"
|
||||
d = addChildDevice("smartthings", "Wemo Switch", selectedSwitch.value.mac, selectedSwitch?.value.hub, [
|
||||
"label": selectedSwitch?.value?.name ?: "Wemo Switch",
|
||||
"data": [
|
||||
"mac": selectedSwitch.value.mac,
|
||||
"ip": selectedSwitch.value.ip,
|
||||
"port": selectedSwitch.value.port
|
||||
]
|
||||
"label": selectedSwitch?.value?.name ?: "Wemo Switch",
|
||||
"data": [
|
||||
"mac": selectedSwitch.value.mac,
|
||||
"ip": selectedSwitch.value.ip,
|
||||
"port": selectedSwitch.value.port
|
||||
]
|
||||
])
|
||||
def ipvalue = convertHexToIP(selectedSwitch.value.ip)
|
||||
def ipvalue = convertHexToIP(selectedSwitch.value.ip)
|
||||
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
|
||||
} else {
|
||||
@@ -279,9 +273,9 @@ def addMotions() {
|
||||
"port": selectedMotion.value.port
|
||||
]
|
||||
])
|
||||
def ipvalue = convertHexToIP(selectedMotion.value.ip)
|
||||
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
|
||||
def ipvalue = convertHexToIP(selectedMotion.value.ip)
|
||||
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
@@ -310,147 +304,26 @@ def addLightSwitches() {
|
||||
"port": selectedLightSwitch.value.port
|
||||
]
|
||||
])
|
||||
def ipvalue = convertHexToIP(selectedLightSwitch.value.ip)
|
||||
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||
def ipvalue = convertHexToIP(selectedLightSwitch.value.ip)
|
||||
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def ssdpSwitchHandler(evt) {
|
||||
def description = evt.description
|
||||
def hub = evt?.hubId
|
||||
def parsedEvent = parseDiscoveryMessage(description)
|
||||
parsedEvent << ["hub":hub]
|
||||
log.debug parsedEvent
|
||||
|
||||
def switches = getWemoSwitches()
|
||||
if (!(switches."${parsedEvent.ssdpUSN.toString()}")) {
|
||||
//if it doesn't already exist
|
||||
switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||
} else {
|
||||
log.debug "Device was already found in state..."
|
||||
def d = switches."${parsedEvent.ssdpUSN.toString()}"
|
||||
boolean deviceChangedValues = false
|
||||
log.debug "$d.ip <==> $parsedEvent.ip"
|
||||
if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
|
||||
d.ip = parsedEvent.ip
|
||||
d.port = parsedEvent.port
|
||||
deviceChangedValues = true
|
||||
log.debug "Device's port or ip changed..."
|
||||
def child = getChildDevice(parsedEvent.mac)
|
||||
child.subscribe(parsedEvent.ip, parsedEvent.port)
|
||||
child.poll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def ssdpMotionHandler(evt) {
|
||||
log.info("ssdpMotionHandler")
|
||||
def description = evt.description
|
||||
def hub = evt?.hubId
|
||||
def parsedEvent = parseDiscoveryMessage(description)
|
||||
parsedEvent << ["hub":hub]
|
||||
log.debug parsedEvent
|
||||
|
||||
def motions = getWemoMotions()
|
||||
if (!(motions."${parsedEvent.ssdpUSN.toString()}")) {
|
||||
//if it doesn't already exist
|
||||
motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||
} else { // just update the values
|
||||
log.debug "Device was already found in state..."
|
||||
|
||||
def d = motions."${parsedEvent.ssdpUSN.toString()}"
|
||||
boolean deviceChangedValues = false
|
||||
|
||||
if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
|
||||
d.ip = parsedEvent.ip
|
||||
d.port = parsedEvent.port
|
||||
deviceChangedValues = true
|
||||
log.debug "Device's port or ip changed..."
|
||||
}
|
||||
|
||||
if (deviceChangedValues) {
|
||||
def children = getChildDevices()
|
||||
log.debug "Found children ${children}"
|
||||
children.each {
|
||||
if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
|
||||
log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}"
|
||||
it.subscribe(parsedEvent.ip, parsedEvent.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def ssdpLightSwitchHandler(evt) {
|
||||
log.info("ssdpLightSwitchHandler")
|
||||
def description = evt.description
|
||||
def hub = evt?.hubId
|
||||
def parsedEvent = parseDiscoveryMessage(description)
|
||||
parsedEvent << ["hub":hub]
|
||||
log.debug parsedEvent
|
||||
|
||||
def lightSwitches = getWemoLightSwitches()
|
||||
|
||||
if (!(lightSwitches."${parsedEvent.ssdpUSN.toString()}")) {
|
||||
//if it doesn't already exist
|
||||
lightSwitches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||
} else {
|
||||
log.debug "Device was already found in state..."
|
||||
|
||||
def d = lightSwitches."${parsedEvent.ssdpUSN.toString()}"
|
||||
boolean deviceChangedValues = false
|
||||
|
||||
if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
|
||||
d.ip = parsedEvent.ip
|
||||
d.port = parsedEvent.port
|
||||
deviceChangedValues = true
|
||||
log.debug "Device's port or ip changed..."
|
||||
def child = getChildDevice(parsedEvent.mac)
|
||||
log.debug "updating ip and port, and resubscribing, for device with mac ${parsedEvent.mac}"
|
||||
child.subscribe(parsedEvent.ip, parsedEvent.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setupHandler(hubResponse) {
|
||||
String contentType = hubResponse?.headers['Content-Type']
|
||||
if (contentType != null && contentType == 'text/xml') {
|
||||
def body = hubResponse.xml
|
||||
def wemoDevices = []
|
||||
String deviceType = body?.device?.deviceType?.text() ?: ""
|
||||
if (deviceType.startsWith("urn:Belkin:device:controllee:1") || deviceType.startsWith("urn:Belkin:device:insight:1")) {
|
||||
wemoDevices = getWemoSwitches()
|
||||
} else if (deviceType.startsWith("urn:Belkin:device:sensor")) {
|
||||
wemoDevices = getWemoMotions()
|
||||
} else if (deviceType.startsWith("urn:Belkin:device:lightswitch")) {
|
||||
wemoDevices = getWemoLightSwitches()
|
||||
}
|
||||
|
||||
def wemoDevice = wemoDevices.find {it?.key?.contains(body?.device?.UDN?.text())}
|
||||
if (wemoDevice) {
|
||||
wemoDevice.value << [name:body?.device?.friendlyName?.text(), verified: true]
|
||||
} else {
|
||||
log.error "/setup.xml returned a wemo device that didn't exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
def locationHandler(evt) {
|
||||
def description = evt.description
|
||||
def hub = evt?.hubId
|
||||
def parsedEvent = parseDiscoveryMessage(description)
|
||||
parsedEvent << ["hub":hub]
|
||||
log.debug parsedEvent
|
||||
log.debug parsedEvent
|
||||
|
||||
if (parsedEvent?.ssdpTerm?.contains("Belkin:device:controllee") || parsedEvent?.ssdpTerm?.contains("Belkin:device:insight")) {
|
||||
def switches = getWemoSwitches()
|
||||
if (!(switches."${parsedEvent.ssdpUSN.toString()}")) {
|
||||
//if it doesn't already exist
|
||||
//if it doesn't already exist
|
||||
switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||
} else {
|
||||
log.debug "Device was already found in state..."
|
||||
@@ -462,16 +335,16 @@ def locationHandler(evt) {
|
||||
d.port = parsedEvent.port
|
||||
deviceChangedValues = true
|
||||
log.debug "Device's port or ip changed..."
|
||||
def child = getChildDevice(parsedEvent.mac)
|
||||
def child = getChildDevice(parsedEvent.mac)
|
||||
child.subscribe(parsedEvent.ip, parsedEvent.port)
|
||||
child.poll()
|
||||
child.poll()
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:sensor")) {
|
||||
def motions = getWemoMotions()
|
||||
if (!(motions."${parsedEvent.ssdpUSN.toString()}")) {
|
||||
//if it doesn't already exist
|
||||
//if it doesn't already exist
|
||||
motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||
} else { // just update the values
|
||||
log.debug "Device was already found in state..."
|
||||
@@ -586,7 +459,6 @@ def locationHandler(evt) {
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
private def parseXmlBody(def body) {
|
||||
def decodedBytes = body.decodeBase64()
|
||||
def bodyString
|
||||
@@ -601,48 +473,68 @@ private def parseXmlBody(def body) {
|
||||
}
|
||||
|
||||
private def parseDiscoveryMessage(String description) {
|
||||
def event = [:]
|
||||
def device = [:]
|
||||
def parts = description.split(',')
|
||||
parts.each { part ->
|
||||
part = part.trim()
|
||||
if (part.startsWith('devicetype:')) {
|
||||
part -= "devicetype:"
|
||||
event.devicetype = part.trim()
|
||||
def valueString = part.split(":")[1].trim()
|
||||
device.devicetype = valueString
|
||||
}
|
||||
else if (part.startsWith('mac:')) {
|
||||
part -= "mac:"
|
||||
event.mac = part.trim()
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
device.mac = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('networkAddress:')) {
|
||||
part -= "networkAddress:"
|
||||
event.ip = part.trim()
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
device.ip = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('deviceAddress:')) {
|
||||
part -= "deviceAddress:"
|
||||
event.port = part.trim()
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
device.port = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpPath:')) {
|
||||
part -= "ssdpPath:"
|
||||
event.ssdpPath = part.trim()
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
device.ssdpPath = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpUSN:')) {
|
||||
part -= "ssdpUSN:"
|
||||
event.ssdpUSN = part.trim()
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
device.ssdpUSN = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpTerm:')) {
|
||||
part -= "ssdpTerm:"
|
||||
event.ssdpTerm = part.trim()
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
device.ssdpTerm = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('headers')) {
|
||||
part -= "headers:"
|
||||
event.headers = part.trim()
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
device.headers = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('body')) {
|
||||
part -= "body:"
|
||||
event.body = part.trim()
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
device.body = valueString
|
||||
}
|
||||
}
|
||||
}
|
||||
event
|
||||
device
|
||||
}
|
||||
|
||||
def doDeviceSync(){
|
||||
@@ -668,4 +560,4 @@ private Boolean hasAllHubsOver(String desiredFirmware) {
|
||||
|
||||
private List getRealHubFirmwareVersions() {
|
||||
return location.hubs*.firmwareVersionString.findAll { it }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user