Compare commits

..

1 Commits

Author SHA1 Message Date
Anna Peacock 8263a6bd8a MSA-2138: See my accessibzj 2017-07-31 13:13:42 -07:00
2 changed files with 77 additions and 376 deletions
@@ -1,299 +0,0 @@
/**
* Visonic Door/Window Sensor
*
* Copyright 2017 Tomas Axerot
*
* 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.
*
*/
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
metadata {
definition (name: "Visonic Door/Window Sensor", namespace: "tomasaxerot", author: "Tomas Axerot") {
capability "Battery"
capability "Configuration"
capability "Contact Sensor"
capability "Refresh"
capability "Temperature Measurement"
capability "Health Check"
capability "Sensor"
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Visonic", model: "MCT-340 SMA"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Visonic", model: "MCT-340 E"
}
simulator {
}
preferences {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
tiles(scale: 2) {
multiAttributeTile(name: "contact", type: "generic", width: 6, height: 4) {
tileAttribute("device.contact", key: "PRIMARY_CONTROL") {
attributeState "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#e86d13"
attributeState "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#00A0DC"
}
}
valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) {
state "temperature", label: '${currentValue}°',
backgroundColors: [
[value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
}
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
state "battery", label: '${currentValue}% battery', unit: ""
}
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", action: "refresh.refresh", icon: "st.secondary.refresh"
}
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
}
main(["contact", "temperature"])
details(["contact", "temperature", "battery", "refresh", "configure"])
}
}
def parse(String description) {
log.debug "description: $description"
Map map = [:]
if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description)
}
else if (description?.startsWith('read attr -')) {
map = parseReportAttributeMessage(description)
}
else if (description?.startsWith('temperature: ')) {
map = parseCustomMessage(description)
}
else if (description?.startsWith('zone status')) {
map = parseIasMessage(description)
}
log.debug "Parse returned $map"
def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) {
List cmds = enrollResponse()
log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
}
return result
}
private Map parseCatchAllMessage(String description) {
Map resultMap = [:]
def cluster = zigbee.parse(description)
if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) {
case 0x0001:
resultMap = getBatteryResult(cluster.data.last())
break
case 0x0402:
log.debug 'TEMP'
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp)
resultMap = getTemperatureResult(value)
break
}
}
return resultMap
}
private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage
}
private Map parseReportAttributeMessage(String description) {
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
log.debug "Desc Map: $descMap"
Map resultMap = [:]
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = getTemperature(descMap.value)
resultMap = getTemperatureResult(value)
}
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
}
return resultMap
}
private Map parseCustomMessage(String description) {
Map resultMap = [:]
if (description?.startsWith('temperature: ')) {
def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
resultMap = getTemperatureResult(value)
}
return resultMap
}
private Map parseIasMessage(String description) {
ZoneStatus zs = zigbee.parseZoneStatus(description)
return zs.isAlarm1Set() ? getContactResult('open') : getContactResult('closed')
}
def getTemperature(value) {
def celsius = Integer.parseInt(value, 16).shortValue() / 100
if(getTemperatureScale() == "C"){
return celsius
} else {
return celsiusToFahrenheit(celsius) as Integer
}
}
private Map getBatteryResult(rawValue) {
log.debug 'Battery'
def linkText = getLinkText(device)
def result = [:]
if (!(rawValue == 0 || rawValue == 255)) {
def volts = rawValue / 10
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%"
result.name = 'battery'
}
return result
}
private Map getTemperatureResult(value) {
log.debug 'TEMP'
def linkText = getLinkText(device)
if (tempOffset) {
def offset = tempOffset as int
def v = value as int
value = v + offset
}
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
return [
name: 'temperature',
value: value,
descriptionText: descriptionText,
unit: temperatureScale
]
}
private Map getContactResult(value) {
log.debug 'Contact Status'
def linkText = getLinkText(device)
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
return [
name: 'contact',
value: value,
descriptionText: descriptionText
]
}
/**
* PING is used by Device-Watch in attempt to reach the Device
* */
def ping() {
return zigbee.readAttribute(0x0402, 0x0000) // Read the Temperature Cluster
}
def refresh()
{
log.debug "Refreshing Temperature and Battery"
def refreshCmds = [
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
]
return refreshCmds + enrollResponse()
}
def configure() {
// Device-Watch allows 2 check-in misses from device
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def enrollCmds = [
"delay 1000",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
//"raw 0x500 {01 23 00 00 00}", "delay 200",
//"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
]
return enrollCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // send refresh cmds as part of config
}
def enrollResponse() {
log.debug "Sending enroll response"
[
"raw 0x500 {01 23 00 00 00}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1"
]
}
private hex(value) {
new BigInteger(Math.round(value).toString()).toString(16)
}
private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
}
private byte[] reverseArray(byte[] array) {
int i = 0;
int j = array.length - 1;
byte tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j--;
i++;
}
return array
}
@@ -1,77 +1,77 @@
/**
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
* Text Me When There's Motion and I'm Not Here
*
* Author: SmartThings
*/
definition(
name: "Text Me When There's Motion and I'm Not Here",
namespace: "smartthings",
author: "SmartThings",
description: "Send a text message when there is motion while you are away.",
category: "Convenience",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/intruder_motion-presence.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/intruder_motion-presence@2x.png"
)
preferences {
section("When there's movement...") {
input "motion1", "capability.motionSensor", title: "Where?"
}
section("While I'm out...") {
input "presence1", "capability.presenceSensor", title: "Who?"
}
section("Text me at...") {
input("recipients", "contact", title: "Send notifications to") {
input "phone1", "phone", title: "Phone number?"
}
}
}
def installed() {
subscribe(motion1, "motion.active", motionActiveHandler)
}
def updated() {
unsubscribe()
subscribe(motion1, "motion.active", motionActiveHandler)
}
def motionActiveHandler(evt) {
log.trace "$evt.value: $evt, $settings"
if (presence1.latestValue("presence") == "not present") {
// Don't send a continuous stream of text messages
def deltaSeconds = 10
def timeAgo = new Date(now() - (1000 * deltaSeconds))
def recentEvents = motion1.eventsSince(timeAgo)
log.debug "Found ${recentEvents?.size() ?: 0} events in the last $deltaSeconds seconds"
def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1
if (alreadySentSms) {
log.debug "SMS already sent within the last $deltaSeconds seconds"
} else {
if (location.contactBookEnabled) {
log.debug "$motion1 has moved while you were out, sending notifications to: ${recipients?.size()}"
sendNotificationToContacts("${motion1.label} ${motion1.name} moved while you were out", recipients)
}
else {
log.debug "$motion1 has moved while you were out, sending text"
sendSms(phone1, "${motion1.label} ${motion1.name} moved while you were out")
}
}
} else {
log.debug "Motion detected, but presence sensor indicates you are present"
}
}
/**
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
* Text Me When There's Motion and I'm Not Here
*
* Author: SmartThings
*/
definition(
name: "Text Me When There's Motion and I'm Not Here",
namespace: "smartthings",
author: "SmartThings",
description: "Send a text message when there is motion while you are away.",
category: "Convenience",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/intruder_motion-presence.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/intruder_motion-presence@2x.png"
)
preferences {
section("When there's movement...") {
input "motion1", "capability.motionSensor", title: "Where?"
}
section("While I'm out...") {
input "presence1", "capability.presenceSensor", title: "Who?"
}
section("Text me at...") {
input("recipients", "contact", title: "Send notifications to") {
input "phone1", "phone", title: "Phone number?"
}
}
}
def installed() {
subscribe(motion1, "motion.active", motionActiveHandler)
}
def updated() {
unsubscribe()
subscribe(motion1, "motion.active", motionActiveHandler)
}
def motionActiveHandler(evt) {
log.trace "$evt.value: $evt, $settings"
if (presence1.latestValue("presence") == "not present") {
// Don't send a continuous stream of text messages
def deltaSeconds = 10
def timeAgo = new Date(now() - (1000 * deltaSeconds))
def recentEvents = motion1.eventsSince(timeAgo)
log.debug "Found ${recentEvents?.size() ?: 0} events in the last $deltaSeconds seconds"
def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1
if (alreadySentSms) {
log.debug "SMS already sent within the last $deltaSeconds seconds"
} else {
if (location.contactBookEnabled) {
log.debug "$motion1 has moved while you were out, sending notifications to: ${recipients?.size()}"
sendNotificationToContacts("${motion1.label} ${motion1.name} moved while you were out", recipients)
}
else {
log.debug "$motion1 has moved while you were out, sending text"
sendSms(phone1, "${motion1.label} ${motion1.name} moved while you were out")
}
}
} else {
log.debug "Motion detected, but presence sensor indicates you are present"
}
}