mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-13 21:03:14 +00:00
Compare commits
37 Commits
MSA-1354-1
...
MSA-1395-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
966bd2c1df | ||
|
|
3472ee329d | ||
|
|
577b127287 | ||
|
|
5f41af35e2 | ||
|
|
962774996e | ||
|
|
d79594cbcb | ||
|
|
bf8fe4cad7 | ||
|
|
65752ce378 | ||
|
|
95f08aeb3d | ||
|
|
cd7bc1b262 | ||
|
|
be7f6a76a9 | ||
|
|
10e5b7e9d7 | ||
|
|
fc38c534f9 | ||
|
|
90e6dc91eb | ||
|
|
2e502024a6 | ||
|
|
fd47bcb8a8 | ||
|
|
972599b1b5 | ||
|
|
f5d3cca6a0 | ||
|
|
6d3ae11e44 | ||
|
|
dd63e76dfb | ||
|
|
7d6f37d98f | ||
|
|
a015742d65 | ||
|
|
3ee8f86aa3 | ||
|
|
9b285ec93b | ||
|
|
23f66e3caa | ||
|
|
7a44c59581 | ||
|
|
4d343d9bcf | ||
|
|
777f8f7e20 | ||
|
|
de6d84acd2 | ||
|
|
eac48382e8 | ||
|
|
d1a910f11f | ||
|
|
9f09a4b0b2 | ||
|
|
3c47fe7b60 | ||
|
|
45a0822e9b | ||
|
|
a94a62d34c | ||
|
|
e861d3c256 | ||
|
|
8d8b039dda |
@@ -1,126 +0,0 @@
|
||||
/**
|
||||
* 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,6 +22,7 @@ metadata {
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Health Check"
|
||||
|
||||
attribute "tamper", "enum", ["detected", "clear"]
|
||||
attribute "batteryStatus", "string"
|
||||
@@ -326,6 +327,9 @@ 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,6 +20,9 @@ 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"
|
||||
}
|
||||
@@ -245,6 +248,8 @@ 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,6 +20,7 @@ metadata {
|
||||
capability "Illuminance Measurement"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Health Check"
|
||||
|
||||
fingerprint deviceId: "0x2001", inClusters: "0x30,0x31,0x80,0x84,0x70,0x85,0x72,0x86"
|
||||
}
|
||||
@@ -180,6 +181,9 @@ 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(),
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||
capability "Battery"
|
||||
capability "Configuration"
|
||||
capability "Contact Sensor"
|
||||
capability "Refresh"
|
||||
capability "Temperature Measurement"
|
||||
capability "Health Check"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
@@ -273,7 +274,8 @@ 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."
|
||||
def configCmds = [
|
||||
|
||||
@@ -28,7 +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
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x72,0x5A,0x80,0x73,0x84,0x85,0x59,0x71,0x70,0x7A,0x98" // Vision door/window
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
@@ -81,22 +81,22 @@ def updated() {
|
||||
def cmds = []
|
||||
if (!state.MSR) {
|
||||
cmds = [
|
||||
zwave.manufacturerSpecificV2.manufacturerSpecificGet().format(),
|
||||
command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()),
|
||||
"delay 1200",
|
||||
zwave.wakeUpV1.wakeUpNoMoreInformation().format()
|
||||
zwave.wakeUpV1.wakeUpNoMoreInformation()
|
||||
]
|
||||
} else if (!state.lastbat) {
|
||||
cmds = []
|
||||
} else {
|
||||
cmds = [zwave.wakeUpV1.wakeUpNoMoreInformation().format()]
|
||||
cmds = [zwave.wakeUpV1.wakeUpNoMoreInformation()]
|
||||
}
|
||||
response(cmds)
|
||||
}
|
||||
|
||||
def configure() {
|
||||
delayBetween([
|
||||
zwave.manufacturerSpecificV2.manufacturerSpecificGet().format(),
|
||||
batteryGetCommand()
|
||||
commands([
|
||||
zwave.manufacturerSpecificV2.manufacturerSpecificGet(),
|
||||
zwave.batteryV1.batteryGet()
|
||||
], 6000)
|
||||
}
|
||||
|
||||
@@ -147,12 +147,11 @@ 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)
|
||||
result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId))
|
||||
if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
|
||||
if(!state.MSR) result << response(command(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(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
|
||||
if(!state.MSR) result << response(command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()))
|
||||
result << createEvent(name: "motion", value: "active", descriptionText:"$device.displayName detected motion")
|
||||
}
|
||||
} else if (cmd.notificationType) {
|
||||
@@ -170,14 +169,13 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
|
||||
def event = createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)
|
||||
def cmds = []
|
||||
if (!state.MSR) {
|
||||
cmds << zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId).format()
|
||||
cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()
|
||||
cmds << command(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
|
||||
cmds << "delay 1200"
|
||||
}
|
||||
if (!state.lastbat || now() - state.lastbat > 53*60*60*1000) {
|
||||
cmds << batteryGetCommand()
|
||||
cmds << command(zwave.batteryV1.batteryGet())
|
||||
} else {
|
||||
cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
|
||||
cmds << zwave.wakeUpV1.wakeUpNoMoreInformation()
|
||||
}
|
||||
[event, response(cmds)]
|
||||
}
|
||||
@@ -212,7 +210,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(batteryGetCommand())
|
||||
result << response(command(zwave.batteryV1.batteryGet()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +218,7 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x85: 2, 0x70: 1])
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1])
|
||||
// log.debug "encapsulated: $encapsulatedCommand"
|
||||
if (encapsulatedCommand) {
|
||||
state.sec = 1
|
||||
@@ -232,12 +230,16 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
createEvent(descriptionText: "$device.displayName: $cmd", displayed: false)
|
||||
}
|
||||
|
||||
def batteryGetCommand() {
|
||||
def cmd = zwave.batteryV1.batteryGet()
|
||||
if (state.sec) {
|
||||
cmd = zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd)
|
||||
private command(physicalgraph.zwave.Command cmd) {
|
||||
if (state.sec == 1) {
|
||||
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||
} else {
|
||||
cmd.format()
|
||||
}
|
||||
cmd.format()
|
||||
}
|
||||
|
||||
private commands(commands, delay=200) {
|
||||
delayBetween(commands.collect{ command(it) }, delay)
|
||||
}
|
||||
|
||||
def retypeBasedOnMSR() {
|
||||
@@ -260,8 +262,12 @@ def retypeBasedOnMSR() {
|
||||
setDeviceType("3-in-1 Multisensor Plus (SG)")
|
||||
break
|
||||
case "0109-2001-0106": // Vision door/window
|
||||
log.debug "Changing device type to Door / Window Sensor Plus (SG)"
|
||||
setDeviceType("Door / Window Sensor Plus (SG)")
|
||||
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")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -1,839 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,536 +0,0 @@
|
||||
/**
|
||||
* 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") : ""
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Smart Family Presence
|
||||
*
|
||||
* Copyright 2016 Darin Spivey
|
||||
*
|
||||
* 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: "Smart Family Presence",
|
||||
namespace: "ddspivey",
|
||||
author: "Darin Spivey",
|
||||
description: "Smart arrival and departure push messages for couples/families that are traveling together. When family members arrive and depart together, there is no need to send an individual push alert for each.",
|
||||
category: "Family",
|
||||
iconUrl: "http://cdn.device-icons.smartthings.com/Home/home4-icn@2x.png",
|
||||
iconX2Url: "http://cdn.device-icons.smartthings.com/Home/home4-icn@2x.png",
|
||||
iconX3Url: "http://cdn.device-icons.smartthings.com/Home/home4-icn@2x.png")
|
||||
|
||||
|
||||
preferences {
|
||||
section("Family Members") {
|
||||
input "familySensors", "capability.presenceSensor", required: true, title: "Who's in your family?", multiple: true
|
||||
}
|
||||
section("Threshold") {
|
||||
paragraph "Set the time in seconds to allow for group arrival/departure"
|
||||
input "timeThreshold", "text", required: false, title: "Default is ${defaultThreshold}."
|
||||
}
|
||||
section("Smart departure alerts") {
|
||||
paragraph "When family members are home together, departure push alerts may not be necessary because most of the time, people are aware when their family members are leaving. This feature will only send a push alert if the the entire family was previously apart."
|
||||
paragraph "For example, when my wife and I are home together, I know when she's leaving; I don't need an alert for that. If this feature is off, it will send a departure alert when she leaves."
|
||||
input("smartDepartureFeature", "enum", title: "Default is On.", default:"On", options: ["On","Off"])
|
||||
}
|
||||
section("Verbose logging") {
|
||||
paragraph "For debugging, you may log all the app's decisions to the notifications log."
|
||||
input("logToNotifications", "enum", title: "Default is No.", default:"No", options: ["Yes", "No" ])
|
||||
}
|
||||
}
|
||||
|
||||
/****************************
|
||||
Auto-getters and setters
|
||||
*****************************/
|
||||
|
||||
def getDefaultThreshold() {
|
||||
60
|
||||
}
|
||||
|
||||
def setInProgress(value) {
|
||||
state.inProgress = value
|
||||
}
|
||||
|
||||
def getInProgress() {
|
||||
state.inProgress == true
|
||||
}
|
||||
|
||||
def getSmartDeparture() {
|
||||
settings.smartDepartureFeature == 'On'
|
||||
}
|
||||
|
||||
def getLogToNotifications() {
|
||||
settings.logToNotifications == 'Yes'
|
||||
}
|
||||
|
||||
def getThreshold() {
|
||||
settings.timeThreshold ? settings.timeThreshold.toInteger() : defaultThreshold
|
||||
}
|
||||
|
||||
/****************************
|
||||
Framework methods
|
||||
*****************************/
|
||||
|
||||
def installed() {
|
||||
initialize()
|
||||
}
|
||||
|
||||
def logit(msg) {
|
||||
log.debug msg
|
||||
if (logToNotifications) {
|
||||
sendNotificationEvent("[Smart Family Presense] $msg")
|
||||
}
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(familySensors, "presence", presenceHandler)
|
||||
log.debug("Subscribed ${familySensors.toString()} to presenceHandler")
|
||||
|
||||
/*
|
||||
Regular usage shows that, during certain cases such as the hub going offline,
|
||||
or power outages, the app may lose state and not send alerts. This will ensure that
|
||||
it re-evaluates its state at least once per hour.
|
||||
*/
|
||||
|
||||
logit "Scheduling a re-calibration at the top of every hour."
|
||||
schedule("0 0 0/1 1/1 * ? *", reset)
|
||||
reset()
|
||||
}
|
||||
|
||||
/*****************************
|
||||
Smart Family Presence methods
|
||||
******************************/
|
||||
|
||||
def reset() {
|
||||
if (inProgress) {
|
||||
logit "Skipping re-calibration, execution in progress!"
|
||||
return
|
||||
}
|
||||
logit "Re-calibrating."
|
||||
state.baseCase = null
|
||||
state.changedThisTime = []
|
||||
wasApart()
|
||||
}
|
||||
|
||||
def isFamilyTogether() {
|
||||
// Check to see if the entire family has arrived/departed together
|
||||
|
||||
if (state.changedThisTime.size() == 0) {
|
||||
logit "No changes."
|
||||
reset()
|
||||
return
|
||||
}
|
||||
|
||||
logit "People who changed presence: ${state.changedThisTime}"
|
||||
|
||||
def theirState = state.baseCase
|
||||
def notTogether = statusNotEquals(theirState)
|
||||
|
||||
if (notTogether) {
|
||||
// The family is not together, send an alert as normal
|
||||
|
||||
logit "${notTogether.join(", ")} is not with the rest of the family (who are $theirState)"
|
||||
sendPushAlert()
|
||||
}
|
||||
else {
|
||||
/*
|
||||
Special case - When everyone is gone, but they *previously* weren't together,
|
||||
then technically they were apart to begin with and are still apart upon leaving
|
||||
*/
|
||||
|
||||
if (state.wasApart && theirState == 'not present') {
|
||||
logit "Family was previously apart and now all gone. Alert."
|
||||
sendPushAlert()
|
||||
}
|
||||
else {
|
||||
logit "OK! Everyone has arrived/departed together. The family is $theirState"
|
||||
}
|
||||
}
|
||||
inProgress = false
|
||||
reset()
|
||||
}
|
||||
|
||||
def wasApart() {
|
||||
// This is true if everyone is gone, or some were home
|
||||
def allGone = statusEquals('not present') == familySensors
|
||||
def someHome = statusEquals('present') != familySensors
|
||||
if (allGone || someHome) {
|
||||
state.wasApart = true
|
||||
}
|
||||
else {
|
||||
state.wasApart = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def presenceHandler(evt) {
|
||||
def person = evt.displayName
|
||||
|
||||
logit "Presence Event: $person is $evt.value"
|
||||
|
||||
if (! inProgress) {
|
||||
inProgress = true
|
||||
state.baseCase = evt.value
|
||||
logit "First person sensed. Checking for others to be $evt.value in ${threshold} seconds"
|
||||
runIn(threshold, isFamilyTogether, [overwrite: false])
|
||||
}
|
||||
|
||||
if (! state.changedThisTime.contains(person)) {
|
||||
state.changedThisTime.push person
|
||||
}
|
||||
else {
|
||||
// Special case - presence has changed within the threshold. Remove this person.
|
||||
state.changedThisTime = state.changedThisTime - person
|
||||
logit "Ignoring flapping presence event for $person"
|
||||
}
|
||||
}
|
||||
|
||||
def statusEquals(status) {
|
||||
if (status == null) return
|
||||
|
||||
familySensors.findAll {
|
||||
it.currentPresence == status
|
||||
}
|
||||
}
|
||||
|
||||
def statusNotEquals(status) {
|
||||
if (status == null) return
|
||||
|
||||
familySensors.findAll {
|
||||
it.currentPresence != status
|
||||
}
|
||||
}
|
||||
|
||||
def sendPushAlert() {
|
||||
def baseCase = state.baseCase
|
||||
def changedPeople = state.changedThisTime
|
||||
|
||||
if (baseCase == 'not present' && state.wasApart == false && smartDeparture) {
|
||||
logit "Not sending departure alert because smartDeparture is $smartDeparture"
|
||||
return
|
||||
}
|
||||
|
||||
def statuses = [ 'present':[], 'not present':[] ]
|
||||
|
||||
for (sensor in familySensors) {
|
||||
def person = sensor.toString()
|
||||
if (changedPeople.contains(person)) {
|
||||
def currentState = sensor.currentPresence
|
||||
log.debug "$person is now $currentState"
|
||||
statuses[currentState].push person
|
||||
}
|
||||
}
|
||||
logit "Statuses: $statuses"
|
||||
|
||||
// Construct the message payload
|
||||
|
||||
def pushMsg = ""
|
||||
def home = statuses.present
|
||||
def notHome = statuses['not present']
|
||||
String adVerb;
|
||||
|
||||
if (home.size() > 0) {
|
||||
adVerb = home.size > 1 ? "have" : "has"
|
||||
pushMsg += "${home.join(", ")} $adVerb arrived $location.name. "
|
||||
}
|
||||
if (notHome.size() > 0) {
|
||||
adVerb = notHome.size > 1 ? "have" : "has"
|
||||
pushMsg += "${notHome.join(", ")} $adVerb left $location.name"
|
||||
}
|
||||
|
||||
sendPush pushMsg
|
||||
}
|
||||
@@ -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,6 +39,7 @@ preferences {
|
||||
page(name: "completionPage")
|
||||
page(name: "numbersPage")
|
||||
page(name: "controllerExplanationPage")
|
||||
page(name: "unsupportedDevicesPage")
|
||||
}
|
||||
|
||||
def rootPage() {
|
||||
@@ -47,6 +48,9 @@ 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")
|
||||
}
|
||||
}
|
||||
@@ -71,6 +75,31 @@ 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") {
|
||||
|
||||
@@ -528,14 +557,16 @@ def updateDimmers(percentComplete) {
|
||||
} else {
|
||||
|
||||
def shouldChangeColors = (colorize && colorize != "false")
|
||||
def canChangeColors = hasSetColorCommand(dimmer)
|
||||
|
||||
log.debug "Setting ${deviceLabel(dimmer)} to ${nextLevel}"
|
||||
|
||||
if (shouldChangeColors && canChangeColors) {
|
||||
dimmer.setColor([hue: getHue(dimmer, nextLevel), saturation: 100, level: nextLevel])
|
||||
} else {
|
||||
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)
|
||||
} else {
|
||||
log.warn "${deviceLabel(dimmer)} does not have setColor or setLevel commands."
|
||||
}
|
||||
|
||||
}
|
||||
@@ -817,24 +848,21 @@ private getRedHue(level) {
|
||||
if (level >= 96) return 17
|
||||
}
|
||||
|
||||
private dimmersContainUnsupportedDevices() {
|
||||
def found = dimmers.find { hasSetLevelCommand(it) == false }
|
||||
return found != null
|
||||
}
|
||||
|
||||
private hasSetLevelCommand(device) {
|
||||
def isDimmer = false
|
||||
device.supportedCommands.each {
|
||||
if (it.name.contains("setLevel")) {
|
||||
isDimmer = true
|
||||
}
|
||||
}
|
||||
return isDimmer
|
||||
return hasCommand(device, "setLevel")
|
||||
}
|
||||
|
||||
private hasSetColorCommand(device) {
|
||||
def hasColor = false
|
||||
device.supportedCommands.each {
|
||||
if (it.name.contains("setColor")) {
|
||||
hasColor = true
|
||||
}
|
||||
}
|
||||
return hasColor
|
||||
return hasCommand(device, "setColor")
|
||||
}
|
||||
|
||||
private hasCommand(device, String command) {
|
||||
return (device.supportedCommands.find { it.name == command } != null)
|
||||
}
|
||||
|
||||
private dimmersWithSetColorCommand() {
|
||||
@@ -1073,4 +1101,4 @@ def hasStartLevel() {
|
||||
|
||||
def hasEndLevel() {
|
||||
return (endLevel != null && endLevel != "")
|
||||
}
|
||||
}
|
||||
@@ -658,29 +658,73 @@ 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 (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"}'
|
||||
}
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
@@ -779,18 +823,33 @@ def deviceHandler(evt) {
|
||||
}
|
||||
|
||||
def sendToHarmony(evt, String callbackUrl) {
|
||||
def callback = new URI(callbackUrl)
|
||||
def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host
|
||||
def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path
|
||||
sendHubCommand(new physicalgraph.device.HubAction(
|
||||
method: "POST",
|
||||
path: path,
|
||||
headers: [
|
||||
"Host": host,
|
||||
"Content-Type": "application/json"
|
||||
],
|
||||
body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
|
||||
))
|
||||
def 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 listHubs() {
|
||||
|
||||
Reference in New Issue
Block a user