mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-23 05:10:50 +00:00
Compare commits
1 Commits
MSA-794-2
...
MSA-753-12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff3489a479 |
@@ -1,128 +0,0 @@
|
|||||||
/**
|
|
||||||
* Simple Sync
|
|
||||||
*
|
|
||||||
* Copyright 2015 Roomie Remote, Inc.
|
|
||||||
*
|
|
||||||
* Date: 2015-09-22
|
|
||||||
*/
|
|
||||||
metadata
|
|
||||||
{
|
|
||||||
definition (name: "Simple Sync", namespace: "roomieremote-agent", author: "Roomie Remote, Inc.")
|
|
||||||
{
|
|
||||||
capability "Media Controller"
|
|
||||||
}
|
|
||||||
|
|
||||||
// simulator metadata
|
|
||||||
simulator
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI tile definitions
|
|
||||||
tiles
|
|
||||||
{
|
|
||||||
standardTile("mainTile", "device.status", width: 1, height: 1, icon: "st.Entertainment.entertainment11")
|
|
||||||
{
|
|
||||||
state "default", label: "Simple Sync", icon: "st.Home.home2", backgroundColor: "#55A7FF"
|
|
||||||
}
|
|
||||||
|
|
||||||
def detailTiles = ["mainTile"]
|
|
||||||
|
|
||||||
main "mainTile"
|
|
||||||
details(detailTiles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def parse(String description)
|
|
||||||
{
|
|
||||||
def results = []
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
def msg = parseLanMessage(description)
|
|
||||||
|
|
||||||
if (msg.headers && msg.body)
|
|
||||||
{
|
|
||||||
switch (msg.headers["X-Roomie-Echo"])
|
|
||||||
{
|
|
||||||
case "getAllActivities":
|
|
||||||
handleGetAllActivitiesResponse(msg)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Throwable t)
|
|
||||||
{
|
|
||||||
sendEvent(name: "parseError", value: "$t", description: description)
|
|
||||||
throw t
|
|
||||||
}
|
|
||||||
|
|
||||||
results
|
|
||||||
}
|
|
||||||
|
|
||||||
def handleGetAllActivitiesResponse(response)
|
|
||||||
{
|
|
||||||
def body = parseJson(response.body)
|
|
||||||
|
|
||||||
if (body.status == "success")
|
|
||||||
{
|
|
||||||
def json = new groovy.json.JsonBuilder()
|
|
||||||
def root = json activities: body.data
|
|
||||||
def data = json.toString()
|
|
||||||
|
|
||||||
sendEvent(name: "activities", value: data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def getAllActivities(evt)
|
|
||||||
{
|
|
||||||
def host = getHostAddress(device.deviceNetworkId)
|
|
||||||
|
|
||||||
def action = new physicalgraph.device.HubAction(method: "GET",
|
|
||||||
path: "/api/v1/activities",
|
|
||||||
headers: [HOST: host, "X-Roomie-Echo": "getAllActivities"])
|
|
||||||
|
|
||||||
action
|
|
||||||
}
|
|
||||||
|
|
||||||
def startActivity(evt)
|
|
||||||
{
|
|
||||||
def uuid = evt
|
|
||||||
def host = getHostAddress(device.deviceNetworkId)
|
|
||||||
def activity = new groovy.json.JsonSlurper().parseText(device.currentValue('activities') ?: "{ 'activities' : [] }").activities.find { it.uuid == uuid }
|
|
||||||
def toggle = activity["toggle"]
|
|
||||||
def jsonMap = ["activity_uuid": uuid]
|
|
||||||
|
|
||||||
if (toggle != null)
|
|
||||||
{
|
|
||||||
jsonMap << ["toggle_state": toggle ? "on" : "off"]
|
|
||||||
}
|
|
||||||
|
|
||||||
def json = new groovy.json.JsonBuilder(jsonMap)
|
|
||||||
def jsonBody = json.toString()
|
|
||||||
def headers = [HOST: host, "Content-Type": "application/json"]
|
|
||||||
|
|
||||||
def action = new physicalgraph.device.HubAction(method: "POST",
|
|
||||||
path: "/api/v1/runactivity",
|
|
||||||
body: jsonBody,
|
|
||||||
headers: headers)
|
|
||||||
|
|
||||||
action
|
|
||||||
}
|
|
||||||
|
|
||||||
def getHostAddress(d)
|
|
||||||
{
|
|
||||||
def parts = d.split(":")
|
|
||||||
def ip = convertHexToIP(parts[0])
|
|
||||||
def port = convertHexToInt(parts[1])
|
|
||||||
return ip + ":" + port
|
|
||||||
}
|
|
||||||
|
|
||||||
def String convertHexToIP(hex)
|
|
||||||
{
|
|
||||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
|
||||||
}
|
|
||||||
|
|
||||||
def Integer convertHexToInt(hex)
|
|
||||||
{
|
|
||||||
Integer.parseInt(hex,16)
|
|
||||||
}
|
|
||||||
@@ -1,153 +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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleReportAttributeMessage(String description) {
|
|
||||||
def descMap = zigbee.parseDescriptionAsMap(description)
|
|
||||||
|
|
||||||
if (descMap.clusterInt == 0x0001 && descMap.attrInt == 0x0020) {
|
|
||||||
handleBatteryEvent(Integer.parseInt(descMap.value, 16))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleBatteryEvent(rawValue) {
|
|
||||||
def linkText = getLinkText(device)
|
|
||||||
|
|
||||||
def eventMap = [
|
|
||||||
name: 'battery',
|
|
||||||
value: '--'
|
|
||||||
]
|
|
||||||
|
|
||||||
def volts = rawValue / 10
|
|
||||||
if (volts > 0){
|
|
||||||
def minVolts = 2.0
|
|
||||||
def maxVolts = 2.8
|
|
||||||
|
|
||||||
if (volts < minVolts)
|
|
||||||
volts = minVolts
|
|
||||||
else if (volts > maxVolts)
|
|
||||||
volts = maxVolts
|
|
||||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
|
||||||
|
|
||||||
eventMap.value = Math.round(pct * 100)
|
|
||||||
eventMap.descriptionText = "${linkText} battery was ${eventMap.value}%"
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug "Creating battery event: ${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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -67,7 +67,7 @@ metadata {
|
|||||||
state "setpoint", action:"raiseSetpoint", icon:"st.thermostat.thermostat-up"
|
state "setpoint", action:"raiseSetpoint", icon:"st.thermostat.thermostat-up"
|
||||||
}
|
}
|
||||||
valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") {
|
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") {
|
valueTile("currentStatus", "device.thermostatStatus", height: 1, width: 2, decoration: "flat") {
|
||||||
state "thermostatStatus", label:'${currentValue}', backgroundColor:"#ffffff"
|
state "thermostatStatus", label:'${currentValue}', backgroundColor:"#ffffff"
|
||||||
|
|||||||
@@ -204,10 +204,8 @@ private List parseReportAttributeMessage(String description) {
|
|||||||
}
|
}
|
||||||
result << getAccelerationResult(descMap.value)
|
result << getAccelerationResult(descMap.value)
|
||||||
}
|
}
|
||||||
else if (descMap.cluster == "FC02" && descMap.attrId == "0012" && descMap.value.size() == 24) {
|
else if (descMap.cluster == "FC02" && descMap.attrId == "0012") {
|
||||||
// The size is checked to ensure the attribute report contains X, Y and Z values
|
result << parseAxis(descMap.value)
|
||||||
// If all three axis are not included then the attribute report is ignored
|
|
||||||
result << parseAxis(descMap.value)
|
|
||||||
}
|
}
|
||||||
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||||
result << getBatteryResult(Integer.parseInt(descMap.value, 16))
|
result << getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||||
@@ -373,50 +371,21 @@ def getTemperature(value) {
|
|||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
log.debug "Refreshing Values "
|
log.debug "Refreshing Values "
|
||||||
|
def refreshCmds = [
|
||||||
|
|
||||||
def refreshCmds = []
|
/* sensitivity - default value (8) */
|
||||||
|
|
||||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||||
|
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
|
||||||
log.debug "Refreshing Values for manufacturer: SmartThings "
|
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||||
refreshCmds = refreshCmds + [
|
|
||||||
|
|
||||||
/* These values of Motion Threshold Multiplier(01) and Motion Threshold (7602)
|
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||||
seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer.
|
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
|
||||||
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 mfg-code ${manufacturerCode}", "delay 200",
|
||||||
"zcl global write 0xFC02 0 0x20 {01}", "delay 200",
|
"zcl global read 0xFC02 0x0010",
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
"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",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
//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"
|
|
||||||
]
|
|
||||||
|
|
||||||
return refreshCmds + enrollResponse()
|
return refreshCmds + enrollResponse()
|
||||||
}
|
}
|
||||||
@@ -478,34 +447,35 @@ def enrollResponse() {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Map parseAxis(String description) {
|
private Map parseAxis(String description) {
|
||||||
def hexToSignedInt = { hexVal ->
|
log.debug "parseAxis"
|
||||||
def unsignedVal = hexToInt(hexVal)
|
def xyzResults = [x: 0, y: 0, z: 0]
|
||||||
unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal
|
def parts = description.split("2900")
|
||||||
}
|
parts[0] = "12" + parts[0]
|
||||||
|
parts.each { part ->
|
||||||
def z = hexToSignedInt(description[0..3])
|
part = part.trim()
|
||||||
def y = hexToSignedInt(description[10..13])
|
if (part.startsWith("12")) {
|
||||||
def x = hexToSignedInt(description[20..23])
|
def unsignedX = hexToInt(part.split("12")[1].trim())
|
||||||
def xyzResults = [x: x, y: y, z: z]
|
def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX
|
||||||
|
xyzResults.x = signedX
|
||||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
log.debug "X Part: ${signedX}"
|
||||||
// This mapping matches the current behavior of the Device Handler for the Centralite sensors
|
}
|
||||||
xyzResults.x = z
|
else if (part.startsWith("13")) {
|
||||||
xyzResults.y = y
|
def unsignedY = hexToInt(part.split("13")[1].trim())
|
||||||
xyzResults.z = -x
|
def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY
|
||||||
} else {
|
xyzResults.y = signedY
|
||||||
// The axises reported by the Device Handler differ from the axises reported by the sensor
|
log.debug "Y Part: ${signedY}"
|
||||||
// This may change in the future
|
}
|
||||||
xyzResults.x = z
|
else if (part.startsWith("14")) {
|
||||||
xyzResults.y = x
|
def unsignedZ = hexToInt(part.split("14")[1].trim())
|
||||||
xyzResults.z = y
|
def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ
|
||||||
}
|
xyzResults.z = signedZ
|
||||||
|
log.debug "Z Part: ${signedZ}"
|
||||||
log.debug "parseAxis -- ${xyzResults}"
|
if (garageSensor == "Yes")
|
||||||
|
garageEvent(signedZ)
|
||||||
if (garageSensor == "Yes")
|
}
|
||||||
garageEvent(xyzResults.z)
|
}
|
||||||
|
|
||||||
getXyzResult(xyzResults, description)
|
getXyzResult(xyzResults, description)
|
||||||
}
|
}
|
||||||
@@ -583,4 +553,3 @@ private byte[] reverseArray(byte[] array) {
|
|||||||
return array
|
return array
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
* 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:
|
* 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.
|
// Automatically generated. Make future change here.
|
||||||
definition (name: "Simulated Thermostat", namespace: "smartthings/testing", author: "SmartThings") {
|
definition (name: "Simulated Thermostat", namespace: "smartthings/testing", author: "SmartThings") {
|
||||||
capability "Thermostat"
|
capability "Thermostat"
|
||||||
capability "Relative Humidity Measurement"
|
|
||||||
|
|
||||||
command "tempUp"
|
command "tempUp"
|
||||||
command "tempDown"
|
command "tempDown"
|
||||||
@@ -23,40 +22,11 @@ metadata {
|
|||||||
command "heatDown"
|
command "heatDown"
|
||||||
command "coolUp"
|
command "coolUp"
|
||||||
command "coolDown"
|
command "coolDown"
|
||||||
command "setTemperature", ["number"]
|
command "setTemperature", ["number"]
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles {
|
||||||
multiAttributeTile(name:"thermostatMulti", type:"thermostat", width:6, height:4) {
|
valueTile("temperature", "device.temperature", width: 1, height: 1) {
|
||||||
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) {
|
|
||||||
state("temperature", label:'${currentValue}', unit:"dF",
|
state("temperature", label:'${currentValue}', unit:"dF",
|
||||||
backgroundColors:[
|
backgroundColors:[
|
||||||
[value: 31, color: "#153591"],
|
[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"
|
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"
|
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"
|
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"
|
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"
|
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"
|
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"
|
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"
|
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 "off", label:'${name}', action:"thermostat.heat", backgroundColor:"#ffffff"
|
||||||
state "heat", label:'${name}', action:"thermostat.cool", backgroundColor:"#ffa81e"
|
state "heat", label:'${name}', action:"thermostat.cool", backgroundColor:"#ffa81e"
|
||||||
state "cool", label:'${name}', action:"thermostat.auto", backgroundColor:"#269bd2"
|
state "cool", label:'${name}', action:"thermostat.auto", backgroundColor:"#269bd2"
|
||||||
state "auto", label:'${name}', action:"thermostat.off", backgroundColor:"#79b821"
|
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 "fanAuto", label:'${name}', action:"thermostat.fanOn", backgroundColor:"#ffffff"
|
||||||
state "fanOn", label:'${name}', action:"thermostat.fanCirculate", backgroundColor:"#ffffff"
|
state "fanOn", label:'${name}', action:"thermostat.fanCirculate", backgroundColor:"#ffffff"
|
||||||
state "fanCirculate", label:'${name}', action:"thermostat.fanAuto", 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 "idle", label:'${name}', backgroundColor:"#ffffff"
|
||||||
state "heating", label:'${name}', backgroundColor:"#ffa81e"
|
state "heating", label:'${name}', backgroundColor:"#ffa81e"
|
||||||
state "cooling", label:'${name}', backgroundColor:"#269bd2"
|
state "cooling", label:'${name}', backgroundColor:"#269bd2"
|
||||||
}
|
}
|
||||||
|
|
||||||
main("thermostatMulti")
|
main("temperature","operatingState")
|
||||||
details([
|
details([
|
||||||
"temperature","tempDown","tempUp",
|
"temperature","tempDown","tempUp",
|
||||||
"mode", "fanMode", "operatingState",
|
"mode", "fanMode", "operatingState",
|
||||||
@@ -131,7 +101,6 @@ def installed() {
|
|||||||
sendEvent(name: "thermostatMode", value: "off")
|
sendEvent(name: "thermostatMode", value: "off")
|
||||||
sendEvent(name: "thermostatFanMode", value: "fanAuto")
|
sendEvent(name: "thermostatFanMode", value: "fanAuto")
|
||||||
sendEvent(name: "thermostatOperatingState", value: "idle")
|
sendEvent(name: "thermostatOperatingState", value: "idle")
|
||||||
sendEvent(name: "humidity", value: 53, unit: "%")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ metadata {
|
|||||||
|
|
||||||
tiles {
|
tiles {
|
||||||
|
|
||||||
valueTile("power", "device.power", canChangeIcon: true) {
|
valueTile("power", "device.power") {
|
||||||
state "power", label: '${currentValue} W'
|
state "power", label: '${currentValue} W'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ User-Agent: CyberGarage-HTTP/1.0
|
|||||||
def poll() {
|
def poll() {
|
||||||
log.debug "Executing 'poll'"
|
log.debug "Executing 'poll'"
|
||||||
if (device.currentValue("currentIP") != "Offline")
|
if (device.currentValue("currentIP") != "Offline")
|
||||||
runIn(30, setOffline)
|
runIn(10, setOffline)
|
||||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||||
Content-Length: 277
|
Content-Length: 277
|
||||||
|
|||||||
@@ -77,8 +77,9 @@ def parse(String description) {
|
|||||||
def result = []
|
def result = []
|
||||||
def bodyString = msg.body
|
def bodyString = msg.body
|
||||||
if (bodyString) {
|
if (bodyString) {
|
||||||
unschedule("setOffline")
|
unschedule("setOffline")
|
||||||
def body = new XmlSlurper().parseText(bodyString)
|
def body = new XmlSlurper().parseText(bodyString)
|
||||||
|
|
||||||
if (body?.property?.TimeSyncRequest?.text()) {
|
if (body?.property?.TimeSyncRequest?.text()) {
|
||||||
log.trace "Got TimeSyncRequest"
|
log.trace "Got TimeSyncRequest"
|
||||||
result << timeSyncResponse()
|
result << timeSyncResponse()
|
||||||
@@ -133,7 +134,7 @@ def refresh() {
|
|||||||
def getStatus() {
|
def getStatus() {
|
||||||
log.debug "Executing WeMo Motion 'getStatus'"
|
log.debug "Executing WeMo Motion 'getStatus'"
|
||||||
if (device.currentValue("currentIP") != "Offline")
|
if (device.currentValue("currentIP") != "Offline")
|
||||||
runIn(30, setOffline)
|
runIn(10, setOffline)
|
||||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||||
Content-Length: 277
|
Content-Length: 277
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
command "subscribe"
|
command "subscribe"
|
||||||
command "resubscribe"
|
command "resubscribe"
|
||||||
command "unsubscribe"
|
command "unsubscribe"
|
||||||
command "setOffline"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// simulator metadata
|
// simulator metadata
|
||||||
@@ -208,7 +207,7 @@ def subscribe(ip, port) {
|
|||||||
def existingIp = getDataValue("ip")
|
def existingIp = getDataValue("ip")
|
||||||
def existingPort = getDataValue("port")
|
def existingPort = getDataValue("port")
|
||||||
if (ip && ip != existingIp) {
|
if (ip && ip != existingIp) {
|
||||||
log.debug "Updating ip from $existingIp to $ip"
|
log.debug "Updating ip from $existingIp to $ip"
|
||||||
updateDataValue("ip", ip)
|
updateDataValue("ip", ip)
|
||||||
def ipvalue = convertHexToIP(getDataValue("ip"))
|
def ipvalue = convertHexToIP(getDataValue("ip"))
|
||||||
sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}")
|
sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}")
|
||||||
@@ -276,7 +275,7 @@ def setOffline() {
|
|||||||
def poll() {
|
def poll() {
|
||||||
log.debug "Executing 'poll'"
|
log.debug "Executing 'poll'"
|
||||||
if (device.currentValue("currentIP") != "Offline")
|
if (device.currentValue("currentIP") != "Offline")
|
||||||
runIn(30, setOffline)
|
runIn(10, setOffline)
|
||||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||||
Content-Length: 277
|
Content-Length: 277
|
||||||
@@ -291,4 +290,4 @@ User-Agent: CyberGarage-HTTP/1.0
|
|||||||
</u:GetBinaryState>
|
</u:GetBinaryState>
|
||||||
</s:Body>
|
</s:Body>
|
||||||
</s:Envelope>""", physicalgraph.device.Protocol.LAN)
|
</s:Envelope>""", physicalgraph.device.Protocol.LAN)
|
||||||
}
|
}
|
||||||
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@@ -1,164 +1,427 @@
|
|||||||
/**
|
/**
|
||||||
* Nobody Home
|
* Nobody Home
|
||||||
*
|
*
|
||||||
* Author: brian@bevey.org
|
* Author: brian@bevey.org, raychi@gmail.com
|
||||||
* Date: 12/19/14
|
* Date: 12/02/2015
|
||||||
*
|
*
|
||||||
* Monitors a set of presence detectors and triggers a mode change when everyone has left.
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* When everyone has left, sets mode to a new defined mode.
|
* you may not use this file except in compliance with the License.
|
||||||
* When at least one person returns home, set the mode back to a new defined mode.
|
* You may obtain a copy of the License at:
|
||||||
* When someone is home - or upon entering the home, their mode may change dependent on sunrise / sunset.
|
*
|
||||||
|
* 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: "Nobody Home",
|
* Monitors a set of presence sensors and trigger appropriate mode
|
||||||
namespace: "imbrianj",
|
* based on configured modes and sunrise/sunset time.
|
||||||
author: "brian@bevey.org",
|
*
|
||||||
description: "When everyone leaves, change mode. If at least one person home, switch mode based on sun position.",
|
* - When everyone is away [Away]
|
||||||
category: "Mode Magic",
|
* - When someone is home during the day [Home]
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
* - When someone is home at the night [Night]
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png"
|
*/
|
||||||
|
|
||||||
|
// ********** App related functions **********
|
||||||
|
|
||||||
|
// The definition provides metadata about the App to SmartThings.
|
||||||
|
definition (
|
||||||
|
name: "Nobody Home",
|
||||||
|
namespace: "imbrianj",
|
||||||
|
author: "brian@bevey.org",
|
||||||
|
description: "Automatically set Away/Home/Night mode based on a set of presence sensors and sunrise/sunset time.",
|
||||||
|
category: "Mode Magic",
|
||||||
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||||
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||||
|
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The preferences defines information the App needs from the user.
|
||||||
preferences {
|
preferences {
|
||||||
section("When all of these people leave home") {
|
section("Presence sensors to monitor") {
|
||||||
input "people", "capability.presenceSensor", multiple: true
|
input "people", "capability.presenceSensor", multiple: true
|
||||||
}
|
|
||||||
|
|
||||||
section("Change to this mode to...") {
|
|
||||||
input "newAwayMode", "mode", title: "Everyone is away"
|
|
||||||
input "newSunsetMode", "mode", title: "At least one person home and nightfall"
|
|
||||||
input "newSunriseMode", "mode", title: "At least one person home and sunrise"
|
|
||||||
}
|
|
||||||
|
|
||||||
section("Away threshold (defaults to 10 min)") {
|
|
||||||
input "awayThreshold", "decimal", title: "Number of minutes", required: false
|
|
||||||
}
|
|
||||||
|
|
||||||
section("Notifications") {
|
|
||||||
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def installed() {
|
|
||||||
init()
|
|
||||||
}
|
|
||||||
|
|
||||||
def updated() {
|
|
||||||
unsubscribe()
|
|
||||||
init()
|
|
||||||
}
|
|
||||||
|
|
||||||
def init() {
|
|
||||||
subscribe(people, "presence", presence)
|
|
||||||
subscribe(location, "sunrise", setSunrise)
|
|
||||||
subscribe(location, "sunset", setSunset)
|
|
||||||
|
|
||||||
state.sunMode = location.mode
|
|
||||||
}
|
|
||||||
|
|
||||||
def setSunrise(evt) {
|
|
||||||
changeSunMode(newSunriseMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
def setSunset(evt) {
|
|
||||||
changeSunMode(newSunsetMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
def changeSunMode(newMode) {
|
|
||||||
state.sunMode = newMode
|
|
||||||
|
|
||||||
if(everyoneIsAway() && (location.mode == newAwayMode)) {
|
|
||||||
log.debug("Mode is away, not evaluating")
|
|
||||||
}
|
|
||||||
|
|
||||||
else if(location.mode != newMode) {
|
|
||||||
def message = "${app.label} changed your mode to '${newMode}'"
|
|
||||||
send(message)
|
|
||||||
setLocationMode(newMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
log.debug("Mode is the same, not evaluating")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def presence(evt) {
|
|
||||||
if(evt.value == "not present") {
|
|
||||||
log.debug("Checking if everyone is away")
|
|
||||||
|
|
||||||
if(everyoneIsAway()) {
|
|
||||||
log.info("Starting ${newAwayMode} sequence")
|
|
||||||
def delay = (awayThreshold != null && awayThreshold != "") ? awayThreshold * 60 : 10 * 60
|
|
||||||
runIn(delay, "setAway")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
if(location.mode != state.sunMode) {
|
|
||||||
log.debug("Checking if anyone is home")
|
|
||||||
|
|
||||||
if(anyoneIsHome()) {
|
|
||||||
log.info("Starting ${state.sunMode} sequence")
|
|
||||||
|
|
||||||
changeSunMode(state.sunMode)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
section("Mode setting") {
|
||||||
log.debug("Mode is the same, not evaluating")
|
input "newAwayMode", "mode", title: "Everyone is away"
|
||||||
}
|
input "newSunriseMode", "mode", title: "Someone is home during the day"
|
||||||
}
|
input "newSunsetMode", "mode", title: "Someone is home at night"
|
||||||
}
|
|
||||||
|
|
||||||
def setAway() {
|
|
||||||
if(everyoneIsAway()) {
|
|
||||||
if(location.mode != newAwayMode) {
|
|
||||||
def message = "${app.label} changed your mode to '${newAwayMode}' because everyone left home"
|
|
||||||
log.info(message)
|
|
||||||
send(message)
|
|
||||||
setLocationMode(newAwayMode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
section("Mode change delay (minutes)") {
|
||||||
log.debug("Mode is the same, not evaluating")
|
input "awayThreshold", "decimal", title: "Away delay [5m]", required: false
|
||||||
|
input "arrivalThreshold", "decimal", title: "Arrival delay [2m]", required: false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
section("Notifications") {
|
||||||
log.info("Somebody returned home before we set to '${newAwayMode}'")
|
input "sendPushMessage", "bool", title: "Push notification", required:false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private everyoneIsAway() {
|
// called when the user installs the App
|
||||||
def result = true
|
def installed()
|
||||||
|
{
|
||||||
if(people.findAll { it?.currentPresence == "present" }) {
|
log.debug("installed() @${location.name}: ${settings}")
|
||||||
result = false
|
initialize(true)
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("everyoneIsAway: ${result}")
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private anyoneIsHome() {
|
// called when the user installs the app, or changes the App
|
||||||
def result = false
|
// preference
|
||||||
|
def updated()
|
||||||
if(people.findAll { it?.currentPresence == "present" }) {
|
{
|
||||||
result = true
|
log.debug("updated() @${location.name}: ${settings}")
|
||||||
}
|
unsubscribe()
|
||||||
|
initialize(false)
|
||||||
log.debug("anyoneIsHome: ${result}")
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private send(msg) {
|
def initialize(isInstall)
|
||||||
if(sendPushMessage != "No") {
|
{
|
||||||
log.debug("Sending push message")
|
// subscribe to all the events we care about
|
||||||
sendPush(msg)
|
log.debug("Subscribing to events ...")
|
||||||
}
|
|
||||||
|
|
||||||
log.debug(msg)
|
// thing to subscribe, attribute/state we care about, and callback fn
|
||||||
|
subscribe(people, "presence", presenceHandler)
|
||||||
|
subscribe(location, "sunrise", sunriseHandler)
|
||||||
|
subscribe(location, "sunset", sunsetHandler)
|
||||||
|
|
||||||
|
// set the optional parameter values. these are not available
|
||||||
|
// directly until the app has initialized (that is,
|
||||||
|
// installed/updated has returned). so here we access them through
|
||||||
|
// the settings object, as otherwise will get an exception.
|
||||||
|
|
||||||
|
// store information we need in state object so we can get access
|
||||||
|
// to it later in our event handlers.
|
||||||
|
|
||||||
|
// calculate the away threshold in seconds. can't use the simpler
|
||||||
|
// default falsy value, as value of 0 (no delay) is evaluated to
|
||||||
|
// false (not specified), but we want 0 to represent no delay. so
|
||||||
|
// we compare against null explicitly to see if the user has set a
|
||||||
|
// value or not.
|
||||||
|
if (settings.awayThreshold == null) {
|
||||||
|
settings.awayThreshold = 5 // default away 5 minute
|
||||||
|
}
|
||||||
|
state.awayDelay = (int) settings.awayThreshold * 60
|
||||||
|
log.debug("awayThreshold set to " + state.awayDelay + " second(s)")
|
||||||
|
|
||||||
|
if (settings.arrivalThreshold == null) {
|
||||||
|
settings.arrivalThreshold = 2 // default arrival 2 minute
|
||||||
|
}
|
||||||
|
state.arrivalDelay = (int) settings.arrivalThreshold * 60
|
||||||
|
log.debug("arrivalThreshold set to " + state.arrivalDelay + " second(s)")
|
||||||
|
|
||||||
|
// get push notification setting
|
||||||
|
state.isPush = settings.sendPushMessage ? true : false
|
||||||
|
log.debug("sendPushMessage set to " + state.isPush)
|
||||||
|
|
||||||
|
// on install (not update), figure out what mode we should be in
|
||||||
|
// IF someone's home. This value is needed so that when a presence
|
||||||
|
// sensor is triggered, we know what mode to set the system to, as
|
||||||
|
// the sunrise/sunset event handler may not be triggered yet after
|
||||||
|
// a fresh install.
|
||||||
|
if (isInstall) {
|
||||||
|
// TODO: for now, we simply assume daytime. a better approach
|
||||||
|
// would be to figure out whether current time is day or
|
||||||
|
// night, and set it appropriately. However there
|
||||||
|
// doesn't seem to be a way to query this directly
|
||||||
|
// without a zip code. This will become the correct
|
||||||
|
// value at the next sunrise/sunset event.
|
||||||
|
log.debug("No sun info yet, assuming daytime")
|
||||||
|
state.modeIfHome = newSunriseMode
|
||||||
|
|
||||||
|
// set keep a separate sun mode state so we can show a more
|
||||||
|
// helpful message when sun events are triggered.
|
||||||
|
state.currentSunMode = "sunUnknown"
|
||||||
|
|
||||||
|
state.eventDevice = "" // last event device
|
||||||
|
|
||||||
|
// device that triggered timer. This is not necessarily the
|
||||||
|
// eventDevice. For example, if A arrives, kick off timer,
|
||||||
|
// then b arrives before timer elapsed, we want the
|
||||||
|
// notification message to reference A, not B.
|
||||||
|
state.timerDevice = null
|
||||||
|
|
||||||
|
// anything in flight? We use this to avoid scheduling
|
||||||
|
// duplicate timers (so we don't extend the timer).
|
||||||
|
state.pendingOp = "init"
|
||||||
|
|
||||||
|
// now set the correct mode for the location. This way, we
|
||||||
|
// don't need to wait for the next sun/presence event.
|
||||||
|
|
||||||
|
// we schedule this action to run after the app has fully
|
||||||
|
// initialized. This way, the app install is faster and the
|
||||||
|
// user customized app name is used in the notification.
|
||||||
|
runIn(7, "setInitialMode")
|
||||||
|
}
|
||||||
|
// On update, we don't change state.modeIfHome. This is so that we
|
||||||
|
// preserve the current sun rise/set state we obtained in earlier
|
||||||
|
// sunset/sunrise handler. This way the app remains in the correct
|
||||||
|
// sun state when the user reconfigures it.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def setInitialMode()
|
||||||
|
{
|
||||||
|
changeSunMode(state.modeIfHome)
|
||||||
|
state.pendingOp = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ********** sunrise/sunset handling **********
|
||||||
|
|
||||||
|
// event handler when the sunrise time is reached
|
||||||
|
def sunriseHandler(evt)
|
||||||
|
{
|
||||||
|
// we store the mode we should be in, IF someone's home
|
||||||
|
state.modeIfHome = newSunriseMode
|
||||||
|
state.currentSunMode = "sunRise"
|
||||||
|
|
||||||
|
// change mode if someone's home, otherwise set to away
|
||||||
|
changeSunMode(newSunriseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// event handler when the sunset time is reached
|
||||||
|
def sunsetHandler(evt)
|
||||||
|
{
|
||||||
|
// we store the mode we should be in, IF someone's home
|
||||||
|
state.modeIfHome = newSunsetMode
|
||||||
|
state.currentSunMode = "sunSet"
|
||||||
|
|
||||||
|
// change mode if someone's home, otherwise set to away
|
||||||
|
changeSunMode(newSunsetMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
def changeSunMode(newMode)
|
||||||
|
{
|
||||||
|
// if everyone is away, we need to check and ensure the system is
|
||||||
|
// in away mode.
|
||||||
|
if (isEveryoneAway()) {
|
||||||
|
// this shouldn not happen normally as the mode should already
|
||||||
|
// be changed during presenceHandler, but in case this is not
|
||||||
|
// done, such as when app is initially installed while away,
|
||||||
|
// and system is not in away mode, then we toggle it to away
|
||||||
|
// at the sun rise/set event.
|
||||||
|
changeMode(newAwayMode, " because no one is present")
|
||||||
|
} else {
|
||||||
|
// someone is home, we update the mode depending on
|
||||||
|
// sunrise/sunset.
|
||||||
|
if (state.currentSunMode == "sunRise") {
|
||||||
|
changeMode(newMode, " because it's sunrise")
|
||||||
|
} else if (state.currentSunMode == "sunSet") {
|
||||||
|
changeMode(newMode, " because it's sunset")
|
||||||
|
} else {
|
||||||
|
changeMode(newMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ********** presence handling **********
|
||||||
|
|
||||||
|
// event handler when presence sensor changes state
|
||||||
|
def presenceHandler(evt)
|
||||||
|
{
|
||||||
|
// get the device name that resulted in the change
|
||||||
|
state.eventDevice= evt.device?.displayName
|
||||||
|
|
||||||
|
// is setInitialMode() still pending?
|
||||||
|
if (state.pendingOp == "init") {
|
||||||
|
log.debug("Pending ${state.pendingOp} op still in progress, ignoring presence event")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt.value == "not present") {
|
||||||
|
handleDeparture()
|
||||||
|
} else {
|
||||||
|
handleArrival()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def handleDeparture()
|
||||||
|
{
|
||||||
|
log.info("${state.eventDevice} left ${location.name}")
|
||||||
|
|
||||||
|
// do nothing if someone's still home
|
||||||
|
if (!isEveryoneAway()) {
|
||||||
|
log.info("Someone is still present, no actions needed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we set away mode. We perform the following actions even if
|
||||||
|
// home is already in away mode because an arrival timer may be
|
||||||
|
// pending, and scheduling delaySetMode() has the nice effect of
|
||||||
|
// canceling any previous pending timer, which is what we want to
|
||||||
|
// do. So we do this even if delay is 0.
|
||||||
|
log.info("Scheduling ${newAwayMode} mode in " + state.awayDelay + "s")
|
||||||
|
state.pendingOp = "away"
|
||||||
|
state.timerDevice = state.eventDevice
|
||||||
|
// we always use runIn(). This has the benefit of automatically
|
||||||
|
// replacing any pending arrival/away timer. if any arrival timer
|
||||||
|
// is active, it will be clobbered with this away timer. If any
|
||||||
|
// away timer is active, it will be extended with this new timeout
|
||||||
|
// (though normally it should not happen)
|
||||||
|
runIn(state.awayDelay, "delaySetMode")
|
||||||
|
}
|
||||||
|
|
||||||
|
def handleArrival()
|
||||||
|
{
|
||||||
|
// someone returned home, set home/night mode after delay
|
||||||
|
log.info("${state.eventDevice} arrived at ${location.name}")
|
||||||
|
|
||||||
|
def numHome = isAnyoneHome()
|
||||||
|
if (!numHome) {
|
||||||
|
// no one home, do nothing for now (should NOT happen)
|
||||||
|
log.warn("${deviceName} arrived, but isAnyoneHome() returned false!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numHome > 1) {
|
||||||
|
// not the first one home, do nothing, as any action that
|
||||||
|
// should happen would've happened when the first sensor
|
||||||
|
// arrived. this is the opposite of isEveryoneAway() where we
|
||||||
|
// don't do anything if someone's still home.
|
||||||
|
log.debug("Someone is already present, no actions needed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if any pending arrival timer is already active. we want
|
||||||
|
// the timer to trigger when the 1st person arrives, but not
|
||||||
|
// extended when the 2nd person arrives later. this should not
|
||||||
|
// happen because of the >1 check above, but just in case.
|
||||||
|
if (state.pendingOp == "arrive") {
|
||||||
|
log.debug("Pending ${state.pendingOp} op already in progress, do nothing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// now we set home/night mode
|
||||||
|
log.info("Scheduling ${state.modeIfHome} mode in " + state.arrivalDelay + "s")
|
||||||
|
state.pendingOp = "arrive"
|
||||||
|
state.timerDevice = state.eventDevice
|
||||||
|
// if any away timer is active, it will be clobbered with
|
||||||
|
// this arrival timer
|
||||||
|
runIn(state.arrivalDelay, "delaySetMode")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ********** helper functions **********
|
||||||
|
|
||||||
|
// change the system to the new mode, unless its already in that mode.
|
||||||
|
def changeMode(newMode, reason="")
|
||||||
|
{
|
||||||
|
if (location.mode != newMode) {
|
||||||
|
// notification message
|
||||||
|
def message = "${location.name} changed mode from '${location.mode}' to '${newMode}'" + reason
|
||||||
|
setLocationMode(newMode)
|
||||||
|
send(message) // send message after changing mode
|
||||||
|
} else {
|
||||||
|
log.debug("${location.name} is already in ${newMode} mode, no actions needed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a useful departure/arrival reason string
|
||||||
|
def reasonStr(isAway, delaySec, delayMin)
|
||||||
|
{
|
||||||
|
def reason
|
||||||
|
|
||||||
|
// if we are invoked by timer, use the stored timer trigger
|
||||||
|
// device, otherwise use the last event device
|
||||||
|
if (state.timerDevice) {
|
||||||
|
reason = " because ${state.timerDevice} "
|
||||||
|
} else {
|
||||||
|
reason = " because ${state.eventDevice} "
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAway) {
|
||||||
|
reason += "left"
|
||||||
|
} else {
|
||||||
|
reason += "arrived"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delaySec) {
|
||||||
|
if (delaySec > 60) {
|
||||||
|
if (delayMin == null) {
|
||||||
|
delayMin = (int) delaySec / 60
|
||||||
|
}
|
||||||
|
reason += " ${delayMin} minutes ago"
|
||||||
|
} else {
|
||||||
|
reason += " ${delaySec}s ago"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reason
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://docs.smartthings.com/en/latest/smartapp-developers-guide/scheduling.html#schedule-from-now
|
||||||
|
//
|
||||||
|
// By default, if a method is scheduled to run in the future, and then
|
||||||
|
// another call to runIn with the same method is made, the last one
|
||||||
|
// overwrites the previously scheduled method.
|
||||||
|
//
|
||||||
|
// We use the above property to schedule our arrval/departure delay
|
||||||
|
// using the same function so we don't have to worry about
|
||||||
|
// arrival/departure timer firing independently and complicating code.
|
||||||
|
def delaySetMode()
|
||||||
|
{
|
||||||
|
def newMode = null
|
||||||
|
def reason = ""
|
||||||
|
|
||||||
|
// timer has elapsed, check presence status to figure out what we
|
||||||
|
// need to do
|
||||||
|
if (isEveryoneAway()) {
|
||||||
|
reason = reasonStr(true, state.awayDelay, awayThreshold)
|
||||||
|
newMode = newAwayMode
|
||||||
|
if (state.pendingOp) {
|
||||||
|
log.debug("${state.pendingOp} timer elapsed: everyone is away")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reason = reasonStr(false, state.arrivalDelay, arrivalThreshold)
|
||||||
|
newMode = state.modeIfHome
|
||||||
|
if (state.pendingOp) {
|
||||||
|
log.debug("${state.pendingOp} timer elapsed: someone is home")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now change the mode
|
||||||
|
changeMode(newMode, reason);
|
||||||
|
|
||||||
|
state.pendingOp = null
|
||||||
|
state.timerDevice = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private isEveryoneAway()
|
||||||
|
{
|
||||||
|
def result = true
|
||||||
|
|
||||||
|
if (people.findAll { it?.currentPresence == "present" }) {
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
// log.debug("isEveryoneAway: ${result}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the number of people that are home
|
||||||
|
private isAnyoneHome()
|
||||||
|
{
|
||||||
|
def result = 0
|
||||||
|
// iterate over our people variable that we defined
|
||||||
|
// in the preferences method
|
||||||
|
for (person in people) {
|
||||||
|
if (person.currentPresence == "present") {
|
||||||
|
result++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private send(msg)
|
||||||
|
{
|
||||||
|
if (state.isPush) {
|
||||||
|
log.debug("Sending push notification")
|
||||||
|
sendPush(msg)
|
||||||
|
} else {
|
||||||
|
log.debug("Sending notification")
|
||||||
|
sendNotificationEvent(msg)
|
||||||
|
}
|
||||||
|
log.info(msg)
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ preferences{
|
|||||||
input "lock1", "capability.lock", required: true
|
input "lock1", "capability.lock", required: true
|
||||||
}
|
}
|
||||||
section("Select the door contact sensor:") {
|
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...") {
|
section("Automatically lock the door when closed...") {
|
||||||
input "minutesLater", "number", title: "Delay (in minutes):", required: true
|
input "minutesLater", "number", title: "Delay (in minutes):", required: true
|
||||||
@@ -22,10 +22,9 @@ preferences{
|
|||||||
input "secondsLater", "number", title: "Delay (in seconds):", required: true
|
input "secondsLater", "number", title: "Delay (in seconds):", required: true
|
||||||
}
|
}
|
||||||
section( "Notifications" ) {
|
section( "Notifications" ) {
|
||||||
input("recipients", "contact", title: "Send notifications to", required: false) {
|
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes", "No"]], required: false
|
||||||
input "phoneNumber", "phone", title: "Warn with text message (optional)", description: "Phone Number", required: false
|
input "phoneNumber", "phone", title: "Enter phone number to send text notification.", required: false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def installed(){
|
def installed(){
|
||||||
@@ -43,73 +42,55 @@ def initialize(){
|
|||||||
subscribe(lock1, "lock", doorHandler, [filterEvents: false])
|
subscribe(lock1, "lock", doorHandler, [filterEvents: false])
|
||||||
subscribe(lock1, "unlock", doorHandler, [filterEvents: false])
|
subscribe(lock1, "unlock", doorHandler, [filterEvents: false])
|
||||||
subscribe(contact, "contact.open", doorHandler)
|
subscribe(contact, "contact.open", doorHandler)
|
||||||
subscribe(contact, "contact.closed", doorHandler)
|
subscribe(contact, "contact.closed", doorHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
def lockDoor(){
|
def lockDoor(){
|
||||||
log.debug "Locking the door."
|
log.debug "Locking the door."
|
||||||
lock1.lock()
|
lock1.lock()
|
||||||
if(location.contactBookEnabled) {
|
log.debug ( "Sending Push Notification..." )
|
||||||
if ( recipients ) {
|
if ( sendPushMessage != "No" ) sendPush( "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!" )
|
||||||
log.debug ( "Sending Push Notification..." )
|
log.debug("Sending text message...")
|
||||||
sendNotificationToContacts( "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!", recipients)
|
if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!" )
|
||||||
}
|
|
||||||
}
|
|
||||||
if (phoneNumber) {
|
|
||||||
log.debug("Sending text message...")
|
|
||||||
sendSms( phoneNumber, "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def unlockDoor(){
|
def unlockDoor(){
|
||||||
log.debug "Unlocking the door."
|
log.debug "Unlocking the door."
|
||||||
lock1.unlock()
|
lock1.unlock()
|
||||||
if(location.contactBookEnabled) {
|
log.debug ( "Sending Push Notification..." )
|
||||||
if ( recipients ) {
|
if ( sendPushMessage != "No" ) sendPush( "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!" )
|
||||||
log.debug ( "Sending Push Notification..." )
|
log.debug("Sending text message...")
|
||||||
sendNotificationToContacts( "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!", recipients)
|
if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!" )
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( phoneNumber ) {
|
|
||||||
log.debug("Sending text message...")
|
|
||||||
sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def doorHandler(evt){
|
def doorHandler(evt){
|
||||||
if ((contact.latestValue("contact") == "open") && (evt.value == "locked")) { // If the door is open and a person locks the door then...
|
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
|
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.
|
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...
|
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.
|
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...
|
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.
|
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...
|
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
|
def delay = (minutesLater * 60) // runIn uses seconds
|
||||||
runIn( (minutesLater * 60), lockDoor ) // ...schedule (in minutes) to lock.
|
runIn( delay, lockDoor ) // ...schedule (in minutes) to lock.
|
||||||
}
|
}
|
||||||
else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "open")) { // If a person opens an unlocked door...
|
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.
|
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...
|
else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "closed")) { // If a person closes an unlocked door...
|
||||||
//def delay = (minutesLater * 60) // runIn uses seconds
|
def delay = (minutesLater * 60) // runIn uses seconds
|
||||||
runIn( (minutesLater * 60), lockDoor ) // ...schedule (in minutes) to lock.
|
runIn( delay, lockDoor ) // ...schedule (in minutes) to lock.
|
||||||
}
|
}
|
||||||
else { //Opening or Closing door when locked (in case you have a handle lock)
|
else { //Opening or Closing door when locked (in case you have a handle lock)
|
||||||
log.debug "Unlocking the door."
|
log.debug "Unlocking the door."
|
||||||
lock1.unlock()
|
lock1.unlock()
|
||||||
if(location.contactBookEnabled) {
|
log.debug ( "Sending Push Notification..." )
|
||||||
if ( recipients ) {
|
if ( sendPushMessage != "No" ) sendPush( "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!" )
|
||||||
log.debug ( "Sending Push Notification..." )
|
log.debug("Sending text message...")
|
||||||
sendNotificationToContacts( "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!", recipients)
|
if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!" )
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if ( phoneNumber ) {
|
|
||||||
log.debug("Sending text message...")
|
|
||||||
sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
/**
|
|
||||||
* Simple Sync Connect
|
|
||||||
*
|
|
||||||
* Copyright 2015 Roomie Remote, Inc.
|
|
||||||
*
|
|
||||||
* Date: 2015-09-22
|
|
||||||
*/
|
|
||||||
|
|
||||||
definition(
|
|
||||||
name: "Simple Sync Connect",
|
|
||||||
namespace: "roomieremote-raconnect",
|
|
||||||
author: "Roomie Remote, Inc.",
|
|
||||||
description: "Integrate SmartThings with your Simple Control activities via Simple Sync.",
|
|
||||||
category: "My Apps",
|
|
||||||
iconUrl: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-60.png",
|
|
||||||
iconX2Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png",
|
|
||||||
iconX3Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png")
|
|
||||||
|
|
||||||
preferences()
|
|
||||||
{
|
|
||||||
page(name: "mainPage", title: "Simple Sync Setup", content: "mainPage", refreshTimeout: 5)
|
|
||||||
page(name:"agentDiscovery", title:"Simple Sync Discovery", content:"agentDiscovery", refreshTimeout:5)
|
|
||||||
page(name:"manualAgentEntry")
|
|
||||||
page(name:"verifyManualEntry")
|
|
||||||
}
|
|
||||||
|
|
||||||
def mainPage()
|
|
||||||
{
|
|
||||||
if (canInstallLabs())
|
|
||||||
{
|
|
||||||
return agentDiscovery()
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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:"mainPage", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
|
|
||||||
section("Upgrade")
|
|
||||||
{
|
|
||||||
paragraph "$upgradeNeeded"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def agentDiscovery(params=[:])
|
|
||||||
{
|
|
||||||
int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int
|
|
||||||
state.refreshCount = refreshCount + 1
|
|
||||||
def refreshInterval = refreshCount == 0 ? 2 : 5
|
|
||||||
|
|
||||||
if (!state.subscribe)
|
|
||||||
{
|
|
||||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
|
||||||
state.subscribe = true
|
|
||||||
}
|
|
||||||
|
|
||||||
//ssdp request every fifth refresh
|
|
||||||
if ((refreshCount % 5) == 0)
|
|
||||||
{
|
|
||||||
discoverAgents()
|
|
||||||
}
|
|
||||||
|
|
||||||
def agentsDiscovered = agentsDiscovered()
|
|
||||||
|
|
||||||
return dynamicPage(name:"agentDiscovery", title:"Pair with Simple Sync", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) {
|
|
||||||
section("Pair with Simple Sync")
|
|
||||||
{
|
|
||||||
input "selectedAgent", "enum", required:true, title:"Select Simple Sync\n(${agentsDiscovered.size() ?: 0} found)", multiple:false, options:agentsDiscovered
|
|
||||||
href(name:"manualAgentEntry",
|
|
||||||
title:"Manually Configure Simple Sync",
|
|
||||||
required:false,
|
|
||||||
page:"manualAgentEntry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def manualAgentEntry()
|
|
||||||
{
|
|
||||||
dynamicPage(name:"manualAgentEntry", title:"Manually Configure Simple Sync", nextPage:"verifyManualEntry", install:false, uninstall:true) {
|
|
||||||
section("Manually Configure Simple Sync")
|
|
||||||
{
|
|
||||||
paragraph "In the event that Simple Sync cannot be automatically discovered by your SmartThings hub, you may enter Simple Sync's IP address here."
|
|
||||||
input(name: "manualIPAddress", type: "text", title: "IP Address", required: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def verifyManualEntry()
|
|
||||||
{
|
|
||||||
def hexIP = convertIPToHexString(manualIPAddress)
|
|
||||||
def hexPort = convertToHexString(47147)
|
|
||||||
def uuid = "593C03D2-1DA9-4CDB-A335-6C6DC98E56C3"
|
|
||||||
def hubId = ""
|
|
||||||
|
|
||||||
for (hub in location.hubs)
|
|
||||||
{
|
|
||||||
if (hub.localIP != null)
|
|
||||||
{
|
|
||||||
hubId = hub.id
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def manualAgent = [deviceType: "04",
|
|
||||||
mac: "unknown",
|
|
||||||
ip: hexIP,
|
|
||||||
port: hexPort,
|
|
||||||
ssdpPath: "/upnp/Roomie.xml",
|
|
||||||
ssdpUSN: "uuid:$uuid::urn:roomieremote-com:device:roomie:1",
|
|
||||||
hub: hubId,
|
|
||||||
verified: true,
|
|
||||||
name: "Simple Sync $manualIPAddress"]
|
|
||||||
|
|
||||||
state.agents[uuid] = manualAgent
|
|
||||||
|
|
||||||
addOrUpdateAgent(state.agents[uuid])
|
|
||||||
|
|
||||||
dynamicPage(name: "verifyManualEntry", title: "Manual Configuration Complete", nextPage: "", install:true, uninstall:true) {
|
|
||||||
section("")
|
|
||||||
{
|
|
||||||
paragraph("Tap Done to complete the installation process.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def discoverAgents()
|
|
||||||
{
|
|
||||||
def urn = getURN()
|
|
||||||
|
|
||||||
sendHubCommand(new physicalgraph.device.HubAction("lan discovery $urn", physicalgraph.device.Protocol.LAN))
|
|
||||||
}
|
|
||||||
|
|
||||||
def agentsDiscovered()
|
|
||||||
{
|
|
||||||
def gAgents = getAgents()
|
|
||||||
def agents = gAgents.findAll { it?.value?.verified == true }
|
|
||||||
def map = [:]
|
|
||||||
agents.each
|
|
||||||
{
|
|
||||||
map["${it.value.uuid}"] = it.value.name
|
|
||||||
}
|
|
||||||
map
|
|
||||||
}
|
|
||||||
|
|
||||||
def getAgents()
|
|
||||||
{
|
|
||||||
if (!state.agents)
|
|
||||||
{
|
|
||||||
state.agents = [:]
|
|
||||||
}
|
|
||||||
|
|
||||||
state.agents
|
|
||||||
}
|
|
||||||
|
|
||||||
def installed()
|
|
||||||
{
|
|
||||||
initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
def updated()
|
|
||||||
{
|
|
||||||
initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
def initialize()
|
|
||||||
{
|
|
||||||
if (state.subscribe)
|
|
||||||
{
|
|
||||||
unsubscribe()
|
|
||||||
state.subscribe = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedAgent)
|
|
||||||
{
|
|
||||||
addOrUpdateAgent(state.agents[selectedAgent])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def addOrUpdateAgent(agent)
|
|
||||||
{
|
|
||||||
def children = getChildDevices()
|
|
||||||
def dni = agent.ip + ":" + agent.port
|
|
||||||
def found = false
|
|
||||||
|
|
||||||
children.each
|
|
||||||
{
|
|
||||||
if ((it.getDeviceDataByName("mac") == agent.mac))
|
|
||||||
{
|
|
||||||
found = true
|
|
||||||
|
|
||||||
if (it.getDeviceNetworkId() != dni)
|
|
||||||
{
|
|
||||||
it.setDeviceNetworkId(dni)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (it.getDeviceNetworkId() == dni)
|
|
||||||
{
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found)
|
|
||||||
{
|
|
||||||
addChildDevice("roomieremote-agent", "Simple Sync", dni, agent.hub, [label: "Simple Sync"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def locationHandler(evt)
|
|
||||||
{
|
|
||||||
def description = evt?.description
|
|
||||||
def urn = getURN()
|
|
||||||
def hub = evt?.hubId
|
|
||||||
def parsedEvent = parseEventMessage(description)
|
|
||||||
|
|
||||||
parsedEvent?.putAt("hub", hub)
|
|
||||||
|
|
||||||
//SSDP DISCOVERY EVENTS
|
|
||||||
if (parsedEvent?.ssdpTerm?.contains(urn))
|
|
||||||
{
|
|
||||||
def agent = parsedEvent
|
|
||||||
def ip = convertHexToIP(agent.ip)
|
|
||||||
def agents = getAgents()
|
|
||||||
|
|
||||||
agent.verified = true
|
|
||||||
agent.name = "Simple Sync $ip"
|
|
||||||
|
|
||||||
if (!agents[agent.uuid])
|
|
||||||
{
|
|
||||||
state.agents[agent.uuid] = agent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def parseEventMessage(String description)
|
|
||||||
{
|
|
||||||
def event = [:]
|
|
||||||
def parts = description.split(',')
|
|
||||||
|
|
||||||
parts.each
|
|
||||||
{ part ->
|
|
||||||
part = part.trim()
|
|
||||||
if (part.startsWith('devicetype:'))
|
|
||||||
{
|
|
||||||
def valueString = part.split(":")[1].trim()
|
|
||||||
event.devicetype = valueString
|
|
||||||
}
|
|
||||||
else if (part.startsWith('mac:'))
|
|
||||||
{
|
|
||||||
def valueString = part.split(":")[1].trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.mac = valueString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.startsWith('networkAddress:'))
|
|
||||||
{
|
|
||||||
def valueString = part.split(":")[1].trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.ip = valueString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.startsWith('deviceAddress:'))
|
|
||||||
{
|
|
||||||
def valueString = part.split(":")[1].trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.port = valueString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.startsWith('ssdpPath:'))
|
|
||||||
{
|
|
||||||
def valueString = part.split(":")[1].trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.ssdpPath = valueString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.startsWith('ssdpUSN:'))
|
|
||||||
{
|
|
||||||
part -= "ssdpUSN:"
|
|
||||||
def valueString = part.trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.ssdpUSN = valueString
|
|
||||||
|
|
||||||
def uuid = getUUIDFromUSN(valueString)
|
|
||||||
|
|
||||||
if (uuid)
|
|
||||||
{
|
|
||||||
event.uuid = uuid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.startsWith('ssdpTerm:'))
|
|
||||||
{
|
|
||||||
part -= "ssdpTerm:"
|
|
||||||
def valueString = part.trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.ssdpTerm = valueString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.startsWith('headers'))
|
|
||||||
{
|
|
||||||
part -= "headers:"
|
|
||||||
def valueString = part.trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.headers = valueString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.startsWith('body'))
|
|
||||||
{
|
|
||||||
part -= "body:"
|
|
||||||
def valueString = part.trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.body = valueString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event
|
|
||||||
}
|
|
||||||
|
|
||||||
def getURN()
|
|
||||||
{
|
|
||||||
return "urn:roomieremote-com:device:roomie:1"
|
|
||||||
}
|
|
||||||
|
|
||||||
def getUUIDFromUSN(usn)
|
|
||||||
{
|
|
||||||
def parts = usn.split(":")
|
|
||||||
|
|
||||||
for (int i = 0; i < parts.size(); ++i)
|
|
||||||
{
|
|
||||||
if (parts[i] == "uuid")
|
|
||||||
{
|
|
||||||
return parts[i + 1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def String convertHexToIP(hex)
|
|
||||||
{
|
|
||||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
|
||||||
}
|
|
||||||
|
|
||||||
def Integer convertHexToInt(hex)
|
|
||||||
{
|
|
||||||
Integer.parseInt(hex,16)
|
|
||||||
}
|
|
||||||
|
|
||||||
def String convertToHexString(n)
|
|
||||||
{
|
|
||||||
String hex = String.format("%X", n.toInteger())
|
|
||||||
}
|
|
||||||
|
|
||||||
def String convertIPToHexString(ipString)
|
|
||||||
{
|
|
||||||
String hex = ipString.tokenize(".").collect {
|
|
||||||
String.format("%02X", it.toInteger())
|
|
||||||
}.join()
|
|
||||||
}
|
|
||||||
|
|
||||||
def Boolean canInstallLabs()
|
|
||||||
{
|
|
||||||
return hasAllHubsOver("000.011.00603")
|
|
||||||
}
|
|
||||||
|
|
||||||
def Boolean hasAllHubsOver(String desiredFirmware)
|
|
||||||
{
|
|
||||||
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
|
||||||
}
|
|
||||||
|
|
||||||
def List getRealHubFirmwareVersions()
|
|
||||||
{
|
|
||||||
return location.hubs*.firmwareVersionString.findAll { it }
|
|
||||||
}
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
/**
|
|
||||||
* Simple Sync Trigger
|
|
||||||
*
|
|
||||||
* Copyright 2015 Roomie Remote, Inc.
|
|
||||||
*
|
|
||||||
* Date: 2015-09-22
|
|
||||||
*/
|
|
||||||
definition(
|
|
||||||
name: "Simple Sync Trigger",
|
|
||||||
namespace: "roomieremote-ratrigger",
|
|
||||||
author: "Roomie Remote, Inc.",
|
|
||||||
description: "Trigger Simple Control activities when certain actions take place in your home.",
|
|
||||||
category: "My Apps",
|
|
||||||
iconUrl: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-60.png",
|
|
||||||
iconX2Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png",
|
|
||||||
iconX3Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png")
|
|
||||||
|
|
||||||
|
|
||||||
preferences {
|
|
||||||
page(name: "agentSelection", title: "Select your Simple Sync")
|
|
||||||
page(name: "refreshActivities", title: "Updating list of Simple Sync activities")
|
|
||||||
page(name: "control", title: "Run a Simple Control activity when something happens")
|
|
||||||
page(name: "timeIntervalInput", title: "Only during a certain time", install: true, uninstall: true) {
|
|
||||||
section {
|
|
||||||
input "starting", "time", title: "Starting", required: false
|
|
||||||
input "ending", "time", title: "Ending", required: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def agentSelection()
|
|
||||||
{
|
|
||||||
if (agent)
|
|
||||||
{
|
|
||||||
state.refreshCount = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
dynamicPage(name: "agentSelection", title: "Select your Simple Sync", nextPage: "control", install: false, uninstall: true) {
|
|
||||||
section {
|
|
||||||
input "agent", "capability.mediaController", title: "Simple Sync", required: true, multiple: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def control()
|
|
||||||
{
|
|
||||||
def activities = agent.latestValue('activities')
|
|
||||||
|
|
||||||
if (!activities || !state.refreshCount)
|
|
||||||
{
|
|
||||||
int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int
|
|
||||||
state.refreshCount = refreshCount + 1
|
|
||||||
def refreshInterval = refreshCount == 0 ? 2 : 4
|
|
||||||
|
|
||||||
// Request activities every 5th attempt
|
|
||||||
if((refreshCount % 5) == 0)
|
|
||||||
{
|
|
||||||
agent.getAllActivities()
|
|
||||||
}
|
|
||||||
|
|
||||||
dynamicPage(name: "control", title: "Updating list of Simple Control activities", nextPage: "", refreshInterval: refreshInterval, install: false, uninstall: true) {
|
|
||||||
section("") {
|
|
||||||
paragraph "Retrieving activities from Simple Sync"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
dynamicPage(name: "control", title: "Run a Simple Control activity when something happens", nextPage: "timeIntervalInput", install: false, uninstall: true) {
|
|
||||||
def anythingSet = anythingSet()
|
|
||||||
if (anythingSet) {
|
|
||||||
section("When..."){
|
|
||||||
ifSet "motion", "capability.motionSensor", title: "Motion Detected", required: false, multiple: true
|
|
||||||
ifSet "motionInactive", "capability.motionSensor", title: "Motion Stops", required: false, multiple: true
|
|
||||||
ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
|
|
||||||
ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
|
|
||||||
ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
|
|
||||||
ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
|
|
||||||
ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
|
|
||||||
ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
|
|
||||||
ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
|
|
||||||
ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
|
|
||||||
ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
|
|
||||||
ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
section(anythingSet ? "Select additional triggers" : "When...", hideable: anythingSet, hidden: true){
|
|
||||||
ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
|
|
||||||
ifUnset "motionInactive", "capability.motionSensor", title: "Motion Stops", required: false, multiple: true
|
|
||||||
ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
|
|
||||||
ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
|
|
||||||
ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
|
|
||||||
ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
|
|
||||||
ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
|
|
||||||
ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
|
|
||||||
ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
|
|
||||||
ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
|
|
||||||
ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
|
|
||||||
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
|
||||||
}
|
|
||||||
section("Run this activity"){
|
|
||||||
input "activity", "enum", title: "Activity?", required: true, options: new groovy.json.JsonSlurper().parseText(activities ?: "[]").activities?.collect { ["${it.uuid}": it.name] }
|
|
||||||
}
|
|
||||||
|
|
||||||
section("More options", hideable: true, hidden: true) {
|
|
||||||
input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
|
|
||||||
href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
|
|
||||||
input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
|
|
||||||
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
|
||||||
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
|
|
||||||
input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
|
|
||||||
}
|
|
||||||
section([mobileOnly:true]) {
|
|
||||||
label title: "Assign a name", required: false
|
|
||||||
mode title: "Set for specific mode(s)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private anythingSet() {
|
|
||||||
for (name in ["motion","motionInactive","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","button1","triggerModes","timeOfDay"]) {
|
|
||||||
if (settings[name]) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private ifUnset(Map options, String name, String capability) {
|
|
||||||
if (!settings[name]) {
|
|
||||||
input(options, name, capability)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ifSet(Map options, String name, String capability) {
|
|
||||||
if (settings[name]) {
|
|
||||||
input(options, name, capability)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def installed() {
|
|
||||||
subscribeToEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
def updated() {
|
|
||||||
unsubscribe()
|
|
||||||
unschedule()
|
|
||||||
subscribeToEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
def subscribeToEvents() {
|
|
||||||
log.trace "subscribeToEvents()"
|
|
||||||
subscribe(app, appTouchHandler)
|
|
||||||
subscribe(contact, "contact.open", eventHandler)
|
|
||||||
subscribe(contactClosed, "contact.closed", eventHandler)
|
|
||||||
subscribe(acceleration, "acceleration.active", eventHandler)
|
|
||||||
subscribe(motion, "motion.active", eventHandler)
|
|
||||||
subscribe(motionInactive, "motion.inactive", eventHandler)
|
|
||||||
subscribe(mySwitch, "switch.on", eventHandler)
|
|
||||||
subscribe(mySwitchOff, "switch.off", eventHandler)
|
|
||||||
subscribe(arrivalPresence, "presence.present", eventHandler)
|
|
||||||
subscribe(departurePresence, "presence.not present", eventHandler)
|
|
||||||
subscribe(button1, "button.pushed", eventHandler)
|
|
||||||
|
|
||||||
if (triggerModes) {
|
|
||||||
subscribe(location, modeChangeHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeOfDay) {
|
|
||||||
schedule(timeOfDay, scheduledTimeHandler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def eventHandler(evt) {
|
|
||||||
if (allOk) {
|
|
||||||
def lastTime = state[frequencyKey(evt)]
|
|
||||||
if (oncePerDayOk(lastTime)) {
|
|
||||||
if (frequency) {
|
|
||||||
if (lastTime == null || now() - lastTime >= frequency * 60000) {
|
|
||||||
startActivity(evt)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
log.debug "Not taking action because $frequency minutes have not elapsed since last action"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
startActivity(evt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
log.debug "Not taking action because it was already taken today"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def modeChangeHandler(evt) {
|
|
||||||
log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
|
|
||||||
if (evt.value in triggerModes) {
|
|
||||||
eventHandler(evt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def scheduledTimeHandler() {
|
|
||||||
eventHandler(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
def appTouchHandler(evt) {
|
|
||||||
startActivity(evt)
|
|
||||||
}
|
|
||||||
|
|
||||||
private startActivity(evt) {
|
|
||||||
agent.startActivity(activity)
|
|
||||||
|
|
||||||
if (frequency) {
|
|
||||||
state.lastActionTimeStamp = now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private frequencyKey(evt) {
|
|
||||||
//evt.deviceId ?: evt.value
|
|
||||||
"lastActionTimeStamp"
|
|
||||||
}
|
|
||||||
|
|
||||||
private dayString(Date date) {
|
|
||||||
def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
|
|
||||||
if (location.timeZone) {
|
|
||||||
df.setTimeZone(location.timeZone)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
|
|
||||||
}
|
|
||||||
df.format(date)
|
|
||||||
}
|
|
||||||
|
|
||||||
private oncePerDayOk(Long lastTime) {
|
|
||||||
def result = true
|
|
||||||
if (oncePerDay) {
|
|
||||||
result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
|
|
||||||
log.trace "oncePerDayOk = $result"
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO - centralize somehow
|
|
||||||
private getAllOk() {
|
|
||||||
modeOk && daysOk && timeOk
|
|
||||||
}
|
|
||||||
|
|
||||||
private getModeOk() {
|
|
||||||
def result = !modes || modes.contains(location.mode)
|
|
||||||
log.trace "modeOk = $result"
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDaysOk() {
|
|
||||||
def result = true
|
|
||||||
if (days) {
|
|
||||||
def df = new java.text.SimpleDateFormat("EEEE")
|
|
||||||
if (location.timeZone) {
|
|
||||||
df.setTimeZone(location.timeZone)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
|
|
||||||
}
|
|
||||||
def day = df.format(new Date())
|
|
||||||
result = days.contains(day)
|
|
||||||
}
|
|
||||||
log.trace "daysOk = $result"
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
private getTimeOk() {
|
|
||||||
def result = true
|
|
||||||
if (starting && ending) {
|
|
||||||
def currTime = now()
|
|
||||||
def start = timeToday(starting).time
|
|
||||||
def stop = timeToday(ending).time
|
|
||||||
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
|
|
||||||
}
|
|
||||||
log.trace "timeOk = $result"
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
private hhmm(time, fmt = "h:mm a")
|
|
||||||
{
|
|
||||||
def t = timeToday(time, location.timeZone)
|
|
||||||
def f = new java.text.SimpleDateFormat(fmt)
|
|
||||||
f.setTimeZone(location.timeZone ?: timeZone(time))
|
|
||||||
f.format(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
private timeIntervalLabel()
|
|
||||||
{
|
|
||||||
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
|
|
||||||
}
|
|
||||||
@@ -1,774 +0,0 @@
|
|||||||
/**
|
|
||||||
* Simple Control
|
|
||||||
*
|
|
||||||
* Copyright 2015 Roomie Remote, Inc.
|
|
||||||
*
|
|
||||||
* Date: 2015-09-22
|
|
||||||
*/
|
|
||||||
|
|
||||||
definition(
|
|
||||||
name: "Simple Control",
|
|
||||||
namespace: "roomieremote-roomieconnect",
|
|
||||||
author: "Roomie Remote, Inc.",
|
|
||||||
description: "Integrate SmartThings with your Simple Control activities.",
|
|
||||||
category: "My Apps",
|
|
||||||
iconUrl: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-60.png",
|
|
||||||
iconX2Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png",
|
|
||||||
iconX3Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png")
|
|
||||||
|
|
||||||
preferences()
|
|
||||||
{
|
|
||||||
section("Allow Simple Control to Monitor and Control 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 "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false
|
|
||||||
input "doorControls", "capability.doorControl", title: "Which Door Controls?", multiple: true, required: false
|
|
||||||
input "colorControls", "capability.colorControl", title: "Which Color Controllers?", multiple: true, required: false
|
|
||||||
input "musicPlayers", "capability.musicPlayer", title: "Which Music Players?", multiple: true, required: false
|
|
||||||
input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false
|
|
||||||
}
|
|
||||||
|
|
||||||
page(name: "mainPage", title: "Simple Control Setup", content: "mainPage", refreshTimeout: 5)
|
|
||||||
page(name:"agentDiscovery", title:"Simple Sync Discovery", content:"agentDiscovery", refreshTimeout:5)
|
|
||||||
page(name:"manualAgentEntry")
|
|
||||||
page(name:"verifyManualEntry")
|
|
||||||
}
|
|
||||||
|
|
||||||
mappings {
|
|
||||||
path("/devices") {
|
|
||||||
action: [
|
|
||||||
GET: "getDevices"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
path("/:deviceType/devices") {
|
|
||||||
action: [
|
|
||||||
GET: "getDevices",
|
|
||||||
POST: "handleDevicesWithIDs"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
path("/device/:deviceType/:id") {
|
|
||||||
action: [
|
|
||||||
GET: "getDevice",
|
|
||||||
POST: "updateDevice"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
path("/subscriptions") {
|
|
||||||
action: [
|
|
||||||
GET: "listSubscriptions",
|
|
||||||
POST: "addSubscription", // {"deviceId":"xxx", "attributeName":"xxx","callbackUrl":"http://..."}
|
|
||||||
DELETE: "removeAllSubscriptions"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
path("/subscriptions/:id") {
|
|
||||||
action: [
|
|
||||||
DELETE: "removeSubscription"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAllDevices()
|
|
||||||
{
|
|
||||||
//log.debug("getAllDevices()")
|
|
||||||
([] + switches + locks + thermostats + imageCaptures + relaySwitches + doorControls + colorControls + musicPlayers + speechSynthesizers + switchLevels + indicators + mediaControllers + tones + tvs + alarms + valves + motionSensors + presenceSensors + beacons + pushButtons + smokeDetectors + coDetectors + contactSensors + accelerationSensors + energyMeters + powerMeters + lightSensors + humiditySensors + temperatureSensors + speechRecognizers + stepSensors + touchSensors)?.findAll()?.unique { it.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
def getDevices()
|
|
||||||
{
|
|
||||||
//log.debug("getDevices, params: ${params}")
|
|
||||||
allDevices.collect {
|
|
||||||
//log.debug("device: ${it}")
|
|
||||||
deviceItem(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def getDevice()
|
|
||||||
{
|
|
||||||
//log.debug("getDevice, params: ${params}")
|
|
||||||
def device = allDevices.find { it.id == params.id }
|
|
||||||
if (!device)
|
|
||||||
{
|
|
||||||
render status: 404, data: '{"msg": "Device not found"}'
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
deviceItem(device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def handleDevicesWithIDs()
|
|
||||||
{
|
|
||||||
//log.debug("handleDevicesWithIDs, params: ${params}")
|
|
||||||
def data = request.JSON
|
|
||||||
def ids = data?.ids?.findAll()?.unique()
|
|
||||||
//log.debug("ids: ${ids}")
|
|
||||||
def command = data?.command
|
|
||||||
def arguments = data?.arguments
|
|
||||||
def type = params?.deviceType
|
|
||||||
//log.debug("device type: ${type}")
|
|
||||||
if (command)
|
|
||||||
{
|
|
||||||
def statusCode = 404
|
|
||||||
//log.debug("command ${command}, arguments ${arguments}")
|
|
||||||
for (devId in ids)
|
|
||||||
{
|
|
||||||
def device = allDevices.find { it.id == devId }
|
|
||||||
//log.debug("device: ${device}")
|
|
||||||
// Check if we have a device that responds to the specified command
|
|
||||||
if (validateCommand(device, type, command)) {
|
|
||||||
if (arguments) {
|
|
||||||
device."$command"(*arguments)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
device."$command"()
|
|
||||||
}
|
|
||||||
statusCode = 200
|
|
||||||
} else {
|
|
||||||
statusCode = 403
|
|
||||||
}
|
|
||||||
}
|
|
||||||
def responseData = "{}"
|
|
||||||
switch (statusCode)
|
|
||||||
{
|
|
||||||
case 403:
|
|
||||||
responseData = '{"msg": "Access denied. This command is not supported by current capability."}'
|
|
||||||
break
|
|
||||||
case 404:
|
|
||||||
responseData = '{"msg": "Device not found"}'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
render status: statusCode, data: responseData
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ids.collect {
|
|
||||||
def currentId = it
|
|
||||||
def device = allDevices.find { it.id == currentId }
|
|
||||||
if (device)
|
|
||||||
{
|
|
||||||
deviceItem(device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private deviceItem(device) {
|
|
||||||
[
|
|
||||||
id: device.id,
|
|
||||||
label: device.displayName,
|
|
||||||
currentState: device.currentStates,
|
|
||||||
capabilities: device.capabilities?.collect {[
|
|
||||||
name: it.name
|
|
||||||
]},
|
|
||||||
attributes: device.supportedAttributes?.collect {[
|
|
||||||
name: it.name,
|
|
||||||
dataType: it.dataType,
|
|
||||||
values: it.values
|
|
||||||
]},
|
|
||||||
commands: device.supportedCommands?.collect {[
|
|
||||||
name: it.name,
|
|
||||||
arguments: it.arguments
|
|
||||||
]},
|
|
||||||
type: [
|
|
||||||
name: device.typeName,
|
|
||||||
author: device.typeAuthor
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
def updateDevice()
|
|
||||||
{
|
|
||||||
//log.debug("updateDevice, params: ${params}")
|
|
||||||
def data = request.JSON
|
|
||||||
def command = data?.command
|
|
||||||
def arguments = data?.arguments
|
|
||||||
def type = params?.deviceType
|
|
||||||
//log.debug("device type: ${type}")
|
|
||||||
|
|
||||||
//log.debug("updateDevice, params: ${params}, request: ${data}")
|
|
||||||
if (!command) {
|
|
||||||
render status: 400, data: '{"msg": "command is required"}'
|
|
||||||
} else {
|
|
||||||
def statusCode = 404
|
|
||||||
def device = allDevices.find { it.id == params.id }
|
|
||||||
if (device) {
|
|
||||||
// Check if we have a device that responds to the specified command
|
|
||||||
if (validateCommand(device, type, command)) {
|
|
||||||
if (arguments) {
|
|
||||||
device."$command"(*arguments)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
device."$command"()
|
|
||||||
}
|
|
||||||
statusCode = 200
|
|
||||||
} else {
|
|
||||||
statusCode = 403
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def responseData = "{}"
|
|
||||||
switch (statusCode)
|
|
||||||
{
|
|
||||||
case 403:
|
|
||||||
responseData = '{"msg": "Access denied. This command is not supported by current capability."}'
|
|
||||||
break
|
|
||||||
case 404:
|
|
||||||
responseData = '{"msg": "Device not found"}'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
render status: statusCode, data: responseData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validating the command passed by the user based on capability.
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
def validateCommand(device, deviceType, command) {
|
|
||||||
//log.debug("validateCommand ${command}")
|
|
||||||
def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
|
|
||||||
//log.debug("capabilityCommands: ${capabilityCommands}")
|
|
||||||
def currentDeviceCapability = getCapabilityName(deviceType)
|
|
||||||
//log.debug("currentDeviceCapability: ${currentDeviceCapability}")
|
|
||||||
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"
|
|
||||||
case "thermostats":
|
|
||||||
return "Thermostat"
|
|
||||||
case "doorControls":
|
|
||||||
return "Door Control"
|
|
||||||
case "colorControls":
|
|
||||||
return "Color Control"
|
|
||||||
case "musicPlayers":
|
|
||||||
return "Music Player"
|
|
||||||
case "switchLevels":
|
|
||||||
return "Switch Level"
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
def listSubscriptions()
|
|
||||||
{
|
|
||||||
//log.debug "listSubscriptions()"
|
|
||||||
app.subscriptions?.findAll { it.deviceId }?.collect {
|
|
||||||
def deviceInfo = state[it.deviceId]
|
|
||||||
def response = [
|
|
||||||
id: it.id,
|
|
||||||
deviceId: it.deviceId,
|
|
||||||
attributeName: it.data,
|
|
||||||
handler: it.handler
|
|
||||||
]
|
|
||||||
//if (!selectedAgent) {
|
|
||||||
response.callbackUrl = deviceInfo?.callbackUrl
|
|
||||||
//}
|
|
||||||
response
|
|
||||||
} ?: []
|
|
||||||
}
|
|
||||||
|
|
||||||
def addSubscription() {
|
|
||||||
def data = request.JSON
|
|
||||||
def attribute = data.attributeName
|
|
||||||
def callbackUrl = data.callbackUrl
|
|
||||||
|
|
||||||
//log.debug "addSubscription, params: ${params}, request: ${data}"
|
|
||||||
if (!attribute) {
|
|
||||||
render status: 400, data: '{"msg": "attributeName is required"}'
|
|
||||||
} else {
|
|
||||||
def device = allDevices.find { it.id == data.deviceId }
|
|
||||||
if (device) {
|
|
||||||
//if (!selectedAgent) {
|
|
||||||
//log.debug "Adding callbackUrl: $callbackUrl"
|
|
||||||
state[device.id] = [callbackUrl: callbackUrl]
|
|
||||||
//}
|
|
||||||
//log.debug "Adding subscription"
|
|
||||||
def subscription = subscribe(device, attribute, deviceHandler)
|
|
||||||
if (!subscription || !subscription.eventSubscription) {
|
|
||||||
//log.debug("subscriptions: ${app.subscriptions}")
|
|
||||||
//for (sub in app.subscriptions)
|
|
||||||
//{
|
|
||||||
//log.debug("subscription.id ${sub.id} subscription.handler ${sub.handler} subscription.deviceId ${sub.deviceId}")
|
|
||||||
//log.debug(sub.properties.collect{it}.join('\n'))
|
|
||||||
//}
|
|
||||||
subscription = app.subscriptions?.find { it.device.id == data.deviceId && it.data == attribute && it.handler == 'deviceHandler' }
|
|
||||||
}
|
|
||||||
|
|
||||||
def response = [
|
|
||||||
id: subscription.id,
|
|
||||||
deviceId: subscription.device?.id,
|
|
||||||
attributeName: subscription.data,
|
|
||||||
handler: subscription.handler
|
|
||||||
]
|
|
||||||
//if (!selectedAgent) {
|
|
||||||
response.callbackUrl = callbackUrl
|
|
||||||
//}
|
|
||||||
response
|
|
||||||
} else {
|
|
||||||
render status: 400, data: '{"msg": "Device not found"}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def removeSubscription()
|
|
||||||
{
|
|
||||||
def subscription = app.subscriptions?.find { it.id == params.id }
|
|
||||||
def device = subscription?.device
|
|
||||||
|
|
||||||
//log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}"
|
|
||||||
if (device) {
|
|
||||||
//log.debug "Removing subscription for device: ${device.id}"
|
|
||||||
state.remove(device.id)
|
|
||||||
unsubscribe(device)
|
|
||||||
}
|
|
||||||
render status: 204, data: "{}"
|
|
||||||
}
|
|
||||||
|
|
||||||
def removeAllSubscriptions()
|
|
||||||
{
|
|
||||||
for (sub in app.subscriptions)
|
|
||||||
{
|
|
||||||
//log.debug("Subscription: ${sub}")
|
|
||||||
//log.debug(sub.properties.collect{it}.join('\n'))
|
|
||||||
def handler = sub.handler
|
|
||||||
def device = sub.device
|
|
||||||
|
|
||||||
if (device && handler == 'deviceHandler')
|
|
||||||
{
|
|
||||||
//log.debug(device.properties.collect{it}.join('\n'))
|
|
||||||
//log.debug("Removing subscription for device: ${device}")
|
|
||||||
state.remove(device.id)
|
|
||||||
unsubscribe(device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def deviceHandler(evt) {
|
|
||||||
def deviceInfo = state[evt.deviceId]
|
|
||||||
//if (selectedAgent) {
|
|
||||||
// sendToRoomie(evt, agentCallbackUrl)
|
|
||||||
//} else if (deviceInfo) {
|
|
||||||
if (deviceInfo)
|
|
||||||
{
|
|
||||||
if (deviceInfo.callbackUrl) {
|
|
||||||
sendToRoomie(evt, deviceInfo.callbackUrl)
|
|
||||||
} else {
|
|
||||||
log.warn "No callbackUrl set for device: ${evt.deviceId}"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.warn "No subscribed device found for device: ${evt.deviceId}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def sendToRoomie(evt, String callbackUrl) {
|
|
||||||
def callback = new URI(callbackUrl)
|
|
||||||
def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host
|
|
||||||
def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path
|
|
||||||
sendHubCommand(new physicalgraph.device.HubAction(
|
|
||||||
method: "POST",
|
|
||||||
path: path,
|
|
||||||
headers: [
|
|
||||||
"Host": host,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
],
|
|
||||||
body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
def mainPage()
|
|
||||||
{
|
|
||||||
if (canInstallLabs())
|
|
||||||
{
|
|
||||||
return agentDiscovery()
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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:"mainPage", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
|
|
||||||
section("Upgrade")
|
|
||||||
{
|
|
||||||
paragraph "$upgradeNeeded"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def agentDiscovery(params=[:])
|
|
||||||
{
|
|
||||||
int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int
|
|
||||||
state.refreshCount = refreshCount + 1
|
|
||||||
def refreshInterval = refreshCount == 0 ? 2 : 5
|
|
||||||
|
|
||||||
if (!state.subscribe)
|
|
||||||
{
|
|
||||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
|
||||||
state.subscribe = true
|
|
||||||
}
|
|
||||||
|
|
||||||
//ssdp request every fifth refresh
|
|
||||||
if ((refreshCount % 5) == 0)
|
|
||||||
{
|
|
||||||
discoverAgents()
|
|
||||||
}
|
|
||||||
|
|
||||||
def agentsDiscovered = agentsDiscovered()
|
|
||||||
|
|
||||||
return dynamicPage(name:"agentDiscovery", title:"Pair with Simple Sync", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) {
|
|
||||||
section("Pair with Simple Sync")
|
|
||||||
{
|
|
||||||
input "selectedAgent", "enum", required:false, title:"Select Simple Sync\n(${agentsDiscovered.size() ?: 0} found)", multiple:false, options:agentsDiscovered
|
|
||||||
href(name:"manualAgentEntry",
|
|
||||||
title:"Manually Configure Simple Sync",
|
|
||||||
required:false,
|
|
||||||
page:"manualAgentEntry")
|
|
||||||
}
|
|
||||||
section("Allow Simple Control to Monitor and Control 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 "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false
|
|
||||||
input "doorControls", "capability.doorControl", title: "Which Door Controls?", multiple: true, required: false
|
|
||||||
input "colorControls", "capability.colorControl", title: "Which Color Controllers?", multiple: true, required: false
|
|
||||||
input "musicPlayers", "capability.musicPlayer", title: "Which Music Players?", multiple: true, required: false
|
|
||||||
input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def manualAgentEntry()
|
|
||||||
{
|
|
||||||
dynamicPage(name:"manualAgentEntry", title:"Manually Configure Simple Sync", nextPage:"verifyManualEntry", install:false, uninstall:true) {
|
|
||||||
section("Manually Configure Simple Sync")
|
|
||||||
{
|
|
||||||
paragraph "In the event that Simple Sync cannot be automatically discovered by your SmartThings hub, you may enter Simple Sync's IP address here."
|
|
||||||
input(name: "manualIPAddress", type: "text", title: "IP Address", required: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def verifyManualEntry()
|
|
||||||
{
|
|
||||||
def hexIP = convertIPToHexString(manualIPAddress)
|
|
||||||
def hexPort = convertToHexString(47147)
|
|
||||||
def uuid = "593C03D2-1DA9-4CDB-A335-6C6DC98E56C3"
|
|
||||||
def hubId = ""
|
|
||||||
|
|
||||||
for (hub in location.hubs)
|
|
||||||
{
|
|
||||||
if (hub.localIP != null)
|
|
||||||
{
|
|
||||||
hubId = hub.id
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def manualAgent = [deviceType: "04",
|
|
||||||
mac: "unknown",
|
|
||||||
ip: hexIP,
|
|
||||||
port: hexPort,
|
|
||||||
ssdpPath: "/upnp/Roomie.xml",
|
|
||||||
ssdpUSN: "uuid:$uuid::urn:roomieremote-com:device:roomie:1",
|
|
||||||
hub: hubId,
|
|
||||||
verified: true,
|
|
||||||
name: "Simple Sync $manualIPAddress"]
|
|
||||||
|
|
||||||
state.agents[uuid] = manualAgent
|
|
||||||
|
|
||||||
addOrUpdateAgent(state.agents[uuid])
|
|
||||||
|
|
||||||
dynamicPage(name: "verifyManualEntry", title: "Manual Configuration Complete", nextPage: "", install:true, uninstall:true) {
|
|
||||||
section("")
|
|
||||||
{
|
|
||||||
paragraph("Tap Done to complete the installation process.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def discoverAgents()
|
|
||||||
{
|
|
||||||
def urn = getURN()
|
|
||||||
|
|
||||||
sendHubCommand(new physicalgraph.device.HubAction("lan discovery $urn", physicalgraph.device.Protocol.LAN))
|
|
||||||
}
|
|
||||||
|
|
||||||
def agentsDiscovered()
|
|
||||||
{
|
|
||||||
def gAgents = getAgents()
|
|
||||||
def agents = gAgents.findAll { it?.value?.verified == true }
|
|
||||||
def map = [:]
|
|
||||||
agents.each
|
|
||||||
{
|
|
||||||
map["${it.value.uuid}"] = it.value.name
|
|
||||||
}
|
|
||||||
map
|
|
||||||
}
|
|
||||||
|
|
||||||
def getAgents()
|
|
||||||
{
|
|
||||||
if (!state.agents)
|
|
||||||
{
|
|
||||||
state.agents = [:]
|
|
||||||
}
|
|
||||||
|
|
||||||
state.agents
|
|
||||||
}
|
|
||||||
|
|
||||||
def installed()
|
|
||||||
{
|
|
||||||
initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
def updated()
|
|
||||||
{
|
|
||||||
initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
def initialize()
|
|
||||||
{
|
|
||||||
if (state.subscribe)
|
|
||||||
{
|
|
||||||
unsubscribe()
|
|
||||||
state.subscribe = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedAgent)
|
|
||||||
{
|
|
||||||
addOrUpdateAgent(state.agents[selectedAgent])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def addOrUpdateAgent(agent)
|
|
||||||
{
|
|
||||||
def children = getChildDevices()
|
|
||||||
def dni = agent.ip + ":" + agent.port
|
|
||||||
def found = false
|
|
||||||
|
|
||||||
children.each
|
|
||||||
{
|
|
||||||
if ((it.getDeviceDataByName("mac") == agent.mac))
|
|
||||||
{
|
|
||||||
found = true
|
|
||||||
|
|
||||||
if (it.getDeviceNetworkId() != dni)
|
|
||||||
{
|
|
||||||
it.setDeviceNetworkId(dni)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (it.getDeviceNetworkId() == dni)
|
|
||||||
{
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found)
|
|
||||||
{
|
|
||||||
addChildDevice("roomieremote-agent", "Simple Sync", dni, agent.hub, [label: "Simple Sync"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def locationHandler(evt)
|
|
||||||
{
|
|
||||||
def description = evt?.description
|
|
||||||
def urn = getURN()
|
|
||||||
def hub = evt?.hubId
|
|
||||||
def parsedEvent = parseEventMessage(description)
|
|
||||||
|
|
||||||
parsedEvent?.putAt("hub", hub)
|
|
||||||
|
|
||||||
//SSDP DISCOVERY EVENTS
|
|
||||||
if (parsedEvent?.ssdpTerm?.contains(urn))
|
|
||||||
{
|
|
||||||
def agent = parsedEvent
|
|
||||||
def ip = convertHexToIP(agent.ip)
|
|
||||||
def agents = getAgents()
|
|
||||||
|
|
||||||
agent.verified = true
|
|
||||||
agent.name = "Simple Sync $ip"
|
|
||||||
|
|
||||||
if (!agents[agent.uuid])
|
|
||||||
{
|
|
||||||
state.agents[agent.uuid] = agent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def parseEventMessage(String description)
|
|
||||||
{
|
|
||||||
def event = [:]
|
|
||||||
def parts = description.split(',')
|
|
||||||
|
|
||||||
parts.each
|
|
||||||
{ part ->
|
|
||||||
part = part.trim()
|
|
||||||
if (part.startsWith('devicetype:'))
|
|
||||||
{
|
|
||||||
def valueString = part.split(":")[1].trim()
|
|
||||||
event.devicetype = valueString
|
|
||||||
}
|
|
||||||
else if (part.startsWith('mac:'))
|
|
||||||
{
|
|
||||||
def valueString = part.split(":")[1].trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.mac = valueString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.startsWith('networkAddress:'))
|
|
||||||
{
|
|
||||||
def valueString = part.split(":")[1].trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.ip = valueString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.startsWith('deviceAddress:'))
|
|
||||||
{
|
|
||||||
def valueString = part.split(":")[1].trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.port = valueString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.startsWith('ssdpPath:'))
|
|
||||||
{
|
|
||||||
def valueString = part.split(":")[1].trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.ssdpPath = valueString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.startsWith('ssdpUSN:'))
|
|
||||||
{
|
|
||||||
part -= "ssdpUSN:"
|
|
||||||
def valueString = part.trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.ssdpUSN = valueString
|
|
||||||
|
|
||||||
def uuid = getUUIDFromUSN(valueString)
|
|
||||||
|
|
||||||
if (uuid)
|
|
||||||
{
|
|
||||||
event.uuid = uuid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.startsWith('ssdpTerm:'))
|
|
||||||
{
|
|
||||||
part -= "ssdpTerm:"
|
|
||||||
def valueString = part.trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.ssdpTerm = valueString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.startsWith('headers'))
|
|
||||||
{
|
|
||||||
part -= "headers:"
|
|
||||||
def valueString = part.trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.headers = valueString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.startsWith('body'))
|
|
||||||
{
|
|
||||||
part -= "body:"
|
|
||||||
def valueString = part.trim()
|
|
||||||
if (valueString)
|
|
||||||
{
|
|
||||||
event.body = valueString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event
|
|
||||||
}
|
|
||||||
|
|
||||||
def getURN()
|
|
||||||
{
|
|
||||||
return "urn:roomieremote-com:device:roomie:1"
|
|
||||||
}
|
|
||||||
|
|
||||||
def getUUIDFromUSN(usn)
|
|
||||||
{
|
|
||||||
def parts = usn.split(":")
|
|
||||||
|
|
||||||
for (int i = 0; i < parts.size(); ++i)
|
|
||||||
{
|
|
||||||
if (parts[i] == "uuid")
|
|
||||||
{
|
|
||||||
return parts[i + 1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def String convertHexToIP(hex)
|
|
||||||
{
|
|
||||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
|
||||||
}
|
|
||||||
|
|
||||||
def Integer convertHexToInt(hex)
|
|
||||||
{
|
|
||||||
Integer.parseInt(hex,16)
|
|
||||||
}
|
|
||||||
|
|
||||||
def String convertToHexString(n)
|
|
||||||
{
|
|
||||||
String hex = String.format("%X", n.toInteger())
|
|
||||||
}
|
|
||||||
|
|
||||||
def String convertIPToHexString(ipString)
|
|
||||||
{
|
|
||||||
String hex = ipString.tokenize(".").collect {
|
|
||||||
String.format("%02X", it.toInteger())
|
|
||||||
}.join()
|
|
||||||
}
|
|
||||||
|
|
||||||
def Boolean canInstallLabs()
|
|
||||||
{
|
|
||||||
return hasAllHubsOver("000.011.00603")
|
|
||||||
}
|
|
||||||
|
|
||||||
def Boolean hasAllHubsOver(String desiredFirmware)
|
|
||||||
{
|
|
||||||
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
|
||||||
}
|
|
||||||
|
|
||||||
def List getRealHubFirmwareVersions()
|
|
||||||
{
|
|
||||||
return location.hubs*.firmwareVersionString.findAll { it }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -22,9 +22,9 @@ definition(
|
|||||||
author: "SmartThings & Joe Geiger",
|
author: "SmartThings & Joe Geiger",
|
||||||
description: "Control your Bose® SoundTouch® when certain actions take place in your home.",
|
description: "Control your Bose® SoundTouch® when certain actions take place in your home.",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||||
iconX2Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon@2x.png",
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png",
|
||||||
iconX3Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon@2x-1.png"
|
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png"
|
||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.8 KiB |
Reference in New Issue
Block a user