mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-09 05:11:52 +00:00
Compare commits
3 Commits
PROD_2016.
...
MSA-1354-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e32676fe5 | ||
|
|
ab61db3699 | ||
|
|
11e047e31d |
126
devicetypes/com-lametric/lametric.src/lametric.groovy
Normal file
126
devicetypes/com-lametric/lametric.src/lametric.groovy
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* LaMetric
|
||||
*
|
||||
* Copyright 2016 Smart Atoms Ltd.
|
||||
* Author: Mykola Kirichuk
|
||||
*
|
||||
* 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: "LaMetric", namespace: "com.lametric", author: "Mykola Kirichuk") {
|
||||
capability "Actuator"
|
||||
capability "Notification"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
|
||||
attribute "currentIP", "string"
|
||||
attribute "serialNumber", "string"
|
||||
attribute "volume", "string"
|
||||
attribute "mode", "enum", ["offline","online"]
|
||||
|
||||
command "setOffline"
|
||||
command "setOnline"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles (scale: 2){
|
||||
// TODO: define your main and details tiles here
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"rich-control"){
|
||||
tileAttribute ("mode", key: "PRIMARY_CONTROL") {
|
||||
attributeState "online", label: "LaMetric", action: "", icon: "https://developer.lametric.com/assets/smart_things/time_100.png", backgroundColor: "#F3C200"
|
||||
attributeState "offline", label: "LaMetric", action: "", icon: "https://developer.lametric.com/assets/smart_things/time_100.png", backgroundColor: "#F3F3F3"
|
||||
}
|
||||
tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
|
||||
attributeState "default", label:'SN: ${currentValue}'
|
||||
}
|
||||
}
|
||||
valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "default", label:'SN: ${currentValue}'
|
||||
}
|
||||
valueTile("networkAddress", "device.currentIP", decoration: "flat", height: 2, width: 4, inactiveLabel: false) {
|
||||
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
|
||||
}
|
||||
|
||||
main (["rich-control"])
|
||||
details(["rich-control","networkAddress"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
if (description)
|
||||
{
|
||||
unschedule("setOffline")
|
||||
}
|
||||
// TODO: handle 'battery' attribute
|
||||
// TODO: handle 'button' attribute
|
||||
// TODO: handle 'status' attribute
|
||||
// TODO: handle 'level' attribute
|
||||
// TODO: handle 'level' attribute
|
||||
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def setOnline()
|
||||
{
|
||||
log.debug("set online");
|
||||
sendEvent(name:"mode", value:"online")
|
||||
unschedule("setOffline")
|
||||
}
|
||||
def setOffline(){
|
||||
log.debug("set offline");
|
||||
sendEvent(name:"mode", value:"offline")
|
||||
}
|
||||
|
||||
def setLevel(level) {
|
||||
log.debug "Executing 'setLevel' ${level}"
|
||||
// TODO: handle 'setLevel' command
|
||||
}
|
||||
|
||||
def deviceNotification(notif) {
|
||||
log.debug "Executing 'deviceNotification' ${notif}"
|
||||
// TODO: handle 'deviceNotification' command
|
||||
def result = parent.sendNotificationMessageToDevice(device.deviceNetworkId, notif);
|
||||
log.debug ("result ${result}");
|
||||
log.debug parent;
|
||||
return result;
|
||||
}
|
||||
|
||||
def poll() {
|
||||
// TODO: handle 'poll' command
|
||||
log.debug "Executing 'poll'"
|
||||
if (device.currentValue("currentIP") != "Offline")
|
||||
{
|
||||
runIn(30, setOffline)
|
||||
}
|
||||
parent.poll(device.deviceNetworkId)
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
// log.debug "${device?.currentIP}"
|
||||
log.debug "${device?.currentValue("currentIP")}"
|
||||
log.debug "${device?.currentValue("serialNumber")}"
|
||||
log.debug "${device?.currentValue("volume")}"
|
||||
// poll()
|
||||
|
||||
}
|
||||
|
||||
/*def setLevel() {
|
||||
log.debug "Executing 'setLevel'"
|
||||
// TODO: handle 'setLevel' command
|
||||
}*/
|
||||
@@ -22,7 +22,6 @@ metadata {
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Health Check"
|
||||
|
||||
attribute "tamper", "enum", ["detected", "clear"]
|
||||
attribute "batteryStatus", "string"
|
||||
@@ -327,9 +326,6 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// allow device user configured or default 16 min to check in; double the periodic reporting interval
|
||||
sendEvent(name: "checkInterval", value: 2* timeOptionValueMap[reportInterval] ?: 2*8*60, displayed: false)
|
||||
|
||||
// This sensor joins as a secure device if you double-click the button to include it
|
||||
log.debug "${device.displayName} is configuring its settings"
|
||||
def request = []
|
||||
|
||||
@@ -20,9 +20,6 @@ metadata {
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Health Check"
|
||||
|
||||
command "configureAfterSecure"
|
||||
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x98,0x7A", outClusters:"0x5A"
|
||||
}
|
||||
@@ -248,8 +245,6 @@ def configureAfterSecure() {
|
||||
def configure() {
|
||||
// log.debug "configure()"
|
||||
//["delay 30000"] + secure(zwave.securityV1.securityCommandsSupportedGet())
|
||||
// allow device 16 min to check in; double the periodic reporting interval
|
||||
sendEvent(name: "checkInterval", value: 2*8*60, displayed: false)
|
||||
}
|
||||
|
||||
private setConfigured() {
|
||||
|
||||
@@ -20,7 +20,6 @@ metadata {
|
||||
capability "Illuminance Measurement"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Health Check"
|
||||
|
||||
fingerprint deviceId: "0x2001", inClusters: "0x30,0x31,0x80,0x84,0x70,0x85,0x72,0x86"
|
||||
}
|
||||
@@ -181,9 +180,6 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// allow device 10 min to check in; double the periodic reporting interval
|
||||
sendEvent(name: "checkInterval", value: 2*5*60, displayed: false)
|
||||
|
||||
delayBetween([
|
||||
// send binary sensor report instead of basic set for motion
|
||||
zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: 2).format(),
|
||||
|
||||
@@ -255,8 +255,7 @@ private Map getBatteryResult(rawValue) {
|
||||
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.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,8 +271,7 @@ private Map getBatteryResult(rawValue) {
|
||||
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.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ metadata {
|
||||
capability "Temperature Measurement"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
}
|
||||
@@ -73,7 +73,7 @@ metadata {
|
||||
|
||||
def parse(String description) {
|
||||
log.debug "description: $description"
|
||||
|
||||
|
||||
Map map = [:]
|
||||
if (description?.startsWith('catchall:')) {
|
||||
map = parseCatchAllMessage(description)
|
||||
@@ -87,10 +87,10 @@ def parse(String 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}"
|
||||
@@ -128,7 +128,7 @@ private Map parseCatchAllMessage(String description) {
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
@@ -141,7 +141,7 @@ private Map parseReportAttributeMessage(String description) {
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
log.debug "Desc Map: $descMap"
|
||||
|
||||
|
||||
Map resultMap = [:]
|
||||
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||
def value = getTemperature(descMap.value)
|
||||
@@ -153,11 +153,11 @@ private Map parseReportAttributeMessage(String description) {
|
||||
else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
|
||||
def value = descMap.value.endsWith("01") ? "active" : "inactive"
|
||||
resultMap = getMotionResult(value)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
if (description?.startsWith('temperature: ')) {
|
||||
@@ -170,7 +170,7 @@ private Map parseCustomMessage(String description) {
|
||||
private Map parseIasMessage(String description) {
|
||||
List parsedMsg = description.split(' ')
|
||||
String msgCode = parsedMsg[2]
|
||||
|
||||
|
||||
Map resultMap = [:]
|
||||
switch(msgCode) {
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
@@ -240,8 +240,7 @@ private Map getBatteryResult(rawValue) {
|
||||
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.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,8 +338,7 @@ private Map getBatteryResult(rawValue) {
|
||||
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.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,8 +234,7 @@ def getTemperature(value) {
|
||||
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.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
|
||||
|
||||
@@ -13,34 +13,32 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
metadata {
|
||||
definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||
definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
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: "CentraLite", model: "3300-S"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300"
|
||||
fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3320-L", deviceJoinName: "Iris Contact Sensor"
|
||||
}
|
||||
|
||||
|
||||
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") {
|
||||
@@ -73,10 +71,10 @@ metadata {
|
||||
details(["contact","temperature","battery","refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def parse(String description) {
|
||||
log.debug "description: $description"
|
||||
|
||||
|
||||
Map map = [:]
|
||||
if (description?.startsWith('catchall:')) {
|
||||
map = parseCatchAllMessage(description)
|
||||
@@ -90,10 +88,10 @@ def parse(String 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}"
|
||||
@@ -101,7 +99,7 @@ def parse(String description) {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
@@ -127,7 +125,7 @@ private Map parseCatchAllMessage(String description) {
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
@@ -137,14 +135,14 @@ private boolean shouldProcessMessage(cluster) {
|
||||
private int getHumidity(value) {
|
||||
return Math.round(Double.parseDouble(value))
|
||||
}
|
||||
|
||||
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
log.debug "Desc Map: $descMap"
|
||||
|
||||
|
||||
Map resultMap = [:]
|
||||
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||
def value = getTemperature(descMap.value)
|
||||
@@ -153,10 +151,10 @@ private Map parseReportAttributeMessage(String description) {
|
||||
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
if (description?.startsWith('temperature: ')) {
|
||||
@@ -169,7 +167,7 @@ private Map parseCustomMessage(String description) {
|
||||
private Map parseIasMessage(String description) {
|
||||
List parsedMsg = description.split(' ')
|
||||
String msgCode = parsedMsg[2]
|
||||
|
||||
|
||||
Map resultMap = [:]
|
||||
switch(msgCode) {
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
@@ -202,7 +200,7 @@ private Map parseIasMessage(String description) {
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
|
||||
def getTemperature(value) {
|
||||
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||
if(getTemperatureScale() == "C"){
|
||||
@@ -215,11 +213,11 @@ def getTemperature(value) {
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
|
||||
def result = [
|
||||
name: 'battery'
|
||||
]
|
||||
|
||||
|
||||
def volts = rawValue / 10
|
||||
def descriptionText
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
@@ -230,8 +228,7 @@ private Map getBatteryResult(rawValue) {
|
||||
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.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
|
||||
@@ -276,7 +273,6 @@ def refresh() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
sendEvent(name: "checkInterval", value: 7200, displayed: false)
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
@@ -205,8 +205,7 @@ private Map getBatteryResult(rawValue) {
|
||||
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.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
metadata {
|
||||
definition (name: "Tyco Door/Window Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Battery"
|
||||
@@ -21,28 +21,28 @@ metadata {
|
||||
capability "Contact Sensor"
|
||||
capability "Refresh"
|
||||
capability "Temperature Measurement"
|
||||
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
|
||||
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Visonic", model: "MCT-340 SMA"
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
standardTile("contact", "device.contact", width: 2, height: 2) {
|
||||
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||
}
|
||||
|
||||
|
||||
valueTile("temperature", "device.temperature", inactiveLabel: false) {
|
||||
state "temperature", label:'${currentValue}°',
|
||||
backgroundColors:[
|
||||
@@ -58,23 +58,23 @@ metadata {
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
|
||||
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||
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)
|
||||
@@ -88,10 +88,10 @@ def parse(String 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}"
|
||||
@@ -99,7 +99,7 @@ def parse(String description) {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
@@ -125,20 +125,20 @@ private Map parseCatchAllMessage(String description) {
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
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)
|
||||
@@ -147,10 +147,10 @@ private Map parseReportAttributeMessage(String description) {
|
||||
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
if (description?.startsWith('temperature: ')) {
|
||||
@@ -163,7 +163,7 @@ private Map parseCustomMessage(String description) {
|
||||
private Map parseIasMessage(String description) {
|
||||
List parsedMsg = description.split(' ')
|
||||
String msgCode = parsedMsg[2]
|
||||
|
||||
|
||||
Map resultMap = [:]
|
||||
switch(msgCode) {
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
@@ -196,7 +196,7 @@ private Map parseIasMessage(String description) {
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
|
||||
def getTemperature(value) {
|
||||
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||
if(getTemperatureScale() == "C"){
|
||||
@@ -209,11 +209,11 @@ def getTemperature(value) {
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
|
||||
def result = [
|
||||
name: 'battery'
|
||||
]
|
||||
|
||||
|
||||
def volts = rawValue / 10
|
||||
def descriptionText
|
||||
if (volts > 3.5) {
|
||||
@@ -223,8 +223,7 @@ private Map getBatteryResult(rawValue) {
|
||||
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.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
|
||||
@@ -262,7 +261,7 @@ def refresh()
|
||||
{
|
||||
log.debug "Refreshing Temperature and Battery"
|
||||
[
|
||||
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
||||
|
||||
@@ -275,24 +274,24 @@ def configure() {
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"delay 1000",
|
||||
|
||||
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "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",
|
||||
|
||||
|
||||
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}",
|
||||
|
||||
|
||||
"delay 500"
|
||||
]
|
||||
return configCmds + enrollResponse() + refresh() // send refresh cmds as part of config
|
||||
@@ -300,11 +299,11 @@ def configure() {
|
||||
|
||||
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) {
|
||||
|
||||
@@ -28,6 +28,7 @@ metadata {
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x98"
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x98", outClusters: "0x5A,0x82"
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x80,0x71,0x85,0x70,0x72,0x86,0x30,0x31,0x84,0x59,0x73,0x5A,0x8F,0x98,0x7A", outClusters:"0x20" // Philio multi+
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x72,0x5A,0x80,0x73,0x86,0x84,0x85,0x59,0x71,0x70,0x7A,0x98" // Vision door/window
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
@@ -35,7 +36,6 @@ metadata {
|
||||
// status messages
|
||||
status "open": "command: 2001, payload: FF"
|
||||
status "closed": "command: 2001, payload: 00"
|
||||
status "wake up": "command: 8407, payload: "
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
@@ -81,7 +81,7 @@ def updated() {
|
||||
def cmds = []
|
||||
if (!state.MSR) {
|
||||
cmds = [
|
||||
command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()),
|
||||
zwave.manufacturerSpecificV2.manufacturerSpecificGet().format(),
|
||||
"delay 1200",
|
||||
zwave.wakeUpV1.wakeUpNoMoreInformation().format()
|
||||
]
|
||||
@@ -94,9 +94,9 @@ def updated() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
commands([
|
||||
zwave.manufacturerSpecificV2.manufacturerSpecificGet(),
|
||||
zwave.batteryV1.batteryGet()
|
||||
delayBetween([
|
||||
zwave.manufacturerSpecificV2.manufacturerSpecificGet().format(),
|
||||
batteryGetCommand()
|
||||
], 6000)
|
||||
}
|
||||
|
||||
@@ -147,11 +147,12 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm
|
||||
result << sensorValueEvent(1)
|
||||
} else if (cmd.event == 0x03) {
|
||||
result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true)
|
||||
if(!state.MSR) result << response(command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()))
|
||||
result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId))
|
||||
if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
|
||||
} else if (cmd.event == 0x05 || cmd.event == 0x06) {
|
||||
result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true)
|
||||
} else if (cmd.event == 0x07) {
|
||||
if(!state.MSR) result << response(command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()))
|
||||
if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
|
||||
result << createEvent(name: "motion", value: "active", descriptionText:"$device.displayName detected motion")
|
||||
}
|
||||
} else if (cmd.notificationType) {
|
||||
@@ -169,11 +170,12 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
|
||||
def event = createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)
|
||||
def cmds = []
|
||||
if (!state.MSR) {
|
||||
cmds << command(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
|
||||
cmds << zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId).format()
|
||||
cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()
|
||||
cmds << "delay 1200"
|
||||
}
|
||||
if (!state.lastbat || now() - state.lastbat > 53*60*60*1000) {
|
||||
cmds << command(zwave.batteryV1.batteryGet())
|
||||
cmds << batteryGetCommand()
|
||||
} else {
|
||||
cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
|
||||
}
|
||||
@@ -210,7 +212,7 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS
|
||||
if (msr == "0086-0102-0059") {
|
||||
result << response(zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format())
|
||||
} else {
|
||||
result << response(command(zwave.batteryV1.batteryGet()))
|
||||
result << response(batteryGetCommand())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +220,7 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1])
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x85: 2, 0x70: 1])
|
||||
// log.debug "encapsulated: $encapsulatedCommand"
|
||||
if (encapsulatedCommand) {
|
||||
state.sec = 1
|
||||
@@ -230,16 +232,12 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
createEvent(descriptionText: "$device.displayName: $cmd", displayed: false)
|
||||
}
|
||||
|
||||
private command(physicalgraph.zwave.Command cmd) {
|
||||
if (state.sec == 1) {
|
||||
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||
} else {
|
||||
cmd.format()
|
||||
def batteryGetCommand() {
|
||||
def cmd = zwave.batteryV1.batteryGet()
|
||||
if (state.sec) {
|
||||
cmd = zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
private commands(commands, delay=200) {
|
||||
delayBetween(commands.collect{ command(it) }, delay)
|
||||
cmd.format()
|
||||
}
|
||||
|
||||
def retypeBasedOnMSR() {
|
||||
@@ -262,12 +260,8 @@ def retypeBasedOnMSR() {
|
||||
setDeviceType("3-in-1 Multisensor Plus (SG)")
|
||||
break
|
||||
case "0109-2001-0106": // Vision door/window
|
||||
log.debug "Changing device type to Z-Wave Plus Door/Window Sensor"
|
||||
setDeviceType("Z-Wave Plus Door/Window Sensor")
|
||||
break
|
||||
case "0109-2002-0205": // Vision Motion
|
||||
log.debug "Changing device type to Z-Wave Plus Motion/Temp Sensor"
|
||||
setDeviceType("Z-Wave Plus Motion/Temp Sensor")
|
||||
log.debug "Changing device type to Door / Window Sensor Plus (SG)"
|
||||
setDeviceType("Door / Window Sensor Plus (SG)")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings
|
||||
* Copyright 2015 AstraLink
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Z-Wave Plus Door/Window Sensor, ZD2102*-5
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Z-Wave Plus Door/Window Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Contact Sensor"
|
||||
capability "Configuration"
|
||||
capability "Battery"
|
||||
capability "Sensor"
|
||||
|
||||
// for Astralink
|
||||
attribute "ManufacturerCode", "string"
|
||||
attribute "ProduceTypeCode", "string"
|
||||
attribute "ProductCode", "string"
|
||||
attribute "WakeUp", "string"
|
||||
attribute "WirelessConfig", "string"
|
||||
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x98, 0x86, 0x72, 0x5A, 0x85, 0x59, 0x73, 0x80, 0x71, 0x70, 0x84, 0x7A"
|
||||
fingerprint type:"8C07", inClusters: "5E,98,86,72,5A,71"
|
||||
fingerprint mfr:"0109", prod:"2001", model:"0106" // not using deviceJoinName because it's sold under different brand names
|
||||
}
|
||||
|
||||
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:"#ffa81e"
|
||||
attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821"
|
||||
}
|
||||
}
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
main (["contact"])
|
||||
details(["contact","battery"])
|
||||
}
|
||||
|
||||
simulator {
|
||||
// messages the device returns in response to commands it receives
|
||||
status "open (basic)" : "command: 9881, payload: 00 20 01 FF"
|
||||
status "closed (basic)" : "command: 9881 payload: 00 20 01 00"
|
||||
status "open (notification)" : "command: 9881, payload: 00 71 05 06 FF 00 FF 06 16 00 00"
|
||||
status "closed (notification)" : "command: 9881, payload: 00 71 05 06 00 00 FF 06 17 00 00"
|
||||
status "tamper: enclosure opened" : "command: 9881, payload: 00 71 05 07 FF 00 FF 07 03 00 00"
|
||||
status "tamper: enclosure replaced" : "command: 9881, payload: 00 71 05 07 00 00 FF 07 00 00 00"
|
||||
status "wake up" : "command: 9881, payload: 00 84 07"
|
||||
status "battery (100%)" : "command: 9881, payload: 00 80 03 64"
|
||||
status "battery low" : "command: 9881, payload: 00 80 03 FF"
|
||||
}
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "configure()"
|
||||
def cmds = []
|
||||
|
||||
if (state.sec != 1) {
|
||||
// secure inclusion may not be complete yet
|
||||
cmds << "delay 1000"
|
||||
}
|
||||
|
||||
cmds += secureSequence([
|
||||
zwave.manufacturerSpecificV2.manufacturerSpecificGet(),
|
||||
zwave.batteryV1.batteryGet(),
|
||||
], 500)
|
||||
|
||||
cmds << "delay 8000"
|
||||
cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||
return cmds
|
||||
}
|
||||
|
||||
private getCommandClassVersions() {
|
||||
[
|
||||
0x71: 3, // Notification
|
||||
0x5E: 2, // ZwaveplusInfo
|
||||
0x59: 1, // AssociationGrpInfo
|
||||
0x85: 2, // Association
|
||||
0x20: 1, // Basic
|
||||
0x80: 1, // Battery
|
||||
0x70: 1, // Configuration
|
||||
0x5A: 1, // DeviceResetLocally
|
||||
0x7A: 2, // FirmwareUpdateMd
|
||||
0x72: 2, // ManufacturerSpecific
|
||||
0x73: 1, // Powerlevel
|
||||
0x98: 1, // Security
|
||||
0x84: 2, // WakeUp
|
||||
0x86: 1, // Version
|
||||
]
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
def result = []
|
||||
def cmd
|
||||
if (description.startsWith("Err 106")) {
|
||||
state.sec = 0
|
||||
result = createEvent( name: "secureInclusion", value: "failed", eventType: "ALERT",
|
||||
descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.")
|
||||
} else if (description.startsWith("Err")) {
|
||||
result = createEvent(descriptionText: "$device.displayName $description", isStateChange: true)
|
||||
} else {
|
||||
cmd = zwave.parse(description, commandClassVersions)
|
||||
if (cmd) {
|
||||
result = zwaveEvent(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
if (result instanceof List) {
|
||||
result = result.flatten()
|
||||
}
|
||||
|
||||
log.debug "Parsed '$description' to $result"
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions)
|
||||
log.debug "encapsulated: $encapsulatedCommand"
|
||||
if (encapsulatedCommand) {
|
||||
state.sec = 1
|
||||
return zwaveEvent(encapsulatedCommand)
|
||||
} else {
|
||||
log.warn "Unable to extract encapsulated cmd from $cmd"
|
||||
return [createEvent(descriptionText: cmd.toString())]
|
||||
}
|
||||
}
|
||||
|
||||
def sensorValueEvent(value) {
|
||||
if (value) {
|
||||
createEvent(name: "contact", value: "open", descriptionText: "$device.displayName is open")
|
||||
} else {
|
||||
createEvent(name: "contact", value: "closed", descriptionText: "$device.displayName is closed")
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||
return sensorValueEvent(cmd.value)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||
return sensorValueEvent(cmd.value)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) {
|
||||
return sensorValueEvent(cmd.sensorValue)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) {
|
||||
return sensorValueEvent(cmd.sensorState)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
|
||||
def result = []
|
||||
if (cmd.notificationType == 0x06 && cmd.event == 0x16) {
|
||||
result << sensorValueEvent(1)
|
||||
} else if (cmd.notificationType == 0x06 && cmd.event == 0x17) {
|
||||
result << sensorValueEvent(0)
|
||||
} else if (cmd.notificationType == 0x07) {
|
||||
if (cmd.event == 0x00) {
|
||||
if (cmd.eventParametersLength == 0 || cmd.eventParameter[0] != 3) {
|
||||
result << createEvent(descriptionText: "$device.displayName covering replaced", isStateChange: true, displayed: false)
|
||||
} else {
|
||||
result << sensorValueEvent(0)
|
||||
}
|
||||
} else if (cmd.event == 0x01 || cmd.event == 0x02) {
|
||||
result << sensorValueEvent(1)
|
||||
} else if (cmd.event == 0x03) {
|
||||
result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true)
|
||||
if (!device.currentState("ManufacturerCode")) {
|
||||
result << response(secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet()))
|
||||
}
|
||||
} else if (cmd.event == 0x05 || cmd.event == 0x06) {
|
||||
result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true)
|
||||
} else {
|
||||
result << createEvent(descriptionText: "$device.displayName event $cmd.event ${cmd.eventParameter.inspect()}", isStateChange: true, displayed: false)
|
||||
}
|
||||
} else if (cmd.notificationType) {
|
||||
result << createEvent(descriptionText: "$device.displayName notification $cmd.notificationType event $cmd.event ${cmd.eventParameter.inspect()}", isStateChange: true, displayed: false)
|
||||
} else {
|
||||
def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive"
|
||||
result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, isStateChange: true, displayed: false)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) {
|
||||
def event = createEvent(name: "WakeUp", value: "wakeup", descriptionText: "${device.displayName} woke up", isStateChange: true, displayed: false) // for Astralink
|
||||
def cmds = []
|
||||
|
||||
if (!device.currentState("ManufacturerCode")) {
|
||||
cmds << secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
|
||||
cmds << "delay 2000"
|
||||
}
|
||||
if (!state.lastbat || now() - state.lastbat > 10*60*60*1000) {
|
||||
event.descriptionText += ", requesting battery"
|
||||
cmds << secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1))
|
||||
cmds << "delay 800"
|
||||
cmds << secure(zwave.batteryV1.batteryGet())
|
||||
cmds << "delay 2000"
|
||||
} else {
|
||||
log.debug "not checking battery, was updated ${(now() - state.lastbat)/60000 as int} min ago"
|
||||
}
|
||||
cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||
|
||||
return [event, response(cmds)]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||
def map = [ name: "battery", unit: "%" ]
|
||||
if (cmd.batteryLevel == 0xFF) {
|
||||
map.value = 1
|
||||
map.descriptionText = "${device.displayName} has a low battery"
|
||||
map.isStateChange = true
|
||||
} else {
|
||||
map.value = cmd.batteryLevel
|
||||
}
|
||||
def event = createEvent(map)
|
||||
|
||||
// Save at least one battery report in events list every few days
|
||||
if (!event.isStateChange && (now() - 3*24*60*60*1000) > device.latestState("battery")?.date?.time) {
|
||||
map.isStateChange = true
|
||||
}
|
||||
state.lastbat = now()
|
||||
return [event]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||
def result = []
|
||||
def manufacturerCode = String.format("%04X", cmd.manufacturerId)
|
||||
def productTypeCode = String.format("%04X", cmd.productTypeId)
|
||||
def productCode = String.format("%04X", cmd.productId)
|
||||
def wirelessConfig = "ZWP"
|
||||
log.debug "MSR ${manufacturerCode} ${productTypeCode} ${productCode}"
|
||||
|
||||
result << createEvent(name: "ManufacturerCode", value: manufacturerCode)
|
||||
result << createEvent(name: "ProduceTypeCode", value: productTypeCode)
|
||||
result << createEvent(name: "ProductCode", value: productCode)
|
||||
result << createEvent(name: "WirelessConfig", value: wirelessConfig)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
return [createEvent(descriptionText: "$device.displayName: $cmd", displayed: false)]
|
||||
}
|
||||
|
||||
private secure(physicalgraph.zwave.Command cmd) {
|
||||
if (state.sec == 0) { // default to secure
|
||||
cmd.format()
|
||||
} else {
|
||||
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||
}
|
||||
}
|
||||
|
||||
private secureSequence(commands, delay=200) {
|
||||
delayBetween(commands.collect{ secure(it) }, delay)
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings
|
||||
* Copyright 2015 AstraLink
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Z-Wave Plus Motion Sensor with Temperature Measurement, ZP3102*-5
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Z-Wave Plus Motion/Temp Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Motion Sensor"
|
||||
capability "Temperature Measurement"
|
||||
capability "Configuration"
|
||||
capability "Battery"
|
||||
capability "Sensor"
|
||||
|
||||
// for Astralink
|
||||
attribute "ManufacturerCode", "string"
|
||||
attribute "ProduceTypeCode", "string"
|
||||
attribute "ProductCode", "string"
|
||||
attribute "WakeUp", "string"
|
||||
attribute "WirelessConfig", "string"
|
||||
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x98, 0x86, 0x72, 0x5A, 0x85, 0x59, 0x73, 0x80, 0x71, 0x31, 0x70, 0x84, 0x7A"
|
||||
fingerprint type:"8C07", inClusters: "5E,98,86,72,5A,31,71"
|
||||
fingerprint mfr:"0109", prod:"2002", model:"0205" // not using deviceJoinName because it's sold under different brand names
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("motion", "device.motion", width: 3, height: 2) {
|
||||
state "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0"
|
||||
state "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff"
|
||||
}
|
||||
|
||||
valueTile("temperature", "device.temperature", inactiveLabel: false) {
|
||||
state "temperature", label:'${currentValue}°',
|
||||
backgroundColors:[
|
||||
[value: 31, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
}
|
||||
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
|
||||
state "battery", label:'${currentValue}% battery', unit:"%"
|
||||
}
|
||||
|
||||
main(["motion", "temperature"])
|
||||
details(["motion", "temperature", "battery"])
|
||||
}
|
||||
}
|
||||
|
||||
def updated() {
|
||||
if (!device.currentState("ManufacturerCode")) {
|
||||
response(secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet()))
|
||||
}
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "configure()"
|
||||
def cmds = []
|
||||
|
||||
if (state.sec != 1) {
|
||||
// secure inclusion may not be complete yet
|
||||
cmds << "delay 1000"
|
||||
}
|
||||
|
||||
cmds += secureSequence([
|
||||
zwave.manufacturerSpecificV2.manufacturerSpecificGet(),
|
||||
zwave.batteryV1.batteryGet(),
|
||||
zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1)
|
||||
], 500)
|
||||
|
||||
cmds << "delay 8000"
|
||||
cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||
return cmds
|
||||
}
|
||||
|
||||
private getCommandClassVersions() {
|
||||
[
|
||||
0x71: 3, // Notification
|
||||
0x5E: 2, // ZwaveplusInfo
|
||||
0x59: 1, // AssociationGrpInfo
|
||||
0x85: 2, // Association
|
||||
0x20: 1, // Basic
|
||||
0x80: 1, // Battery
|
||||
0x70: 1, // Configuration
|
||||
0x5A: 1, // DeviceResetLocally
|
||||
0x7A: 2, // FirmwareUpdateMd
|
||||
0x72: 2, // ManufacturerSpecific
|
||||
0x73: 1, // Powerlevel
|
||||
0x98: 1, // Security
|
||||
0x31: 5, // SensorMultilevel
|
||||
0x84: 2 // WakeUp
|
||||
]
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
def result = []
|
||||
def cmd
|
||||
if (description.startsWith("Err 106")) {
|
||||
state.sec = 0
|
||||
result = createEvent( name: "secureInclusion", value: "failed", eventType: "ALERT",
|
||||
descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.")
|
||||
} else if (description.startsWith("Err")) {
|
||||
result = createEvent(descriptionText: "$device.displayName $description", isStateChange: true)
|
||||
} else {
|
||||
cmd = zwave.parse(description, commandClassVersions)
|
||||
if (cmd) {
|
||||
result = zwaveEvent(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
if (result instanceof List) {
|
||||
result = result.flatten()
|
||||
}
|
||||
|
||||
log.debug "Parsed '$description' to $result"
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions)
|
||||
log.debug "encapsulated: $encapsulatedCommand"
|
||||
if (encapsulatedCommand) {
|
||||
state.sec = 1
|
||||
return zwaveEvent(encapsulatedCommand)
|
||||
} else {
|
||||
log.warn "Unable to extract encapsulated cmd from $cmd"
|
||||
return [createEvent(descriptionText: cmd.toString())]
|
||||
}
|
||||
}
|
||||
|
||||
def sensorValueEvent(value) {
|
||||
def result = []
|
||||
if (value) {
|
||||
log.debug "sensorValueEvent($value) : active"
|
||||
result << createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion")
|
||||
} else {
|
||||
log.debug "sensorValueEvent($value) : inactive"
|
||||
result << createEvent(name: "motion", value: "inactive", descriptionText: "$device.displayName motion has stopped")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||
return sensorValueEvent(cmd.value)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||
return sensorValueEvent(cmd.value)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
|
||||
return sensorValueEvent(cmd.value)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) {
|
||||
return sensorValueEvent(cmd.sensorValue)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) {
|
||||
return sensorValueEvent(cmd.sensorState)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
|
||||
def result = []
|
||||
if (cmd.notificationType == 0x07) {
|
||||
if (cmd.event == 0x01 || cmd.event == 0x02) {
|
||||
result << sensorValueEvent(1)
|
||||
} else if (cmd.event == 0x03) {
|
||||
result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true)
|
||||
result << response(secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet()))
|
||||
} else if (cmd.event == 0x05 || cmd.event == 0x06) {
|
||||
result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true)
|
||||
} else if (cmd.event == 0x07) {
|
||||
result << sensorValueEvent(1)
|
||||
} else if (cmd.event == 0x08) {
|
||||
result << sensorValueEvent(1)
|
||||
} else if (cmd.event == 0x00) {
|
||||
if (cmd.eventParametersLength && cmd.eventParameter[0] == 3) {
|
||||
result << createEvent(descriptionText: "$device.displayName covering replaced", isStateChange: true, displayed: false)
|
||||
} else {
|
||||
result << sensorValueEvent(0)
|
||||
}
|
||||
} else if (cmd.event == 0xFF) {
|
||||
result << sensorValueEvent(1)
|
||||
} else {
|
||||
result << createEvent(descriptionText: "$device.displayName sent event $cmd.event")
|
||||
}
|
||||
} else if (cmd.notificationType) {
|
||||
def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}"
|
||||
result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, displayed: false)
|
||||
} else {
|
||||
def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive"
|
||||
result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) {
|
||||
def event = createEvent(name: "WakeUp", value: "wakeup", descriptionText: "${device.displayName} woke up", isStateChange: true, displayed: false) // for Astralink
|
||||
def cmds = []
|
||||
|
||||
if (!device.currentState("ManufacturerCode")) {
|
||||
cmds << secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
|
||||
cmds << "delay 2000"
|
||||
}
|
||||
if (!state.lastbat || now() - state.lastbat > 10*60*60*1000) {
|
||||
event.descriptionText += ", requesting battery"
|
||||
cmds << secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1))
|
||||
cmds << "delay 800"
|
||||
cmds << secure(zwave.batteryV1.batteryGet())
|
||||
cmds << "delay 2000"
|
||||
} else {
|
||||
log.debug "not checking battery, was updated ${(now() - state.lastbat)/60000 as int} min ago"
|
||||
}
|
||||
cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||
|
||||
return [event, response(cmds)]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||
def result = []
|
||||
def map = [ name: "battery", unit: "%" ]
|
||||
if (cmd.batteryLevel == 0xFF) {
|
||||
map.value = 1
|
||||
map.descriptionText = "${device.displayName} has a low battery"
|
||||
map.isStateChange = true
|
||||
} else {
|
||||
map.value = cmd.batteryLevel
|
||||
}
|
||||
def event = createEvent(map)
|
||||
|
||||
// Save at least one battery report in events list every few days
|
||||
if (!event.isStateChange && (now() - 3*24*60*60*1000) > device.latestState("battery")?.date?.time) {
|
||||
map.isStateChange = true
|
||||
}
|
||||
state.lastbat = now()
|
||||
return [event]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) {
|
||||
def result = []
|
||||
def map = [:]
|
||||
switch (cmd.sensorType) {
|
||||
case 1:
|
||||
def cmdScale = cmd.scale == 1 ? "F" : "C"
|
||||
map.name = "temperature"
|
||||
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
|
||||
map.unit = getTemperatureScale()
|
||||
break;
|
||||
case 3:
|
||||
map.name = "illuminance"
|
||||
map.value = cmd.scaledSensorValue.toInteger().toString()
|
||||
map.unit = "lux"
|
||||
break;
|
||||
case 5:
|
||||
map.name = "humidity"
|
||||
map.value = cmd.scaledSensorValue.toInteger().toString()
|
||||
map.unit = cmd.scale == 0 ? "%" : ""
|
||||
break;
|
||||
case 0x1E:
|
||||
map.name = "loudness"
|
||||
map.unit = cmd.scale == 1 ? "dBA" : "dB"
|
||||
map.value = cmd.scaledSensorValue.toString()
|
||||
break;
|
||||
default:
|
||||
map.descriptionText = cmd.toString()
|
||||
}
|
||||
result << createEvent(map)
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||
def result = []
|
||||
def manufacturerCode = String.format("%04X", cmd.manufacturerId)
|
||||
def productTypeCode = String.format("%04X", cmd.productTypeId)
|
||||
def productCode = String.format("%04X", cmd.productId)
|
||||
def wirelessConfig = "ZWP"
|
||||
log.debug "MSR ${manufacturerCode} ${productTypeCode} ${productCode}"
|
||||
|
||||
result << createEvent(name: "ManufacturerCode", value: manufacturerCode)
|
||||
result << createEvent(name: "ProduceTypeCode", value: productTypeCode)
|
||||
result << createEvent(name: "ProductCode", value: productCode)
|
||||
result << createEvent(name: "WirelessConfig", value: wirelessConfig)
|
||||
|
||||
if (manufacturerCode == "0109" && productTypeCode == "2002") {
|
||||
result << response(secureSequence([
|
||||
// Change re-trigger duration to 1 minute
|
||||
zwave.configurationV1.configurationSet(parameterNumber: 1, configurationValue: [1], size: 1),
|
||||
zwave.batteryV1.batteryGet(),
|
||||
zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1)
|
||||
], 400))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
return [createEvent(descriptionText: "$device.displayName: $cmd", displayed: false)]
|
||||
}
|
||||
|
||||
private secure(physicalgraph.zwave.Command cmd) {
|
||||
if (state.sec == 0) { // default to secure
|
||||
cmd.format()
|
||||
} else {
|
||||
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||
}
|
||||
}
|
||||
|
||||
private secureSequence(commands, delay=200) {
|
||||
delayBetween(commands.collect{ secure(it) }, delay)
|
||||
}
|
||||
@@ -0,0 +1,839 @@
|
||||
/**
|
||||
* LaMetric (Connect)
|
||||
*
|
||||
* Copyright 2016 Smart Atoms Ltd.
|
||||
* Author: Mykola Kirichuk
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "LaMetric (Connect)",
|
||||
namespace: "com.lametric",
|
||||
author: "Mykola Kirichuk",
|
||||
description: "Control your LaMetric Time smart display",
|
||||
category: "Family",
|
||||
iconUrl: "https://developer.lametric.com/assets/smart_things/smart_things_60.png",
|
||||
iconX2Url: "https://developer.lametric.com/assets/smart_things/smart_things_120.png",
|
||||
iconX3Url: "https://developer.lametric.com/assets/smart_things/smart_things_120.png",
|
||||
singleInstance: true)
|
||||
{
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
}
|
||||
|
||||
preferences {
|
||||
page(name: "auth", title: "LaMetric", nextPage:"", content:"authPage", uninstall: true, install:true)
|
||||
page(name:"deviceDiscovery", title:"Device Setup", content:"deviceDiscovery", refreshTimeout:5);
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
|
||||
path("/oauth/callback") {action: [GET: "callback"]}
|
||||
}
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
|
||||
def getEventNameListOfUserDeviceParsed(){ "EventListOfUserRemoteDevicesParsed" }
|
||||
def getEventNameTokenRefreshed(){ "EventAuthTokenRefreshed" }
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
sendEvent(name:"Updated", value:true)
|
||||
unsubscribe()
|
||||
initialize()
|
||||
|
||||
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// TODO: subscribe to attributes, devices, locations, etc.
|
||||
log.debug("initialize");
|
||||
state.subscribe = false;
|
||||
if (selecteddevice) {
|
||||
addDevice()
|
||||
subscribeNetworkEvents(true)
|
||||
refreshDevices();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the new device to instantiate in the user's smartapps
|
||||
* This must be an app owned by the namespace (see #getNameSpace).
|
||||
*
|
||||
* @return name
|
||||
*/
|
||||
|
||||
def getDeviceName() {
|
||||
return "LaMetric"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the namespace this app and siblings use
|
||||
*
|
||||
* @return namespace
|
||||
*/
|
||||
def getNameSpace() {
|
||||
return "com.lametric"
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns all discovered devices or an empty array if none
|
||||
*
|
||||
* @return array of devices
|
||||
*/
|
||||
def getDevices() {
|
||||
state.remoteDevices = state.remoteDevices ?: [:]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of devices which have been verified
|
||||
*
|
||||
* @return array of verified devices
|
||||
*/
|
||||
def getVerifiedDevices() {
|
||||
getDevices().findAll{ it?.value?.verified == true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Map object which can be used with a preference page
|
||||
* to represent a list of devices detected and verified.
|
||||
*
|
||||
* @return Map with zero or more devices
|
||||
*/
|
||||
Map getSelectableDevice() {
|
||||
def devices = getVerifiedDevices()
|
||||
def map = [:]
|
||||
devices.each {
|
||||
def value = "${it.value.name}"
|
||||
def key = it.value.id
|
||||
map["${key}"] = value
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the refresh loop, making sure to keep us up-to-date with changes
|
||||
*
|
||||
*/
|
||||
private refreshDevices(){
|
||||
log.debug "refresh device list"
|
||||
listOfUserRemoteDevices()
|
||||
//every 30 min
|
||||
runIn(1800, "refreshDevices")
|
||||
}
|
||||
|
||||
/**
|
||||
* The deviceDiscovery page used by preferences. Will automatically
|
||||
* make calls to the underlying discovery mechanisms as well as update
|
||||
* whenever new devices are discovered AND verified.
|
||||
*
|
||||
* @return a dynamicPage() object
|
||||
*/
|
||||
/******************************************************************************************************************
|
||||
DEVICE DISCOVERY AND VALIDATION
|
||||
******************************************************************************************************************/
|
||||
def deviceDiscovery()
|
||||
{
|
||||
// if(canInstallLabs())
|
||||
if (1)
|
||||
{
|
||||
// userDeviceList();
|
||||
log.debug("deviceDiscovery")
|
||||
def refreshInterval = 3 // Number of seconds between refresh
|
||||
int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
|
||||
state.deviceRefreshCount = deviceRefreshCount + refreshInterval
|
||||
|
||||
def devices = getSelectableDevice()
|
||||
def numFound = devices.size() ?: 0
|
||||
|
||||
// Make sure we get location updates (contains LAN data such as SSDP results, etc)
|
||||
subscribeNetworkEvents()
|
||||
|
||||
//device discovery request every 15s
|
||||
// if((deviceRefreshCount % 15) == 0) {
|
||||
// discoverLaMetrics()
|
||||
// }
|
||||
|
||||
// Verify request every 3 seconds except on discoveries
|
||||
if(((deviceRefreshCount % 5) == 0)) {
|
||||
verifyDevices()
|
||||
}
|
||||
|
||||
log.trace "Discovered devices: ${devices}"
|
||||
|
||||
return dynamicPage(name:"deviceDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||
section("Please wait while we discover your ${getDeviceName()}. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selecteddevice", "enum", required:false, title:"Select ${getDeviceName()} (${numFound} found)", multiple:true, options:devices
|
||||
}
|
||||
}
|
||||
}
|
||||
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:"deviceDiscovery", title:"Upgrade needed!", nextPage:"", install:true, uninstall: true) {
|
||||
section("Upgrade") {
|
||||
paragraph "$upgradeNeeded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* Starts a subscription for network events
|
||||
*
|
||||
* @param force If true, will unsubscribe and subscribe if necessary (Optional, default false)
|
||||
*/
|
||||
private subscribeNetworkEvents(force=false) {
|
||||
if (force) {
|
||||
unsubscribe()
|
||||
state.subscribe = false
|
||||
}
|
||||
if(!state.subscribe) {
|
||||
log.debug("subscribe on network events")
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
// subscribe(app, appHandler)
|
||||
state.subscribe = true
|
||||
}
|
||||
}
|
||||
|
||||
private verifyDevices()
|
||||
{
|
||||
log.debug "verify.devices"
|
||||
def devices = getDevices();
|
||||
for (it in devices) {
|
||||
log.trace ("verify device ${it.value}")
|
||||
def localIp = it?.value?.ipv4_internal;
|
||||
def apiKey = it?.value?.api_key;
|
||||
getAllInfoFromDevice(localIp, apiKey);
|
||||
}
|
||||
}
|
||||
def appHandler(evt)
|
||||
{
|
||||
log.debug("application event handler ${evt.name}")
|
||||
if (evt.name == eventNameListOfUserDeviceParsed)
|
||||
{
|
||||
log.debug ("new account device list received ${evt.value}")
|
||||
def newRemoteDeviceList
|
||||
try {
|
||||
newRemoteDeviceList = parseJson(evt.value)
|
||||
} catch (e)
|
||||
{
|
||||
log.debug "Wrong value ${e}"
|
||||
}
|
||||
if (newRemoteDeviceList)
|
||||
{
|
||||
def remoteDevices = getDevices();
|
||||
newRemoteDeviceList.each{deviceInfo ->
|
||||
if (deviceInfo) {
|
||||
def device = remoteDevices[deviceInfo.id]?:[:];
|
||||
log.debug "before list ${device} ${deviceInfo} ${deviceInfo.id} ${remoteDevices[deviceInfo.id]}";
|
||||
deviceInfo.each() {
|
||||
device[it.key] = it.value;
|
||||
}
|
||||
remoteDevices[deviceInfo.id] = device;
|
||||
} else {
|
||||
log.debug ("empty device info")
|
||||
}
|
||||
}
|
||||
verifyDevices();
|
||||
} else {
|
||||
log.debug "wrong value ${newRemoteDeviceList}"
|
||||
}
|
||||
} else if (evt.name == getEventNameTokenRefreshed())
|
||||
{
|
||||
log.debug "token refreshed"
|
||||
state.refreshToken = evt.refreshToken
|
||||
state.authToken = evt.access_token
|
||||
}
|
||||
}
|
||||
|
||||
def locationHandler(evt)
|
||||
{
|
||||
log.debug("network event handler ${evt.name}")
|
||||
if (evt.name == "ssdpTerm")
|
||||
{
|
||||
log.debug "ignore ssdp"
|
||||
} else {
|
||||
def lanEvent = parseLanMessage(evt.description, true)
|
||||
log.debug lanEvent.headers;
|
||||
if (lanEvent.body)
|
||||
{
|
||||
log.trace "lan event ${lanEvent}";
|
||||
def parsedJsonBody;
|
||||
try {
|
||||
parsedJsonBody = parseJson(lanEvent.body);
|
||||
} catch (e)
|
||||
{
|
||||
log.debug ("not json responce ignore $e");
|
||||
}
|
||||
if (parsedJsonBody)
|
||||
{
|
||||
log.trace (parsedJsonBody)
|
||||
log.debug("responce for device ${parsedJsonBody?.id}")
|
||||
//put or post response
|
||||
if (parsedJsonBody.success)
|
||||
{
|
||||
|
||||
} else {
|
||||
//poll response
|
||||
log.debug "poll responce"
|
||||
log.debug ("poll responce ${parsedJsonBody}")
|
||||
def deviceId = parsedJsonBody?.id;
|
||||
if (deviceId)
|
||||
{
|
||||
def devices = getDevices();
|
||||
def device = devices."${deviceId}";
|
||||
|
||||
device.verified = true;
|
||||
device.dni = [device.serial_number, device.id].join('.')
|
||||
device.hub = evt?.hubId;
|
||||
device.volume = parsedJsonBody?.audio?.volume;
|
||||
log.debug "verified device ${deviceId}"
|
||||
def childDevice = getChildDevice(device.dni)
|
||||
//update device info
|
||||
if (childDevice)
|
||||
{
|
||||
log.debug("send event to ${childDevice}")
|
||||
childDevice.sendEvent(name:"currentIP",value:device?.ipv4_internal);
|
||||
childDevice.sendEvent(name:"volume",value:device?.volume);
|
||||
childDevice.setOnline();
|
||||
}
|
||||
log.trace device
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the child devices based on the user's selection
|
||||
*
|
||||
* Uses selecteddevice defined in the deviceDiscovery() page
|
||||
*/
|
||||
def addDevice() {
|
||||
def devices = getVerifiedDevices()
|
||||
def devlist
|
||||
log.trace "Adding childs"
|
||||
|
||||
// If only one device is selected, we don't get a list (when using simulator)
|
||||
if (!(selecteddevice instanceof List)) {
|
||||
devlist = [selecteddevice]
|
||||
} else {
|
||||
devlist = selecteddevice
|
||||
}
|
||||
|
||||
log.trace "These are being installed: ${devlist}"
|
||||
log.debug ("devlist" + devlist)
|
||||
devlist.each { dni ->
|
||||
def newDevice = devices[dni];
|
||||
if (newDevice)
|
||||
{
|
||||
def d = getChildDevice(newDevice.dni)
|
||||
if(!d) {
|
||||
log.debug ("get child devices" + getChildDevices())
|
||||
log.trace "concrete device ${newDevice}"
|
||||
def deviceName = newDevice.name
|
||||
d = addChildDevice(getNameSpace(), getDeviceName(), newDevice.dni, newDevice.hub, [label:"${deviceName}"])
|
||||
def childDevice = getChildDevice(d.deviceNetworkId)
|
||||
childDevice.sendEvent(name:"serialNumber", value:newDevice.serial_number)
|
||||
log.trace "Created ${d.displayName} with id $dni"
|
||||
} else {
|
||||
log.trace "${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//******************************************************************************************************************
|
||||
// OAUTH
|
||||
//******************************************************************************************************************
|
||||
|
||||
def getServerUrl() { "https://graph.api.smartthings.com" }
|
||||
def getShardUrl() { getApiServerUrl() }
|
||||
def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" }
|
||||
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${shardUrl}" }
|
||||
def getApiEndpoint() { "https://developer.lametric.com" }
|
||||
def getTokenUrl() { "${apiEndpoint}${apiTokenPath}" }
|
||||
def getAuthScope() { [ "basic", "devices_read" ] }
|
||||
def getSmartThingsClientId() { appSettings.clientId }
|
||||
def getSmartThingsClientSecret() { appSettings.clientSecret }
|
||||
def getApiTokenPath() { "/api/v2/oauth2/token" }
|
||||
def getApiUserMeDevicesList() { "/api/v2/users/me/devices" }
|
||||
|
||||
def toQueryString(Map m) {
|
||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||
}
|
||||
def composeScope(List scopes)
|
||||
{
|
||||
def result = "";
|
||||
scopes.each(){ scope ->
|
||||
result += "${scope} "
|
||||
}
|
||||
if (result.length())
|
||||
return result.substring(0, result.length() - 1);
|
||||
return "";
|
||||
}
|
||||
|
||||
def authPage() {
|
||||
log.debug "authPage()"
|
||||
|
||||
if(!state.accessToken) { //this is to access token for 3rd party to make a call to connect app
|
||||
state.accessToken = createAccessToken()
|
||||
}
|
||||
|
||||
def description
|
||||
def uninstallAllowed = false
|
||||
def oauthTokenProvided = false
|
||||
|
||||
if(state.authToken) {
|
||||
description = "You are connected."
|
||||
uninstallAllowed = true
|
||||
oauthTokenProvided = true
|
||||
} else {
|
||||
description = "Click to enter LaMetric Credentials"
|
||||
}
|
||||
|
||||
def redirectUrl = buildRedirectUrl
|
||||
log.debug "RedirectUrl = ${redirectUrl}"
|
||||
// get rid of next button until the user is actually auth'd
|
||||
if (!oauthTokenProvided) {
|
||||
return dynamicPage(name: "auth", title: "Login", nextPage: "", uninstall:uninstallAllowed) {
|
||||
section(){
|
||||
paragraph "Tap below to log in to the LaMatric service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button."
|
||||
href url:redirectUrl, style:"embedded", required:true, title:"LaMetric", description:description
|
||||
}
|
||||
}
|
||||
} else {
|
||||
subscribeNetworkEvents()
|
||||
listOfUserRemoteDevices()
|
||||
return deviceDiscovery();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private refreshAuthToken() {
|
||||
log.debug "refreshing auth token"
|
||||
|
||||
if(!state.refreshToken) {
|
||||
log.warn "Can not refresh OAuth token since there is no refreshToken stored"
|
||||
} else {
|
||||
def refreshParams = [
|
||||
method: 'POST',
|
||||
uri : apiEndpoint,
|
||||
path : apiTokenPath,
|
||||
body : [grant_type: 'refresh_token',
|
||||
refresh_token: "${state.refreshToken}",
|
||||
client_id : smartThingsClientId,
|
||||
client_secret: smartThingsClientSecret,
|
||||
redirect_uri: callbackUrl],
|
||||
]
|
||||
|
||||
log.debug refreshParams
|
||||
|
||||
def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the LaMetric (Connect) SmartApp and re-enter your account login credentials."
|
||||
//changed to httpPost
|
||||
try {
|
||||
def jsonMap
|
||||
httpPost(refreshParams) { resp ->
|
||||
if(resp.status == 200) {
|
||||
log.debug "Token refreshed...calling saved RestAction now! $resp.data"
|
||||
jsonMap = resp.data
|
||||
if(resp.data) {
|
||||
state.refreshToken = resp?.data?.refresh_token
|
||||
state.authToken = resp?.data?.access_token
|
||||
if(state.action && state.action != "") {
|
||||
log.debug "Executing next action: ${state.action}"
|
||||
|
||||
"${state.action}"()
|
||||
|
||||
state.action = ""
|
||||
}
|
||||
|
||||
} else {
|
||||
log.warn ("No data in refresh token!");
|
||||
}
|
||||
state.action = ""
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
|
||||
log.debug e.response.data;
|
||||
def reAttemptPeriod = 300 // in sec
|
||||
if (e.statusCode != 401) { // this issue might comes from exceed 20sec app execution, connectivity issue etc.
|
||||
runIn(reAttemptPeriod, "refreshAuthToken")
|
||||
} else if (e.statusCode == 401) { // unauthorized
|
||||
state.reAttempt = state.reAttempt + 1
|
||||
log.warn "reAttempt refreshAuthToken to try = ${state.reAttempt}"
|
||||
if (state.reAttempt <= 3) {
|
||||
runIn(reAttemptPeriod, "refreshAuthToken")
|
||||
} else {
|
||||
sendPushAndFeeds(notificationMessage)
|
||||
state.reAttempt = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def callback() {
|
||||
log.debug "callback()>> params: $params, params.code ${params.code}"
|
||||
|
||||
def code = params.code
|
||||
def oauthState = params.state
|
||||
|
||||
if (oauthState == state.oauthInitState){
|
||||
|
||||
def tokenParams = [
|
||||
grant_type: "authorization_code",
|
||||
code : code,
|
||||
client_id : smartThingsClientId,
|
||||
client_secret: smartThingsClientSecret,
|
||||
redirect_uri: callbackUrl
|
||||
]
|
||||
log.trace tokenParams
|
||||
log.trace tokenUrl
|
||||
try {
|
||||
httpPost(uri: tokenUrl, body: tokenParams) { resp ->
|
||||
log.debug "swapped token: $resp.data"
|
||||
state.refreshToken = resp.data.refresh_token
|
||||
state.authToken = resp.data.access_token
|
||||
}
|
||||
} catch (e)
|
||||
{
|
||||
log.debug "fail ${e}";
|
||||
}
|
||||
if (state.authToken) {
|
||||
success()
|
||||
} else {
|
||||
fail()
|
||||
}
|
||||
} else {
|
||||
log.error "callback() failed oauthState != state.oauthInitState"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def oauthInitUrl() {
|
||||
log.debug "oauthInitUrl with callback: ${callbackUrl}"
|
||||
|
||||
state.oauthInitState = UUID.randomUUID().toString()
|
||||
|
||||
def oauthParams = [
|
||||
response_type: "code",
|
||||
scope: composeScope(authScope),
|
||||
client_id: smartThingsClientId,
|
||||
state: state.oauthInitState,
|
||||
redirect_uri: callbackUrl
|
||||
]
|
||||
log.debug oauthParams
|
||||
log.debug "${apiEndpoint}/api/v2/oauth2/authorize?${toQueryString(oauthParams)}"
|
||||
|
||||
redirect(location: "${apiEndpoint}/api/v2/oauth2/authorize?${toQueryString(oauthParams)}")
|
||||
}
|
||||
|
||||
def success() {
|
||||
def message = """
|
||||
<p>Your LaMetric Account is now connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
"""
|
||||
connectionStatus(message)
|
||||
}
|
||||
|
||||
def fail() {
|
||||
def message = """
|
||||
<p>The connection could not be established!</p>
|
||||
<p>Click 'Done' to return to the menu.</p>
|
||||
"""
|
||||
connectionStatus(message)
|
||||
}
|
||||
|
||||
def connectionStatus(message, redirectUrl = null) {
|
||||
def redirectHtml = ""
|
||||
if (redirectUrl) {
|
||||
redirectHtml = """
|
||||
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
|
||||
"""
|
||||
}
|
||||
|
||||
def html = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"><head>
|
||||
<meta charset="UTF-8">
|
||||
<meta content="width=device-width" id="viewport" name="viewport">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'latoRegular';
|
||||
src: url("https://developer.lametric.com/assets/fonts/lato-regular-webfont.eot");
|
||||
src: url("https://developer.lametric.com/assets/fonts/lato-regular-webfont.eot?#iefix") format("embedded-opentype"),
|
||||
url("https://developer.lametric.com/assets/fonts/lato-regular-webfont.woff") format("woff"),
|
||||
url("https://developer.lametric.com/assets/fonts/lato-regular-webfont.ttf") format("truetype"),
|
||||
url("https://developer.lametric.com/assets/fonts/lato-regular-webfont.svg#latoRegular") format("svg");
|
||||
font-style: normal;
|
||||
font-weight: normal; }
|
||||
.clearfix:after, .mobile .connect:after {
|
||||
content: "";
|
||||
clear: both;
|
||||
display: table; }
|
||||
|
||||
.transition {
|
||||
transition: all .3s ease 0s; }
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
body{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #f0f0f0;
|
||||
color: #5c5c5c;
|
||||
min-width: 1149px;
|
||||
font-family: 'latoRegular', 'Lato';
|
||||
}
|
||||
.fixed-page #page {
|
||||
min-height: 100%;
|
||||
background: url(https://developer.lametric.com/assets/smart_things/page-bg.png) 50% 0 repeat-y;
|
||||
}
|
||||
.mobile {
|
||||
min-width: 100%;
|
||||
color: #757575; }
|
||||
.mobile .wrap {
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
max-width: 640px;
|
||||
min-width: inherit; }
|
||||
.mobile .connect {
|
||||
width: 100%;
|
||||
padding-top: 230px;
|
||||
margin-bottom: 50px;
|
||||
text-align: center; }
|
||||
.mobile .connect img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
margin-left: 2%;
|
||||
border-radius: 15px; }
|
||||
.mobile .connect img:first-child {
|
||||
margin-left: 0; }
|
||||
.mobile .info {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
margin-top: 50px;
|
||||
margin-bottom: 50px; }
|
||||
.mobile .info p {
|
||||
max-width: 80%;
|
||||
margin: 0 auto;
|
||||
margin-top: 50px;
|
||||
font-size: 28px;
|
||||
line-height: 50px;
|
||||
text-align: center; }
|
||||
|
||||
@media screen and (max-width: 639px) {
|
||||
.mobile .connect{
|
||||
padding-top: 100px; }
|
||||
.mobile .wrap {
|
||||
margin: 0 20px; }
|
||||
.mobile .connect img {
|
||||
width: 16%; }
|
||||
.mobile .connect img:first-child, .mobile .connect img:last-child {
|
||||
width: 40%; }
|
||||
.mobile .info p{
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
margin-top: 20px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="fixed-page mobile">
|
||||
|
||||
<div id="page">
|
||||
<div class="wrap">
|
||||
|
||||
<div class="connect">
|
||||
<img src="https://developer.lametric.com/assets/smart_things/product.png" width="190" height="190"><img src="https://developer.lametric.com/assets/smart_things/connected.png" width="87" height="19"><img src="https://developer.lametric.com/assets/smart_things/product-1.png" width="192" height="192">
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
${message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
"""
|
||||
render contentType: 'text/html', data: html
|
||||
}
|
||||
|
||||
|
||||
|
||||
//******************************************************************************************************************
|
||||
// LOCAL API
|
||||
//******************************************************************************************************************
|
||||
|
||||
def getLocalApiDeviceInfoPath() { "/api/v2/info" }
|
||||
def getLocalApiSendNotificationPath() { "/api/v2/device/notifications" }
|
||||
def getLocalApiIndexPath() { "/api/v2/device" }
|
||||
def getLocalApiUser() { "dev" }
|
||||
|
||||
|
||||
void requestDeviceInfo(localIp, apiKey)
|
||||
{
|
||||
if (localIp && apiKey)
|
||||
{
|
||||
log.debug("request info ${localIp}");
|
||||
def command = new physicalgraph.device.HubAction([
|
||||
method: "GET",
|
||||
path: localApiDeviceInfoPath,
|
||||
headers: [
|
||||
HOST: "${localIp}:8080",
|
||||
Authorization: "Basic ${"${localApiUser}:${apiKey}".bytes.encodeBase64()}"
|
||||
]])
|
||||
log.debug command
|
||||
sendHubCommand(command)
|
||||
command;
|
||||
} else {
|
||||
log.debug ("Unknown api key or ip address ${localIp} ${apiKey}")
|
||||
}
|
||||
}
|
||||
|
||||
def sendNotificationMessageToDevice(dni, data)
|
||||
{
|
||||
log.debug "send something"
|
||||
def device = resolveDNI2Device(dni);
|
||||
def localIp = device?.ipv4_internal;
|
||||
def apiKey = device?.api_key;
|
||||
if (localIp && apiKey)
|
||||
{
|
||||
log.debug "send notification message to device ${localIp}:8080 ${data}"
|
||||
sendHubCommand(new physicalgraph.device.HubAction([
|
||||
method: "POST",
|
||||
path: localApiSendNotificationPath,
|
||||
body: data,
|
||||
headers: [
|
||||
HOST: "${localIp}:8080",
|
||||
Authorization: "Basic ${"${localApiUser}:${apiKey}".bytes.encodeBase64()}",
|
||||
"Content-type":"application/json",
|
||||
"Accept":"application/json"
|
||||
]]))
|
||||
}
|
||||
}
|
||||
|
||||
def getAllInfoFromDevice(localIp, apiKey)
|
||||
{
|
||||
log.debug "send something"
|
||||
if (localIp && apiKey)
|
||||
{
|
||||
def hubCommand = new physicalgraph.device.HubAction([
|
||||
method: "GET",
|
||||
path: localApiIndexPath+"?fields=info,wifi,volume,bluetooth,id,name,mode,model,serial_number,os_version",
|
||||
headers: [
|
||||
HOST: "${localIp}:8080",
|
||||
Authorization: "Basic ${"${localApiUser}:${apiKey}".bytes.encodeBase64()}"
|
||||
]])
|
||||
log.debug "sending request ${hubCommand}"
|
||||
sendHubCommand(hubCommand)
|
||||
}
|
||||
}
|
||||
//******************************************************************************************************************
|
||||
// DEVICE HANDLER COMMANDs API
|
||||
//******************************************************************************************************************
|
||||
|
||||
def resolveDNI2Device(dni)
|
||||
{
|
||||
getDevices().find { it?.value?.dni == dni }?.value;
|
||||
}
|
||||
|
||||
def requestRefreshDeviceInfo (dni)
|
||||
{
|
||||
log.debug "device ${dni} request refresh";
|
||||
// def devices = getDevices();
|
||||
// def concreteDevice = devices[dni];
|
||||
// requestDeviceInfo(conreteDevice);
|
||||
}
|
||||
|
||||
private poll(dni) {
|
||||
def device = resolveDNI2Device(dni);
|
||||
def localIp = device?.ipv4_internal;
|
||||
def apiKey = device?.api_key;
|
||||
getAllInfoFromDevice(localIp, apiKey);
|
||||
}
|
||||
|
||||
//******************************************************************************************************************
|
||||
// CLOUD METHODS
|
||||
//******************************************************************************************************************
|
||||
|
||||
|
||||
void listOfUserRemoteDevices()
|
||||
{
|
||||
log.debug "get user device list"
|
||||
def deviceList = []
|
||||
if (state.accessToken)
|
||||
{
|
||||
def deviceListParams = [
|
||||
uri: apiEndpoint,
|
||||
path: apiUserMeDevicesList,
|
||||
headers: ["Content-Type": "text/json", "Authorization": "Bearer ${state.authToken}"]
|
||||
]
|
||||
log.debug "making request ${deviceListParams}"
|
||||
def result;
|
||||
try {
|
||||
httpGet(deviceListParams){ resp ->
|
||||
if (resp.status == 200)
|
||||
{
|
||||
deviceList = resp.data
|
||||
|
||||
def remoteDevices = getDevices();
|
||||
for (deviceInfo in deviceList) {
|
||||
if (deviceInfo)
|
||||
{
|
||||
def device = remoteDevices."${deviceInfo.id}"?:[:];
|
||||
log.debug "before list ${device} ${deviceInfo} ${deviceInfo.id} ${remoteDevices[deviceInfo.id]}";
|
||||
for (it in deviceInfo ) {
|
||||
device."${it.key}" = it.value;
|
||||
}
|
||||
remoteDevices."${deviceInfo.id}" = device;
|
||||
} else {
|
||||
log.debug ("empty device info")
|
||||
}
|
||||
}
|
||||
verifyDevices();
|
||||
} else {
|
||||
log.debug "http status: ${resp.status}"
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e)
|
||||
{
|
||||
log.debug("failed to get device list ${e}")
|
||||
def status = e.response.status
|
||||
if (status == 401) {
|
||||
state.action = "refreshDevices"
|
||||
log.debug "Refreshing your auth_token!"
|
||||
refreshAuthToken()
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
log.debug ("no access token to fetch user device list");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
/**
|
||||
* Lametric Notifier
|
||||
*
|
||||
* Copyright 2016 Smart Atoms Ltd.
|
||||
* Author: Mykola Kirichuk
|
||||
*
|
||||
* 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 groovy.json.JsonOutput
|
||||
|
||||
definition(
|
||||
name: "LaMetric Notifier",
|
||||
namespace: "com.lametric",
|
||||
author: "Mykola Kirichuk",
|
||||
description: "Allows you to send notifications to your LaMetric Time when something happens in your home to notify the whole family.",
|
||||
category: "Family",
|
||||
iconUrl: "https://developer.lametric.com/assets/smart_things/weather_60.png",
|
||||
iconX2Url: "https://developer.lametric.com/assets/smart_things/weather_120.png",
|
||||
iconX3Url: "https://developer.lametric.com/assets/smart_things/weather_120.png")
|
||||
|
||||
|
||||
preferences {
|
||||
page(name: "mainPage", title: "Show a message on your LaMetric when something happens", install: true, uninstall: true)
|
||||
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
||||
section {
|
||||
input "starting", "time", title: "Starting", required: false
|
||||
input "ending", "time", title: "Ending", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
def getSoundList() {
|
||||
[
|
||||
"none":"No Sound",
|
||||
"car" : "Car",
|
||||
"cash" : "Cash Register",
|
||||
"cat" : "Cat Meow",
|
||||
"dog" : "Dog Bark",
|
||||
"dog2" : "Dog Bark 2",
|
||||
"letter_email" : "The mail has arrived",
|
||||
"knock-knock" : "Knocking Sound",
|
||||
"bicycle" : "Bicycle",
|
||||
"negative1" : "Negative 1",
|
||||
"negative2" : "Negative 2",
|
||||
"negative3" : "Negative 3",
|
||||
"negative4" : "Negative 4",
|
||||
"negative5" : "Negative 5",
|
||||
"lose1" : "Lose 1",
|
||||
"lose2" : "Lose 2",
|
||||
"energy" : "Energy",
|
||||
"water1" : "Water 1",
|
||||
"water2" : "Water 2",
|
||||
"notification" : "Notification 1",
|
||||
"notification2" : "Notification 2",
|
||||
"notification3" : "Notification 3",
|
||||
"notification4" : "Notification 4",
|
||||
"open_door" : "Door unlocked",
|
||||
"win" : "Win",
|
||||
"win2" : "Win 2",
|
||||
"positive1" : "Positive 1",
|
||||
"positive2" : "Positive 2",
|
||||
"positive3" : "Positive 3",
|
||||
"positive4" : "Positive 4",
|
||||
"positive5" : "Positive 5",
|
||||
"positive6" : "Positive 6",
|
||||
"statistic" : "Page turning",
|
||||
"wind" : "Wind",
|
||||
"wind_short" : "Small Wind",
|
||||
]
|
||||
}
|
||||
|
||||
def getControlToAttributeMap(){
|
||||
[
|
||||
"motion": "motion.active",
|
||||
"contact": "contact.open",
|
||||
"contactClosed": "contact.close",
|
||||
"acceleration": "acceleration.active",
|
||||
"mySwitch": "switch.on",
|
||||
"mySwitchOff": "switch.off",
|
||||
"arrivalPresence": "presence.present",
|
||||
"departurePresence": "presence.not present",
|
||||
"smoke": "smoke.detected",
|
||||
"smoke1": "smoke.tested",
|
||||
"water": "water.wet",
|
||||
"button1": "button.pushed",
|
||||
"triggerModes": "mode",
|
||||
"timeOfDay": "time",
|
||||
]
|
||||
}
|
||||
|
||||
def getPriorityList(){
|
||||
[
|
||||
"warning":"Not So Important (may be ignored at night)",
|
||||
"critical": "Very Important"
|
||||
]
|
||||
}
|
||||
|
||||
def getIconsList(){
|
||||
state.icons = state.icons?:["1":"default"]
|
||||
}
|
||||
|
||||
|
||||
def getIconLabels() {
|
||||
state.iconLabels = state.iconLabels?:["1":"Default Icon"]
|
||||
}
|
||||
|
||||
def getSortedIconLabels() {
|
||||
state.iconLabels = state.iconLabels?:["1":"Default Icon"]
|
||||
state.iconLabels.sort {a,b -> a.key.toInteger() <=> b.key.toInteger()};
|
||||
}
|
||||
def getLametricHost() { "https://developer.lametric.com" }
|
||||
def getDefaultIconData() { """data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAe0lEQVQYlWNUVFBgYGBgYNi6bdt/BiTg7eXFyMDAwMCELBmz7z9DzL7/KBoYr127BpeEgbV64QzfRVYxMDAwMLAgSy5xYoSoeMPAwPkmjOG7yCqIgjf8WVC90xnQAdwKj7OZcMGD8m/hVjDBXLvDGKEbJunt5cXISMibAF0FMibYF7nMAAAAAElFTkSuQmCC""" }
|
||||
|
||||
def mainPage() {
|
||||
def iconRequestOptions = [headers: ["Accept": "application/json"],
|
||||
uri: "${lametricHost}/api/v2/icons", query:["fields":"id,title,type,code", "order":"title"]]
|
||||
|
||||
def icons = getIconsList();
|
||||
def iconLabels = getIconLabels();
|
||||
if (icons?.size() <= 2)
|
||||
{
|
||||
log.debug iconRequestOptions
|
||||
try {
|
||||
httpGet(iconRequestOptions) { resp ->
|
||||
int i = 2;
|
||||
resp.data.data.each(){
|
||||
def iconId = it?.id
|
||||
def iconType = it?.type
|
||||
def prefix = "i"
|
||||
if (iconId)
|
||||
{
|
||||
if (iconType == "movie")
|
||||
{
|
||||
prefix = "a"
|
||||
}
|
||||
def iconurl = "${lametricHost}/content/apps/icon_thumbs/${prefix}${iconId}_icon_thumb_big.png";
|
||||
icons["$i"] = it.code
|
||||
iconLabels["$i"] = it.title
|
||||
} else {
|
||||
log.debug "wrong id"
|
||||
}
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} catch (e)
|
||||
{
|
||||
log.debug "fail ${e}";
|
||||
}
|
||||
}
|
||||
dynamicPage(name: "mainPage") {
|
||||
def anythingSet = anythingSet()
|
||||
def notificationMessage = defaultNotificationMessage();
|
||||
log.debug "set $anythingSet"
|
||||
if (anythingSet) {
|
||||
section("Show message when"){
|
||||
ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true , submitOnChange:true
|
||||
ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true, submitOnChange:true
|
||||
ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true, submitOnChange:true
|
||||
ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true, submitOnChange:true
|
||||
ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true, submitOnChange:true
|
||||
ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true, submitOnChange:true
|
||||
ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true, submitOnChange:true
|
||||
ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true, submitOnChange:true
|
||||
ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true, submitOnChange:true
|
||||
ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true, submitOnChange: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, submitOnChange:true
|
||||
ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false, submitOnChange:true
|
||||
}
|
||||
}
|
||||
def hideable = anythingSet || app.installationState == "COMPLETE"
|
||||
def sectionTitle = anythingSet ? "Select additional triggers" : "Show message when..."
|
||||
|
||||
section(sectionTitle, hideable: hideable, hidden: true){
|
||||
ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true, submitOnChange:true
|
||||
ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true, submitOnChange:true
|
||||
ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true, submitOnChange:true
|
||||
ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true, submitOnChange:true
|
||||
ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true, submitOnChange:true
|
||||
ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true, submitOnChange:true
|
||||
ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true, submitOnChange:true
|
||||
ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true, submitOnChange:true
|
||||
ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true, submitOnChange:true
|
||||
ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true, submitOnChange:true
|
||||
ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
|
||||
ifUnset "triggerModes", "mode", title: "System Changes Mode", description: "Select mode(s)", required: false, multiple: true, submitOnChange:true
|
||||
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false, submitOnChange:true
|
||||
}
|
||||
|
||||
section (title:"Select LaMetrics"){
|
||||
input "selectedDevices", "capability.notification", required: true, multiple:true
|
||||
}
|
||||
section (title: "Configure message"){
|
||||
input "defaultMessage", "bool", title: "Use Default Text:\n\"$notificationMessage\"", required: false, defaultValue: true, submitOnChange:true
|
||||
def showMessageInput = (settings["defaultMessage"] == null || settings["defaultMessage"] == true) ? false : true;
|
||||
if (showMessageInput)
|
||||
{
|
||||
input "customMessage","text",title:"Use Custom Text", defaultValue:"", required:false, multiple: false
|
||||
}
|
||||
input "selectedIcon", "enum", title: "With Icon", required: false, multiple: false, defaultValue:"1", options: getSortedIconLabels()
|
||||
input "selectedSound", "enum", title: "With Sound", required: true, defaultValue:"none" , options: soundList
|
||||
input "showPriority", "enum", title: "Is This Notification Very Important?", required: true, multiple:false, defaultValue: "warning", options: priorityList
|
||||
}
|
||||
section("More options", hideable: true, hidden: true) {
|
||||
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"]
|
||||
if (settings.modes) {
|
||||
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
section([mobileOnly:true]) {
|
||||
label title: "Assign a name", required: false
|
||||
mode title: "Set for specific mode(s)", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private songOptions() {
|
||||
log.trace "song option"
|
||||
// Make sure current selection is in the set
|
||||
|
||||
def options = new LinkedHashSet()
|
||||
if (state.selectedSong?.station) {
|
||||
options << state.selectedSong.station
|
||||
}
|
||||
else if (state.selectedSong?.description) {
|
||||
// TODO - Remove eventually? 'description' for backward compatibility
|
||||
options << state.selectedSong.description
|
||||
}
|
||||
|
||||
// Query for recent tracks
|
||||
def states = sonos.statesSince("trackData", new Date(0), [max:30])
|
||||
def dataMaps = states.collect{it.jsonValue}
|
||||
options.addAll(dataMaps.collect{it.station})
|
||||
|
||||
log.trace "${options.size()} songs in list"
|
||||
options.take(20) as List
|
||||
}
|
||||
|
||||
private anythingSet() {
|
||||
for (it in controlToAttributeMap) {
|
||||
log.debug ("key ${it.key} value ${settings[it.key]} ${settings[it.key]?true:false}")
|
||||
if (settings[it.key]) {
|
||||
log.debug constructMessageFor(it.value, settings[it.key])
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
def defaultNotificationMessage(){
|
||||
def message = "";
|
||||
for (it in controlToAttributeMap) {
|
||||
if (settings[it.key]) {
|
||||
message = constructMessageFor(it.value, settings[it.key])
|
||||
break;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
def constructMessageFor(group, device)
|
||||
{
|
||||
log.debug ("$group $device")
|
||||
def message;
|
||||
def firstDevice;
|
||||
if (device instanceof List)
|
||||
{
|
||||
firstDevice = device[0];
|
||||
} else {
|
||||
firstDevice = device;
|
||||
}
|
||||
switch(group)
|
||||
{
|
||||
case "motion.active":
|
||||
message = "Motion detected by $firstDevice.displayName at $location.name"
|
||||
break;
|
||||
case "contact.open":
|
||||
message = "Openning detected by $firstDevice.displayName at $location.name"
|
||||
break;
|
||||
case "contact.closed":
|
||||
message = "Closing detected by $firstDevice.displayName at $location.name"
|
||||
break;
|
||||
case "acceleration.active":
|
||||
message = "Acceleration detected by $firstDevice.displayName at $location.name"
|
||||
break;
|
||||
case "switch.on":
|
||||
message = "$firstDevice.displayName turned on at $location.name"
|
||||
break;
|
||||
case "switch.off":
|
||||
message = "$firstDevice.displayName turned off at $location.name"
|
||||
break;
|
||||
case "presence.present":
|
||||
message = "$firstDevice.displayName detected arrival at $location.name"
|
||||
break;
|
||||
case "presence.not present":
|
||||
message = "$firstDevice.displayName detected departure at $location.name"
|
||||
break;
|
||||
case "smoke.detected":
|
||||
message = "Smoke detected by $firstDevice.displayName at $location.name"
|
||||
break;
|
||||
case "smoke.tested":
|
||||
message = "Smoke tested by $firstDevice.displayName at $location.name"
|
||||
break;
|
||||
case "water.wet":
|
||||
message = "Dampness detected by $firstDevice.displayName at $location.name"
|
||||
break;
|
||||
case "button.pushed":
|
||||
message = "$firstDevice.displayName pushed at $location.name"
|
||||
break;
|
||||
case "time":
|
||||
break;
|
||||
// case "mode":
|
||||
// message = "Mode changed to ??? at $location.name"
|
||||
break;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def subscribeToEvents() {
|
||||
log.trace "subscribe to events"
|
||||
log.debug "${contact} ${contactClosed} ${mySwitch} ${mySwitchOff} ${acceleration}${arrivalPresence} ${button1}"
|
||||
// subscribe(app, appTouchHandler)
|
||||
subscribe(contact, "contact.open", eventHandler)
|
||||
subscribe(contactClosed, "contact.closed", eventHandler)
|
||||
subscribe(acceleration, "acceleration.active", eventHandler)
|
||||
subscribe(motion, "motion.active", eventHandler)
|
||||
subscribe(mySwitch, "switch.on", eventHandler)
|
||||
subscribe(mySwitchOff, "switch.off", eventHandler)
|
||||
subscribe(arrivalPresence, "presence.present", eventHandler)
|
||||
subscribe(departurePresence, "presence.not present", eventHandler)
|
||||
subscribe(smoke, "smoke.detected", eventHandler)
|
||||
subscribe(smoke, "smoke.tested", eventHandler)
|
||||
subscribe(smoke, "carbonMonoxide.detected", eventHandler)
|
||||
subscribe(water, "water.wet", eventHandler)
|
||||
subscribe(button1, "button.pushed", eventHandler)
|
||||
|
||||
if (triggerModes) {
|
||||
subscribe(location, modeChangeHandler)
|
||||
}
|
||||
|
||||
if (timeOfDay) {
|
||||
schedule(timeOfDay, scheduledTimeHandler)
|
||||
}
|
||||
}
|
||||
|
||||
def eventHandler(evt) {
|
||||
log.trace "eventHandler(${evt?.name}: ${evt?.value})"
|
||||
def name = evt?.name;
|
||||
def value = evt?.value;
|
||||
|
||||
if (allOk) {
|
||||
log.trace "allOk"
|
||||
takeAction(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) {
|
||||
takeAction(evt)
|
||||
}
|
||||
|
||||
private takeAction(evt) {
|
||||
|
||||
log.trace "takeAction()"
|
||||
def messageToShow
|
||||
if (defaultMessage)
|
||||
{
|
||||
messageToShow = constructMessageFor("${evt.name}.${evt.value}", evt.device);
|
||||
} else {
|
||||
messageToShow = customMessage;
|
||||
}
|
||||
if (messageToShow)
|
||||
{
|
||||
log.debug "text ${messageToShow}"
|
||||
def notification = [:];
|
||||
def frame1 = [:];
|
||||
frame1.text = messageToShow;
|
||||
if (selectedIcon != "1")
|
||||
{
|
||||
frame1.icon = state.icons[selectedIcon];
|
||||
} else {
|
||||
frame1.icon = defaultIconData;
|
||||
}
|
||||
def soundId = sound;
|
||||
def sound = [:];
|
||||
sound.id = selectedSound;
|
||||
sound.category = "notifications";
|
||||
def frames = [];
|
||||
frames << frame1;
|
||||
def model = [:];
|
||||
model.frames = frames;
|
||||
if (selectedSound != "none")
|
||||
{
|
||||
model.sound = sound;
|
||||
}
|
||||
notification.model = model;
|
||||
notification.priority = showPriority;
|
||||
def serializedData = new JsonOutput().toJson(notification);
|
||||
|
||||
selectedDevices.each { lametricDevice ->
|
||||
log.trace "send notification to ${lametricDevice} ${serializedData}"
|
||||
lametricDevice.deviceNotification(serializedData)
|
||||
}
|
||||
} else {
|
||||
log.debug "No message to show"
|
||||
}
|
||||
|
||||
log.trace "Exiting takeAction()"
|
||||
}
|
||||
|
||||
private frequencyKey(evt) {
|
||||
"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, location?.timeZone).time
|
||||
def stop = timeToday(ending, location?.timeZone).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 getTimeLabel()
|
||||
{
|
||||
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
|
||||
}
|
||||
@@ -337,10 +337,10 @@ def initialize() {
|
||||
|
||||
settings.devices.each {
|
||||
def deviceId = it
|
||||
def detail = state?.deviceDetail[deviceId]
|
||||
def detail = state.deviceDetail[deviceId]
|
||||
|
||||
try {
|
||||
switch(detail?.type) {
|
||||
switch(detail.type) {
|
||||
case 'NAMain':
|
||||
log.debug "Base station"
|
||||
createChildDevice("Netatmo Basestation", deviceId, "${detail.type}.${deviceId}", detail.module_name)
|
||||
@@ -487,12 +487,12 @@ def poll() {
|
||||
log.debug "State: ${state.deviceState}"
|
||||
|
||||
settings.devices.each { deviceId ->
|
||||
def detail = state?.deviceDetail[deviceId]
|
||||
def data = state?.deviceState[deviceId]
|
||||
def child = children?.find { it.deviceNetworkId == deviceId }
|
||||
def detail = state.deviceDetail[deviceId]
|
||||
def data = state.deviceState[deviceId]
|
||||
def child = children.find { it.deviceNetworkId == deviceId }
|
||||
|
||||
log.debug "Update: $child";
|
||||
switch(detail?.type) {
|
||||
switch(detail.type) {
|
||||
case 'NAMain':
|
||||
log.debug "Updating NAMain $data"
|
||||
child?.sendEvent(name: 'temperature', value: cToPref(data['Temperature']) as float, unit: getTemperatureScale())
|
||||
|
||||
@@ -39,7 +39,6 @@ preferences {
|
||||
page(name: "completionPage")
|
||||
page(name: "numbersPage")
|
||||
page(name: "controllerExplanationPage")
|
||||
page(name: "unsupportedDevicesPage")
|
||||
}
|
||||
|
||||
def rootPage() {
|
||||
@@ -48,9 +47,6 @@ def rootPage() {
|
||||
section("What to dim") {
|
||||
input(name: "dimmers", type: "capability.switchLevel", title: "Dimmers", description: null, multiple: true, required: true, submitOnChange: true)
|
||||
if (dimmers) {
|
||||
if (dimmersContainUnsupportedDevices()) {
|
||||
href(name: "toUnsupportedDevicesPage", page: "unsupportedDevicesPage", title: "Some of your selected dimmers don't seem to be supported", description: "Tap here to fix it", required: true)
|
||||
}
|
||||
href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete")
|
||||
}
|
||||
}
|
||||
@@ -75,31 +71,6 @@ def rootPage() {
|
||||
}
|
||||
}
|
||||
|
||||
def unsupportedDevicesPage() {
|
||||
|
||||
def unsupportedDimmers = dimmers.findAll { !hasSetLevelCommand(it) }
|
||||
|
||||
dynamicPage(name: "unsupportedDevicesPage") {
|
||||
if (unsupportedDimmers) {
|
||||
section("These devices do not support the setLevel command") {
|
||||
unsupportedDimmers.each {
|
||||
paragraph deviceLabel(it)
|
||||
}
|
||||
}
|
||||
section {
|
||||
input(name: "dimmers", type: "capability.sensor", title: "Please remove the above devices from this list.", submitOnChange: true, multiple: true)
|
||||
}
|
||||
section {
|
||||
paragraph "If you think there is a mistake here, please contact support."
|
||||
}
|
||||
} else {
|
||||
section {
|
||||
paragraph "You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def controllerExplanationPage() {
|
||||
dynamicPage(name: "controllerExplanationPage", title: "How To Control Gentle Wake Up") {
|
||||
|
||||
@@ -557,16 +528,14 @@ def updateDimmers(percentComplete) {
|
||||
} else {
|
||||
|
||||
def shouldChangeColors = (colorize && colorize != "false")
|
||||
def canChangeColors = hasSetColorCommand(dimmer)
|
||||
|
||||
if (shouldChangeColors && hasSetColorCommand(dimmer)) {
|
||||
def hue = getHue(dimmer, nextLevel)
|
||||
log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel} and hue to ${hue}"
|
||||
dimmer.setColor([hue: hue, saturation: 100, level: nextLevel])
|
||||
} else if (hasSetLevelCommand(dimmer)) {
|
||||
log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel}"
|
||||
dimmer.setLevel(nextLevel)
|
||||
log.debug "Setting ${deviceLabel(dimmer)} to ${nextLevel}"
|
||||
|
||||
if (shouldChangeColors && canChangeColors) {
|
||||
dimmer.setColor([hue: getHue(dimmer, nextLevel), saturation: 100, level: nextLevel])
|
||||
} else {
|
||||
log.warn "${deviceLabel(dimmer)} does not have setColor or setLevel commands."
|
||||
dimmer.setLevel(nextLevel)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -720,7 +689,7 @@ def completionPercentage() {
|
||||
|
||||
def now = new Date().getTime()
|
||||
def timeElapsed = now - atomicState.start
|
||||
def totalRunTime = totalRunTimeMillis() ?: 1
|
||||
def totalRunTime = totalRunTimeMillis()
|
||||
def percentComplete = timeElapsed / totalRunTime * 100
|
||||
log.debug "percentComplete: ${percentComplete}"
|
||||
|
||||
@@ -848,21 +817,24 @@ private getRedHue(level) {
|
||||
if (level >= 96) return 17
|
||||
}
|
||||
|
||||
private dimmersContainUnsupportedDevices() {
|
||||
def found = dimmers.find { hasSetLevelCommand(it) == false }
|
||||
return found != null
|
||||
}
|
||||
|
||||
private hasSetLevelCommand(device) {
|
||||
return hasCommand(device, "setLevel")
|
||||
def isDimmer = false
|
||||
device.supportedCommands.each {
|
||||
if (it.name.contains("setLevel")) {
|
||||
isDimmer = true
|
||||
}
|
||||
}
|
||||
return isDimmer
|
||||
}
|
||||
|
||||
private hasSetColorCommand(device) {
|
||||
return hasCommand(device, "setColor")
|
||||
}
|
||||
|
||||
private hasCommand(device, String command) {
|
||||
return (device.supportedCommands.find { it.name == command } != null)
|
||||
def hasColor = false
|
||||
device.supportedCommands.each {
|
||||
if (it.name.contains("setColor")) {
|
||||
hasColor = true
|
||||
}
|
||||
}
|
||||
return hasColor
|
||||
}
|
||||
|
||||
private dimmersWithSetColorCommand() {
|
||||
@@ -1101,4 +1073,4 @@ def hasStartLevel() {
|
||||
|
||||
def hasEndLevel() {
|
||||
return (endLevel != null && endLevel != "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,7 +689,7 @@ def parse(childDevice, description) {
|
||||
log.warn "Parsing Body failed - trying again..."
|
||||
poll()
|
||||
}
|
||||
if (body instanceof java.util.Map) {
|
||||
if (body instanceof java.util.HashMap) {
|
||||
//poll response
|
||||
def bulbs = getChildDevices()
|
||||
for (bulb in body) {
|
||||
@@ -830,22 +830,22 @@ def setColorTemperature(childDevice, huesettings) {
|
||||
|
||||
def setColor(childDevice, huesettings) {
|
||||
log.debug "Executing 'setColor($huesettings)'"
|
||||
|
||||
|
||||
def value = [:]
|
||||
def hue = null
|
||||
def sat = null
|
||||
def xy = null
|
||||
|
||||
|
||||
if (huesettings.hex != null) {
|
||||
value.xy = getHextoXY(huesettings.hex)
|
||||
} else {
|
||||
if (huesettings.hue != null)
|
||||
value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
||||
if (huesettings.saturation != null)
|
||||
if (huesettings.saturation != null)
|
||||
value.sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
|
||||
}
|
||||
|
||||
// Default behavior is to turn light on
|
||||
|
||||
// Default behavior is to turn light on
|
||||
value.on = true
|
||||
|
||||
if (huesettings.level != null) {
|
||||
@@ -853,7 +853,7 @@ def setColor(childDevice, huesettings) {
|
||||
value.on = false
|
||||
else if (huesettings.level == 1)
|
||||
value.bri = 1
|
||||
else
|
||||
else
|
||||
value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
|
||||
}
|
||||
value.alert = huesettings.alert ? huesettings.alert : "none"
|
||||
|
||||
@@ -658,73 +658,29 @@ def updateDevice() {
|
||||
def data = request.JSON
|
||||
def command = data.command
|
||||
def arguments = data.arguments
|
||||
|
||||
log.debug "updateDevice, params: ${params}, request: ${data}"
|
||||
if (!command) {
|
||||
render status: 400, data: '{"msg": "command is required"}'
|
||||
} else {
|
||||
def device = allDevices.find { it.id == params.id }
|
||||
if (device) {
|
||||
if (validateCommand(device, command)) {
|
||||
if (arguments) {
|
||||
device."$command"(*arguments)
|
||||
} else {
|
||||
device."$command"()
|
||||
}
|
||||
render status: 204, data: "{}"
|
||||
} else {
|
||||
render status: 403, data: '{"msg": "Access denied. This command is not supported by current capability."}'
|
||||
}
|
||||
} else {
|
||||
render status: 404, data: '{"msg": "Device not found"}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validating the command passed by the user based on capability.
|
||||
* @return boolean
|
||||
*/
|
||||
def validateCommand(device, command) {
|
||||
def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
|
||||
def currentDeviceCapability = getCapabilityName(device)
|
||||
if (currentDeviceCapability != "" && capabilityCommands[currentDeviceCapability]) {
|
||||
return (command in capabilityCommands[currentDeviceCapability] || (currentDeviceCapability == "Switch" && command == "setLevel" && device.hasCommand("setLevel"))) ? true : false
|
||||
} else {
|
||||
// Handling other device types here, which don't accept commands
|
||||
httpError(400, "Bad request.")
|
||||
if (device) {
|
||||
if (device.hasCommand("$command")) {
|
||||
if (arguments) {
|
||||
device."$command"(*arguments)
|
||||
} else {
|
||||
device."$command"()
|
||||
}
|
||||
render status: 204, data: "{}"
|
||||
} else {
|
||||
render status: 404, data: '{"msg": "Command not supported by this Device"}'
|
||||
}
|
||||
} else {
|
||||
render status: 404, data: '{"msg": "Device not found"}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(device) {
|
||||
def capName = ""
|
||||
if (switches.find{it.id == device.id})
|
||||
capName = "Switch"
|
||||
else if (alarms.find{it.id == device.id})
|
||||
capName = "Alarm"
|
||||
else if (locks.find{it.id == device.id})
|
||||
capName = "Lock"
|
||||
log.trace "Device: $device - Capability Name: $capName"
|
||||
return capName
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.device?.device && it.device.id }?.collect {
|
||||
@@ -823,33 +779,18 @@ def deviceHandler(evt) {
|
||||
}
|
||||
|
||||
def sendToHarmony(evt, String callbackUrl) {
|
||||
def callback = new URI(callbackUrl)
|
||||
if (callback.port != -1) {
|
||||
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]]
|
||||
))
|
||||
} else {
|
||||
def params = [
|
||||
uri: callbackUrl,
|
||||
body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
|
||||
]
|
||||
try {
|
||||
log.debug "Sending data to Harmony Cloud: $params"
|
||||
httpPostJson(params) { resp ->
|
||||
log.debug "Harmony Cloud - Response: ${resp.status}"
|
||||
}
|
||||
} catch (e) {
|
||||
log.error "Harmony Cloud - Something went wrong: $e"
|
||||
}
|
||||
}
|
||||
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 listHubs() {
|
||||
|
||||
Reference in New Issue
Block a user