mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-09 05:11:52 +00:00
Compare commits
33 Commits
MSA-1306-2
...
MSA-1349-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee5077aeda | ||
|
|
ad1f1b2dc9 | ||
|
|
c875547942 | ||
|
|
1676a9c381 | ||
|
|
5d1b033486 | ||
|
|
31f77513da | ||
|
|
98d7829d1a | ||
|
|
c6f706e47a | ||
|
|
532afd7336 | ||
|
|
ac7f1a0c66 | ||
|
|
cb26f055d7 | ||
|
|
34107f935e | ||
|
|
cc2d19e951 | ||
|
|
031a15ec86 | ||
|
|
fc2db2575d | ||
|
|
fd549631e6 | ||
|
|
417c246d61 | ||
|
|
038d770691 | ||
|
|
ce28ec2039 | ||
|
|
0f1781c02e | ||
|
|
4ce6ee0890 | ||
|
|
f8050a5cd5 | ||
|
|
d44dac448b | ||
|
|
67c20abcba | ||
|
|
d56e132896 | ||
|
|
009ec2539d | ||
|
|
ff0860cbe1 | ||
|
|
ecfb99974b | ||
|
|
c2f18a91be | ||
|
|
0c1208928f | ||
|
|
02d9963fab | ||
|
|
32f0385e30 | ||
|
|
d17cadc4c7 |
@@ -21,6 +21,7 @@ metadata {
|
||||
attribute "tamper", "enum", ["detected", "clear"]
|
||||
attribute "heatAlarm", "enum", ["overheat detected", "clear", "rapid temperature rise", "underheat detected"]
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x86, 0x72, 0x5A, 0x59, 0x85, 0x73, 0x84, 0x80, 0x71, 0x56, 0x70, 0x31, 0x8E, 0x22, 0x9C, 0x98, 0x7A", outClusters: "0x20, 0x8B"
|
||||
fingerprint mfr:"010F", prod:"0C02", model:"1002"
|
||||
}
|
||||
simulator {
|
||||
//battery
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Hue White Ambiance Bulb
|
||||
*
|
||||
* Philips Hue Type "Color Temperature Light"
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
|
||||
// for the UI
|
||||
metadata {
|
||||
// Automatically generated. Make future change here.
|
||||
definition (name: "Hue White Ambiance Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Switch Level"
|
||||
capability "Actuator"
|
||||
capability "Color Temperature"
|
||||
capability "Switch"
|
||||
capability "Refresh"
|
||||
|
||||
command "refresh"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles (scale: 2){
|
||||
multiAttributeTile(name:"rich-control", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
|
||||
}
|
||||
}
|
||||
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") {
|
||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "colorTemperature", label: '${currentValue} K'
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main(["rich-control"])
|
||||
details(["rich-control", "colorTempSliderControl", "colorTemp", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(description) {
|
||||
log.debug "parse() - $description"
|
||||
def results = []
|
||||
|
||||
def map = description
|
||||
if (description instanceof String) {
|
||||
log.debug "Hue Ambience Bulb stringToMap - ${map}"
|
||||
map = stringToMap(description)
|
||||
}
|
||||
|
||||
if (map?.name && map?.value) {
|
||||
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
// handle commands
|
||||
void on() {
|
||||
log.trace parent.on(this)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
void off() {
|
||||
log.trace parent.off(this)
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
|
||||
void setLevel(percent) {
|
||||
log.debug "Executing 'setLevel'"
|
||||
if (percent != null && percent >= 0 && percent <= 100) {
|
||||
parent.setLevel(this, percent)
|
||||
sendEvent(name: "level", value: percent)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.warn "$percent is not 0-100"
|
||||
}
|
||||
}
|
||||
|
||||
void setColorTemperature(value) {
|
||||
if (value) {
|
||||
log.trace "setColorTemperature: ${value}k"
|
||||
parent.setColorTemperature(this, value)
|
||||
sendEvent(name: "colorTemperature", value: value)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.warn "Invalid color temperature"
|
||||
}
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
parent.manualRefresh()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
@@ -0,0 +1,30 @@
|
||||
# Smartsense Motion Sensor
|
||||
|
||||
|
||||
|
||||
Works with:
|
||||
|
||||
* [Samsung SmartThings Motion Sensor](https://shop.smartthings.com/#!/products/samsung-smartthings-motion-sensor)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health]($health)
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
|
||||
* **Motion Sensor** - can detect motion
|
||||
* **Battery** - defines device uses a battery
|
||||
* **Refresh** - _refresh()_ command for status updates
|
||||
* **Health Check** - indicates ability to get device health notifications
|
||||
|
||||
## Device Health
|
||||
|
||||
A Category C2 motion sensor that has 120min check-in interval
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -403,39 +403,21 @@ def refresh() {
|
||||
|
||||
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",
|
||||
]
|
||||
/* These values of Motion Threshold Multiplier(0x01) and Motion Threshold (0x0276)
|
||||
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.
|
||||
*/
|
||||
refreshCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x01, [mfgCode: manufacturerCode])
|
||||
refreshCmds += zigbee.writeAttribute(0xFC02, 0x0002, 0x21, 0x0276, [mfgCode: manufacturerCode])
|
||||
} 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",
|
||||
]
|
||||
refreshCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x02, [mfgCode: manufacturerCode])
|
||||
}
|
||||
|
||||
//Common refresh commands
|
||||
refreshCmds = refreshCmds + [
|
||||
"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"
|
||||
]
|
||||
refreshCmds += zigbee.readAttribute(0x0402, 0x0000) +
|
||||
zigbee.readAttribute(0x0001, 0x0020) +
|
||||
zigbee.readAttribute(0xFC02, 0x0010, [mfgCode: manufacturerCode])
|
||||
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
@@ -443,38 +425,15 @@ def refresh() {
|
||||
def configure() {
|
||||
sendEvent(name: "checkInterval", value: 7200, displayed: false)
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting"
|
||||
|
||||
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
|
||||
"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",
|
||||
"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",
|
||||
"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 {0100}", "delay 200",
|
||||
"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 {0100}", "delay 200",
|
||||
"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 {0100}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
]
|
||||
def configCmds = enrollResponse() +
|
||||
zigbee.batteryConfig() +
|
||||
zigbee.temperatureConfig() +
|
||||
zigbee.configureReporting(0xFC02, 0x0010, 0x18, 10, 3600, 0x01, [mfgCode: manufacturerCode]) +
|
||||
zigbee.configureReporting(0xFC02, 0x0012, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
|
||||
zigbee.configureReporting(0xFC02, 0x0013, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
|
||||
zigbee.configureReporting(0xFC02, 0x0014, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode])
|
||||
|
||||
return configCmds + refresh()
|
||||
}
|
||||
|
||||
@@ -21,6 +21,13 @@ metadata {
|
||||
capability "Motion Sensor"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
|
||||
fingerprint mfr: "011F", prod: "0001", model: "0001", deviceJoinName: "Schlage Motion Sensor" // Schlage motion
|
||||
fingerprint mfr: "014A", prod: "0001", model: "0001", deviceJoinName: "Ecolink Motion Sensor" // Ecolink motion
|
||||
fingerprint mfr: "014A", prod: "0004", model: "0001", deviceJoinName: "Ecolink Motion Sensor" // Ecolink motion +
|
||||
fingerprint mfr: "0060", prod: "0001", model: "0002", deviceJoinName: "Everspring Motion Sensor" // Everspring SP814
|
||||
fingerprint mfr: "0060", prod: "0001", model: "0003", deviceJoinName: "Everspring Motion Sensor" // Everspring HSP02
|
||||
fingerprint mfr: "011A", prod: "0601", model: "0901", deviceJoinName: "Enerwave Motion Sensor" // Enerwave ZWN-BPC
|
||||
}
|
||||
|
||||
simulator {
|
||||
@@ -125,9 +132,9 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
|
||||
}
|
||||
if (!state.lastbat || (new Date().time) - state.lastbat > 53*60*60*1000) {
|
||||
result << response(zwave.batteryV1.batteryGet())
|
||||
result << response("delay 1200")
|
||||
} else {
|
||||
result << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||
}
|
||||
result << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
/**
|
||||
* SmartThings service for Prempoint
|
||||
*
|
||||
* Author: Prempoint Inc. (c) 2016
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Prempoint",
|
||||
namespace: "prempoint.com",
|
||||
author: "Prempoint Inc.",
|
||||
description: "SmartThings service for Prempoint",
|
||||
category: "Connections",
|
||||
iconUrl: "http://www.prempoint.com/images/social_app_emblem_50x50.png",
|
||||
iconX2Url: "http://www.prempoint.com/images/social_app_emblem_100x100.png",
|
||||
iconX3Url: "http://www.prempoint.com/images/social_app_emblem_150x150.png",
|
||||
oauth: [displayName: "Prempoint", displayLink: "http://www.prempoint.com/"])
|
||||
|
||||
preferences {
|
||||
section("Allow Prempoint to Control & Access These Things...") {
|
||||
input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
|
||||
input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false
|
||||
input "garagedoors", "capability.garageDoorControl", title: "Which Garage Doors?", multiple: true, required: false
|
||||
//input "doors", "capability.doorControl", title: "Which Doors?", multiple: true, required: false
|
||||
input "cameras", "capability.imageCapture", title: "Which Cameras?", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/list") {
|
||||
action: [
|
||||
GET: "listDevices"
|
||||
]
|
||||
}
|
||||
path("/switches") {
|
||||
action: [
|
||||
GET: "listSwitches"
|
||||
]
|
||||
}
|
||||
path("/switches/:id") {
|
||||
action: [
|
||||
GET: "showSwitch"
|
||||
]
|
||||
}
|
||||
path("/switches/:id/:command") {
|
||||
action: [
|
||||
GET: "updateSwitch"
|
||||
]
|
||||
}
|
||||
path("/switches/:id/:command/:level") {
|
||||
action: [
|
||||
GET: "updateSwitch"
|
||||
]
|
||||
}
|
||||
path("/locks") {
|
||||
action: [
|
||||
GET: "listLocks"
|
||||
]
|
||||
}
|
||||
path("/locks/:id") {
|
||||
action: [
|
||||
GET: "showLock"
|
||||
]
|
||||
}
|
||||
path("/locks/:id/:command") {
|
||||
action: [
|
||||
GET: "updateLock"
|
||||
]
|
||||
}
|
||||
path("/doors/:id") {
|
||||
action: [
|
||||
GET: "showDoor"
|
||||
]
|
||||
}
|
||||
path("/doors/:id/:command") {
|
||||
action: [
|
||||
GET: "updateDoor"
|
||||
]
|
||||
}
|
||||
path("/garagedoors/:id") {
|
||||
action: [
|
||||
GET: "showGarageDoor"
|
||||
]
|
||||
}
|
||||
path("/garagedoors/:id/:command") {
|
||||
action: [
|
||||
GET: "updateGarageDoor"
|
||||
]
|
||||
}
|
||||
path("/cameras/:id") {
|
||||
action: [
|
||||
GET: "showCamera"
|
||||
]
|
||||
}
|
||||
path("/cameras/:id/:command") {
|
||||
action: [
|
||||
GET: "updateCamera"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {}
|
||||
|
||||
def updated() {}
|
||||
|
||||
def listDevices() {
|
||||
log.debug "entering listDevices"
|
||||
//return listSwitches() + listLocks() + listGarageDoors() + listDoors() + listCameras()
|
||||
return listSwitches() + listLocks() + listGarageDoors() + listCameras()
|
||||
}
|
||||
|
||||
//switches
|
||||
def listSwitches() {
|
||||
log.debug "entering listSwitches"
|
||||
switches.collect{showDevice(it,"switch")}
|
||||
}
|
||||
|
||||
def showSwitch() {
|
||||
log.debug "entering showSwitches"
|
||||
show(switches, "switch")
|
||||
}
|
||||
|
||||
def updateSwitch() {
|
||||
log.debug "entering updateSwitches"
|
||||
update(switches, "switch")
|
||||
}
|
||||
|
||||
//locks
|
||||
def listLocks() {
|
||||
log.debug "entering listLocks"
|
||||
locks.collect{showDevice(it,"lock")}
|
||||
}
|
||||
|
||||
def showLock() {
|
||||
log.debug "entering showLock"
|
||||
show(locks, "lock")
|
||||
}
|
||||
|
||||
def updateLock() {
|
||||
log.debug "entering updateLock"
|
||||
update(locks, "lock")
|
||||
}
|
||||
|
||||
//doors
|
||||
def listDoors() {
|
||||
log.debug "entering listDoors"
|
||||
locks.collect{showDevice(it,"door")}
|
||||
}
|
||||
|
||||
def showDoor() {
|
||||
log.debug "entering showDoors"
|
||||
show(doors, "door")
|
||||
}
|
||||
|
||||
def updateDoor() {
|
||||
log.debug "entering updateDoor"
|
||||
update(doors, "door")
|
||||
}
|
||||
|
||||
//garagedoors
|
||||
def listGarageDoors() {
|
||||
log.debug "entering listGarageDoors"
|
||||
locks.collect{showDevice(it,"garagedoor")}
|
||||
}
|
||||
|
||||
def showGarageDoor() {
|
||||
log.debug "entering showGarageDoors"
|
||||
show(garagedoors, "garagedoor")
|
||||
}
|
||||
|
||||
def updateGarageDoor() {
|
||||
log.debug "entering updateGarageDoor"
|
||||
update(gargedoors, "garagedoor")
|
||||
}
|
||||
|
||||
//cameras
|
||||
def listCameras() {
|
||||
log.debug "entering listCameras"
|
||||
cameras.collect{showDevice(it,"image")}
|
||||
}
|
||||
|
||||
def showCamera() {
|
||||
log.debug "entering showCameras"
|
||||
show(cameras, "camera")
|
||||
}
|
||||
|
||||
def updateCamera() {
|
||||
log.debug "entering updateCamera"
|
||||
update(cameras, "camera")
|
||||
}
|
||||
|
||||
def deviceHandler(evt) {}
|
||||
|
||||
private update(devices, type) {
|
||||
def rc = null
|
||||
|
||||
//def command = request.JSON?.command
|
||||
def command = params.command
|
||||
|
||||
log.debug "update, request: params: ${params}, devices: $devices.id type=$type command=$command"
|
||||
|
||||
// Process the command.
|
||||
if (command)
|
||||
{
|
||||
def dev = devices.find { it.id == params.id }
|
||||
if (!dev) {
|
||||
httpError(404, "Device not found: $params.id")
|
||||
} else if (type == "switch") {
|
||||
switch(command) {
|
||||
case "on":
|
||||
rc = dev.on()
|
||||
break
|
||||
case "off":
|
||||
rc = dev.off()
|
||||
break
|
||||
default:
|
||||
httpError(400, "Device command=$command is not a valid for device=$it.id $dev")
|
||||
}
|
||||
} else if (type == "lock") {
|
||||
switch(command) {
|
||||
case "lock":
|
||||
rc = dev.lock()
|
||||
break
|
||||
case "unlock":
|
||||
rc = dev.unlock()
|
||||
break
|
||||
default:
|
||||
httpError(400, "Device command=$command is not a valid for device:=$it.id $dev")
|
||||
}
|
||||
} else if (type == "door") {
|
||||
switch(command) {
|
||||
case "open":
|
||||
rc = dev.open()
|
||||
break
|
||||
case "close":
|
||||
rc = dev.close()
|
||||
break
|
||||
default:
|
||||
httpError(400, "Device command=$command is not a valid for device=$it.id $dev")
|
||||
}
|
||||
} else if (type == "garagedoor") {
|
||||
switch(command) {
|
||||
case "open":
|
||||
rc = dev.open()
|
||||
break
|
||||
case "close":
|
||||
rc = dev.close()
|
||||
break
|
||||
default:
|
||||
httpError(400, "Device command=$command is not a valid for device=$it.id $dev")
|
||||
}
|
||||
} else if (type == "camera") {
|
||||
switch(command) {
|
||||
case "take":
|
||||
rc = dev.take()
|
||||
log.debug "Device command=$command device=$it.id $dev current image=$it.currentImage"
|
||||
break
|
||||
default:
|
||||
httpError(400, "Device command=$command is not a valid for device=$it.id $dev")
|
||||
}
|
||||
}
|
||||
|
||||
log.debug "executed device=$it.id $dev command=$command rc=$rc"
|
||||
|
||||
// Check that the device is a switch that is currently on, supports 'setLevel"
|
||||
// and that a level was specified.
|
||||
int level = params.level ? params.level as int : -1;
|
||||
if ((type == "switch") && (dev.currentValue('switch') == "on") && hasLevel(dev) && (level != -1)) {
|
||||
log.debug "device about to setLevel=$level"
|
||||
dev.setLevel(level);
|
||||
}
|
||||
|
||||
// Show the device info if necessary.
|
||||
if (rc == null) {
|
||||
rc = showDevice(dev, type)
|
||||
}
|
||||
}
|
||||
|
||||
return rc
|
||||
}
|
||||
|
||||
private show(devices, type) {
|
||||
def dev = devices.find { it.id == params.id }
|
||||
if (!dev) {
|
||||
httpError(404, "Device not found")
|
||||
} else {
|
||||
// Show the device info.
|
||||
showDevice(dev, type)
|
||||
}
|
||||
}
|
||||
|
||||
private showDevice(it, type) {
|
||||
def props = null
|
||||
|
||||
// Get the current state for the device type.
|
||||
def state = [it.currentState(type)]
|
||||
|
||||
// Check that whether the a switch device with level support is located and update the returned device type.
|
||||
def devType = type
|
||||
|
||||
if (type == "switch" && hasLevel(it)) {
|
||||
// Assign "switchWithLevel" to device type.
|
||||
devType = "switchWithLevel"
|
||||
// Add the level state.
|
||||
def levelState = it.currentState("level")
|
||||
if (levelState) {
|
||||
state.add(levelState)
|
||||
}
|
||||
}
|
||||
|
||||
log.debug "device label=$it.label type=$devType"
|
||||
|
||||
// Assign the device item properties if appropriate.
|
||||
if (it) {
|
||||
props = [id: it.id, label: it.label, type: devType, state: state]
|
||||
// Add the hub information to the device properties
|
||||
// if appropriate.
|
||||
if (it.hub) {
|
||||
props.put("location", it.hub.hub.location)
|
||||
}
|
||||
if (it.currentImage) {
|
||||
props.put("currentImage", it.currentImage)
|
||||
}
|
||||
}
|
||||
|
||||
return props
|
||||
}
|
||||
|
||||
private hasLevel(device) {
|
||||
// Default return value.
|
||||
def rc = false;
|
||||
|
||||
// Get the device supported commands.
|
||||
def supportedCommands = device.supportedCommands
|
||||
|
||||
// Check to see if the "setLevel" was found and assign
|
||||
// the appropriate return value.
|
||||
if (supportedCommands) {
|
||||
// Find the "setLevel" command.
|
||||
rc = supportedCommands.toString().indexOf("setLevel") != -1
|
||||
}
|
||||
|
||||
log.debug "hasLevel device label=$device.label supportedCommands=$supportedCommands rc=$rc"
|
||||
|
||||
return rc
|
||||
}
|
||||
@@ -30,6 +30,7 @@ definition(
|
||||
preferences {
|
||||
page(name:"mainPage", title:"Hue Device Setup", content:"mainPage", refreshTimeout:5)
|
||||
page(name:"bridgeDiscovery", title:"Hue Bridge Discovery", content:"bridgeDiscovery", refreshTimeout:5)
|
||||
page(name:"bridgeDiscoveryFailed", title:"Bridge Discovery Failed", content:"bridgeDiscoveryFailed", refreshTimeout:0)
|
||||
page(name:"bridgeBtnPush", title:"Linking with your Hue", content:"bridgeLinking", refreshTimeout:5)
|
||||
page(name:"bulbDiscovery", title:"Hue Device Setup", content:"bulbDiscovery", refreshTimeout:5)
|
||||
}
|
||||
@@ -53,12 +54,21 @@ def bridgeDiscovery(params=[:])
|
||||
def options = bridges ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
if (numFound == 0 && state.bridgeRefreshCount > 25) {
|
||||
log.trace "Cleaning old bridges memory"
|
||||
state.bridges = [:]
|
||||
state.bridgeRefreshCount = 0
|
||||
app.updateSetting("selectedHue", "")
|
||||
}
|
||||
if (numFound == 0) {
|
||||
if (state.bridgeRefreshCount == 25) {
|
||||
log.trace "Cleaning old bridges memory"
|
||||
state.bridges = [:]
|
||||
app.updateSetting("selectedHue", "")
|
||||
} else if (state.bridgeRefreshCount > 100) {
|
||||
// five minutes have passed, give up
|
||||
// there seems to be a problem going back from discovey failed page in some instances (compared to pressing next)
|
||||
// however it is probably a SmartThings settings issue
|
||||
state.bridges = [:]
|
||||
app.updateSetting("selectedHue", "")
|
||||
state.bridgeRefreshCount = 0
|
||||
return bridgeDiscoveryFailed()
|
||||
}
|
||||
}
|
||||
|
||||
ssdpSubscribe()
|
||||
|
||||
@@ -79,6 +89,13 @@ def bridgeDiscovery(params=[:])
|
||||
}
|
||||
}
|
||||
|
||||
def bridgeDiscoveryFailed() {
|
||||
return dynamicPage(name:"bridgeDiscoveryFailed", title: "Bridge Discovery Failed", nextPage: "bridgeDiscovery") {
|
||||
section("Failed to discover any Hue Bridges. Please confirm that the Hue Bridge is connected to the same network as your SmartThings Hub, and that it has power.") {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def bridgeLinking()
|
||||
{
|
||||
int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int
|
||||
@@ -88,19 +105,15 @@ def bridgeLinking()
|
||||
def nextPage = ""
|
||||
def title = "Linking with your Hue"
|
||||
def paragraphText
|
||||
def hueimage = null
|
||||
if (selectedHue) {
|
||||
paragraphText = "Press the button on your Hue Bridge to setup a link. "
|
||||
hueimage = "http://huedisco.mediavibe.nl/wp-content/uploads/2013/09/pair-bridge.png"
|
||||
} else {
|
||||
paragraphText = "You haven't selected a Hue Bridge, please Press \"Done\" and select one before clicking next."
|
||||
hueimage = null
|
||||
}
|
||||
if (state.username) { //if discovery worked
|
||||
nextPage = "bulbDiscovery"
|
||||
title = "Success!"
|
||||
paragraphText = "Linking to your hub was a success! Please click 'Next'!"
|
||||
hueimage = null
|
||||
}
|
||||
|
||||
if((linkRefreshcount % 2) == 0 && !state.username) {
|
||||
@@ -110,8 +123,6 @@ def bridgeLinking()
|
||||
return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
|
||||
section("") {
|
||||
paragraph """${paragraphText}"""
|
||||
if (hueimage != null)
|
||||
image "${hueimage}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,13 +146,14 @@ def bulbDiscovery() {
|
||||
if((bulbRefreshCount % 5) == 0) {
|
||||
discoverHueBulbs()
|
||||
}
|
||||
def selectedBridge = state.bridges.find { key, value -> value?.serialNumber?.equalsIgnoreCase(selectedHue) }
|
||||
def title = selectedBridge?.value?.name ?: "Find bridges"
|
||||
|
||||
return dynamicPage(name:"bulbDiscovery", title:"Bulb Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||
section("Please wait while we discover your Hue Bulbs. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:bulboptions
|
||||
}
|
||||
section {
|
||||
def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges"
|
||||
href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
|
||||
|
||||
}
|
||||
@@ -323,6 +335,8 @@ private getDeviceType(hueType) {
|
||||
return "Hue Bulb"
|
||||
else if (hueType?.equalsIgnoreCase("Color Light"))
|
||||
return "Hue Bloom"
|
||||
else if (hueType?.equalsIgnoreCase("Color Temperature Light"))
|
||||
return "Hue White Ambiance Bulb"
|
||||
else
|
||||
return null
|
||||
}
|
||||
@@ -346,26 +360,29 @@ def addBulbs() {
|
||||
def newHueBulb
|
||||
if (bulbs instanceof java.util.Map) {
|
||||
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
||||
if (newHueBulb != null) {
|
||||
d = addChildBulb(dni, newHueBulb?.value?.type, newHueBulb?.value?.name, newHueBulb?.value?.hub)
|
||||
|
||||
if (newHueBulb != null) {
|
||||
d = addChildBulb(dni, newHueBulb?.value?.type, newHueBulb?.value?.name, newHueBulb?.value?.hub)
|
||||
if (d) {
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
d.completedSetup = true
|
||||
d.refresh()
|
||||
}
|
||||
} else {
|
||||
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
|
||||
}
|
||||
} else {
|
||||
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
|
||||
}
|
||||
} else {
|
||||
//backwards compatable
|
||||
//backwards compatable
|
||||
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
||||
d = addChildBulb(dni, "Extended Color Light", newHueBulb?.value?.name, newHueBulb?.value?.hub)
|
||||
d?.completedSetup = true
|
||||
d?.refresh()
|
||||
}
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
|
||||
if (bulbs instanceof java.util.Map) {
|
||||
// Update device type if incorrect
|
||||
def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
||||
def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
||||
upgradeDeviceType(d, newHueBulb?.value?.type)
|
||||
}
|
||||
}
|
||||
@@ -397,6 +414,7 @@ def addBridge() {
|
||||
}
|
||||
if (newbridge) {
|
||||
d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub)
|
||||
d?.completedSetup = true
|
||||
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
|
||||
def childDevice = getChildDevice(d.deviceNetworkId)
|
||||
childDevice.sendEvent(name: "serialNumber", value: vbridge.value.serialNumber)
|
||||
@@ -484,7 +502,21 @@ void bridgeDescriptionHandler(physicalgraph.device.HubResponse hubResponse) {
|
||||
def bridges = getHueBridges()
|
||||
def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())}
|
||||
if (bridge) {
|
||||
bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true]
|
||||
// serialNumber from API is in format of 0017882413ad (mac address), however on the actual bridge only last six
|
||||
// characters are printed on the back so using that to identify bridge
|
||||
def idNumber = body?.device?.serialNumber?.text()
|
||||
if (idNumber?.size() >= 6)
|
||||
idNumber = idNumber[-6..-1].toUpperCase()
|
||||
|
||||
// usually in form of bridge name followed by (ip), i.e. defaults to Philips Hue (192.168.1.2)
|
||||
// replace IP with serial number to make it easier for user to identify
|
||||
def name = body?.device?.friendlyName?.text()
|
||||
def index = name?.indexOf('(')
|
||||
if (index != -1) {
|
||||
name = name.substring(0,index)
|
||||
name += " ($idNumber)"
|
||||
}
|
||||
bridge.value << [name:name, serialNumber:body?.device?.serialNumber?.text(), verified: true]
|
||||
} else {
|
||||
log.error "/description.xml returned a bridge that didn't exist"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* Programmable Thermostat
|
||||
*
|
||||
* Copyright 2016 Raymond Ciarcia
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Programmable Thermostat",
|
||||
namespace: "smartthings",
|
||||
author: "Raymond Ciarcia",
|
||||
description: "A full-featured, easy to use interface for programming your thermostat based on schedule setpoints and mode changes",
|
||||
category: "Convenience",
|
||||
iconUrl: "http://cdn.device-icons.smartthings.com/Home/home1-icn@2x.png",
|
||||
iconX2Url: "http://cdn.device-icons.smartthings.com/Home/home1-icn@2x.png",
|
||||
iconX3Url: "http://cdn.device-icons.smartthings.com/Home/home1-icn@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name:"Settings", title:"Settings", uninstall:true, install:true ) {
|
||||
section() {
|
||||
input (name:"thermostat", type: "capability.thermostat", title: "Select thermostat", required: true, multiple: false)
|
||||
}
|
||||
section ("Scheduled Setpoints") {
|
||||
input (name: "son", type: "bool", title: "Run scheduled setpoints", required:true)
|
||||
input (name:"numscheduled", title: "Number of scheduled setpoints", type: "number",refreshAfterSelection: true)
|
||||
href(title: "Schedule Setpoints", page: "ScheduledChanges")
|
||||
}
|
||||
section ("Mode-Based Setpoints") {
|
||||
input (name: "eon", type: "bool", title: "Run mode-based setpoints", required:true)
|
||||
input (name:"numevent", title: "Number of mode-based setpoints", type: "number",refreshAfterSelection: true)
|
||||
href(title: "Mode-Based Setpoints Setpoints", page: "EventBasedChanges")
|
||||
}
|
||||
section("Auto Thermostat Mode Control") {
|
||||
input (name: "auto", type: "enum", title: "Adjust thermostat heating/cooling mode based on current temperature and setpoint", required:true, multiple: false, options: ['Never','When setpoints are executed','Any time'])
|
||||
}
|
||||
section("Notifications") {
|
||||
input (name: "snotifications", type: "bool", title: "Notify when scheduled setpoints execute", required:true)
|
||||
input (name: "enotifications", type: "bool", title: "Notify when mode-based setpoints execute", required:true)
|
||||
input (name: "eventlogging", type: "enum", title: "Set the level of event logging in the notification feed", required:true, multiple: false, options: ['None','Normal','Detailed'])
|
||||
}
|
||||
section("Command Acknowledgement Failure Response and Notification") {
|
||||
input (name: "fnotifications", type: "bool", title: "Resend commands not acknowledged by the theromstat and notify after multiple failed attempts. Increases thermostat reliability but may not be compatible with all thermostats; disable if every command results in a failure notification.", required:true)
|
||||
}
|
||||
}
|
||||
page(name: "ScheduledChanges")
|
||||
page(name: "EventBasedChanges")
|
||||
}
|
||||
|
||||
def ScheduledChanges() {
|
||||
dynamicPage(name: "ScheduledChanges", uninstall: true, install: false) {
|
||||
for (int i = 1; i <= settings.numscheduled; i++) {
|
||||
section("Scheduled Setpoint $i") {
|
||||
input "stime${i}", "time", title: "At this time:", required: true
|
||||
input "sheatset${i}", "decimal", title: "Set this heating temperature:", required: true
|
||||
input "scoolset${i}", "decimal", title: "Set this cooling temperature:", required: true
|
||||
input "sdays${i}", "enum", title: "Only on these days (no selection is equivalent to selecting all):", required: false, multiple: true, options: ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
|
||||
input "smodes${i}", "mode", title: "Only in these modes (no selection is equivalent to selecting all):", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def EventBasedChanges() {
|
||||
dynamicPage(name: "EventBasedChanges", uninstall: true, install: false) {
|
||||
for (int i = 1; i <= settings.numevent; i++) {
|
||||
section("Mode-Based Setpoint $i") {
|
||||
input "emodes${i}", "mode", title: "On transition to this mode:", multiple: false, required: true
|
||||
input "eheatset${i}", "decimal", title: "Set this heating temperature:", required: true
|
||||
input "ecoolset${i}", "decimal", title: "Set this cooling temperature:", required: true
|
||||
input "edays${i}", "enum", title: "Only on these days (no selection is equivalent to selecting all):", required: false, multiple: true, options: ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//---- INSTALL AND UPDATE
|
||||
|
||||
def installed() {initialize()}
|
||||
|
||||
def updated() {initialize()}
|
||||
|
||||
def initialize() {
|
||||
try {
|
||||
unschedule()
|
||||
} catch(e) {
|
||||
try {
|
||||
unschedule(SchedulerIntegrityChecker)
|
||||
unschedule(MidnightRunner)
|
||||
} catch(ev) {}
|
||||
}
|
||||
|
||||
unsubscribe()
|
||||
subscribe(settings.thermostat, "temperature", tempChangeHandler)
|
||||
if ((settings.numevent > 0) && (settings.eon)) {subscribe(location, modeChangeHandler)}
|
||||
|
||||
state.scheduledindex = 0
|
||||
state.pendingindex = 0
|
||||
state.dayoflastrun = TodayAsString()
|
||||
state.timeoflastevent = now()
|
||||
state.nextscheduledtime = now()
|
||||
state.failedcommandcount = 0
|
||||
state.schedulestring = ""
|
||||
state.checkcommandstring = ""
|
||||
|
||||
state.eventlogging = 0
|
||||
if (settings.eventlogging == "Normal"){state.notificationlevel = 1}
|
||||
if (settings.eventlogging == "Detailed"){state.notificationlevel = 2}
|
||||
|
||||
if ((settings.numscheduled > 0) && (settings.son)) {
|
||||
schedule(timeToday("2015-08-04T00:00:00.000",location.timeZone), MidnightRunner)
|
||||
SchedulerFunction()
|
||||
}
|
||||
log.debug "Programmable Thermostat: successfully initialized."
|
||||
if (state.notificationlevel>0) {sendNotificationEvent("Programmable Thermostat successfully initialized.$state.schedulestring.")}
|
||||
state.schedulestring = ""
|
||||
}
|
||||
|
||||
//---- SCHEDULING FUNCTIONS
|
||||
|
||||
//At midnight, runs scheduler function to set the first scheduled event of the new day
|
||||
def MidnightRunner() {
|
||||
state.dayoflastrun = TodayAsString()
|
||||
state.timeoflastevent = now()
|
||||
SchedulerFunction()
|
||||
def i = SearchSchedulePoints("2015-08-04T00:00:00.000")
|
||||
if (i>0) {ThermostatCommander(settings."sheatset${i}", settings."scoolset${i}", settings.snotifications, "per scheduled setpoint.$state.schedulestring")}
|
||||
}
|
||||
|
||||
//Determines and schedules the next scheduled setpoint
|
||||
def SchedulerFunction(){
|
||||
def mindiff = 60*60*1000*24*7
|
||||
def timeNow = now()
|
||||
def todaystring = TodayAsString()
|
||||
for (int i = 1; i <= settings.numscheduled; i++) {
|
||||
def ScheduledTime = timeToday(settings["stime$i"],location.timeZone)
|
||||
def ScheduledDays = settings["sdays$i"]
|
||||
if (ScheduledDays == null) {ScheduledDays = TodayAsString()}
|
||||
if (ScheduledTime != null) {
|
||||
if ((ScheduledTime.time >= timeNow) && (ScheduledDays.contains(TodayAsString())) && (ScheduledTime.time - timeNow < mindiff)){
|
||||
mindiff = ScheduledTime.time - timeNow
|
||||
state.scheduledindex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mindiff < 60*60*1000*24*7) {
|
||||
int i = state.scheduledindex
|
||||
def nextrun = timeToday(settings["stime$i"],location.timeZone)
|
||||
state.nextscheduledtime = nextrun.time
|
||||
def nextrunstring = DisplayTime(nextrun)
|
||||
runOnce(nextrun, ScheduleExecuter)
|
||||
if (state.notificationlevel>1) {state.schedulestring=" Next scheduled setpoint for $thermostat.label is today at $nextrunstring"}
|
||||
} else {
|
||||
state.nextscheduledtime = -1
|
||||
if (state.notificationlevel>1) {state.schedulestring=" There are no remaining scheduled setpoints for $thermostat.label today"}
|
||||
}
|
||||
state.timeoflastevent = now()
|
||||
}
|
||||
|
||||
def SearchSchedulePoints(time) {
|
||||
for (int i = 1; i <= settings.numscheduled; i++) {
|
||||
def Modes = settings["smodes$i"]
|
||||
if (Modes == null) {Modes = location.mode}
|
||||
def Days = settings["sdays$i"]
|
||||
if (Days == null) {Days = TodayAsString()}
|
||||
if(timeToday(settings["stime$i"],location.timeZone) == timeToday(time,location.timeZone) && Modes.contains(location.mode) && Days.contains(TodayAsString())) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
//---- EXECUTION FUNCTIONS
|
||||
|
||||
//Runs at scheduled setpoints to determine whether a setpoint should be executed; if yes, calls thermostat commander to execute command
|
||||
def ScheduleExecuter() {
|
||||
int i = state.scheduledindex
|
||||
SchedulerFunction()
|
||||
state.timeoflastevent = now()
|
||||
def valid = false
|
||||
def Modes = settings["smodes$i"]
|
||||
if (Modes == null) {Modes = location.mode}
|
||||
if(Modes.contains(location.mode)){
|
||||
valid = true
|
||||
} else {
|
||||
i = SearchSchedulePoints(settings["stime$i"])
|
||||
if (i > 0) {valid = true}
|
||||
}
|
||||
if (valid) {
|
||||
state.failedcommandcount = 0
|
||||
state.pendingindex = i
|
||||
ThermostatCommander(settings."sheatset${i}", settings."scoolset${i}", settings.snotifications, "per scheduled setpoint.$state.schedulestring")
|
||||
} else {
|
||||
if (state.notificationlevel>1) {sendNotificationEvent("Scheduled setpoint for $thermostat.label not executed because the current home mode, $location.mode, does not match a setpoint mode.$state.schedulestring.")}
|
||||
}
|
||||
state.schedulestring = ""
|
||||
}
|
||||
|
||||
//Sends commands to the thermostat
|
||||
def ThermostatCommander(hvalue, cvalue, notifications, notificationphrase) {
|
||||
state.timeoflastevent = now()
|
||||
if((hvalue == null) || (cvalue == null)) {return}
|
||||
if (settings.auto != "Never") {ThermostatModeSetter(hvalue, cvalue, 0)}
|
||||
|
||||
def notificationstring = ""
|
||||
state.checkcommandstring = ""
|
||||
def thermMode = thermostat.currentValue("thermostatMode")
|
||||
def name = thermostat.label
|
||||
|
||||
def currentheatsetpoint = settings.thermostat.currentValue("heatingSetpoint")
|
||||
def currentcoolsetpoint = settings.thermostat.currentValue("coolingSetpoint")
|
||||
|
||||
if ("$currentcoolsetpoint" != "$cvalue") {state.checkcommandstring = "c"}
|
||||
if ("$currentheatsetpoint" != "$hvalue") {state.checkcommandstring = "h$state.checkcommandstring"}
|
||||
log.debug "Programmable Thermostat: check string is $state.checkcommandstring; values are $currentcoolsetpoint and $currentheatsetpoint"
|
||||
|
||||
def primarysetpoint = hvalue
|
||||
if (thermMode == "cool") {primarysetpoint = cvalue}
|
||||
if (thermMode == "heat" || thermMode == "cool") {notificationstring = "$name set to $primarysetpoint in $thermMode mode $notificationphrase."}
|
||||
else {notificationstring = "$name set to $hvalue / $cvalue $notificationphrase."}
|
||||
|
||||
if (settings.fnotifications && state.checkcommandstring != "") (runIn(10 + state.failedcommandcount*30, CommandIntegrityChecker))
|
||||
|
||||
if (hvalue!=0) {
|
||||
log.debug "Programmable Thermostat: Heat command set to $hvalue"
|
||||
thermostat.setHeatingSetpoint(hvalue)
|
||||
}
|
||||
if (cvalue!=0) {
|
||||
log.debug "Programmable Thermostat: Cool command set to $cvalue"
|
||||
thermostat.setCoolingSetpoint(cvalue)
|
||||
}
|
||||
if (notifications && state.failedcommandcount==0) {
|
||||
sendPush(notificationstring)
|
||||
} else if (state.notificationlevel>0 && state.failedcommandcount==0) {
|
||||
sendNotificationEvent(notificationstring)
|
||||
if (state.checkcommandstring == "" && state.notificationlevel>2) {sendNotificationEvent("$name confirmed that it was already set to $primarysetpoint in $thermMode mode.")}
|
||||
}
|
||||
if (state.checkcommandstring == "") {log.debug "Programmable Thermostat: $name confirmed that it was already set to $primarysetpoint in $thermMode mode"}
|
||||
}
|
||||
|
||||
//Auto Sets Thermostat Mode
|
||||
def ThermostatModeSetter(hvalue, cvalue, notifications) {
|
||||
if (hvalue==0 || cvalue==0) {return}
|
||||
def currentTemp = settings.thermostat.latestValue("temperature")
|
||||
if (currentTemp > cvalue && settings.thermostat.currentValue("thermostatMode") != "cool") {
|
||||
thermostat.cool()
|
||||
if (notifications > 0) {sendNotificationEvent("$thermostat.label mode changed to cooling when temperature reached $currentTemp")}
|
||||
} else if (currentTemp < hvalue && settings.thermostat.currentValue("thermostatMode") != "heat") {
|
||||
thermostat.heat()
|
||||
if (notifications > 0) {sendNotificationEvent("$thermostat.label mode changed to heating when temperature fell to $currentTemp")}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//---- INTEGRITY CHECKERS
|
||||
|
||||
//Determines whether the last scheduled setpoint was executed; if not, reinitializes or sends missed command
|
||||
def SchedulerIntegrityChecker() {
|
||||
def i = state.scheduledindex
|
||||
if ((settings.numscheduled == 0) || (settings.son == false)) {return}
|
||||
if (state.dayoflastrun != TodayAsString()) {initialize()}
|
||||
}
|
||||
|
||||
//Determines whether commands sent to the thermostat have been properly acknowledged; if not, calls thermostat commander to reissue failed command(s)
|
||||
def CommandIntegrityChecker() {
|
||||
state.timeoflastevent = now()
|
||||
if (state.pendingindex == 0) {return}
|
||||
def currentheatsetpoint = settings.thermostat.currentValue("heatingSetpoint")
|
||||
def currentcoolsetpoint = settings.thermostat.currentValue("coolingSetpoint")
|
||||
def thermMode = thermostat.currentValue("thermostatMode")
|
||||
def lastheatcommand = IndexLookUp("heat")
|
||||
def lastcoolcommand = IndexLookUp("cool")
|
||||
def failedstring = ""
|
||||
log.debug "Programmable Thermostat: $thermostat.label heating setpoint was commanded to $lastheatcommand and is currently $currentheatsetpoint; cooling setpoint was commanded to $lastcoolcommand and is currently $currentcoolsetpoint"
|
||||
|
||||
if (("$currentheatsetpoint" == "$lastheatcommand") && ("$currentcoolsetpoint" == "$lastcoolcommand")) {return}
|
||||
|
||||
state.failedcommandcount = state.failedcommandcount + 1
|
||||
if ("$currentheatsetpoint" != "$lastheatcommand" && "$currentcoolsetpoint" != "$lastcoolcommand" && state.checkcommandstring == "hc") {
|
||||
failedstring = "$thermostat.label is non-responsive to setpoint commands."
|
||||
ThermostatCommander(lastheatcommand, lastcoolcommand, false, "")
|
||||
} else if ("$currentheatsetpoint" != "$lastheatcommand" && (state.checkcommandstring == "hc" || state.checkcommandstring == "h")) {
|
||||
if (thermMode == "heat") {failedstring = "$thermostat.label is non-responsive to heat setpoint commands."}
|
||||
ThermostatCommander(lastheatcommand, 0, false, "")
|
||||
} else if ("$currentcoolsetpoint" != "$lastcoolcommand" && (state.checkcommandstring == "hc" || state.checkcommandstring == "c")) {
|
||||
if (thermMode == "cool") failedstring = "$thermostat.label is non-responsive to cool setpoint commands."
|
||||
ThermostatCommander(0, lastcoolcommand, false, "")
|
||||
}
|
||||
|
||||
if (state.failedcommandcount == 4) {
|
||||
state.failedcommandcount = 0
|
||||
state.pendingindex = 0
|
||||
if (failedstring != "") {sendPush(failedstring)}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//---- EVENT HANDLERS
|
||||
|
||||
//Runs every time a mode change is detected. Used to execute mode-based setpoints; also used to trigger schedule integrity checks in case all scheduled functions have failed
|
||||
def modeChangeHandler(evt) {
|
||||
if (state.notificationlevel>2) {sendNotificationEvent("Programmable Thermostat detected home mode change to $evt.value.")}
|
||||
for (int i = 1; i <= settings.numevent; i++) {
|
||||
def ScheduledDays = settings["edays$i"]
|
||||
if (ScheduledDays == null) {ScheduledDays = TodayAsString()}
|
||||
if ((evt.value == settings["emodes$i"]) && (ScheduledDays.contains(TodayAsString()))) {
|
||||
state.failedcommandcount = 0
|
||||
state.pendingindex = -i
|
||||
ThermostatCommander(settings."eheatset${i}", settings."ecoolset${i}", settings.enotifications, "with change to $evt.value")
|
||||
i = settings.numevent + 1
|
||||
}
|
||||
}
|
||||
SchedulerIntegrityChecker()
|
||||
}
|
||||
|
||||
//Runs every time the temperature reported by the thermostat changes. Used to trigger schedule integrity checks in case all scheduled functions have failed.
|
||||
def tempChangeHandler(evt) {
|
||||
SchedulerIntegrityChecker()
|
||||
if (settings.auto == "Any time") {ThermostatModeSetter(settings.thermostat.latestValue("heatingSetpoint"), settings.thermostat.latestValue("coolingSetpoint"), state.notificationlevel)}
|
||||
}
|
||||
|
||||
//---- OTHER
|
||||
|
||||
//Returns the setpoint temperature associated with a settings index
|
||||
def IndexLookUp(mode) {
|
||||
def result = 0
|
||||
if (mode == "cool") {
|
||||
if (state.pendingindex > 0) {result = settings."scoolset${state.pendingindex}"}
|
||||
if (state.pendingindex < 0) {result = settings."ecoolset${-state.pendingindex}"}
|
||||
} else if (mode == "heat") {
|
||||
if (state.pendingindex > 0) {result = settings."sheatset${state.pendingindex}"}
|
||||
if (state.pendingindex < 0) {result = settings."eheatset${-state.pendingindex}"}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
//Returns the current day of the week as a string
|
||||
def TodayAsString() {
|
||||
return (new Date(now())).format("EEEEEEE", location.timeZone)
|
||||
}
|
||||
|
||||
//Returns time as a string in 12 hour format
|
||||
def DisplayTime(time) {
|
||||
def tz = location.timeZone
|
||||
def hour = time.format("H",tz)
|
||||
def min = time.format("m",tz)
|
||||
def sec = time.format("s",tz)
|
||||
def ampm = "am"
|
||||
def hournum = hour.toInteger()
|
||||
def minnum = min.toInteger()
|
||||
if (hournum == 0) {hournum = 12}
|
||||
if (hournum > 12) {
|
||||
hournum = hournum - 12
|
||||
ampm = "pm"
|
||||
}
|
||||
if (minnum < 10) {min = "0$min"}
|
||||
return "$hournum:$min $ampm"
|
||||
}
|
||||
@@ -107,8 +107,8 @@ mappings {
|
||||
path("/locks") {
|
||||
action: [
|
||||
GET: "listLocks",
|
||||
PUT: "updateLock",
|
||||
POST: "updateLock"
|
||||
PUT: "updateLocks",
|
||||
POST: "updateLocks"
|
||||
]
|
||||
}
|
||||
path("/locks/:id") {
|
||||
@@ -442,31 +442,87 @@ def executePhrase() {
|
||||
}
|
||||
|
||||
private void updateAll(devices) {
|
||||
def type = params.param1
|
||||
def command = request.JSON?.command
|
||||
if (command)
|
||||
{
|
||||
command = command.toLowerCase()
|
||||
devices."$command"()
|
||||
if (!devices) {
|
||||
httpError(404, "Devices not found")
|
||||
}
|
||||
if (command){
|
||||
devices.each { device ->
|
||||
executeCommand(device, type, command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void update(devices) {
|
||||
log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id"
|
||||
//def command = request.JSON?.command
|
||||
def command = params.command
|
||||
if (command)
|
||||
{
|
||||
command = command.toLowerCase()
|
||||
def device = devices.find { it.id == params.id }
|
||||
if (!device)
|
||||
{
|
||||
def type = params.param1
|
||||
def command = request.JSON?.command
|
||||
def device = devices?.find { it.id == params.id }
|
||||
|
||||
if (!device) {
|
||||
httpError(404, "Device not found")
|
||||
}
|
||||
else
|
||||
{
|
||||
device."$command"()
|
||||
}
|
||||
}
|
||||
|
||||
if (command) {
|
||||
executeCommand(device, type, command)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validating the command passed by the user based on capability.
|
||||
* @return boolean
|
||||
*/
|
||||
def validateCommand(device, deviceType, command) {
|
||||
def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
|
||||
def currentDeviceCapability = getCapabilityName(deviceType)
|
||||
if (capabilityCommands[currentDeviceCapability]) {
|
||||
return command in capabilityCommands[currentDeviceCapability] ? true : false
|
||||
} else {
|
||||
// Handling other device types here, which don't accept commands
|
||||
httpError(400, "Bad request.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Need to get the attribute name to do the lookup. Only
|
||||
* doing it for the device types which accept commands
|
||||
* @return attribute name of the device type
|
||||
*/
|
||||
def getCapabilityName(type) {
|
||||
switch(type) {
|
||||
case "switches":
|
||||
return "Switch"
|
||||
case "locks":
|
||||
return "Lock"
|
||||
default:
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructing the map over here of
|
||||
* supported commands by device capability
|
||||
* @return a map of device capability -> supported commands
|
||||
*/
|
||||
def getDeviceCapabilityCommands(deviceCapabilities) {
|
||||
def map = [:]
|
||||
deviceCapabilities.collect {
|
||||
map[it.name] = it.commands.collect{ it.name.toString() }
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and executes the command
|
||||
* on the device or devices
|
||||
*/
|
||||
def executeCommand(device, type, command) {
|
||||
if (validateCommand(device, type, command)) {
|
||||
device."$command"()
|
||||
} else {
|
||||
httpError(403, "Access denied. This command is not supported by current capability.")
|
||||
}
|
||||
}
|
||||
|
||||
private show(devices, type) {
|
||||
|
||||
Reference in New Issue
Block a user