mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-08 05:31:56 +00:00
Initial commit
This commit is contained in:
15
README.md
Normal file
15
README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# SmartThings Public Github Repo
|
||||||
|
|
||||||
|
An official list of SmartApps and Device Types from SmartThings.
|
||||||
|
|
||||||
|
Here are some links to help you get started coding right away:
|
||||||
|
|
||||||
|
* [Github-specific Documentation](http://docs.smartthings.com/en/latest/tools-and-ide/github-integration.html)
|
||||||
|
* [Full Documentation](http://docs.smartthings.com)
|
||||||
|
* [IDE & Simulator](http://ide.smartthings.com)
|
||||||
|
* [Community Forums](http://community.smartthings.com)
|
||||||
|
|
||||||
|
Follow us on the web:
|
||||||
|
|
||||||
|
* Twitter: http://twitter.com/smartthingsdev
|
||||||
|
* Facebook: http://facebook.com/smartthingsdevelopers
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Acceleration Sensor Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Acceleration Sensor"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "active": "acceleration:active"
|
||||||
|
status "inactive": "acceleration:inactive"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("acceleration", "device.acceleration", width: 2, height: 2) {
|
||||||
|
state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff")
|
||||||
|
state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0")
|
||||||
|
}
|
||||||
|
|
||||||
|
main "acceleration"
|
||||||
|
details "acceleration"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def pair = description.split(":")
|
||||||
|
createEvent(name: pair[0].trim(), value: pair[1].trim())
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Alarm Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Alarm"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// reply messages
|
||||||
|
["strobe","siren","both","off"].each {
|
||||||
|
reply "$it": "alarm:$it"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("alarm", "device.alarm", width: 2, height: 2) {
|
||||||
|
state "off", label:'off', action:'alarm.strobe', icon:"st.alarm.alarm.alarm", backgroundColor:"#ffffff"
|
||||||
|
state "strobe", label:'strobe!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13"
|
||||||
|
state "siren", label:'siren!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13"
|
||||||
|
state "both", label:'alarm!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13"
|
||||||
|
}
|
||||||
|
standardTile("strobe", "device.alarm", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "off", label:'', action:"alarm.strobe", icon:"st.secondary.strobe", backgroundColor:"#cccccc"
|
||||||
|
state "siren", label:'', action:"alarm.strobe", icon:"st.secondary.strobe", backgroundColor:"#cccccc"
|
||||||
|
state "strobe", label:'', action:'alarm.strobe', icon:"st.secondary.strobe", backgroundColor:"#e86d13"
|
||||||
|
state "both", label:'', action:'alarm.strobe', icon:"st.secondary.strobe", backgroundColor:"#e86d13"
|
||||||
|
}
|
||||||
|
standardTile("siren", "device.alarm", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "off", label:'', action:"alarm.siren", icon:"st.secondary.siren", backgroundColor:"#cccccc"
|
||||||
|
state "strobe", label:'', action:"alarm.siren", icon:"st.secondary.siren", backgroundColor:"#cccccc"
|
||||||
|
state "siren", label:'', action:'alarm.siren', icon:"st.secondary.siren", backgroundColor:"#e86d13"
|
||||||
|
state "both", label:'', action:'alarm.siren', icon:"st.secondary.siren", backgroundColor:"#e86d13"
|
||||||
|
}
|
||||||
|
standardTile("off", "device.alarm", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"alarm.off", icon:"st.secondary.off"
|
||||||
|
}
|
||||||
|
main "alarm"
|
||||||
|
details(["alarm","strobe","siren","test","off"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def strobe() {
|
||||||
|
"strobe"
|
||||||
|
}
|
||||||
|
|
||||||
|
def siren() {
|
||||||
|
"siren"
|
||||||
|
}
|
||||||
|
|
||||||
|
def both() {
|
||||||
|
"both"
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
"off"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
def pair = description.split(":")
|
||||||
|
createEvent(name: pair[0].trim(), value: pair[1].trim())
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Button Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Button"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "button 1 pushed": "command: 2001, payload: 01"
|
||||||
|
status "button 1 held": "command: 2001, payload: 15"
|
||||||
|
status "button 2 pushed": "command: 2001, payload: 29"
|
||||||
|
status "button 2 held": "command: 2001, payload: 3D"
|
||||||
|
status "wakeup": "command: 8407, payload: "
|
||||||
|
}
|
||||||
|
tiles {
|
||||||
|
standardTile("button", "device.button", width: 2, height: 2) {
|
||||||
|
state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff"
|
||||||
|
}
|
||||||
|
main "button"
|
||||||
|
details "button"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def results = []
|
||||||
|
if (description.startsWith("Err")) {
|
||||||
|
results = createEvent(descriptionText:description, displayed:true)
|
||||||
|
} else {
|
||||||
|
def cmd = zwave.parse(description, [0x2B: 1, 0x80: 1, 0x84: 1])
|
||||||
|
if(cmd) results += zwaveEvent(cmd)
|
||||||
|
if(!results) results = [ descriptionText: cmd, displayed: false ]
|
||||||
|
}
|
||||||
|
// log.debug("Parsed '$description' to $results")
|
||||||
|
return results
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Contact Sensor Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Contact Sensor"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "open": "contact:open"
|
||||||
|
status "closed": "contact:closed"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("contact", "device.contact", width: 2, height: 2) {
|
||||||
|
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||||
|
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||||
|
}
|
||||||
|
main "contact"
|
||||||
|
details "contact"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def pair = description.split(":")
|
||||||
|
createEvent(name: pair[0].trim(), value: pair[1].trim())
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Illuminance Measurement Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Illuminance Measurement"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
for (i in [0,5,10,15,20,30,40,50,100,200,300,400,600,800,1000]) {
|
||||||
|
status "${i} lux": "illuminance:${i}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
valueTile("illuminance", "device.illuminance", width: 2, height: 2) {
|
||||||
|
state "luminosity", label:'${currentValue} ${unit}', unit:"lux"
|
||||||
|
}
|
||||||
|
main(["illuminance"])
|
||||||
|
details(["illuminance"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description)
|
||||||
|
{
|
||||||
|
def pair = description.split(":")
|
||||||
|
createEvent(name: pair[0].trim(), value: pair[1].trim())
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Lock Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Lock"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "locked": "lock:locked"
|
||||||
|
status "unlocked": "lock:unlocked"
|
||||||
|
|
||||||
|
reply "lock": "lock:locked"
|
||||||
|
reply "unlock": "lock:unlocked"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("toggle", "device.lock", width: 2, height: 2) {
|
||||||
|
state "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff"
|
||||||
|
state "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821"
|
||||||
|
}
|
||||||
|
standardTile("lock", "device.lock", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked"
|
||||||
|
}
|
||||||
|
standardTile("unlock", "device.lock", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "toggle"
|
||||||
|
details(["toggle", "lock", "unlock", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def pair = description.split(":")
|
||||||
|
createEvent(name: pair[0].trim(), value: pair[1].trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
def lock() {
|
||||||
|
"lock"
|
||||||
|
}
|
||||||
|
|
||||||
|
def unlock() {
|
||||||
|
"unlock"
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Momentary Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Momentary"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
// none
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "'on','delay 2000','off'": "switch:off"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "on"
|
||||||
|
state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||||
|
}
|
||||||
|
main "switch"
|
||||||
|
details "switch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def pair = description.split(":")
|
||||||
|
createEvent(name: pair[0].trim(), value: pair[1].trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
def push() {
|
||||||
|
['on','delay 2000','off']
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
'off'
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Motion Sensor Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Motion Sensor"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "active": "motion:active"
|
||||||
|
status "inactive": "motion:inactive"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("motion", "device.motion", width: 2, height: 2) {
|
||||||
|
state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff")
|
||||||
|
state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0")
|
||||||
|
}
|
||||||
|
main "motion"
|
||||||
|
details "motion"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def pair = description.split(":")
|
||||||
|
createEvent(name: pair[0].trim(), value: pair[1].trim())
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Presence Sensor Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Presence Sensor"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "present": "presence: present"
|
||||||
|
status "not present": "presence: not present"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("presence", "device.presence", width: 2, height: 2) {
|
||||||
|
state("not present", label:'not present', icon:"st.presence.tile.not-present", backgroundColor:"#ffffff")
|
||||||
|
state("present", label:'present', icon:"st.presence.tile.present", backgroundColor:"#53a7c0")
|
||||||
|
}
|
||||||
|
main "presence"
|
||||||
|
details "presence"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def pair = description.split(":")
|
||||||
|
createEvent(name: pair[0].trim(), value: pair[1].trim())
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Relative Humidity Measurement Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Relative Humidity Measurement"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
for (int i = 0; i <= 100; i += 10) {
|
||||||
|
status "${i}%": "humidity: ${i}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
valueTile("humidity", "device.humidity", width: 2, height: 2) {
|
||||||
|
state "humidity", label:'${currentValue}%', unit:""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
def pair = description.split(":")
|
||||||
|
createEvent(name: pair[0].trim(), value: pair[1].trim(), unit:"%")
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Switch Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Switch"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "on": "switch:on"
|
||||||
|
status "off": "switch:off"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "on": "switch:on"
|
||||||
|
reply "off": "switch:off"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
|
||||||
|
state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||||
|
}
|
||||||
|
main "switch"
|
||||||
|
details "switch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def pair = description.split(":")
|
||||||
|
createEvent(name: pair[0].trim(), value: pair[1].trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
'on'
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
'off'
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Switch Level Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Switch Level"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "on": "switch:on"
|
||||||
|
status "off": "switch:off"
|
||||||
|
|
||||||
|
reply "on":"on"
|
||||||
|
reply "off":"off"
|
||||||
|
|
||||||
|
[5,10,25,33,50,66,75,99].each {
|
||||||
|
status "$it%": "switch:on,level:$it"
|
||||||
|
}
|
||||||
|
reply "setLevel: 0":"switch:off,level:0"
|
||||||
|
(1..99).each {
|
||||||
|
reply "setLevel: $it":"switch:on,level:$it"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2) {
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821"
|
||||||
|
state "turningOff", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 2, width: 1, inactiveLabel: false) {
|
||||||
|
state "level", action:"setLevel"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "switch"
|
||||||
|
details "switch", "levelSliderControl", "refresh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
log.trace description
|
||||||
|
def pairs = description.split(",")
|
||||||
|
def result = []
|
||||||
|
pairs.each {
|
||||||
|
def pair = it.split(":")
|
||||||
|
result << createEvent(name: pair[0].trim(), value: pair[1].trim())
|
||||||
|
}
|
||||||
|
log.trace result
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
'on'
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
'off'
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
"setLevel: $value"
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
'refresh'
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Temperature Measurement Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
for (int i = 0; i <= 100; i += 10) {
|
||||||
|
status "${i} F": "temperature:$i"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||||
|
state("temperature", label:'${currentValue}°', unit:"F",
|
||||||
|
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"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
main "temperature"
|
||||||
|
details "temperature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
def pair = description.split(":")
|
||||||
|
createEvent(name: pair[0].trim(), value: pair[1].trim(), unit:"F")
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Thermostat Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Thermostat"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
["on","off","heat","cool","emergency heat"].each {
|
||||||
|
status "$it": "thermostatMode:$it"
|
||||||
|
}
|
||||||
|
|
||||||
|
["on","auto","circulate"].each {
|
||||||
|
status "fan $it": "thermostatFanMode:$it"
|
||||||
|
}
|
||||||
|
|
||||||
|
[60,68,72].each {
|
||||||
|
status "heat $it": "heatingSetpoint:$it"
|
||||||
|
}
|
||||||
|
|
||||||
|
[72,76,80,85].each {
|
||||||
|
status "cool $it": "coolingSetpoint:$it"
|
||||||
|
}
|
||||||
|
|
||||||
|
[40,58,62,70,74,78,82,86].each {
|
||||||
|
status "temp $it": "temperature:$it"
|
||||||
|
}
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
//reply "2502": "command: 2503, payload: FF"
|
||||||
|
["on","off","heat","cool","emergency heat"].each {
|
||||||
|
reply "thermostatMode:$it": "thermostatMode:$it"
|
||||||
|
}
|
||||||
|
["on","auto","circulate"].each {
|
||||||
|
reply "thermostatFanMode:$it": "thermostatFanMode:$it"
|
||||||
|
}
|
||||||
|
for (n in 60..90) {
|
||||||
|
reply "heatingSetpoint:${n}": "heatingSetpoint:$n"
|
||||||
|
reply "heatingSetpoint:${n}.0": "heatingSetpoint:$n"
|
||||||
|
}
|
||||||
|
for (n in 60..90) {
|
||||||
|
reply "coolingSetpoint:${n}": "coolingSetpoint:$n"
|
||||||
|
reply "coolingSetpoint:${n}.0": "coolingSetpoint:$n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||||
|
state("temperature", label:'${currentValue}°', unit:"F",
|
||||||
|
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("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "heat", label:'${currentValue}° heat', unit: "F", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "off", label:'${name}', action:"thermostat.emergencyHeat", backgroundColor:"#ffffff"
|
||||||
|
state "emergencyHeat", label:'${name}', action:"thermostat.heat", backgroundColor:"#e86d13"
|
||||||
|
state "heat", label:'${name}', action:"thermostat.cool", backgroundColor:"#ffc000"
|
||||||
|
state "cool", label:'${name}', action:"thermostat.off", backgroundColor:"#269bd2"
|
||||||
|
}
|
||||||
|
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "fanAuto", label:'${name}', action:"thermostat.fanOn", backgroundColor:"#ffffff"
|
||||||
|
state "fanOn", label:'${name}', action:"thermostat.fanCirculate", backgroundColor:"#ffffff"
|
||||||
|
state "fanCirculate", label:'${name}', action:"thermostat.fanAuto", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "temperature"
|
||||||
|
details(["temperature", "heatingSetpoint", "coolingSetpoint", "mode", "fanMode"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description)
|
||||||
|
{
|
||||||
|
def pair = description.split(":")
|
||||||
|
def map = createEvent(name: pair[0].trim(), value: pair[1].trim())
|
||||||
|
def result = [map]
|
||||||
|
|
||||||
|
if (map.isStateChange && map.name in ["heatingSetpoint","coolingSetpoint","thermostatMode"]) {
|
||||||
|
def map2 = [
|
||||||
|
name: "thermostatSetpoint",
|
||||||
|
unit: "F"
|
||||||
|
]
|
||||||
|
if (map.name == "thermostatMode") {
|
||||||
|
if (map.value == "cool") {
|
||||||
|
map2.value = device.latestValue("coolingSetpoint")
|
||||||
|
log.info "THERMOSTAT, latest cooling setpoint = ${map2.value}"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
map2.value = device.latestValue("heatingSetpoint")
|
||||||
|
log.info "THERMOSTAT, latest heating setpoint = ${map2.value}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
def mode = device.latestValue("thermostatMode")
|
||||||
|
log.info "THERMOSTAT, latest mode = ${mode}"
|
||||||
|
if ((map.name == "heatingSetpoint" && mode == "heat") || (map.name == "coolingSetpoint" && mode == "cool")) {
|
||||||
|
map2.value = map.value
|
||||||
|
map2.unit = map.unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (map2.value != null) {
|
||||||
|
log.debug "THERMOSTAT, adding setpoint event: $map"
|
||||||
|
result << createEvent(map2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def setHeatingSetpoint(Double degreesF) {
|
||||||
|
"heatingSetpoint:$degreesF"
|
||||||
|
}
|
||||||
|
|
||||||
|
def setCoolingSetpoint(Double degreesF) {
|
||||||
|
"coolingSetpoint:$degreesF"
|
||||||
|
}
|
||||||
|
|
||||||
|
def setThermostatMode(String value) {
|
||||||
|
"thermostatMode:$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
def setThermostatFanMode(String value) {
|
||||||
|
"thermostatFanMode:$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
"thermostatMode:off"
|
||||||
|
}
|
||||||
|
|
||||||
|
def heat() {
|
||||||
|
"thermostatMode:heat"
|
||||||
|
}
|
||||||
|
|
||||||
|
def emergencyHeat() {
|
||||||
|
"thermostatMode:emergency heat"
|
||||||
|
}
|
||||||
|
|
||||||
|
def cool() {
|
||||||
|
"thermostatMode:cool"
|
||||||
|
}
|
||||||
|
|
||||||
|
def fanOn() {
|
||||||
|
"thermostatFanMode:on"
|
||||||
|
}
|
||||||
|
|
||||||
|
def fanAuto() {
|
||||||
|
"thermostatMode:auto"
|
||||||
|
}
|
||||||
|
|
||||||
|
def fanCirculate() {
|
||||||
|
"thermostatMode:circulate"
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Three Axis Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Three Axis"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "x,y,z: 0,0,0": "threeAxis:0,0,0"
|
||||||
|
status "x,y,z: 1000,0,0": "threeAxis:1000,0,0"
|
||||||
|
status "x,y,z: 0,1000,0": "threeAxis:0,1000,0"
|
||||||
|
status "x,y,z: 0,0,1000": "xthreeAxis:0,0,1000"
|
||||||
|
status "x,y,z: -1000,0,0": "threeAxis:-1000,0,0"
|
||||||
|
status "x,y,z: 0,-1000,0": "threeAxis:0,-1000,0"
|
||||||
|
status "x,y,z: 0,0,-1000": "xthreeAxis:0,0,-1000"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
valueTile("3axis", "device.threeAxis", decoration: "flat") {
|
||||||
|
state("threeAxis", label:'${currentValue}', unit:"", backgroundColor:"#ffffff")
|
||||||
|
}
|
||||||
|
|
||||||
|
main "3axis"
|
||||||
|
details "3axis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def pair = description.split(":")
|
||||||
|
createEvent(name: pair[0].trim(), value: pair[1].trim())
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Water Sensor Capability", namespace: "capabilities", author: "SmartThings") {
|
||||||
|
capability "Water Sensor"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "wet": "water:wet"
|
||||||
|
status "dry": "water:dry"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("water", "device.water", width: 2, height: 2) {
|
||||||
|
state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff"
|
||||||
|
state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "water"
|
||||||
|
details "water"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def pair = description.split(":")
|
||||||
|
createEvent(name: pair[0].trim(), value: pair[1].trim())
|
||||||
|
}
|
||||||
376
devicetypes/com-obycode/obything-music.src/obything-music.groovy
Normal file
376
devicetypes/com-obycode/obything-music.src/obything-music.groovy
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
/**
|
||||||
|
* ObyThing Music
|
||||||
|
*
|
||||||
|
* Copyright 2014 obycode
|
||||||
|
*
|
||||||
|
* 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.JsonSlurper
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "ObyThing Music", namespace: "com.obycode", author: "obycode") {
|
||||||
|
capability "Music Player"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Switch"
|
||||||
|
|
||||||
|
command "playTrackAtVolume", ["string","number"]
|
||||||
|
command "playTrackAndResume", ["string","number","number"]
|
||||||
|
command "playTextAndResume", ["string","number"]
|
||||||
|
command "playTrackAndRestore", ["string","number","number"]
|
||||||
|
command "playTextAndRestore", ["string","number"]
|
||||||
|
command "playSoundAndTrack", ["string","number","json_object","number"]
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
// Main
|
||||||
|
standardTile("main", "device.status", width: 1, height: 1, canChangeIcon: true) {
|
||||||
|
state "paused", label:'Paused', action:"music Player.play", icon:"st.Electronics.electronics19", nextState:"playing", backgroundColor:"#ffffff"
|
||||||
|
state "playing", label:'Playing', action:"music Player.pause", icon:"st.Electronics.electronics19", nextState:"paused", backgroundColor:"#79b821"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row 1
|
||||||
|
standardTile("nextTrack", "device.status", width: 1, height: 1, decoration: "flat") {
|
||||||
|
state "next", label:'', action:"music Player.nextTrack", icon:"st.sonos.next-btn", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("playpause", "device.status", width: 1, height: 1, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"music Player.play", icon:"st.sonos.play-btn", nextState:"playing", backgroundColor:"#ffffff"
|
||||||
|
state "playing", label:'', action:"music Player.pause", icon:"st.sonos.pause-btn", nextState:"paused", backgroundColor:"#ffffff"
|
||||||
|
state "paused", label:'', action:"music Player.play", icon:"st.sonos.play-btn", nextState:"playing", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("previousTrack", "device.status", width: 1, height: 1, decoration: "flat") {
|
||||||
|
state "previous", label:'', action:"music Player.previousTrack", icon:"st.sonos.previous-btn", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row 2
|
||||||
|
standardTile("airplay", "device.switch", width: 1, height: 1, decoration: "flat", canChangeIcon: true) {
|
||||||
|
state "on", label:'AirPlay On', action:"switch.off", icon:"st.Electronics.electronics14", nextState:"off", backgroundColor:"#ffffff"
|
||||||
|
state "off", label:'AirPlay Off', action:"switch.on", icon:"st.Electronics.electronics16", nextState:"on", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("status", "device.status", width: 1, height: 1, decoration: "flat", canChangeIcon: true) {
|
||||||
|
state "playing", label:'Playing', action:"music Player.pause", icon:"st.Electronics.electronics19", nextState:"paused", backgroundColor:"#ffffff"
|
||||||
|
state "stopped", label:'Stopped', action:"music Player.play", icon:"st.Electronics.electronics19", nextState:"playing", backgroundColor:"#ffffff"
|
||||||
|
state "paused", label:'Paused', action:"music Player.play", icon:"st.Electronics.electronics19", nextState:"playing", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("mute", "device.mute", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "unmuted", label:"Mute", action:"music Player.mute", icon:"st.custom.sonos.unmuted", backgroundColor:"#ffffff", nextState:"muted"
|
||||||
|
state "muted", label:"Unmute", action:"music Player.unmute", icon:"st.custom.sonos.muted", backgroundColor:"#ffffff", nextState:"unmuted"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row 3
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) {
|
||||||
|
state "level", action:"music Player.setLevel", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row 4 - Disable this for now until we get communication back to hub working
|
||||||
|
// valueTile("currentSong", "device.trackDescription", inactiveLabel: true, height:1, width:3, decoration: "flat") {
|
||||||
|
// state "default", label:'${currentValue}', backgroundColor:"#ffffff"
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Row 5
|
||||||
|
standardTile("refresh", "device.status", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "main"
|
||||||
|
|
||||||
|
details([
|
||||||
|
"previousTrack","playpause","nextTrack",
|
||||||
|
"airplay","status","mute",
|
||||||
|
"levelSliderControl",
|
||||||
|
// "currentSong",
|
||||||
|
"refresh"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// mappings {
|
||||||
|
// path("/obything/:message") {
|
||||||
|
// action: [
|
||||||
|
// GET: "updateState"
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse events into attributes
|
||||||
|
def parse(String description) {
|
||||||
|
//log.debug "Parsing '${description}'"
|
||||||
|
def map = stringToMap(description)
|
||||||
|
if (map.headers && map.body) { //got device info response
|
||||||
|
if (map.body) {
|
||||||
|
def bodyString = new String(map.body.decodeBase64())
|
||||||
|
//log.debug "body = $bodyString"
|
||||||
|
def slurper = new JsonSlurper()
|
||||||
|
def result = slurper.parseText(bodyString)
|
||||||
|
if (result.containsKey("volume")) {
|
||||||
|
log.debug "setting volume to ${result.volume}"
|
||||||
|
sendEvent(name: "level", value: result.volume)
|
||||||
|
}
|
||||||
|
if (result.containsKey("mute")) {
|
||||||
|
log.debug "setting mute to ${result.mute}"
|
||||||
|
sendEvent(name: "mute", value: result.mute)
|
||||||
|
}
|
||||||
|
if (result.containsKey("status")) {
|
||||||
|
log.debug "setting status to ${result.status}"
|
||||||
|
sendEvent(name: "status", value: result.status)
|
||||||
|
}
|
||||||
|
if (result.containsKey("trackData")) {
|
||||||
|
def json = new groovy.json.JsonBuilder(result.trackData)
|
||||||
|
log.debug "setting trackData to ${json.toString()}"
|
||||||
|
sendEvent(name: "trackData", value: json.toString())
|
||||||
|
}
|
||||||
|
if (result.containsKey("trackDescription")) {
|
||||||
|
log.debug "setting trackDescription info to ${result.trackDescription}"
|
||||||
|
sendEvent(name: "trackDescription", value: result.trackDescription)
|
||||||
|
}
|
||||||
|
if (result.containsKey("airplay")) {
|
||||||
|
log.debug "setting airplay to ${result.airplay}"
|
||||||
|
sendEvent(name: "switch", value: result.airplay)
|
||||||
|
}
|
||||||
|
if (result.containsKey("playlists")) {
|
||||||
|
result.playlists.each() {
|
||||||
|
sendEvent(name: "trackData", value: "{\"station\": \"${it}\"}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateState() {
|
||||||
|
log.debug "updateState: ${params.message}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def installed() {
|
||||||
|
// subscribeAction("/subscribe")
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle commands
|
||||||
|
def refresh() {
|
||||||
|
log.debug "refreshing"
|
||||||
|
//def address = getCallBackAddress()
|
||||||
|
//sendCommand("subscribe=$address")
|
||||||
|
sendCommand("refresh")
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
log.debug "Turn AirPlay on"
|
||||||
|
sendCommand("airplay=on")
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
log.debug "Turn AirPlay off"
|
||||||
|
sendCommand("airplay=off")
|
||||||
|
}
|
||||||
|
|
||||||
|
def play() {
|
||||||
|
log.debug "Executing 'play'"
|
||||||
|
sendCommand("command=play")
|
||||||
|
}
|
||||||
|
|
||||||
|
def pause() {
|
||||||
|
log.debug "Executing 'pause'"
|
||||||
|
sendCommand("command=pause")
|
||||||
|
}
|
||||||
|
|
||||||
|
def stop() {
|
||||||
|
log.debug "Executing 'stop'"
|
||||||
|
sendCommand("command=stop")
|
||||||
|
}
|
||||||
|
|
||||||
|
def nextTrack() {
|
||||||
|
log.debug "Executing 'nextTrack'"
|
||||||
|
sendCommand("command=next")
|
||||||
|
}
|
||||||
|
|
||||||
|
def playTrack(String uri, metaData="") {
|
||||||
|
log.debug "Executing 'playTrack'"
|
||||||
|
sendCommand("playTrack&track=${uri}")
|
||||||
|
}
|
||||||
|
|
||||||
|
def playTrack(Map trackData) {
|
||||||
|
log.debug "Executing 'playTrack'"
|
||||||
|
sendCommand("playlist=${trackData.station}")
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
log.debug "Executing 'setLevel' to $value"
|
||||||
|
sendCommand("volume=$value")
|
||||||
|
}
|
||||||
|
|
||||||
|
def playText(String msg) {
|
||||||
|
log.debug "Executing 'playText'"
|
||||||
|
sendCommand("say=$msg")
|
||||||
|
}
|
||||||
|
|
||||||
|
def mute() {
|
||||||
|
log.debug "Executing 'mute'"
|
||||||
|
sendCommand("command=mute")
|
||||||
|
}
|
||||||
|
|
||||||
|
def previousTrack() {
|
||||||
|
log.debug "Executing 'previousTrack'"
|
||||||
|
sendCommand("command=previous")
|
||||||
|
}
|
||||||
|
|
||||||
|
def unmute() {
|
||||||
|
log.debug "Executing 'unmute'"
|
||||||
|
sendCommand("command=unmute")
|
||||||
|
}
|
||||||
|
|
||||||
|
def setTrack(String uri, metaData="") {
|
||||||
|
log.debug "Executing 'setTrack'"
|
||||||
|
sendCommand("track=$uri")
|
||||||
|
}
|
||||||
|
|
||||||
|
def resumeTrack() {
|
||||||
|
log.debug "Executing 'resumeTrack'"
|
||||||
|
// TODO: handle 'resumeTrack' command
|
||||||
|
}
|
||||||
|
|
||||||
|
def restoreTrack() {
|
||||||
|
log.debug "Executing 'restoreTrack'"
|
||||||
|
// TODO: handle 'restoreTrack' command
|
||||||
|
}
|
||||||
|
|
||||||
|
def playTrackAtVolume(String uri, volume) {
|
||||||
|
log.trace "playTrackAtVolume($uri, $volume)"
|
||||||
|
sendCommand("playTrack&track=${uri}&volume=${volume}")
|
||||||
|
}
|
||||||
|
|
||||||
|
def playTrackAndResume(uri, duration, volume=null) {
|
||||||
|
log.debug "playTrackAndResume($uri, $duration, $volume)"
|
||||||
|
def cmd = "playTrack&track=${uri}&resume"
|
||||||
|
if (volume) {
|
||||||
|
cmd += "&volume=${volume}"
|
||||||
|
}
|
||||||
|
sendCommand(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
def playTextAndResume(text, volume=null)
|
||||||
|
{
|
||||||
|
log.debug "playTextAndResume($text, $volume)"
|
||||||
|
def sound = textToSpeech(text)
|
||||||
|
playTrackAndResume(sound.uri, (sound.duration as Integer) + 1, volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
def playTrackAndRestore(uri, duration, volume=null) {
|
||||||
|
log.debug "playTrackAndResume($uri, $duration, $volume)"
|
||||||
|
def cmd = "playTrack&track=${uri}&restore"
|
||||||
|
if (volume) {
|
||||||
|
cmd += "&volume=${volume}"
|
||||||
|
}
|
||||||
|
sendCommand(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
def playTextAndRestore(text, volume=null)
|
||||||
|
{
|
||||||
|
log.debug "playTextAndResume($text, $volume)"
|
||||||
|
def sound = textToSpeech(text)
|
||||||
|
playTrackAndRestore(sound.uri, (sound.duration as Integer) + 1, volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
def playURL(theURL) {
|
||||||
|
log.debug "Executing 'playURL'"
|
||||||
|
sendCommand("url=$theURL")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def playSoundAndTrack(soundUri, duration, trackData, volume=null) {
|
||||||
|
log.debug "playSoundAndTrack($uri, $duration, $trackData, $volume)"
|
||||||
|
def cmd = "playTrack&track=${soundUri}&playlist=${trackData.station}"
|
||||||
|
if (volume) {
|
||||||
|
cmd += "&volume=${volume}"
|
||||||
|
}
|
||||||
|
sendCommand(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private functions used internally
|
||||||
|
private Integer convertHexToInt(hex) {
|
||||||
|
Integer.parseInt(hex,16)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String convertHexToIP(hex) {
|
||||||
|
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHostAddress() {
|
||||||
|
def parts = device.deviceNetworkId.split(":")
|
||||||
|
def ip = convertHexToIP(parts[0])
|
||||||
|
def port = convertHexToInt(parts[1])
|
||||||
|
return ip + ":" + port
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendCommand(command) {
|
||||||
|
def path = "/post.html"
|
||||||
|
|
||||||
|
def headers = [:]
|
||||||
|
headers.put("HOST", getHostAddress())
|
||||||
|
headers.put("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
def method = "POST"
|
||||||
|
|
||||||
|
def result = new physicalgraph.device.HubAction(
|
||||||
|
method: method,
|
||||||
|
path: path,
|
||||||
|
body: command,
|
||||||
|
headers: headers
|
||||||
|
)
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPlaylists() {
|
||||||
|
log.debug "in getPlaylists!!!"
|
||||||
|
def path = "/get.html?list=playlists"
|
||||||
|
|
||||||
|
def headers = [:]
|
||||||
|
headers.put("GET", getHostAddress())
|
||||||
|
headers.put("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
def method = "GET"
|
||||||
|
|
||||||
|
def result = new physicalgraph.device.HubAction(
|
||||||
|
method: method,
|
||||||
|
path: path,
|
||||||
|
headers: headers
|
||||||
|
)
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCallBackAddress()
|
||||||
|
{
|
||||||
|
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
|
||||||
|
}
|
||||||
|
|
||||||
|
private subscribeAction(path, callbackPath="") {
|
||||||
|
def address = device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
|
||||||
|
def parts = device.deviceNetworkId.split(":")
|
||||||
|
def ip = convertHexToIP(parts[0])
|
||||||
|
def port = convertHexToInt(parts[1])
|
||||||
|
ip = ip + ":" + port
|
||||||
|
|
||||||
|
def result = new physicalgraph.device.HubAction(
|
||||||
|
method: "SUBSCRIBE",
|
||||||
|
path: path,
|
||||||
|
headers: [
|
||||||
|
HOST: ip,
|
||||||
|
CALLBACK: "<http://${address}/obything>",
|
||||||
|
NT: "upnp:event",
|
||||||
|
TIMEOUT: "Second-3600"])
|
||||||
|
result
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* netatmo-basestation
|
||||||
|
*
|
||||||
|
* Copyright 2014 Brian Steere
|
||||||
|
*
|
||||||
|
* 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: "Netatmo Additional Module", namespace: "dianoga", author: "Brian Steere") {
|
||||||
|
capability "Relative Humidity Measurement"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
|
||||||
|
attribute "carbonDioxide", "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||||
|
state("temperature", label: '${currentValue}°', unit:"F", 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("humidity", "device.humidity", inactiveLabel: false) {
|
||||||
|
state "default", label:'${currentValue}%', unit:"Humidity"
|
||||||
|
}
|
||||||
|
valueTile("carbonDioxide", "device.carbonDioxide", inactiveLabel: false) {
|
||||||
|
state "default", label:'${currentValue}ppm', unit:"CO2"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.pressure", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"device.poll", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
main "temperature"
|
||||||
|
details(["temperature", "humidity", "carbonDioxide", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse events into attributes
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "Parsing '${description}'"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
parent.poll()
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* netatmo-basestation
|
||||||
|
*
|
||||||
|
* Copyright 2014 Brian Steere
|
||||||
|
*
|
||||||
|
* 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: "Netatmo Basestation", namespace: "dianoga", author: "Brian Steere") {
|
||||||
|
capability "Relative Humidity Measurement"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
|
||||||
|
attribute "carbonDioxide", "string"
|
||||||
|
attribute "noise", "string"
|
||||||
|
attribute "pressure", "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||||
|
state("temperature", label: '${currentValue}°', backgroundColors: [
|
||||||
|
[value: 31, color: "#153591"],
|
||||||
|
[value: 44, color: "#1e9cbb"],
|
||||||
|
[value: 59, color: "#90d2a7"],
|
||||||
|
[value: 74, color: "#44b621"],
|
||||||
|
[value: 84, color: "#f1d801"],
|
||||||
|
[value: 95, color: "#d04e00"],
|
||||||
|
[value: 96, color: "#bc2323"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
valueTile("humidity", "device.humidity", inactiveLabel: false) {
|
||||||
|
state "humidity", label:'${currentValue}%', unit:"Humidity"
|
||||||
|
}
|
||||||
|
valueTile("carbonDioxide", "device.carbonDioxide", inactiveLabel: false) {
|
||||||
|
state "carbonDioxide", label:'${currentValue}ppm', unit:"CO2", backgroundColors: [
|
||||||
|
[value: 600, color: "#44B621"],
|
||||||
|
[value: 999, color: "#ffcc00"],
|
||||||
|
[value: 1000, color: "#e86d13"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
valueTile("noise", "device.noise", inactiveLabel: false) {
|
||||||
|
state "noise", label:'${currentValue}db', unit:"Noise"
|
||||||
|
}
|
||||||
|
valueTile("pressure", "device.pressure", inactiveLabel: false) {
|
||||||
|
state "pressure", label:'${currentValue}mbar', unit:"Pressure"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.pressure", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"device.poll", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
main(["temperature", "humidity", "carbonDioxide", "noise", "pressure"])
|
||||||
|
details(["temperature", "humidity", "carbonDioxide", "noise", "pressure", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse events into attributes
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "Parsing '${description}'"
|
||||||
|
// TODO: handle 'humidity' attribute
|
||||||
|
// TODO: handle 'temperature' attribute
|
||||||
|
// TODO: handle 'carbonDioxide' attribute
|
||||||
|
// TODO: handle 'noise' attribute
|
||||||
|
// TODO: handle 'pressure' attribute
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
parent.poll()
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* netatmo-outdoor
|
||||||
|
*
|
||||||
|
* Copyright 2014 Brian Steere
|
||||||
|
*
|
||||||
|
* 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: "Netatmo Outdoor Module", namespace: "dianoga", author: "Brian Steere") {
|
||||||
|
capability "Relative Humidity Measurement"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||||
|
state("temperature", label: '${currentValue}°', backgroundColors: [
|
||||||
|
[value: 31, color: "#153591"],
|
||||||
|
[value: 44, color: "#1e9cbb"],
|
||||||
|
[value: 59, color: "#90d2a7"],
|
||||||
|
[value: 74, color: "#44b621"],
|
||||||
|
[value: 84, color: "#f1d801"],
|
||||||
|
[value: 95, color: "#d04e00"],
|
||||||
|
[value: 96, color: "#bc2323"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
valueTile("humidity", "device.humidity", inactiveLabel: false) {
|
||||||
|
state "humidity", label:'${currentValue}%', unit:"Humidity"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"device.poll", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
main (["temperature", "humidity"])
|
||||||
|
details(["temperature", "humidity", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse events into attributes
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "Parsing '${description}'"
|
||||||
|
// TODO: handle 'humidity' attribute
|
||||||
|
// TODO: handle 'temperature' attribute
|
||||||
|
// TODO: handle 'carbonDioxide' attribute
|
||||||
|
// TODO: handle 'noise' attribute
|
||||||
|
// TODO: handle 'pressure' attribute
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
parent.poll()
|
||||||
|
}
|
||||||
|
|
||||||
55
devicetypes/dianoga/netatmo-rain.src/netatmo-rain.groovy
Normal file
55
devicetypes/dianoga/netatmo-rain.src/netatmo-rain.groovy
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* netatmo-basestation
|
||||||
|
*
|
||||||
|
* Copyright 2014 Brian Steere
|
||||||
|
*
|
||||||
|
* 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: "Netatmo Rain", namespace: "dianoga", author: "Brian Steere") {
|
||||||
|
attribute "rain", "number"
|
||||||
|
attribute "rainSumHour", "number"
|
||||||
|
attribute "rainSumDay", "number"
|
||||||
|
attribute "units", "string"
|
||||||
|
|
||||||
|
command "poll"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
valueTile("rain", "device.rain", width: 2, height: 2, inactiveLabel: false) {
|
||||||
|
state "default", label:'${currentValue}'
|
||||||
|
}
|
||||||
|
valueTile("rainSumHour", "device.rainSumHour", inactiveLabel: false) {
|
||||||
|
state "default", label:'${currentValue}\nhour'
|
||||||
|
}
|
||||||
|
valueTile("rainSumDay", "device.rainSumDay", inactiveLabel: false) {
|
||||||
|
state "default", label:'${currentValue}\nday'
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.rain", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"refresh.poll", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
main (["rain", "rainSumHour", "rainSumDay"])
|
||||||
|
details(["rain", "rainSumHour", "rainSumDay", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse events into attributes
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "Parsing '${description}'"
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
parent.poll()
|
||||||
|
}
|
||||||
122
devicetypes/juano2310/jawbone-user.src/jawbone-user.groovy
Normal file
122
devicetypes/juano2310/jawbone-user.src/jawbone-user.groovy
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Jawbone-User
|
||||||
|
*
|
||||||
|
* Author: juano23@gmail.com
|
||||||
|
* Date: 2013-08-15
|
||||||
|
*/
|
||||||
|
// for the UI
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
// Automatically generated. Make future change here.
|
||||||
|
definition (name: "Jawbone User", namespace: "juano2310", author: "juano23@gmail.com") {
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Button"
|
||||||
|
capability "Sleep Sensor"
|
||||||
|
capability "Step Sensor"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "sleeping": "sleeping: 1"
|
||||||
|
status "not sleeping": "sleeping: 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("sleeping", "device.sleeping", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false) {
|
||||||
|
state("sleeping", label: "Sleeping", icon:"st.Bedroom.bedroom12", backgroundColor:"#ffffff")
|
||||||
|
state("not sleeping", label: "Awake", icon:"st.Health & Wellness.health12", backgroundColor:"#79b821")
|
||||||
|
}
|
||||||
|
standardTile("steps", "device.steps", width: 2, height: 2, canChangeIcon: false, canChangeBackground: false) {
|
||||||
|
state("steps", label: '${currentValue} Steps', icon:"st.Health & Wellness.health11", backgroundColor:"#ffffff")
|
||||||
|
}
|
||||||
|
standardTile("goal", "device.goal", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") {
|
||||||
|
state("goal", label: '${currentValue} Steps', icon:"st.Health & Wellness.health5", backgroundColor:"#ffffff")
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.steps", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"polling.poll", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
main "steps"
|
||||||
|
details(["steps", "goal", "sleeping", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def generateSleepingEvent(boolean sleeping) {
|
||||||
|
log.debug "Here in generateSleepingEvent!"
|
||||||
|
def value = formatValue(sleeping)
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = formatDescriptionText(linkText, sleeping)
|
||||||
|
def handlerName = getState(sleeping)
|
||||||
|
|
||||||
|
def results = [
|
||||||
|
name: "sleeping",
|
||||||
|
value: value,
|
||||||
|
unit: null,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: handlerName
|
||||||
|
]
|
||||||
|
|
||||||
|
sendEvent (results)
|
||||||
|
|
||||||
|
log.debug "Generating Sleep Event: ${results}"
|
||||||
|
|
||||||
|
|
||||||
|
def results2 = [
|
||||||
|
name: "button",
|
||||||
|
value: "held",
|
||||||
|
unit: null,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: "${linkText} button was pressed",
|
||||||
|
handlerName: "buttonHandler",
|
||||||
|
data: [buttonNumber: 1],
|
||||||
|
isStateChange: true
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
log.debug "Generating Button Event: ${results2}"
|
||||||
|
|
||||||
|
sendEvent (results2)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
log.debug "Executing 'poll'"
|
||||||
|
def results = parent.pollChild(this)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
def setMemberId (String memberId) {
|
||||||
|
log.debug "MemberId = ${memberId}"
|
||||||
|
state.jawboneMemberId = memberId
|
||||||
|
}
|
||||||
|
|
||||||
|
def getMemberId () {
|
||||||
|
log.debug "MemberId = ${state.jawboneMemberId}"
|
||||||
|
return(state.jawboneMemberId)
|
||||||
|
}
|
||||||
|
|
||||||
|
def uninstalled() {
|
||||||
|
log.debug "Uninstalling device, then app"
|
||||||
|
parent.app.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatValue(boolean sleeping) {
|
||||||
|
if (sleeping)
|
||||||
|
return "sleeping"
|
||||||
|
else
|
||||||
|
return "not sleeping"
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDescriptionText(String linkText, boolean sleeping) {
|
||||||
|
if (sleeping)
|
||||||
|
return "$linkText is sleeping"
|
||||||
|
else
|
||||||
|
return "$linkText is not sleeping"
|
||||||
|
}
|
||||||
|
|
||||||
|
private getState(boolean sleeping) {
|
||||||
|
if (sleeping)
|
||||||
|
return "sleeping"
|
||||||
|
else
|
||||||
|
return "not sleeping"
|
||||||
|
}
|
||||||
@@ -0,0 +1,710 @@
|
|||||||
|
/**
|
||||||
|
* Aeon Home Energy Meter + C3
|
||||||
|
*
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
// Automatically generated. Make future change here.
|
||||||
|
definition(name: "Aeon Home Energy Meter + C3", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Energy Meter"
|
||||||
|
capability "Power Meter"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
command "reset"
|
||||||
|
|
||||||
|
// fingerprint deviceId: "0x2101", inClusters: " 0x70,0x31,0x72,0x86,0x32,0x80,0x85,0x60"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
for (int i = 0; i <= 10000; i += 1000) {
|
||||||
|
status "power ${i} W": new physicalgraph.zwave.Zwave().meterV1.meterReport(
|
||||||
|
scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage()
|
||||||
|
}
|
||||||
|
for (int i = 0; i <= 100; i += 10) {
|
||||||
|
status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport(
|
||||||
|
scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tile definitions
|
||||||
|
tiles {
|
||||||
|
valueTile("power", "device.power", decoration: "flat") {
|
||||||
|
state "default", label: '${currentValue} W'
|
||||||
|
}
|
||||||
|
valueTile("energy", "device.energy", decoration: "flat") {
|
||||||
|
state "default", label: '${currentValue} kWh'
|
||||||
|
}
|
||||||
|
standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label: 'reset kWh', action: "reset"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh"
|
||||||
|
}
|
||||||
|
standardTile("configure", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "configure", label: '', action: "configuration.configure", icon: "st.secondary.configure"
|
||||||
|
}
|
||||||
|
|
||||||
|
PLATFORM_graphTile(name: "powerGraph", attribute: "device.power")
|
||||||
|
|
||||||
|
main(["power", "energy"])
|
||||||
|
details(["powerGraph", "power", "energy", "reset", "refresh", "configure"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// PREFERENCES
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
input name: "graphPrecision", type: "enum", title: "Graph Precision", description: "Daily", required: true, options: PLATFORM_graphPrecisionOptions(), defaultValue: "Daily"
|
||||||
|
input name: "graphType", type: "enum", title: "Graph Type", description: selectedGraphType(), required: false, options: PLATFORM_graphTypeOptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
def selectedGraphPrecision() {
|
||||||
|
graphPrecision ?: "Daily"
|
||||||
|
}
|
||||||
|
|
||||||
|
def selectedGraphType() {
|
||||||
|
graphType ?: "line"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// MAPPINGS
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
mappings {
|
||||||
|
path("/graph/:attribute") {
|
||||||
|
action:
|
||||||
|
[
|
||||||
|
GET: "renderGraph"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/graphDataSizes") { // for testing. remove before publishing
|
||||||
|
action:
|
||||||
|
[
|
||||||
|
GET: "graphDataSizes"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def graphDataSizes() { // for testing. remove before publishing
|
||||||
|
state.findAll { k, v -> k.startsWith("measure.") }.inject([:]) { attributes, attributeData ->
|
||||||
|
attributes[attributeData.key] = attributeData.value.inject([:]) { dateTypes, dateTypeData ->
|
||||||
|
dateTypes[dateTypeData.key] = dateTypeData.value.size()
|
||||||
|
dateTypes
|
||||||
|
}
|
||||||
|
attributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// Z-WAVE
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def result = null
|
||||||
|
def cmd = zwave.parse(description, [0x31: 1, 0x32: 1, 0x60: 3])
|
||||||
|
if (cmd) {
|
||||||
|
result = createEvent(zwaveEvent(cmd))
|
||||||
|
}
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
|
||||||
|
PLATFORM_migrateGraphDataIfNeeded()
|
||||||
|
PLATFORM_storeData(result.name, result.value)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.meterv1.MeterReport cmd) {
|
||||||
|
if (cmd.scale == 0) {
|
||||||
|
[name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]
|
||||||
|
} else if (cmd.scale == 1) {
|
||||||
|
[name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"]
|
||||||
|
} else {
|
||||||
|
[name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
// Handles all Z-Wave commands we aren't interested in
|
||||||
|
[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.meterV2.meterGet(scale: 0).format(),
|
||||||
|
zwave.meterV2.meterGet(scale: 2).format()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset() {
|
||||||
|
// No V1 available
|
||||||
|
return [
|
||||||
|
zwave.meterV2.meterReset().format(),
|
||||||
|
zwave.meterV2.meterGet(scale: 0).format()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
def cmd = delayBetween([
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 4).format(), // combined power in watts
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).format(), // every 5 min
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 8).format(), // combined energy in kWh
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 300).format(), // every 5 min
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 0).format(), // no third report
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 113, size: 4, scaledConfigurationValue: 300).format() // every 5 min
|
||||||
|
])
|
||||||
|
log.debug cmd
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// GRAPH RENDERING // written by developer. Alter at will
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
def renderGraph() {
|
||||||
|
|
||||||
|
def data = PLATFORM_fetchGraphData(params.attribute)
|
||||||
|
|
||||||
|
def totalData = data*.runningSum
|
||||||
|
|
||||||
|
def xValues = data*.unixTime
|
||||||
|
|
||||||
|
def yValues = [
|
||||||
|
Total: [color: "#49a201", data: totalData, type: selectedGraphType()]
|
||||||
|
]
|
||||||
|
|
||||||
|
PLATFORM_renderGraph(attribute: params.attribute, xValues: xValues, yValues: yValues, focus: "Total", label: "Watts")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: // ========================================================
|
||||||
|
// TODO: // PLATFORM CODE !!! DO NOT ALTER !!!
|
||||||
|
// TODO: // ========================================================
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// PLATFORM TILES
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
def PLATFORM_graphTile(Map tileParams) {
|
||||||
|
def cleanAttribute = tileParams.attribute - "device." - "capability."
|
||||||
|
htmlTile([name: tileParams.name, attribute: tileParams.attribute, action: "graph/${cleanAttribute}", width: 3, height: 2] + tileParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// PLATFORM GRAPH RENDERING
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
private PLATFORM_graphTypeOptions() {
|
||||||
|
[
|
||||||
|
"line", // DEFAULT
|
||||||
|
"spline",
|
||||||
|
"step",
|
||||||
|
"area",
|
||||||
|
"area-spline",
|
||||||
|
"area-step",
|
||||||
|
"bar",
|
||||||
|
"scatter",
|
||||||
|
"pie",
|
||||||
|
"donut",
|
||||||
|
"gauge",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private PLATFORM_renderGraph(graphParams) {
|
||||||
|
|
||||||
|
String attribute = graphParams.attribute
|
||||||
|
List xValues = graphParams.xValues
|
||||||
|
Map yValues = graphParams.yValues
|
||||||
|
String focus = graphParams.focus ?: ""
|
||||||
|
String label = graphParams.label ?: ""
|
||||||
|
|
||||||
|
/*
|
||||||
|
def xValues = [1, 2]
|
||||||
|
|
||||||
|
def yValues = [
|
||||||
|
High: [type: "spline", data: [5, 6], color: "#bc2323"],
|
||||||
|
Low: [type: "spline", data: [0, 1], color: "#153591"]
|
||||||
|
]
|
||||||
|
|
||||||
|
Available type values:
|
||||||
|
line // DEFAULT
|
||||||
|
spline
|
||||||
|
step
|
||||||
|
area
|
||||||
|
area-spline
|
||||||
|
area-step
|
||||||
|
bar
|
||||||
|
scatter
|
||||||
|
pie
|
||||||
|
donut
|
||||||
|
gauge
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
def graphData = PLATFORM_buildGraphData(xValues, yValues, label)
|
||||||
|
|
||||||
|
def legendData = yValues*.key
|
||||||
|
def focusJS = focus ? "chart.focus('${focus}')" : "// focus not specified"
|
||||||
|
def flowColumn = focus ?: yValues ? yValues.keySet().first() : null
|
||||||
|
|
||||||
|
def htmlTitle = "${(device.label ?: device.name)} ${attribute.capitalize()} Graph"
|
||||||
|
renderHTML(htmlTitle) { html ->
|
||||||
|
html.head {
|
||||||
|
"""
|
||||||
|
<!-- Load c3.css -->
|
||||||
|
<link href="https://www.dropbox.com/s/m6ptp72cw4nx0sp/c3.css?dl=1" rel="stylesheet" type="text/css">
|
||||||
|
|
||||||
|
<!-- Load d3.js and c3.js -->
|
||||||
|
<script src="https://www.dropbox.com/s/9x22jyfu5qyacpp/d3.v3.min.js?dl=1" charset="utf-8"></script>
|
||||||
|
<script src="https://www.dropbox.com/s/to7dtcn403l7mza/c3.js?dl=1"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function getDocumentHeight() {
|
||||||
|
var body = document.body;
|
||||||
|
var html = document.documentElement;
|
||||||
|
|
||||||
|
return html.clientHeight;
|
||||||
|
}
|
||||||
|
function getDocumentWidth() {
|
||||||
|
var body = document.body;
|
||||||
|
var html = document.documentElement;
|
||||||
|
|
||||||
|
return html.clientWidth;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.legend {
|
||||||
|
position: absolute;
|
||||||
|
width: 80%;
|
||||||
|
padding-left: 15%;
|
||||||
|
z-index: 999;
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
.legend span {
|
||||||
|
width: ${100 / yValues.size()}%;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
html.body {
|
||||||
|
"""
|
||||||
|
<div class="legend"></div>
|
||||||
|
<div id="chart" style="max-height: 120px; position: relative;"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
// Generate the chart
|
||||||
|
var chart = c3.generate(${graphData as grails.converters.JSON});
|
||||||
|
|
||||||
|
// Resize the chart to the size of the device tile
|
||||||
|
chart.resize({height:getDocumentHeight(), width:getDocumentWidth()});
|
||||||
|
|
||||||
|
// Focus data if specified
|
||||||
|
${focusJS}
|
||||||
|
|
||||||
|
// Update the chart when ${attribute} events are received
|
||||||
|
function ${attribute}(evt) {
|
||||||
|
var newValue = ['${flowColumn}'];
|
||||||
|
newValue.push(evt.value);
|
||||||
|
|
||||||
|
var newX = ['x'];
|
||||||
|
newX.push(evt.unixTime);
|
||||||
|
|
||||||
|
chart.flow({
|
||||||
|
columns: [
|
||||||
|
newX,
|
||||||
|
newValue
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the custom legend
|
||||||
|
d3.select('.legend').selectAll('span')
|
||||||
|
.data(${legendData as grails.converters.JSON})
|
||||||
|
.enter().append('span')
|
||||||
|
.attr('data-id', function (id) { return id; })
|
||||||
|
.html(function (id) { return id; })
|
||||||
|
.each(function (id) {
|
||||||
|
d3.select(this).style('background-color', chart.color(id));
|
||||||
|
})
|
||||||
|
.on('mouseover', function (id) {
|
||||||
|
chart.focus(id);
|
||||||
|
})
|
||||||
|
.on('mouseout', function (id) {
|
||||||
|
chart.revert();
|
||||||
|
})
|
||||||
|
.on('click', function (id) {
|
||||||
|
chart.toggle(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PLATFORM_buildGraphData(List xValues, Map yValues, String label = "") {
|
||||||
|
|
||||||
|
/*
|
||||||
|
def xValues = [1, 2]
|
||||||
|
|
||||||
|
def yValues = [
|
||||||
|
High: [type: "spline", data: [5, 6], color: "#bc2323"],
|
||||||
|
Low: [type: "spline", data: [0, 1], color: "#153591"]
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
|
||||||
|
[
|
||||||
|
interaction: [
|
||||||
|
enabled: false
|
||||||
|
],
|
||||||
|
bindto : '#chart',
|
||||||
|
padding : [
|
||||||
|
left : 30,
|
||||||
|
right : 30,
|
||||||
|
bottom: 0,
|
||||||
|
top : 0
|
||||||
|
],
|
||||||
|
legend : [
|
||||||
|
show: false,
|
||||||
|
// hide : false,//(yValues.keySet().size() < 2),
|
||||||
|
// position: 'inset',
|
||||||
|
// inset: [
|
||||||
|
// anchor: "top-right"
|
||||||
|
// ],
|
||||||
|
// item: [
|
||||||
|
// onclick: "do nothing" // (yValues.keySet().size() > 1) ? null : "do nothing"
|
||||||
|
// ]
|
||||||
|
],
|
||||||
|
data : [
|
||||||
|
x : "x",
|
||||||
|
columns: [(["x"] + xValues)] + yValues.collect { k, v -> [k] + v.data },
|
||||||
|
types : yValues.inject([:]) { total, current -> total[current.key] = current.value.type; return total },
|
||||||
|
colors : yValues.inject([:]) { total, current -> total[current.key] = current.value.color; return total }
|
||||||
|
],
|
||||||
|
axis : [
|
||||||
|
x: [
|
||||||
|
type: 'timeseries',
|
||||||
|
tick: [
|
||||||
|
centered: true,
|
||||||
|
culling : [max: 7],
|
||||||
|
fit : true,
|
||||||
|
format : PLATFORM_getGraphDateFormat()
|
||||||
|
// format: PLATFORM_getGraphDateFormatFunction() // throws securityException when trying to escape javascript
|
||||||
|
]
|
||||||
|
],
|
||||||
|
y: [
|
||||||
|
label : label,
|
||||||
|
padding: [
|
||||||
|
top: 50
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private PLATFORM_getGraphDateFormat(dateType = selectedGraphPrecision()) {
|
||||||
|
// https://github.com/mbostock/d3/wiki/Time-Formatting
|
||||||
|
def graphDateFormat
|
||||||
|
switch (dateType) {
|
||||||
|
case "Live":
|
||||||
|
graphDateFormat = "%I:%M" // hour (12-hour clock) as a decimal number [00,12] // AM or PM
|
||||||
|
break
|
||||||
|
case "Hourly":
|
||||||
|
graphDateFormat = "%I %p" // hour (12-hour clock) as a decimal number [00,12] // AM or PM
|
||||||
|
break
|
||||||
|
case "Daily":
|
||||||
|
graphDateFormat = "%a" // abbreviated weekday name
|
||||||
|
break
|
||||||
|
case "Monthly":
|
||||||
|
graphDateFormat = "%b" // abbreviated month name
|
||||||
|
break
|
||||||
|
case "Annually":
|
||||||
|
graphDateFormat = "%y" // year without century as a decimal number [00,99]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
graphDateFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
private String PLATFORM_getGraphDateFormatFunction(dateType = selectedGraphPrecision()) {
|
||||||
|
def graphDateFunction = "function(date) { return date; }"
|
||||||
|
switch (dateType) {
|
||||||
|
case "Live":
|
||||||
|
graphDateFunction = """
|
||||||
|
function(date) {
|
||||||
|
return.getMinutes();
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
break;
|
||||||
|
case "Hourly":
|
||||||
|
graphDateFunction = """ function(date) {
|
||||||
|
var hour = date.getHours();
|
||||||
|
if (hour == 0) {
|
||||||
|
return String(/12 am/).substring(1).slice(0,-1);
|
||||||
|
} else if (hour > 12) {
|
||||||
|
return hour -12 + String(/ pm/).substring(1).slice(0,-1);
|
||||||
|
} else {
|
||||||
|
return hour + String(/ am/).substring(1).slice(0,-1);
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
break
|
||||||
|
case "Daily":
|
||||||
|
graphDateFunction = """ function(date) {
|
||||||
|
var day = date.getDay();
|
||||||
|
switch(day) {
|
||||||
|
case 0: return String(/Sun/).substring(1).slice(0,-1);
|
||||||
|
case 1: return String(/Mon/).substring(1).slice(0,-1);
|
||||||
|
case 2: return String(/Tue/).substring(1).slice(0,-1);
|
||||||
|
case 3: return String(/Wed/).substring(1).slice(0,-1);
|
||||||
|
case 4: return String(/Thu/).substring(1).slice(0,-1);
|
||||||
|
case 5: return String(/Fri/).substring(1).slice(0,-1);
|
||||||
|
case 6: return String(/Sat/).substring(1).slice(0,-1);
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
break
|
||||||
|
case "Monthly":
|
||||||
|
graphDateFunction = """ function(date) {
|
||||||
|
var month = date.getMonth();
|
||||||
|
switch(month) {
|
||||||
|
case 0: return String(/Jan/).substring(1).slice(0,-1);
|
||||||
|
case 1: return String(/Feb/).substring(1).slice(0,-1);
|
||||||
|
case 2: return String(/Mar/).substring(1).slice(0,-1);
|
||||||
|
case 3: return String(/Apr/).substring(1).slice(0,-1);
|
||||||
|
case 4: return String(/May/).substring(1).slice(0,-1);
|
||||||
|
case 5: return String(/Jun/).substring(1).slice(0,-1);
|
||||||
|
case 6: return String(/Jul/).substring(1).slice(0,-1);
|
||||||
|
case 7: return String(/Aug/).substring(1).slice(0,-1);
|
||||||
|
case 8: return String(/Sep/).substring(1).slice(0,-1);
|
||||||
|
case 9: return String(/Oct/).substring(1).slice(0,-1);
|
||||||
|
case 10: return String(/Nov/).substring(1).slice(0,-1);
|
||||||
|
case 11: return String(/Dec/).substring(1).slice(0,-1);
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
break
|
||||||
|
case "Annually":
|
||||||
|
graphDateFunction = """
|
||||||
|
function(date) {
|
||||||
|
return.getFullYear();
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
break
|
||||||
|
}
|
||||||
|
groovy.json.StringEscapeUtils.escapeJavaScript(graphDateFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
private jsEscapeString(str = "") {
|
||||||
|
"String(/${str}/).substring(1).slice(0,-1);"
|
||||||
|
}
|
||||||
|
|
||||||
|
private PLATFORM_fetchGraphData(attribute) {
|
||||||
|
|
||||||
|
log.debug "PLATFORM_fetchGraphData(${attribute})"
|
||||||
|
|
||||||
|
/*
|
||||||
|
[
|
||||||
|
[
|
||||||
|
dateString: "2014-12-1",
|
||||||
|
unixTime: 1421931600000,
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
average: 5
|
||||||
|
],
|
||||||
|
...
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
|
||||||
|
def attributeBucket = state["measure.${attribute}"] ?: [:]
|
||||||
|
def dateType = selectedGraphPrecision()
|
||||||
|
attributeBucket[dateType]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// PLATFORM DATA STORAGE
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
private PLATFORM_graphPrecisionOptions() { ["Live", "Hourly", "Daily", "Monthly", "Annually"] }
|
||||||
|
|
||||||
|
private PLATFORM_storeData(attribute, value) {
|
||||||
|
PLATFORM_graphPrecisionOptions().each { dateType ->
|
||||||
|
PLATFORM_addDataToBucket(attribute, value, dateType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
[
|
||||||
|
Hourly: [
|
||||||
|
[
|
||||||
|
dateString: "2014-12-1",
|
||||||
|
unixTime: 1421931600000,
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
average: 5
|
||||||
|
],
|
||||||
|
...
|
||||||
|
],
|
||||||
|
...
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
|
||||||
|
private PLATFORM_addDataToBucket(attribute, value, dateType) {
|
||||||
|
|
||||||
|
def numberValue = value.toBigDecimal()
|
||||||
|
|
||||||
|
def attributeKey = "measure.${attribute}"
|
||||||
|
def attributeBucket = state[attributeKey] ?: [:]
|
||||||
|
|
||||||
|
def dateTypeBucket = attributeBucket[dateType] ?: []
|
||||||
|
|
||||||
|
def now = new Date()
|
||||||
|
def itemDateString = now.format("PLATFORM_get${dateType}Format"())
|
||||||
|
def item = dateTypeBucket.find { it.dateString == itemDateString }
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
// no entry for this data point yet, fill with initial values
|
||||||
|
item = [:]
|
||||||
|
item.average = numberValue
|
||||||
|
item.runningSum = numberValue
|
||||||
|
item.runningCount = 1
|
||||||
|
item.min = numberValue
|
||||||
|
item.max = numberValue
|
||||||
|
item.unixTime = now.getTime()
|
||||||
|
item.dateString = itemDateString
|
||||||
|
|
||||||
|
// add the new data point
|
||||||
|
dateTypeBucket << item
|
||||||
|
|
||||||
|
// clear out old data points
|
||||||
|
def old = PLATFORM_getOldDateString(dateType)
|
||||||
|
if (old) { // annual data never gets cleared
|
||||||
|
dateTypeBucket.findAll { it.unixTime < old }.each { dateTypeBucket.remove(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// limit the size of the bucket. Live data can stack up fast
|
||||||
|
def sizeLimit = 25
|
||||||
|
if (dateTypeBucket.size() > sizeLimit) {
|
||||||
|
dateTypeBucket = dateTypeBucket[-sizeLimit..-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
//re-calculate average/min/max for this bucket
|
||||||
|
item.runningSum = (item.runningSum.toBigDecimal()) + numberValue
|
||||||
|
item.runningCount = item.runningCount.toInteger() + 1
|
||||||
|
item.average = item.runningSum.toBigDecimal() / item.runningCount.toInteger()
|
||||||
|
|
||||||
|
if (item.min == null) {
|
||||||
|
item.min = numberValue
|
||||||
|
} else if (numberValue < item.min.toBigDecimal()) {
|
||||||
|
item.min = numberValue
|
||||||
|
}
|
||||||
|
if (item.max == null) {
|
||||||
|
item.max = numberValue
|
||||||
|
} else if (numberValue > item.max.toBigDecimal()) {
|
||||||
|
item.max = numberValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeBucket[dateType] = dateTypeBucket
|
||||||
|
state[attributeKey] = attributeBucket
|
||||||
|
}
|
||||||
|
|
||||||
|
private PLATFORM_getOldDateString(dateType) {
|
||||||
|
def now = new Date()
|
||||||
|
def date
|
||||||
|
switch (dateType) {
|
||||||
|
case "Live":
|
||||||
|
date = now.getTime() - 60 * 60 * 1000 // 1h * 60m * 60s * 1000ms // 1 hour
|
||||||
|
break
|
||||||
|
case "Hourly":
|
||||||
|
date = (now - 1).getTime()
|
||||||
|
break
|
||||||
|
case "Daily":
|
||||||
|
date = (now - 10).getTime()
|
||||||
|
break
|
||||||
|
case "Monthly":
|
||||||
|
date = (now - 30).getTime()
|
||||||
|
break
|
||||||
|
case "Annually":
|
||||||
|
break
|
||||||
|
}
|
||||||
|
date
|
||||||
|
}
|
||||||
|
|
||||||
|
private PLATFORM_getLiveFormat() { "HH:mm:ss" }
|
||||||
|
|
||||||
|
private PLATFORM_getHourlyFormat() { "yyyy-MM-dd'T'HH" }
|
||||||
|
|
||||||
|
private PLATFORM_getDailyFormat() { "yyyy-MM-dd" }
|
||||||
|
|
||||||
|
private PLATFORM_getMonthlyFormat() { "yyyy-MM" }
|
||||||
|
|
||||||
|
private PLATFORM_getAnnuallyFormat() { "yyyy" }
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// PLATFORM GRAPH DATA MIGRATION
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
private PLATFORM_migrateGraphDataIfNeeded() {
|
||||||
|
if (!state.hasMigratedOldGraphData) {
|
||||||
|
def acceptableKeys = PLATFORM_graphPrecisionOptions()
|
||||||
|
def needsMigration = state.findAll { k, v -> v.keySet().findAll { !acceptableKeys.contains(it) } }.keySet()
|
||||||
|
needsMigration.each { PLATFORM_migrateGraphData(it) }
|
||||||
|
state.hasMigratedOldGraphData = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PLATFORM_migrateGraphData(attribute) {
|
||||||
|
|
||||||
|
log.trace "about to migrate ${attribute}"
|
||||||
|
|
||||||
|
def attributeBucket = state[attribute] ?: [:]
|
||||||
|
def migratedAttributeBucket = [:]
|
||||||
|
|
||||||
|
attributeBucket.findAll { k, v -> !PLATFORM_graphPrecisionOptions().contains(k) }.each { oldDateString, oldItem ->
|
||||||
|
|
||||||
|
def dateType = oldDateString.contains('T') ? "Hourly" : PLATFORM_graphPrecisionOptions().find {
|
||||||
|
"PLATFORM_get${it}Format"().size() == oldDateString.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
def dateTypeFormat = "PLATFORM_get${dateType}Format"()
|
||||||
|
|
||||||
|
def newBucket = attributeBucket[dateType] ?: []
|
||||||
|
/*
|
||||||
|
def existingNewItem = newBucket.find { it.dateString == oldDateString }
|
||||||
|
if (existingNewItem) {
|
||||||
|
newBucket.remove(existingNewItem)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
def newItem = [
|
||||||
|
min : oldItem.min,
|
||||||
|
max : oldItem.max,
|
||||||
|
average : oldItem.average,
|
||||||
|
runningSum : oldItem.runningSum,
|
||||||
|
runningCount: oldItem.runningCount,
|
||||||
|
dateString : oldDateString,
|
||||||
|
unixTime : new Date().parse(dateTypeFormat, oldDateString).getTime()
|
||||||
|
]
|
||||||
|
|
||||||
|
newBucket << newItem
|
||||||
|
migratedAttributeBucket[dateType] = newBucket
|
||||||
|
}
|
||||||
|
|
||||||
|
state[attribute] = migratedAttributeBucket
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* Aeon Home Energy Meter
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
*
|
||||||
|
* Date: 2013-05-30
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Aeon Home Energy Meter", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Energy Meter"
|
||||||
|
capability "Power Meter"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
command "reset"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x2101", inClusters: " 0x70,0x31,0x72,0x86,0x32,0x80,0x85,0x60"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
for (int i = 0; i <= 10000; i += 1000) {
|
||||||
|
status "power ${i} W": new physicalgraph.zwave.Zwave().meterV1.meterReport(
|
||||||
|
scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage()
|
||||||
|
}
|
||||||
|
for (int i = 0; i <= 100; i += 10) {
|
||||||
|
status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport(
|
||||||
|
scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tile definitions
|
||||||
|
tiles {
|
||||||
|
valueTile("power", "device.power", decoration: "flat") {
|
||||||
|
state "default", label:'${currentValue} W'
|
||||||
|
}
|
||||||
|
valueTile("energy", "device.energy", decoration: "flat") {
|
||||||
|
state "default", label:'${currentValue} kWh'
|
||||||
|
}
|
||||||
|
standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'reset kWh', action:"reset"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
standardTile("configure", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||||
|
}
|
||||||
|
|
||||||
|
main (["power","energy"])
|
||||||
|
details(["power","energy", "reset","refresh", "configure"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def result = null
|
||||||
|
def cmd = zwave.parse(description, [0x31: 1, 0x32: 1, 0x60: 3])
|
||||||
|
if (cmd) {
|
||||||
|
result = createEvent(zwaveEvent(cmd))
|
||||||
|
}
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.meterv1.MeterReport cmd) {
|
||||||
|
if (cmd.scale == 0) {
|
||||||
|
[name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]
|
||||||
|
} else if (cmd.scale == 1) {
|
||||||
|
[name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
// Handles all Z-Wave commands we aren't interested in
|
||||||
|
[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.meterV2.meterGet(scale: 0).format(),
|
||||||
|
zwave.meterV2.meterGet(scale: 2).format()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset() {
|
||||||
|
// No V1 available
|
||||||
|
return [
|
||||||
|
zwave.meterV2.meterReset().format(),
|
||||||
|
zwave.meterV2.meterGet(scale: 0).format()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
def cmd = delayBetween([
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 4).format(), // combined power in watts
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).format(), // every 5 min
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 8).format(), // combined energy in kWh
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 300).format(), // every 5 min
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 0).format(), // no third report
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 113, size: 4, scaledConfigurationValue: 300).format() // every 5 min
|
||||||
|
])
|
||||||
|
log.debug cmd
|
||||||
|
cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Aeon Illuminator Module", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Energy Meter"
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
command "reset"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x1104", inClusters: "0x26,0x32,0x27,0x2C,0x2B,0x70,0x85,0x72,0x86", outClusters: "0x82"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "on": "command: 2003, payload: FF"
|
||||||
|
status "off": "command: 2003, payload: 00"
|
||||||
|
status "09%": "command: 2003, payload: 09"
|
||||||
|
status "10%": "command: 2003, payload: 0A"
|
||||||
|
status "33%": "command: 2003, payload: 21"
|
||||||
|
status "66%": "command: 2003, payload: 42"
|
||||||
|
status "99%": "command: 2003, payload: 63"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "2001FF,delay 5000,2602": "command: 2603, payload: FF"
|
||||||
|
reply "200100,delay 5000,2602": "command: 2603, payload: 00"
|
||||||
|
reply "200119,delay 5000,2602": "command: 2603, payload: 19"
|
||||||
|
reply "200132,delay 5000,2602": "command: 2603, payload: 32"
|
||||||
|
reply "20014B,delay 5000,2602": "command: 2603, payload: 4B"
|
||||||
|
reply "200163,delay 5000,2602": "command: 2603, payload: 63"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821"
|
||||||
|
state "turningOff", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 2, width: 1, inactiveLabel: false) {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
valueTile("energy", "device.energy", decoration: "flat") {
|
||||||
|
state "default", label:'${currentValue} kWh'
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch", "level"])
|
||||||
|
details(["switch", "levelSliderControl","energy","refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def item1 = [
|
||||||
|
canBeCurrentState: false,
|
||||||
|
linkText: getLinkText(device),
|
||||||
|
isStateChange: false,
|
||||||
|
displayed: false,
|
||||||
|
descriptionText: description,
|
||||||
|
value: description
|
||||||
|
]
|
||||||
|
def result
|
||||||
|
def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1, 0x32: 2])
|
||||||
|
if (cmd) {
|
||||||
|
result = createEvent(cmd, item1)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
item1.displayed = displayed(description, item1.isStateChange)
|
||||||
|
result = [item1]
|
||||||
|
}
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) {
|
||||||
|
def result = doCreateEvent(cmd, item1)
|
||||||
|
for (int i = 0; i < result.size(); i++) {
|
||||||
|
result[i].type = "physical"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd, Map item1) {
|
||||||
|
def result = doCreateEvent(cmd, item1)
|
||||||
|
result[0].descriptionText = "${item1.linkText} is ${item1.value}"
|
||||||
|
result[0].handlerName = cmd.value ? "statusOn" : "statusOff"
|
||||||
|
for (int i = 0; i < result.size(); i++) {
|
||||||
|
result[i].type = "digital"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) {
|
||||||
|
def result = [item1]
|
||||||
|
|
||||||
|
item1.name = "switch"
|
||||||
|
item1.value = cmd.value ? "on" : "off"
|
||||||
|
item1.handlerName = item1.value
|
||||||
|
item1.descriptionText = "${item1.linkText} was turned ${item1.value}"
|
||||||
|
item1.canBeCurrentState = true
|
||||||
|
item1.isStateChange = isStateChange(device, item1.name, item1.value)
|
||||||
|
item1.displayed = item1.isStateChange
|
||||||
|
|
||||||
|
if (cmd.value > 15) {
|
||||||
|
def item2 = new LinkedHashMap(item1)
|
||||||
|
item2.name = "level"
|
||||||
|
item2.value = cmd.value as String
|
||||||
|
item2.unit = "%"
|
||||||
|
item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %"
|
||||||
|
item2.canBeCurrentState = true
|
||||||
|
item2.isStateChange = isStateChange(device, item2.name, item2.value)
|
||||||
|
item2.displayed = false
|
||||||
|
result << item2
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.meterv2.MeterReport cmd, Map item1)
|
||||||
|
{
|
||||||
|
if (cmd.scale == 0) {
|
||||||
|
createEvent([name: "energy", value: cmd.scaledMeterValue, unit: "kWh"])
|
||||||
|
} else if (cmd.scale == 1) {
|
||||||
|
createEvent([name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
createEvent([name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.Command cmd, Map map) {
|
||||||
|
// Handles any Z-Wave commands we aren't interested in
|
||||||
|
log.debug "UNHANDLED COMMAND $cmd"
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
log.info "on"
|
||||||
|
delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
delayBetween ([zwave.basicV1.basicSet(value: 0x00).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
delayBetween ([zwave.basicV1.basicSet(value: value).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value, duration) {
|
||||||
|
def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60)
|
||||||
|
zwave.switchMultilevelV2.switchMultilevelSet(value: value, dimmingDuration: dimmingDuration).format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset() {
|
||||||
|
return [
|
||||||
|
zwave.meterV2.meterReset().format(),
|
||||||
|
zwave.meterV2.meterGet().format()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 8).format(), // energy in kWh
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).format(), // every 5 min
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 0).format(),
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 0).format()
|
||||||
|
])
|
||||||
|
}
|
||||||
120
devicetypes/smartthings/aeon-key-fob.src/aeon-key-fob.groovy
Normal file
120
devicetypes/smartthings/aeon-key-fob.src/aeon-key-fob.groovy
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Aeon Key Fob", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Button"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Battery"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x0101", inClusters: "0x86,0x72,0x70,0x80,0x84,0x85"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "button 1 pushed": "command: 2001, payload: 01"
|
||||||
|
status "button 1 held": "command: 2001, payload: 15"
|
||||||
|
status "button 2 pushed": "command: 2001, payload: 29"
|
||||||
|
status "button 2 held": "command: 2001, payload: 3D"
|
||||||
|
status "button 3 pushed": "command: 2001, payload: 51"
|
||||||
|
status "button 3 held": "command: 2001, payload: 65"
|
||||||
|
status "button 4 pushed": "command: 2001, payload: 79"
|
||||||
|
status "button 4 held": "command: 2001, payload: 8D"
|
||||||
|
status "wakeup": "command: 8407, payload: "
|
||||||
|
}
|
||||||
|
tiles {
|
||||||
|
standardTile("button", "device.button", width: 2, height: 2) {
|
||||||
|
state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff"
|
||||||
|
state "button 1 pushed", label: "pushed #1", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#79b821"
|
||||||
|
state "button 2 pushed", label: "pushed #2", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#79b821"
|
||||||
|
state "button 3 pushed", label: "pushed #3", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#79b821"
|
||||||
|
state "button 4 pushed", label: "pushed #4", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#79b821"
|
||||||
|
state "button 1 held", label: "held #1", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffa81e"
|
||||||
|
state "button 2 held", label: "held #2", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffa81e"
|
||||||
|
state "button 3 held", label: "held #3", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffa81e"
|
||||||
|
state "button 4 held", label: "held #4", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffa81e"
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
main "button"
|
||||||
|
details(["button", "battery"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def results = []
|
||||||
|
if (description.startsWith("Err")) {
|
||||||
|
results = createEvent(descriptionText:description, displayed:true)
|
||||||
|
} else {
|
||||||
|
def cmd = zwave.parse(description, [0x2B: 1, 0x80: 1, 0x84: 1])
|
||||||
|
if(cmd) results += zwaveEvent(cmd)
|
||||||
|
if(!results) results = [ descriptionText: cmd, displayed: false ]
|
||||||
|
}
|
||||||
|
// log.debug("Parsed '$description' to $results")
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) {
|
||||||
|
def results = [createEvent(descriptionText: "$device.displayName woke up", isStateChange: false)]
|
||||||
|
|
||||||
|
def prevBattery = device.currentState("battery")
|
||||||
|
if (!prevBattery || (new Date().time - prevBattery.date.time)/60000 >= 60 * 53) {
|
||||||
|
results << response(zwave.batteryV1.batteryGet().format())
|
||||||
|
}
|
||||||
|
results += configurationCmds().collect{ response(it) }
|
||||||
|
results << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format())
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
def buttonEvent(button, held) {
|
||||||
|
button = button as Integer
|
||||||
|
if (held) {
|
||||||
|
createEvent(name: "button", value: "held", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was held", isStateChange: true)
|
||||||
|
} else {
|
||||||
|
createEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sceneactivationv1.SceneActivationSet cmd) {
|
||||||
|
Integer button = ((cmd.sceneId + 1) / 2) as Integer
|
||||||
|
Boolean held = !(cmd.sceneId % 2)
|
||||||
|
buttonEvent(button, held)
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
} else {
|
||||||
|
map.value = cmd.batteryLevel
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
[ descriptionText: "$device.displayName: $cmd", linkText:device.displayName, displayed: false ]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configurationCmds() {
|
||||||
|
[ zwave.configurationV1.configurationSet(parameterNumber: 250, scaledConfigurationValue: 1).format(),
|
||||||
|
zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId).format() ]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
def cmd = configurationCmds()
|
||||||
|
log.debug("Sending configuration: $cmd")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
109
devicetypes/smartthings/aeon-minimote.src/aeon-minimote.groovy
Normal file
109
devicetypes/smartthings/aeon-minimote.src/aeon-minimote.groovy
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Aeon Minimote", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Button"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x0101", inClusters: "0x86,0x72,0x70,0x9B", outClusters: "0x26,0x2B"
|
||||||
|
fingerprint deviceId: "0x0101", inClusters: "0x86,0x72,0x70,0x9B,0x85,0x84", outClusters: "0x26" // old style with numbered buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "button 1 pushed": "command: 2001, payload: 01"
|
||||||
|
status "button 1 held": "command: 2001, payload: 15"
|
||||||
|
status "button 2 pushed": "command: 2001, payload: 29"
|
||||||
|
status "button 2 held": "command: 2001, payload: 3D"
|
||||||
|
status "button 3 pushed": "command: 2001, payload: 51"
|
||||||
|
status "button 3 held": "command: 2001, payload: 65"
|
||||||
|
status "button 4 pushed": "command: 2001, payload: 79"
|
||||||
|
status "button 4 held": "command: 2001, payload: 8D"
|
||||||
|
status "wakeup": "command: 8407, payload: "
|
||||||
|
}
|
||||||
|
tiles {
|
||||||
|
standardTile("button", "device.button", width: 2, height: 2) {
|
||||||
|
state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff"
|
||||||
|
}
|
||||||
|
main "button"
|
||||||
|
details(["button"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def results = []
|
||||||
|
if (description.startsWith("Err")) {
|
||||||
|
results = createEvent(descriptionText:description, displayed:true)
|
||||||
|
} else {
|
||||||
|
def cmd = zwave.parse(description, [0x2B: 1, 0x80: 1, 0x84: 1])
|
||||||
|
if(cmd) results += zwaveEvent(cmd)
|
||||||
|
if(!results) results = [ descriptionText: cmd, displayed: false ]
|
||||||
|
}
|
||||||
|
// log.debug("Parsed '$description' to $results")
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) {
|
||||||
|
def results = [createEvent(descriptionText: "$device.displayName woke up", isStateChange: false)]
|
||||||
|
|
||||||
|
results += configurationCmds().collect{ response(it) }
|
||||||
|
results << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format())
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
def buttonEvent(button, held) {
|
||||||
|
button = button as Integer
|
||||||
|
if (held) {
|
||||||
|
createEvent(name: "button", value: "held", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was held", isStateChange: true)
|
||||||
|
} else {
|
||||||
|
createEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sceneactivationv1.SceneActivationSet cmd) {
|
||||||
|
Integer button = ((cmd.sceneId + 1) / 2) as Integer
|
||||||
|
Boolean held = !(cmd.sceneId % 2)
|
||||||
|
buttonEvent(button, held)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||||
|
Integer button = (cmd.value / 40 + 1) as Integer
|
||||||
|
Boolean held = (button * 40 - cmd.value) <= 20
|
||||||
|
buttonEvent(button, held)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
[ descriptionText: "$device.displayName: $cmd", linkText:device.displayName, displayed: false ]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configurationCmds() {
|
||||||
|
def cmds = []
|
||||||
|
def hubId = zwaveHubNodeId
|
||||||
|
(1..4).each { button ->
|
||||||
|
cmds << zwave.configurationV1.configurationSet(parameterNumber: 240+button, scaledConfigurationValue: 1).format()
|
||||||
|
}
|
||||||
|
(1..4).each { button ->
|
||||||
|
cmds << zwave.configurationV1.configurationSet(parameterNumber: (button-1)*40, configurationValue: [hubId, (button-1)*40 + 1, 0, 0]).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(parameterNumber: (button-1)*40 + 20, configurationValue: [hubId, (button-1)*40 + 21, 0, 0]).format()
|
||||||
|
}
|
||||||
|
cmds
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
def cmds = configurationCmds()
|
||||||
|
log.debug("Sending configuration: $cmds")
|
||||||
|
return cmds
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "Aeon Multisensor 6", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Motion Sensor"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
capability "Relative Humidity Measurement"
|
||||||
|
capability "Illuminance Measurement"
|
||||||
|
capability "Ultraviolet Index"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Battery"
|
||||||
|
|
||||||
|
attribute "tamper", "enum", ["detected", "clear"]
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "no motion" : "command: 9881, payload: 00300300"
|
||||||
|
status "motion" : "command: 9881, payload: 003003FF"
|
||||||
|
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "temperature ${i}F": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
|
||||||
|
new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(
|
||||||
|
scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1)
|
||||||
|
).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "humidity ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
|
||||||
|
new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(scaledSensorValue: i, sensorType: 5)
|
||||||
|
).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i in [0, 20, 89, 100, 200, 500, 1000]) {
|
||||||
|
status "illuminance ${i} lux": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
|
||||||
|
new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(scaledSensorValue: i, sensorType: 3)
|
||||||
|
).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i in [0, 5, 10, 15, 50, 99, 100]) {
|
||||||
|
status "battery ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
|
||||||
|
new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i)
|
||||||
|
).incomingMessage()
|
||||||
|
}
|
||||||
|
status "low battery alert": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
|
||||||
|
new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: 255)
|
||||||
|
).incomingMessage()
|
||||||
|
|
||||||
|
status "wake up" : "command: 8407, payload: "
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("motion", "device.motion", width: 2, 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: 32, color: "#153591"],
|
||||||
|
[value: 44, color: "#1e9cbb"],
|
||||||
|
[value: 59, color: "#90d2a7"],
|
||||||
|
[value: 74, color: "#44b621"],
|
||||||
|
[value: 84, color: "#f1d801"],
|
||||||
|
[value: 92, color: "#d04e00"],
|
||||||
|
[value: 98, color: "#bc2323"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
valueTile("humidity", "device.humidity", inactiveLabel: false) {
|
||||||
|
state "humidity", label:'${currentValue}% humidity', unit:""
|
||||||
|
}
|
||||||
|
valueTile("illuminance", "device.illuminance", inactiveLabel: false) {
|
||||||
|
state "luminosity", label:'${currentValue} ${unit}', unit:"lux"
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["motion", "temperature", "humidity", "illuminance"])
|
||||||
|
details(["motion", "temperature", "humidity", "illuminance", "battery"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated()
|
||||||
|
{
|
||||||
|
if (state.sec && !isConfigured()) {
|
||||||
|
// in case we miss the SCSR
|
||||||
|
response(configure())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description)
|
||||||
|
{
|
||||||
|
def result = null
|
||||||
|
if (description.startsWith("Err 106")) {
|
||||||
|
state.sec = 0
|
||||||
|
result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true,
|
||||||
|
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 != "updated") {
|
||||||
|
def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1])
|
||||||
|
if (cmd) {
|
||||||
|
result = zwaveEvent(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug "Parsed '${description}' to ${result.inspect()}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
|
||||||
|
{
|
||||||
|
def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)]
|
||||||
|
|
||||||
|
if (!isConfigured()) {
|
||||||
|
// we're still in the process of configuring a newly joined device
|
||||||
|
log.debug("late configure")
|
||||||
|
result += response(configure())
|
||||||
|
} else {
|
||||||
|
result += response(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||||
|
def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x30: 2, 0x84: 1])
|
||||||
|
state.sec = 1
|
||||||
|
log.debug "encapsulated: ${encapsulatedCommand}"
|
||||||
|
if (encapsulatedCommand) {
|
||||||
|
zwaveEvent(encapsulatedCommand)
|
||||||
|
} else {
|
||||||
|
log.warn "Unable to extract encapsulated cmd from $cmd"
|
||||||
|
createEvent(descriptionText: cmd.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
|
||||||
|
response(configure())
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||||
|
def map = [ name: "battery", unit: "%" ]
|
||||||
|
if (cmd.batteryLevel == 0xFF) {
|
||||||
|
map.value = 1
|
||||||
|
map.descriptionText = "${device.displayName} battery is low"
|
||||||
|
map.isStateChange = true
|
||||||
|
} else {
|
||||||
|
map.value = cmd.batteryLevel
|
||||||
|
}
|
||||||
|
state.lastbatt = now()
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
switch (cmd.sensorType) {
|
||||||
|
case 1:
|
||||||
|
map.name = "temperature"
|
||||||
|
def cmdScale = cmd.scale == 1 ? "F" : "C"
|
||||||
|
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
|
||||||
|
map.unit = getTemperatureScale()
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
map.name = "illuminance"
|
||||||
|
map.value = cmd.scaledSensorValue.toInteger()
|
||||||
|
map.unit = "lux"
|
||||||
|
break
|
||||||
|
case 5:
|
||||||
|
map.name = "humidity"
|
||||||
|
map.value = cmd.scaledSensorValue.toInteger()
|
||||||
|
map.unit = "%"
|
||||||
|
break
|
||||||
|
case 0x1B:
|
||||||
|
map.name = "ultravioletIndex"
|
||||||
|
map.value = cmd.scaledSensorValue.toInteger()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
map.descriptionText = cmd.toString()
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def motionEvent(value) {
|
||||||
|
def map = [name: "motion"]
|
||||||
|
if (value) {
|
||||||
|
map.value = "active"
|
||||||
|
map.descriptionText = "$device.displayName detected motion"
|
||||||
|
} else {
|
||||||
|
map.value = "inactive"
|
||||||
|
map.descriptionText = "$device.displayName motion has stopped"
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) {
|
||||||
|
setConfigured()
|
||||||
|
motionEvent(cmd.sensorValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||||
|
motionEvent(cmd.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
|
||||||
|
def result = []
|
||||||
|
if (cmd.notificationType == 7) {
|
||||||
|
switch (cmd.event) {
|
||||||
|
case 0:
|
||||||
|
result << motionEvent(0)
|
||||||
|
result << createEvent(name: "tamper", value: "clear", displayed: false)
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was moved")
|
||||||
|
break
|
||||||
|
case 7:
|
||||||
|
result << motionEvent(1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result << createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
// This sensor joins as a secure device if you double-click the button to include it
|
||||||
|
if (device.device.rawDescription =~ /98/ && !state.sec) {
|
||||||
|
log.debug "Multi 6 not sending configure until secure"
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
log.debug "Multi 6 configure()"
|
||||||
|
def request = [
|
||||||
|
// send no-motion report 20 seconds after motion stops
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: 20),
|
||||||
|
|
||||||
|
// report every 8 minutes (threshold reports don't work on battery power)
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 8*60),
|
||||||
|
|
||||||
|
// report automatically on threshold change
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1),
|
||||||
|
|
||||||
|
zwave.batteryV1.batteryGet(),
|
||||||
|
zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C),
|
||||||
|
]
|
||||||
|
commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]
|
||||||
|
}
|
||||||
|
|
||||||
|
private setConfigured() {
|
||||||
|
updateDataValue("configured", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
private isConfigured() {
|
||||||
|
getDataValue("configured") == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
private command(physicalgraph.zwave.Command cmd) {
|
||||||
|
if (state.sec) {
|
||||||
|
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||||
|
} else {
|
||||||
|
cmd.format()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private commands(commands, delay=200) {
|
||||||
|
delayBetween(commands.collect{ command(it) }, delay)
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Aeon Multisensor Gen5", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Motion Sensor"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
capability "Relative Humidity Measurement"
|
||||||
|
capability "Illuminance Measurement"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Battery"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x98,0x7A", outClusters:"0x5A"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "no motion" : "command: 9881, payload: 00300300"
|
||||||
|
status "motion" : "command: 9881, payload: 003003FF"
|
||||||
|
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "temperature ${i}F": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
|
||||||
|
new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(
|
||||||
|
scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1)
|
||||||
|
).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "humidity ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
|
||||||
|
new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(scaledSensorValue: i, sensorType: 5)
|
||||||
|
).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i in [0, 20, 89, 100, 200, 500, 1000]) {
|
||||||
|
status "illuminance ${i} lux": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
|
||||||
|
new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(scaledSensorValue: i, sensorType: 3)
|
||||||
|
).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i in [0, 5, 10, 15, 50, 99, 100]) {
|
||||||
|
status "battery ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
|
||||||
|
new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i)
|
||||||
|
).incomingMessage()
|
||||||
|
}
|
||||||
|
status "low battery alert": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
|
||||||
|
new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: 255)
|
||||||
|
).incomingMessage()
|
||||||
|
|
||||||
|
status "wake up" : "command: 8407, payload: "
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("motion", "device.motion", width: 2, 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: 32, color: "#153591"],
|
||||||
|
[value: 44, color: "#1e9cbb"],
|
||||||
|
[value: 59, color: "#90d2a7"],
|
||||||
|
[value: 74, color: "#44b621"],
|
||||||
|
[value: 84, color: "#f1d801"],
|
||||||
|
[value: 92, color: "#d04e00"],
|
||||||
|
[value: 98, color: "#bc2323"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
valueTile("humidity", "device.humidity", inactiveLabel: false) {
|
||||||
|
state "humidity", label:'${currentValue}% humidity', unit:""
|
||||||
|
}
|
||||||
|
valueTile("illuminance", "device.illuminance", inactiveLabel: false) {
|
||||||
|
state "luminosity", label:'${currentValue} ${unit}', unit:"lux"
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
standardTile("configureAfterSecure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "configure", label:'', action:"configureAfterSecure", icon:"st.secondary.configure"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["motion", "temperature", "humidity", "illuminance"])
|
||||||
|
details(["motion", "temperature", "humidity", "illuminance", "battery", "configureAfterSecure"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description)
|
||||||
|
{
|
||||||
|
def result = null
|
||||||
|
if (description == "updated") {
|
||||||
|
result = null
|
||||||
|
} else {
|
||||||
|
def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1])
|
||||||
|
if (cmd) {
|
||||||
|
result = zwaveEvent(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug "Parsed '${description}' to ${result.inspect()}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
|
||||||
|
{
|
||||||
|
def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)]
|
||||||
|
|
||||||
|
if (!isConfigured()) {
|
||||||
|
// we're still in the process of configuring a newly joined device
|
||||||
|
log.debug("not sending wakeUpNoMoreInformation yet")
|
||||||
|
result += response(configureAfterSecure())
|
||||||
|
} else {
|
||||||
|
result += response(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||||
|
def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x30: 2, 0x84: 1])
|
||||||
|
// log.debug "encapsulated: ${encapsulatedCommand}"
|
||||||
|
if (encapsulatedCommand) {
|
||||||
|
zwaveEvent(encapsulatedCommand)
|
||||||
|
} else {
|
||||||
|
log.warn "Unable to extract encapsulated cmd from $cmd"
|
||||||
|
createEvent(descriptionText: cmd.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
|
||||||
|
// log.debug "Received SecurityCommandsSupportedReport"
|
||||||
|
response(configureAfterSecure())
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||||
|
def map = [ name: "battery", unit: "%" ]
|
||||||
|
if (cmd.batteryLevel == 0xFF) {
|
||||||
|
map.value = 1
|
||||||
|
map.descriptionText = "${device.displayName} battery is low"
|
||||||
|
map.isStateChange = true
|
||||||
|
} else {
|
||||||
|
map.value = cmd.batteryLevel
|
||||||
|
}
|
||||||
|
state.lastbatt = new Date().time
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
switch (cmd.sensorType) {
|
||||||
|
case 1:
|
||||||
|
map.name = "temperature"
|
||||||
|
def cmdScale = cmd.scale == 1 ? "F" : "C"
|
||||||
|
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
|
||||||
|
map.unit = getTemperatureScale()
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
map.name = "illuminance"
|
||||||
|
map.value = cmd.scaledSensorValue.toInteger()
|
||||||
|
map.unit = "lux"
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
map.name = "humidity"
|
||||||
|
map.value = cmd.scaledSensorValue.toInteger()
|
||||||
|
map.unit = "%"
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
map.descriptionText = cmd.toString()
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def motionEvent(value) {
|
||||||
|
def map = [name: "motion"]
|
||||||
|
if (value) {
|
||||||
|
map.value = "active"
|
||||||
|
map.descriptionText = "$device.displayName detected motion"
|
||||||
|
} else {
|
||||||
|
map.value = "inactive"
|
||||||
|
map.descriptionText = "$device.displayName motion has stopped"
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) {
|
||||||
|
motionEvent(cmd.sensorValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||||
|
motionEvent(cmd.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
|
||||||
|
if (cmd.notificationType == 7 && cmd.event == 7) {
|
||||||
|
motionEvent(cmd.notificationStatus)
|
||||||
|
} else {
|
||||||
|
createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
def configureAfterSecure() {
|
||||||
|
// log.debug "configureAfterSecure()"
|
||||||
|
|
||||||
|
def request = [
|
||||||
|
// send temperature, humidity, and illuminance every 8 minutes
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 128|64|32),
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 8*60),
|
||||||
|
|
||||||
|
// send battery every 20 hours
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1),
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 20*60*60),
|
||||||
|
|
||||||
|
// send no-motion report 60 seconds after motion stops
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: 60),
|
||||||
|
|
||||||
|
// send binary sensor report instead of basic set for motion
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: 2),
|
||||||
|
|
||||||
|
// disable notification-style motion events
|
||||||
|
zwave.notificationV3.notificationSet(notificationType: 7, notificationStatus: 0),
|
||||||
|
|
||||||
|
zwave.batteryV1.batteryGet(),
|
||||||
|
zwave.sensorBinaryV2.sensorBinaryGet(sensorType:0x0C)
|
||||||
|
]
|
||||||
|
|
||||||
|
setConfigured()
|
||||||
|
|
||||||
|
secureSequence(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
// log.debug "configure()"
|
||||||
|
//["delay 30000"] + secure(zwave.securityV1.securityCommandsSupportedGet())
|
||||||
|
}
|
||||||
|
|
||||||
|
private setConfigured() {
|
||||||
|
device.updateDataValue("configured", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
private isConfigured() {
|
||||||
|
device.getDataValue(["configured"]).toString() == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
private secure(physicalgraph.zwave.Command cmd) {
|
||||||
|
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||||
|
}
|
||||||
|
|
||||||
|
private secureSequence(commands, delay=200) {
|
||||||
|
delayBetween(commands.collect{ secure(it) }, delay)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Aeon Multisensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Motion Sensor"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
capability "Relative Humidity Measurement"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Illuminance Measurement"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Battery"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x2001", inClusters: "0x30,0x31,0x80,0x84,0x70,0x85,0x72,0x86"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// messages the device returns in response to commands it receives
|
||||||
|
status "motion (basic)" : "command: 2001, payload: FF"
|
||||||
|
status "no motion (basic)" : "command: 2001, payload: 00"
|
||||||
|
status "motion (binary)" : "command: 3003, payload: FF"
|
||||||
|
status "no motion (binary)" : "command: 3003, payload: 00"
|
||||||
|
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "temperature ${i}F": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(
|
||||||
|
scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "humidity ${i}%": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(
|
||||||
|
scaledSensorValue: i, precision: 0, sensorType: 5).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "luminance ${i} lux": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(
|
||||||
|
scaledSensorValue: i, precision: 0, sensorType: 3).incomingMessage()
|
||||||
|
}
|
||||||
|
for (int i = 200; i <= 1000; i += 200) {
|
||||||
|
status "luminance ${i} lux": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(
|
||||||
|
scaledSensorValue: i, precision: 0, sensorType: 3).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport(
|
||||||
|
batteryLevel: i).incomingMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("motion", "device.motion", width: 2, 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("humidity", "device.humidity", inactiveLabel: false) {
|
||||||
|
state "humidity", label:'${currentValue}% humidity', unit:""
|
||||||
|
}
|
||||||
|
valueTile("illuminance", "device.illuminance", inactiveLabel: false) {
|
||||||
|
state "luminosity", label:'${currentValue} ${unit}', unit:"lux"
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["motion", "temperature", "humidity", "illuminance"])
|
||||||
|
details(["motion", "temperature", "humidity", "illuminance", "battery", "configure"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description)
|
||||||
|
{
|
||||||
|
def result = []
|
||||||
|
def cmd = zwave.parse(description, [0x31: 2, 0x30: 1, 0x84: 1])
|
||||||
|
if (cmd) {
|
||||||
|
if( cmd.CMD == "8407" ) { result << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) }
|
||||||
|
result << createEvent(zwaveEvent(cmd))
|
||||||
|
}
|
||||||
|
log.debug "Parse returned ${result}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Generation
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
|
||||||
|
{
|
||||||
|
[descriptionText: "${device.displayName} woke up", isStateChange: false]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
switch (cmd.sensorType) {
|
||||||
|
case 1:
|
||||||
|
// temperature
|
||||||
|
def cmdScale = cmd.scale == 1 ? "F" : "C"
|
||||||
|
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
|
||||||
|
map.unit = getTemperatureScale()
|
||||||
|
map.name = "temperature"
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// luminance
|
||||||
|
map.value = cmd.scaledSensorValue.toInteger().toString()
|
||||||
|
map.unit = "lux"
|
||||||
|
map.name = "illuminance"
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
// humidity
|
||||||
|
map.value = cmd.scaledSensorValue.toInteger().toString()
|
||||||
|
map.unit = "%"
|
||||||
|
map.name = "humidity"
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||||
|
def map = [:]
|
||||||
|
map.name = "battery"
|
||||||
|
map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1
|
||||||
|
map.unit = "%"
|
||||||
|
map.displayed = false
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) {
|
||||||
|
def map = [:]
|
||||||
|
map.value = cmd.sensorValue ? "active" : "inactive"
|
||||||
|
map.name = "motion"
|
||||||
|
if (map.value == "active") {
|
||||||
|
map.descriptionText = "$device.displayName detected motion"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
map.descriptionText = "$device.displayName motion has stopped"
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||||
|
def map = [:]
|
||||||
|
map.value = cmd.value ? "active" : "inactive"
|
||||||
|
map.name = "motion"
|
||||||
|
if (map.value == "active") {
|
||||||
|
map.descriptionText = "$device.displayName detected motion"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
map.descriptionText = "$device.displayName motion has stopped"
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
log.debug "Catchall reached for cmd: ${cmd.toString()}}"
|
||||||
|
[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
delayBetween([
|
||||||
|
// send binary sensor report instead of basic set for motion
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: 2).format(),
|
||||||
|
|
||||||
|
// send no-motion report 15 seconds after motion stops
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: 15).format(),
|
||||||
|
|
||||||
|
// send all data (temperature, humidity, illuminance & battery) periodically
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 225).format(),
|
||||||
|
|
||||||
|
// set data reporting period to 5 minutes
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).format()
|
||||||
|
])
|
||||||
|
}
|
||||||
148
devicetypes/smartthings/aeon-outlet.src/aeon-outlet.groovy
Normal file
148
devicetypes/smartthings/aeon-outlet.src/aeon-outlet.groovy
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Aeon Outlet", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Energy Meter"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
command "reset"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x1001", inClusters: "0x25,0x32,0x27,0x2C,0x2B,0x70,0x85,0x56,0x72,0x86", outClusters: "0x82"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
status "on": "command: 2003, payload: FF"
|
||||||
|
status "off": "command: 2003, payload: 00"
|
||||||
|
|
||||||
|
for (int i = 0; i <= 100; i += 10) {
|
||||||
|
status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV2.meterReport(
|
||||||
|
scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "2001FF,delay 100,2502": "command: 2503, payload: FF"
|
||||||
|
reply "200100,delay 100,2502": "command: 2503, payload: 00"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||||
|
state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
|
||||||
|
}
|
||||||
|
valueTile("energy", "device.energy", decoration: "flat") {
|
||||||
|
state "default", label:'${currentValue} kWh'
|
||||||
|
}
|
||||||
|
standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'reset kWh', action:"reset"
|
||||||
|
}
|
||||||
|
standardTile("configure", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "switch"
|
||||||
|
details(["switch","energy","reset","refresh","configure"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def result = null
|
||||||
|
def cmd = zwave.parse(description, [0x20: 1, 0x32: 2])
|
||||||
|
if (cmd) {
|
||||||
|
log.debug cmd
|
||||||
|
result = createEvent(zwaveEvent(cmd))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.meterv2.MeterReport cmd) {
|
||||||
|
if (cmd.scale == 0) {
|
||||||
|
[name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]
|
||||||
|
} else if (cmd.scale == 1) {
|
||||||
|
[name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd)
|
||||||
|
{
|
||||||
|
[
|
||||||
|
name: "switch", value: cmd.value ? "on" : "off", type: "physical"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd)
|
||||||
|
{
|
||||||
|
[
|
||||||
|
name: "switch", value: cmd.value ? "on" : "off", type: "digital"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
// Handles all Z-Wave commands we aren't interested in
|
||||||
|
[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.basicV1.basicSet(value: 0xFF).format(),
|
||||||
|
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.basicV1.basicSet(value: 0x00).format(),
|
||||||
|
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.switchBinaryV1.switchBinaryGet().format(),
|
||||||
|
zwave.meterV2.meterGet().format()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset() {
|
||||||
|
return [
|
||||||
|
zwave.meterV2.meterReset().format(),
|
||||||
|
zwave.meterV2.meterGet().format()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 8).format(), // energy in kWh
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).format(), // every 5 min
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 0).format(),
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 0).format()
|
||||||
|
])
|
||||||
|
}
|
||||||
254
devicetypes/smartthings/aeon-rgbw-bulb.src/aeon-rgbw-bulb.groovy
Normal file
254
devicetypes/smartthings/aeon-rgbw-bulb.src/aeon-rgbw-bulb.groovy
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* Aeon RGBW LED Bulb
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2015-7-12
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "Aeon LED Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Color Control"
|
||||||
|
capability "Color Temperature"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
command "reset"
|
||||||
|
|
||||||
|
fingerprint inClusters: "0x26,0x33,0x98"
|
||||||
|
fingerprint deviceId: "0x11", inClusters: "0x98,0x33"
|
||||||
|
fingerprint deviceId: "0x1102", inClusters: "0x98"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) {
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
}
|
||||||
|
standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
|
||||||
|
state "color", action:"setColor"
|
||||||
|
}
|
||||||
|
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "level", label: 'Level ${currentValue}%'
|
||||||
|
}
|
||||||
|
controlTile("colorTempControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "colorTemperature", action:"setColorTemperature"
|
||||||
|
}
|
||||||
|
valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "hue", label: 'Hue ${currentValue} '
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch", "levelSliderControl", "rgbSelector", "reset", "colorTempControl", "refresh"])
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
response(refresh())
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(description) {
|
||||||
|
def result = null
|
||||||
|
if (description.startsWith("Err 106")) {
|
||||||
|
state.sec = 0
|
||||||
|
} else if (description != "updated") {
|
||||||
|
def cmd = zwave.parse(description, [0x20: 1, 0x26: 3, 0x70: 1, 0x33:3])
|
||||||
|
if (cmd) {
|
||||||
|
result = zwaveEvent(cmd)
|
||||||
|
log.debug("'$description' parsed to $result")
|
||||||
|
} else {
|
||||||
|
log.debug("Couldn't zwave.parse '$description'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||||
|
dimmerEvents(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||||
|
dimmerEvents(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) {
|
||||||
|
dimmerEvents(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
private dimmerEvents(physicalgraph.zwave.Command cmd) {
|
||||||
|
def value = (cmd.value ? "on" : "off")
|
||||||
|
def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")]
|
||||||
|
if (cmd.value) {
|
||||||
|
result << createEvent(name: "level", value: cmd.value, unit: "%")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) {
|
||||||
|
response(command(zwave.switchMultilevelV1.switchMultilevelGet()))
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||||
|
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x84: 1])
|
||||||
|
if (encapsulatedCommand) {
|
||||||
|
state.sec = 1
|
||||||
|
def result = zwaveEvent(encapsulatedCommand)
|
||||||
|
result = result.collect {
|
||||||
|
if (it instanceof physicalgraph.device.HubAction && !it.toString().startsWith("9881")) {
|
||||||
|
response(cmd.CMD + "00" + it.toString())
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
def linkText = device.label ?: device.name
|
||||||
|
[linkText: linkText, descriptionText: "$linkText: $cmd", displayed: false]
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
command(zwave.basicV1.basicSet(value: 0xFF))
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
command(zwave.basicV1.basicSet(value: 0x00))
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(level) {
|
||||||
|
setLevel(level, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(level, duration) {
|
||||||
|
if(level > 99) level = 99
|
||||||
|
command(zwave.switchMultilevelV3.switchMultilevelSet(value: level, dimmingDuration: duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
commands([
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet(),
|
||||||
|
], 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setSaturation(percent) {
|
||||||
|
log.debug "setSaturation($percent)"
|
||||||
|
setColor(saturation: percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setHue(value) {
|
||||||
|
log.debug "setHue($value)"
|
||||||
|
setColor(hue: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setColor(value) {
|
||||||
|
def result = []
|
||||||
|
log.debug "setColor: ${value}"
|
||||||
|
if (value.hex) {
|
||||||
|
def c = value.hex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) }
|
||||||
|
result << zwave.switchColorV3.switchColorSet(red:c[0], green:c[1], blue:c[2], warmWhite:0, coldWhite:0)
|
||||||
|
} else {
|
||||||
|
def hue = value.hue ?: device.currentValue("hue")
|
||||||
|
def saturation = value.saturation ?: device.currentValue("saturation")
|
||||||
|
if(hue == null) hue = 13
|
||||||
|
if(saturation == null) saturation = 13
|
||||||
|
def rgb = huesatToRGB(hue, saturation)
|
||||||
|
result << zwave.switchColorV3.switchColorSet(red: rgb[0], green: rgb[1], blue: rgb[2], warmWhite:0, coldWhite:0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(value.hue) sendEvent(name: "hue", value: value.hue)
|
||||||
|
if(value.hex) sendEvent(name: "color", value: value.hex)
|
||||||
|
if(value.switch) sendEvent(name: "switch", value: value.switch)
|
||||||
|
if(value.saturation) sendEvent(name: "saturation", value: value.saturation)
|
||||||
|
|
||||||
|
commands(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setColorTemperature(percent) {
|
||||||
|
if(percent > 99) percent = 99
|
||||||
|
int warmValue = percent * 255 / 99
|
||||||
|
command(zwave.switchColorV3.switchColorSet(red:0, green:0, blue:0, warmWhite:warmValue, coldWhite:(255 - warmValue)))
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset() {
|
||||||
|
log.debug "reset()"
|
||||||
|
sendEvent(name: "color", value: "#ffffff")
|
||||||
|
setColorTemperature(99)
|
||||||
|
}
|
||||||
|
|
||||||
|
private command(physicalgraph.zwave.Command cmd) {
|
||||||
|
if (state.sec != 0) {
|
||||||
|
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||||
|
} else {
|
||||||
|
cmd.format()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private commands(commands, delay=200) {
|
||||||
|
delayBetween(commands.collect{ command(it) }, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def rgbToHSV(red, green, blue) {
|
||||||
|
float r = red / 255f
|
||||||
|
float g = green / 255f
|
||||||
|
float b = blue / 255f
|
||||||
|
float max = [r, g, b].max()
|
||||||
|
float delta = max - [r, g, b].min()
|
||||||
|
def hue = 13
|
||||||
|
def saturation = 0
|
||||||
|
if (max && delta) {
|
||||||
|
saturation = 100 * delta / max
|
||||||
|
if (r == max) {
|
||||||
|
hue = ((g - b) / delta) * 100 / 6
|
||||||
|
} else if (g == max) {
|
||||||
|
hue = (2 + (b - r) / delta) * 100 / 6
|
||||||
|
} else {
|
||||||
|
hue = (4 + (r - g) / delta) * 100 / 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[hue: hue, saturation: saturation, value: max * 100]
|
||||||
|
}
|
||||||
|
|
||||||
|
def huesatToRGB(float hue, float sat) {
|
||||||
|
while(hue >= 100) hue -= 100
|
||||||
|
int h = (int)(hue / 100 * 6)
|
||||||
|
float f = hue / 100 * 6 - h
|
||||||
|
int p = Math.round(255 * (1 - (sat / 100)))
|
||||||
|
int q = Math.round(255 * (1 - (sat / 100) * f))
|
||||||
|
int t = Math.round(255 * (1 - (sat / 100) * (1 - f)))
|
||||||
|
switch (h) {
|
||||||
|
case 0: return [255, t, p]
|
||||||
|
case 1: return [q, 255, p]
|
||||||
|
case 2: return [p, 255, t]
|
||||||
|
case 3: return [p, q, 255]
|
||||||
|
case 4: return [t, p, 255]
|
||||||
|
case 5: return [255, p, q]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
// This device file is based on work previous work done by "Mike '@jabbera'"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Aeon Secure Smart Energy Switch UK", namespace: "smartthings", author: "jabbera") {
|
||||||
|
capability "Energy Meter"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Power Meter"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Configuration"
|
||||||
|
|
||||||
|
command "reset"
|
||||||
|
command "configureAfterSecure"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x1001", inClusters: "0x25,0x32,0x27,0x2C,0x2B,0x70,0x85,0x56,0x72,0x86,0x98", outClusters: "0x82"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
status "on": "command: 2003, payload: FF"
|
||||||
|
status "off": "command: 2003, payload: 00"
|
||||||
|
|
||||||
|
for (int i = 0; i <= 10000; i += 1000) {
|
||||||
|
status "power ${i} W": new physicalgraph.zwave.Zwave().meterV1.meterReport(
|
||||||
|
scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage()
|
||||||
|
}
|
||||||
|
for (int i = 0; i <= 100; i += 10) {
|
||||||
|
status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport(
|
||||||
|
scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "2001FF,delay 100,2502": "command: 2503, payload: FF"
|
||||||
|
reply "200100,delay 100,2502": "command: 2503, payload: 00"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||||
|
state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
|
||||||
|
}
|
||||||
|
valueTile("power", "device.power", decoration: "flat") {
|
||||||
|
state "default", label:'${currentValue} W'
|
||||||
|
}
|
||||||
|
valueTile("energy", "device.energy", decoration: "flat") {
|
||||||
|
state "default", label:'${currentValue} kWh'
|
||||||
|
}
|
||||||
|
standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'reset kWh', action:"reset"
|
||||||
|
}
|
||||||
|
standardTile("configureAfterSecure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "configure", label:'', action:"configureAfterSecure", icon:"st.secondary.configure"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "switch"
|
||||||
|
details(["switch","power","energy","reset","configureAfterSecure","refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def result = null
|
||||||
|
|
||||||
|
if (description != "updated") {
|
||||||
|
def cmd = zwave.parse(description, [0x20: 1, 0x32: 1, 0x25: 1, 0x98: 1, 0x70: 1, 0x85: 2, 0x9B: 1, 0x90: 1, 0x73: 1, 0x30: 1, 0x28: 1, 0x72: 1])
|
||||||
|
if (cmd) {
|
||||||
|
result = zwaveEvent(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug "Parsed '${description}' to ${result.inspect()}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devices that support the Security command class can send messages in an encrypted form;
|
||||||
|
// they arrive wrapped in a SecurityMessageEncapsulation command and must be unencapsulated
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||||
|
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x32: 1, 0x25: 1, 0x98: 1, 0x70: 1, 0x85: 2, 0x9B: 1, 0x90: 1, 0x73: 1, 0x30: 1, 0x28: 1]) // can specify command class versions here like in zwave.parse
|
||||||
|
if (encapsulatedCommand) {
|
||||||
|
return zwaveEvent(encapsulatedCommand)
|
||||||
|
} else {
|
||||||
|
log.warn "Unable to extract encapsulated cmd from $cmd"
|
||||||
|
createEvent(descriptionText: cmd.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.meterv1.MeterReport cmd) {
|
||||||
|
def newEvent = null
|
||||||
|
if (cmd.scale == 0) {
|
||||||
|
newEvent = [name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]
|
||||||
|
} else if (cmd.scale == 1) {
|
||||||
|
newEvent = [name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"]
|
||||||
|
} else {
|
||||||
|
newEvent = [name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"]
|
||||||
|
}
|
||||||
|
|
||||||
|
createEvent(newEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd)
|
||||||
|
{
|
||||||
|
createEvent([
|
||||||
|
name: "switch", value: cmd.value ? "on" : "off", type: "physical"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd)
|
||||||
|
{
|
||||||
|
createEvent([
|
||||||
|
name: "switch", value: cmd.value ? "on" : "off", type: "digital"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
log.debug "No handler for $cmd"
|
||||||
|
// Handles all Z-Wave commands we aren't interested in
|
||||||
|
createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
secureSequence([
|
||||||
|
zwave.basicV1.basicSet(value: 0xFF),
|
||||||
|
zwave.switchBinaryV1.switchBinaryGet()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
secureSequence([
|
||||||
|
zwave.basicV1.basicSet(value: 0x00),
|
||||||
|
zwave.switchBinaryV1.switchBinaryGet()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
secureSequence([
|
||||||
|
zwave.switchBinaryV1.switchBinaryGet(),
|
||||||
|
zwave.meterV2.meterGet(scale: 0),
|
||||||
|
zwave.meterV2.meterGet(scale: 2)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
secureSequence([
|
||||||
|
zwave.switchBinaryV1.switchBinaryGet(),
|
||||||
|
zwave.meterV2.meterGet(scale: 0),
|
||||||
|
zwave.meterV2.meterGet(scale: 2)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset() {
|
||||||
|
return secureSequence([
|
||||||
|
zwave.meterV2.meterReset(),
|
||||||
|
zwave.meterV2.meterGet(scale: 0)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def configureAfterSecure() {
|
||||||
|
log.debug "configureAfterSecure()"
|
||||||
|
|
||||||
|
secureSequence([
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 252, size: 1, scaledConfigurationValue: 0), // Enable/disable Configuration Locked (0 =disable, 1 = enable).
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 80, size: 1, scaledConfigurationValue: 2), // Enable to send notifications to associated devices (Group 1) when the state of Micro Switch’s load changed (0=nothing, 1=hail CC, 2=basic CC report).
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 90, size: 1, scaledConfigurationValue: 1), // Enables/disables parameter 91 and 92 below (1=enabled, 0=disabled).
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 91, size: 2, scaledConfigurationValue: 2), // The value here represents minimum change in wattage (in terms of wattage) for a REPORT to be sent (Valid values 0‐ 60000).
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 92, size: 1, scaledConfigurationValue: 5), // The value here represents minimum change in wattage percent (in terms of percentage) for a REPORT to be sent (Valid values 0‐100).
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 4), // Which reports need to send in Report group 1 (See flags in table below).
|
||||||
|
// Disable a time interval to receive immediate updates of power change.
|
||||||
|
//zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300), // The time interval of sending Report group 1 (Valid values 0x01‐0xFFFFFFFF).
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 8), // Which reports need to send in Report group 2 (See flags in table below).
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 300), // The time interval of sending Report group 2 (Valid values 0x01‐0xFFFFFFFF).
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 252, size: 1, scaledConfigurationValue: 1), // Enable/disable Configuration Locked (0 =disable, 1 = enable).
|
||||||
|
|
||||||
|
// Register for Group 1
|
||||||
|
zwave.associationV2.associationSet(groupingIdentifier:1, nodeId: [zwaveHubNodeId]),
|
||||||
|
// Register for Group 2
|
||||||
|
zwave.associationV2.associationSet(groupingIdentifier:2, nodeId: [zwaveHubNodeId])
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
// Wait until after the secure exchange for this
|
||||||
|
log.debug "configure()"
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
log.debug "updated()"
|
||||||
|
response(["delay 2000"] + configureAfterSecure() + refresh())
|
||||||
|
}
|
||||||
|
|
||||||
|
private secure(physicalgraph.zwave.Command cmd) {
|
||||||
|
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||||
|
}
|
||||||
|
|
||||||
|
private secureSequence(commands, delay=200) {
|
||||||
|
delayBetween(commands.collect{ secure(it) }, delay)
|
||||||
|
}
|
||||||
148
devicetypes/smartthings/aeon-siren.src/aeon-siren.groovy
Normal file
148
devicetypes/smartthings/aeon-siren.src/aeon-siren.groovy
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* Aeon Siren
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2014-07-15
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Aeon Siren", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Alarm"
|
||||||
|
capability "Switch"
|
||||||
|
|
||||||
|
command "test"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x1005", inClusters: "0x5E,0x98"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// reply messages
|
||||||
|
reply "9881002001FF,9881002002": "command: 9881, payload: 002003FF"
|
||||||
|
reply "988100200100,9881002002": "command: 9881, payload: 00200300"
|
||||||
|
reply "9881002001FF,delay 3000,988100200100,9881002002": "command: 9881, payload: 00200300"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("alarm", "device.alarm", width: 2, height: 2) {
|
||||||
|
state "off", label:'off', action:'alarm.siren', icon:"st.alarm.alarm.alarm", backgroundColor:"#ffffff"
|
||||||
|
state "both", label:'alarm!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13"
|
||||||
|
}
|
||||||
|
standardTile("test", "device.alarm", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"test", icon:"st.secondary.test"
|
||||||
|
}
|
||||||
|
standardTile("off", "device.alarm", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"alarm.off", icon:"st.secondary.off"
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
input "sound", "number", title: "Siren sound (1-5)", defaultValue: 1, required: true//, displayDuringSetup: true // don't display during setup until defaultValue is shown
|
||||||
|
input "volume", "number", title: "Volume (1-3)", defaultValue: 3, required: true//, displayDuringSetup: true
|
||||||
|
}
|
||||||
|
|
||||||
|
main "alarm"
|
||||||
|
details(["alarm", "test", "off"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
if(!state.sound) state.sound = 1
|
||||||
|
if(!state.volume) state.volume = 3
|
||||||
|
|
||||||
|
log.debug "settings: ${settings.inspect()}, state: ${state.inspect()}"
|
||||||
|
|
||||||
|
Short sound = (settings.sound as Short) ?: 1
|
||||||
|
Short volume = (settings.volume as Short) ?: 3
|
||||||
|
|
||||||
|
if (sound != state.sound || volume != state.volume) {
|
||||||
|
state.sound = sound
|
||||||
|
state.volume = volume
|
||||||
|
return response([
|
||||||
|
secure(zwave.configurationV1.configurationSet(parameterNumber: 37, size: 2, configurationValue: [sound, volume])),
|
||||||
|
"delay 1000",
|
||||||
|
secure(zwave.basicV1.basicSet(value: 0x00)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "parse($description)"
|
||||||
|
def result = null
|
||||||
|
def cmd = zwave.parse(description, [0x98: 1, 0x20: 1, 0x70: 1])
|
||||||
|
if (cmd) {
|
||||||
|
result = zwaveEvent(cmd)
|
||||||
|
}
|
||||||
|
log.debug "Parse returned ${result?.inspect()}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||||
|
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x85: 2, 0x70: 1])
|
||||||
|
// log.debug "encapsulated: $encapsulatedCommand"
|
||||||
|
if (encapsulatedCommand) {
|
||||||
|
zwaveEvent(encapsulatedCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||||
|
log.debug "rx $cmd"
|
||||||
|
[
|
||||||
|
createEvent([name: "switch", value: cmd.value ? "on" : "off", displayed: false]),
|
||||||
|
createEvent([name: "alarm", value: cmd.value ? "both" : "off"])
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
createEvent(displayed: false, descriptionText: "$device.displayName: $cmd")
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
log.debug "sending on"
|
||||||
|
[
|
||||||
|
secure(zwave.basicV1.basicSet(value: 0xFF)),
|
||||||
|
secure(zwave.basicV1.basicGet())
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
log.debug "sending off"
|
||||||
|
[
|
||||||
|
secure(zwave.basicV1.basicSet(value: 0x00)),
|
||||||
|
secure(zwave.basicV1.basicGet())
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def strobe() {
|
||||||
|
on()
|
||||||
|
}
|
||||||
|
|
||||||
|
def siren() {
|
||||||
|
on()
|
||||||
|
}
|
||||||
|
|
||||||
|
def both() {
|
||||||
|
on()
|
||||||
|
}
|
||||||
|
|
||||||
|
def test() {
|
||||||
|
[
|
||||||
|
secure(zwave.basicV1.basicSet(value: 0xFF)),
|
||||||
|
"delay 3000",
|
||||||
|
secure(zwave.basicV1.basicSet(value: 0x00)),
|
||||||
|
secure(zwave.basicV1.basicGet())
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private secure(physicalgraph.zwave.Command cmd) {
|
||||||
|
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Aeon SmartStrip", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Switch"
|
||||||
|
capability "Energy Meter"
|
||||||
|
capability "Power Meter"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
command "reset"
|
||||||
|
|
||||||
|
(1..4).each { n ->
|
||||||
|
attribute "switch$n", "enum", ["on", "off"]
|
||||||
|
attribute "power$n", "number"
|
||||||
|
attribute "energy$n", "number"
|
||||||
|
command "on$n"
|
||||||
|
command "off$n"
|
||||||
|
command "reset$n"
|
||||||
|
}
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x1001", inClusters: "0x25,0x32,0x27,0x70,0x85,0x72,0x86,0x60", outClusters: "0x82"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
status "on": "command: 2003, payload: FF"
|
||||||
|
status "off": "command: 2003, payload: 00"
|
||||||
|
status "switch1 on": "command: 600D, payload: 01 00 25 03 FF"
|
||||||
|
status "switch1 off": "command: 600D, payload: 01 00 25 03 00"
|
||||||
|
status "switch4 on": "command: 600D, payload: 04 00 25 03 FF"
|
||||||
|
status "switch4 off": "command: 600D, payload: 04 00 25 03 00"
|
||||||
|
status "power": new physicalgraph.zwave.Zwave().meterV1.meterReport(
|
||||||
|
scaledMeterValue: 30, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage()
|
||||||
|
status "energy": new physicalgraph.zwave.Zwave().meterV1.meterReport(
|
||||||
|
scaledMeterValue: 200, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage()
|
||||||
|
status "power1": "command: 600D, payload: 0100" + new physicalgraph.zwave.Zwave().meterV1.meterReport(
|
||||||
|
scaledMeterValue: 30, precision: 3, meterType: 4, scale: 2, size: 4).format()
|
||||||
|
status "energy2": "command: 600D, payload: 0200" + new physicalgraph.zwave.Zwave().meterV1.meterReport(
|
||||||
|
scaledMeterValue: 200, precision: 3, meterType: 0, scale: 0, size: 4).format()
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "2001FF,delay 100,2502": "command: 2503, payload: FF"
|
||||||
|
reply "200100,delay 100,2502": "command: 2503, payload: 00"
|
||||||
|
}
|
||||||
|
|
||||||
|
// tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||||
|
state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
|
||||||
|
}
|
||||||
|
valueTile("power", "device.power", decoration: "flat") {
|
||||||
|
state "default", label:'${currentValue} W'
|
||||||
|
}
|
||||||
|
valueTile("energy", "device.energy", decoration: "flat") {
|
||||||
|
state "default", label:'${currentValue} kWh'
|
||||||
|
}
|
||||||
|
standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'reset kWh', action:"reset"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
(1..4).each { n ->
|
||||||
|
standardTile("switch$n", "switch$n", canChangeIcon: true) {
|
||||||
|
state "on", label: '${name}', action: "off$n", icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||||
|
state "off", label: '${name}', action: "on$n", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
|
||||||
|
}
|
||||||
|
valueTile("power$n", "power$n", decoration: "flat") {
|
||||||
|
state "default", label:'${currentValue} W'
|
||||||
|
}
|
||||||
|
valueTile("energy$n", "energy$n", decoration: "flat") {
|
||||||
|
state "default", label:'${currentValue} kWh'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch", "power", "energy", "switch1", "switch2", "switch3", "switch4"])
|
||||||
|
details(["switch","power","energy",
|
||||||
|
"switch1","power1","energy1",
|
||||||
|
"switch2","power2","energy2",
|
||||||
|
"switch3","power3","energy3",
|
||||||
|
"switch4","power4","energy4",
|
||||||
|
"refresh","reset"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def result = null
|
||||||
|
if (description.startsWith("Err")) {
|
||||||
|
result = createEvent(descriptionText:description, isStateChange:true)
|
||||||
|
} else if (description != "updated") {
|
||||||
|
def cmd = zwave.parse(description, [0x60: 3, 0x32: 3, 0x25: 1, 0x20: 1])
|
||||||
|
if (cmd) {
|
||||||
|
result = zwaveEvent(cmd, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug "parsed '${description}' to ${result.inspect()}"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def endpointEvent(endpoint, map) {
|
||||||
|
if (endpoint) {
|
||||||
|
map.name = map.name + endpoint.toString()
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, ep) {
|
||||||
|
def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1])
|
||||||
|
if (encapsulatedCommand) {
|
||||||
|
if (encapsulatedCommand.commandClassId == 0x32) {
|
||||||
|
// Metered outlets are numbered differently than switches
|
||||||
|
Integer endpoint = cmd.sourceEndPoint
|
||||||
|
if (endpoint > 2) {
|
||||||
|
zwaveEvent(encapsulatedCommand, endpoint - 2)
|
||||||
|
} else if (endpoint == 0) {
|
||||||
|
zwaveEvent(encapsulatedCommand, 0)
|
||||||
|
} else {
|
||||||
|
log.debug("Ignoring metered outlet $endpoint msg: $encapsulatedCommand")
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, endpoint) {
|
||||||
|
def map = [name: "switch", type: "physical", value: (cmd.value ? "on" : "off")]
|
||||||
|
def events = [endpointEvent(endpoint, map)]
|
||||||
|
def cmds = []
|
||||||
|
if (endpoint) {
|
||||||
|
cmds += delayBetween([2,0].collect { s -> encap(zwave.meterV3.meterGet(scale: s), endpoint) }, 1000)
|
||||||
|
if(endpoint < 4) cmds += ["delay 1500", encap(zwave.basicV1.basicGet(), endpoint + 1)]
|
||||||
|
} else if (events[0].isStateChange) {
|
||||||
|
events += (1..4).collect { ep -> endpointEvent(ep, map.clone()) }
|
||||||
|
cmds << "delay 3000"
|
||||||
|
cmds += delayBetween((0..4).collect { ep -> encap(zwave.meterV3.meterGet(scale: 2), ep) }, 800)
|
||||||
|
}
|
||||||
|
if(cmds) events << response(cmds)
|
||||||
|
events
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, endpoint) {
|
||||||
|
def map = [name: "switch", value: (cmd.value ? "on" : "off")]
|
||||||
|
def events = [endpointEvent(endpoint, map)]
|
||||||
|
def cmds = []
|
||||||
|
if (!endpoint && events[0].isStateChange) {
|
||||||
|
events += (1..4).collect { ep -> endpointEvent(ep, map.clone()) }
|
||||||
|
cmds << "delay 3000"
|
||||||
|
cmds += delayBetween((1..4).collect { ep -> encap(zwave.meterV3.meterGet(scale: 2), ep) })
|
||||||
|
}
|
||||||
|
if(cmds) events << response(cmds)
|
||||||
|
events
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, ep) {
|
||||||
|
def event = [:]
|
||||||
|
def cmds = []
|
||||||
|
if (cmd.scale < 2) {
|
||||||
|
def val = Math.round(cmd.scaledMeterValue*100)/100.0
|
||||||
|
event = endpointEvent(ep, [name: "energy", value: val, unit: ["kWh", "kVAh"][cmd.scale]])
|
||||||
|
} else {
|
||||||
|
event = endpointEvent(ep, [name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"])
|
||||||
|
}
|
||||||
|
if (!ep && event.isStateChange && event.name == "energy") {
|
||||||
|
// Total strip energy consumption changed, check individual outlets
|
||||||
|
(1..4).each { endpoint ->
|
||||||
|
cmds << encap(zwave.meterV2.meterGet(scale: 0), endpoint)
|
||||||
|
cmds << "delay 400"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmds ? [event, response(cmds)] : event
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd, ep) {
|
||||||
|
updateDataValue("MSR", String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId))
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd, ep) {
|
||||||
|
log.debug "${device.displayName}: Unhandled ${cmd}" + (ep ? " from endpoint $ep" : "")
|
||||||
|
}
|
||||||
|
|
||||||
|
def onOffCmd(value, endpoint = null) {
|
||||||
|
[
|
||||||
|
encap(zwave.basicV1.basicSet(value: value), endpoint),
|
||||||
|
"delay 500",
|
||||||
|
encap(zwave.switchBinaryV1.switchBinaryGet(), endpoint),
|
||||||
|
"delay 3000",
|
||||||
|
encap(zwave.meterV3.meterGet(scale: 2), endpoint)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() { onOffCmd(0xFF) }
|
||||||
|
def off() { onOffCmd(0x0) }
|
||||||
|
|
||||||
|
def on1() { onOffCmd(0xFF, 1) }
|
||||||
|
def on2() { onOffCmd(0xFF, 2) }
|
||||||
|
def on3() { onOffCmd(0xFF, 3) }
|
||||||
|
def on4() { onOffCmd(0xFF, 4) }
|
||||||
|
|
||||||
|
def off1() { onOffCmd(0, 1) }
|
||||||
|
def off2() { onOffCmd(0, 2) }
|
||||||
|
def off3() { onOffCmd(0, 3) }
|
||||||
|
def off4() { onOffCmd(0, 4) }
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.basicV1.basicGet().format(),
|
||||||
|
zwave.meterV3.meterGet(scale: 0).format(),
|
||||||
|
zwave.meterV3.meterGet(scale: 2).format(),
|
||||||
|
encap(zwave.basicV1.basicGet(), 1) // further gets are sent from the basic report handler
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def resetCmd(endpoint = null) {
|
||||||
|
delayBetween([
|
||||||
|
encap(zwave.meterV2.meterReset(), endpoint),
|
||||||
|
encap(zwave.meterV2.meterGet(scale: 0), endpoint)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset() {
|
||||||
|
delayBetween([resetCmd(null), reset1(), reset2(), reset3(), reset4()])
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset1() { resetCmd(1) }
|
||||||
|
def reset2() { resetCmd(2) }
|
||||||
|
def reset3() { resetCmd(3) }
|
||||||
|
def reset4() { resetCmd(4) }
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
def cmds = [
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, configurationValue: [0, 0, 0, 1]).format(),
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, configurationValue: [0, 0, 0x79, 0]).format(),
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 90).format(),
|
||||||
|
]
|
||||||
|
[5, 8, 9, 10, 11].each { p ->
|
||||||
|
cmds << zwave.configurationV1.configurationSet(parameterNumber: p, size: 2, scaledConfigurationValue: 5).format()
|
||||||
|
}
|
||||||
|
[12, 15, 16, 17, 18].each { p ->
|
||||||
|
cmds << zwave.configurationV1.configurationSet(parameterNumber: p, size: 1, scaledConfigurationValue: 50).format()
|
||||||
|
}
|
||||||
|
cmds += [
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 15*60).format(),
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, configurationValue: [1]).format(),
|
||||||
|
]
|
||||||
|
delayBetween(cmds) + "delay 5000" + refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
private encap(cmd, endpoint) {
|
||||||
|
if (endpoint) {
|
||||||
|
if (cmd.commandClassId == 0x32) {
|
||||||
|
// Metered outlets are numbered differently than switches
|
||||||
|
if (endpoint < 0x80) {
|
||||||
|
endpoint += 2
|
||||||
|
} else {
|
||||||
|
endpoint = ((endpoint & 0x7F) << 2) | 0x80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd).format()
|
||||||
|
} else {
|
||||||
|
cmd.format()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Arduino ThingShield", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", deviceId: "0138", inClusters: "0000"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "ping": "catchall: 0104 0000 01 01 0040 00 6A67 00 00 0000 0A 00 0A70696E67"
|
||||||
|
status "hello": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A48656c6c6f20576f726c6421"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("shield", "device.shield", width: 2, height: 2) {
|
||||||
|
state "default", icon:"st.shields.shields.arduino", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "shield"
|
||||||
|
details "shield"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
def value = zigbee.parse(description)?.text
|
||||||
|
def name = value && value != "ping" ? "response" : null
|
||||||
|
def result = createEvent(name: name, value: value)
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands to device
|
||||||
|
// TBD
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* CentraLite Dimmer
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2013-12-04
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "CentraLite Dimmer", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Power Meter"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B04,0B05", outClusters: "0019"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "on": "on/off: 1"
|
||||||
|
status "off": "on/off: 0"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "zcl on-off on": "on/off: 1"
|
||||||
|
reply "zcl on-off off": "on/off: 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
valueTile("power", "device.power", decoration: "flat") {
|
||||||
|
state "power", label:'${currentValue} W'
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
main "switch"
|
||||||
|
details(["switch","power","refresh","levelSliderControl"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "Parse description $description"
|
||||||
|
def name = null
|
||||||
|
def value = null
|
||||||
|
if (description?.startsWith("catchall:")) {
|
||||||
|
def msg = zigbee.parse(description)
|
||||||
|
log.trace msg
|
||||||
|
log.trace "data: $msg.data"
|
||||||
|
} else if (description?.startsWith("read attr -")) {
|
||||||
|
def descMap = parseDescriptionAsMap(description)
|
||||||
|
log.debug "Read attr: $description"
|
||||||
|
if (descMap.cluster == "0006" && descMap.attrId == "0000") {
|
||||||
|
name = "switch"
|
||||||
|
value = descMap.value.endsWith("01") ? "on" : "off"
|
||||||
|
} else {
|
||||||
|
def reportValue = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim()
|
||||||
|
name = "power"
|
||||||
|
// assume 16 bit signed for encoding and power divisor is 10
|
||||||
|
value = Integer.parseInt(reportValue, 16) / 10
|
||||||
|
}
|
||||||
|
} else if (description?.startsWith("on/off:")) {
|
||||||
|
log.debug "Switch command"
|
||||||
|
name = "switch"
|
||||||
|
value = description?.endsWith(" 1") ? "on" : "off"
|
||||||
|
}
|
||||||
|
|
||||||
|
def result = createEvent(name: name, value: value)
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseDescriptionAsMap(description) {
|
||||||
|
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands to device
|
||||||
|
def on() {
|
||||||
|
'zcl on-off on'
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
'zcl on-off off'
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
log.trace "setLevel($value)"
|
||||||
|
sendEvent(name: "level", value: value)
|
||||||
|
def level = hexString(Math.round(value * 255/100))
|
||||||
|
def cmd = "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 2000}"
|
||||||
|
log.debug cmd
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
def meter() {
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B"
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B"
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
[
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0xB04 {${device.zigbeeId}} {}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value, width=2) {
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() < width) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* CentraLite Thermostat
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2013-12-02
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "CentraLite Thermostat", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
capability "Thermostat"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0020,0201,0202,0204,0B05", outClusters: "000A, 0019"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator { }
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||||
|
state("temperature", label:'${currentValue}°', unit:"F",
|
||||||
|
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"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "off", label:'${name}', action:"thermostat.setThermostatMode"
|
||||||
|
state "cool", label:'${name}', action:"thermostat.setThermostatMode"
|
||||||
|
state "heat", label:'${name}', action:"thermostat.setThermostatMode"
|
||||||
|
state "emergencyHeat", label:'${name}', action:"thermostat.setThermostatMode"
|
||||||
|
}
|
||||||
|
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "fanAuto", label:'${name}', action:"thermostat.setThermostatFanMode"
|
||||||
|
state "fanOn", label:'${name}', action:"thermostat.setThermostatFanMode"
|
||||||
|
}
|
||||||
|
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00"
|
||||||
|
}
|
||||||
|
valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "heat", label:'${currentValue}° heat', unit:"F", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "setCoolingSetpoint", action:"thermostat.setCoolingSetpoint", backgroundColor: "#1e9cbb"
|
||||||
|
}
|
||||||
|
valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||||
|
}
|
||||||
|
main "temperature"
|
||||||
|
details(["temperature", "mode", "fanMode", "heatSliderControl", "heatingSetpoint", "coolSliderControl", "coolingSetpoint", "refresh", "configure"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// parse events into attributes
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "Parse description $description"
|
||||||
|
def map = [:]
|
||||||
|
if (description?.startsWith("read attr -")) {
|
||||||
|
def descMap = parseDescriptionAsMap(description)
|
||||||
|
log.debug "Desc Map: $descMap"
|
||||||
|
if (descMap.cluster == "0201" && descMap.attrId == "0000") {
|
||||||
|
log.debug "TEMP"
|
||||||
|
map.name = "temperature"
|
||||||
|
map.value = getTemperature(descMap.value)
|
||||||
|
} else if (descMap.cluster == "0201" && descMap.attrId == "0011") {
|
||||||
|
log.debug "COOLING SETPOINT"
|
||||||
|
map.name = "coolingSetpoint"
|
||||||
|
map.value = getTemperature(descMap.value)
|
||||||
|
} else if (descMap.cluster == "0201" && descMap.attrId == "0012") {
|
||||||
|
log.debug "HEATING SETPOINT"
|
||||||
|
map.name = "heatingSetpoint"
|
||||||
|
map.value = getTemperature(descMap.value)
|
||||||
|
} else if (descMap.cluster == "0201" && descMap.attrId == "001c") {
|
||||||
|
log.debug "MODE"
|
||||||
|
map.name = "thermostatMode"
|
||||||
|
map.value = getModeMap()[descMap.value]
|
||||||
|
} else if (descMap.cluster == "0202" && descMap.attrId == "0000") {
|
||||||
|
log.debug "FAN MODE"
|
||||||
|
map.name = "thermostatFanMode"
|
||||||
|
map.value = getFanModeMap()[descMap.value]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def result = null
|
||||||
|
if (map) {
|
||||||
|
result = createEvent(map)
|
||||||
|
}
|
||||||
|
log.debug "Parse returned $map"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseDescriptionAsMap(description) {
|
||||||
|
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getModeMap() { [
|
||||||
|
"00":"off",
|
||||||
|
"03":"cool",
|
||||||
|
"04":"heat",
|
||||||
|
"05":"emergencyHeat"
|
||||||
|
]}
|
||||||
|
|
||||||
|
def getFanModeMap() { [
|
||||||
|
"04":"fanOn",
|
||||||
|
"05":"fanAuto"
|
||||||
|
]}
|
||||||
|
|
||||||
|
def refresh()
|
||||||
|
{
|
||||||
|
log.debug "refresh called"
|
||||||
|
[
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0x201 0", "delay 200",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0x201 0x11", "delay 200",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0x201 0x12", "delay 200",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0x201 0x1C", "delay 200",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0x202 0"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
// Leaving out for now as its killing the batteries.
|
||||||
|
//def poll() {
|
||||||
|
// log.debug "Executing 'poll'"
|
||||||
|
// refresh()
|
||||||
|
//}
|
||||||
|
|
||||||
|
def getTemperature(value) {
|
||||||
|
def celsius = Integer.parseInt(value, 16) / 100
|
||||||
|
if(getTemperatureScale() == "C"){
|
||||||
|
return celsius
|
||||||
|
} else {
|
||||||
|
return celsiusToFahrenheit(celsius) as Integer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def setHeatingSetpoint(degrees) {
|
||||||
|
def temperatureScale = getTemperatureScale()
|
||||||
|
|
||||||
|
def degreesInteger = degrees as Integer
|
||||||
|
log.debug "setHeatingSetpoint({$degreesInteger} ${temperatureScale})"
|
||||||
|
sendEvent("name":"heatingSetpoint", "value":degreesInteger)
|
||||||
|
|
||||||
|
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
||||||
|
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100) + "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def setCoolingSetpoint(degrees) {
|
||||||
|
def degreesInteger = degrees as Integer
|
||||||
|
log.debug "setCoolingSetpoint({$degreesInteger} ${temperatureScale})"
|
||||||
|
sendEvent("name":"coolingSetpoint", "value":degreesInteger)
|
||||||
|
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
||||||
|
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100) + "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def modes() {
|
||||||
|
["off", "heat", "cool"]
|
||||||
|
}
|
||||||
|
|
||||||
|
def setThermostatMode() {
|
||||||
|
log.debug "switching thermostatMode"
|
||||||
|
def currentMode = device.currentState("thermostatMode")?.value
|
||||||
|
def modeOrder = modes()
|
||||||
|
def index = modeOrder.indexOf(currentMode)
|
||||||
|
def next = index >= 0 && index < modeOrder.size() - 1 ? modeOrder[index + 1] : modeOrder[0]
|
||||||
|
log.debug "switching mode from $currentMode to $next"
|
||||||
|
"$next"()
|
||||||
|
}
|
||||||
|
|
||||||
|
def setThermostatFanMode() {
|
||||||
|
log.debug "Switching fan mode"
|
||||||
|
def currentFanMode = device.currentState("thermostatFanMode")?.value
|
||||||
|
log.debug "switching fan from current mode: $currentFanMode"
|
||||||
|
def returnCommand
|
||||||
|
|
||||||
|
switch (currentFanMode) {
|
||||||
|
case "fanAuto":
|
||||||
|
returnCommand = fanOn()
|
||||||
|
break
|
||||||
|
case "fanOn":
|
||||||
|
returnCommand = fanAuto()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if(!currentFanMode) { returnCommand = fanAuto() }
|
||||||
|
returnCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
def setThermostatMode(String value) {
|
||||||
|
log.debug "setThermostatMode({$value})"
|
||||||
|
"$value"()
|
||||||
|
}
|
||||||
|
|
||||||
|
def setThermostatFanMode(String value) {
|
||||||
|
log.debug "setThermostatFanMode({$value})"
|
||||||
|
"$value"()
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
log.debug "off"
|
||||||
|
sendEvent("name":"thermostatMode", "value":"off")
|
||||||
|
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {00}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def cool() {
|
||||||
|
log.debug "cool"
|
||||||
|
sendEvent("name":"thermostatMode", "value":"cool")
|
||||||
|
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {03}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def heat() {
|
||||||
|
log.debug "heat"
|
||||||
|
sendEvent("name":"thermostatMode", "value":"heat")
|
||||||
|
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {04}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def emergencyHeat() {
|
||||||
|
log.debug "emergencyHeat"
|
||||||
|
sendEvent("name":"thermostatMode", "value":"emergency heat")
|
||||||
|
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {05}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
fanOn()
|
||||||
|
}
|
||||||
|
|
||||||
|
def fanOn() {
|
||||||
|
log.debug "fanOn"
|
||||||
|
sendEvent("name":"thermostatFanMode", "value":"fanOn")
|
||||||
|
"st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {04}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def auto() {
|
||||||
|
fanAuto()
|
||||||
|
}
|
||||||
|
|
||||||
|
def fanAuto() {
|
||||||
|
log.debug "fanAuto"
|
||||||
|
sendEvent("name":"thermostatFanMode", "value":"fanAuto")
|
||||||
|
"st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {05}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
|
||||||
|
log.debug "binding to Thermostat and Fan Control cluster"
|
||||||
|
[
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x202 {${device.zigbeeId}} {}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value) {
|
||||||
|
new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
}
|
||||||
184
devicetypes/smartthings/cooper-rf9500.src/cooper-rf9500.groovy
Normal file
184
devicetypes/smartthings/cooper-rf9500.src/cooper-rf9500.groovy
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Cooper RF9500", namespace: "smartthings", author: "juano23@gmail.com") {
|
||||||
|
capability "Switch"
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Button"
|
||||||
|
capability "Actuator"
|
||||||
|
|
||||||
|
//fingerprint deviceId: "0x1200", inClusters: "0x77 0x86 0x75 0x73 0x85 0x72 0xEF", outClusters: "0x26"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "on": "on/off: 1"
|
||||||
|
status "off": "on/off: 0"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "zcl on-off on": "on/off: 1"
|
||||||
|
reply "zcl on-off off": "on/off: 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "off", label: '${name}', action: "switch.on", icon: "st.Home.home30", backgroundColor: "#ffffff"
|
||||||
|
state "on", label: '${name}', action: "switch.off", icon: "st.Home.home30", backgroundColor: "#79b821"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
main "switch"
|
||||||
|
details(["switch", "refresh", "level", "levelSliderControl"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def results = []
|
||||||
|
if (description.startsWith("Err")) {
|
||||||
|
results = createEvent(descriptionText:description, displayed:true)
|
||||||
|
} else {
|
||||||
|
def cmd = zwave.parse(description, [0x26: 1, 0x2B: 1, 0x80: 1, 0x84: 1])
|
||||||
|
if(cmd) results += zwaveEvent(cmd)
|
||||||
|
if(!results) results = [ descriptionText: cmd, displayed: false ]
|
||||||
|
}
|
||||||
|
log.debug("Parsed '$description' to $results")
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
}
|
||||||
|
|
||||||
|
def levelup() {
|
||||||
|
def curlevel = device.currentValue('level') as Integer
|
||||||
|
if (curlevel <= 90)
|
||||||
|
setLevel(curlevel + 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
def leveldown() {
|
||||||
|
def curlevel = device.currentValue('level') as Integer
|
||||||
|
if (curlevel >= 10)
|
||||||
|
setLevel(curlevel - 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
log.trace "setLevel($value)"
|
||||||
|
sendEvent(name: "level", value: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) {
|
||||||
|
def results = [createEvent(descriptionText: "$device.displayName woke up", isStateChange: false)]
|
||||||
|
|
||||||
|
results += configurationCmds().collect{ response(it) }
|
||||||
|
results << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format())
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// A zwave command for a button press was received convert to button number
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd) {
|
||||||
|
[ descriptionText: "startlevel $cmd"]
|
||||||
|
log.info "startlevel $cmd"
|
||||||
|
if (cmd.upDown == true) {
|
||||||
|
Integer buttonid = 2
|
||||||
|
leveldown()
|
||||||
|
checkbuttonEvent(buttonid)
|
||||||
|
} else if (cmd.upDown == false) {
|
||||||
|
Integer buttonid = 3
|
||||||
|
levelup()
|
||||||
|
checkbuttonEvent(buttonid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The controller likes to repeat the command... ignore repeats
|
||||||
|
def checkbuttonEvent(buttonid){
|
||||||
|
|
||||||
|
if (state.lastScene == buttonid && (state.repeatCount < 4) && (now() - state.repeatStart < 2000)) {
|
||||||
|
log.debug "Button ${buttonid} repeat ${state.repeatCount}x ${now()}"
|
||||||
|
state.repeatCount = state.repeatCount + 1
|
||||||
|
createEvent([:])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If the button was really pressed, store the new scene and handle the button press
|
||||||
|
state.lastScene = buttonid
|
||||||
|
state.lastLevel = 0
|
||||||
|
state.repeatCount = 0
|
||||||
|
state.repeatStart = now()
|
||||||
|
|
||||||
|
buttonEvent(buttonid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle a button being pressed
|
||||||
|
def buttonEvent(button) {
|
||||||
|
button = button as Integer
|
||||||
|
log.trace "Button $button pressed"
|
||||||
|
def result = []
|
||||||
|
if (button == 1) {
|
||||||
|
def mystate = device.currentValue('switch');
|
||||||
|
if (mystate == "on")
|
||||||
|
off()
|
||||||
|
else
|
||||||
|
on()
|
||||||
|
}
|
||||||
|
updateState("currentButton", "$button")
|
||||||
|
// update the device state, recording the button press
|
||||||
|
result << createEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicGet cmd) {
|
||||||
|
[ descriptionText: "$cmd"]
|
||||||
|
if(1){
|
||||||
|
Integer buttonid = 1
|
||||||
|
checkbuttonEvent(buttonid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||||
|
[ descriptionText: "$cmd"]
|
||||||
|
if(1){
|
||||||
|
Integer buttonid = 1
|
||||||
|
log.info "button $buttonid pressed"
|
||||||
|
checkbuttonEvent(buttonid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd) {
|
||||||
|
createEvent([:])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
[ descriptionText: "$cmd"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update State
|
||||||
|
def updateState(String name, String value) {
|
||||||
|
state[name] = value
|
||||||
|
device.updateDataValue(name, value)
|
||||||
|
}
|
||||||
206
devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy
Normal file
206
devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
/**
|
||||||
|
* Cree Bulb
|
||||||
|
*
|
||||||
|
* Copyright 2014 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "Cree Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Switch Level"
|
||||||
|
|
||||||
|
fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0000,0019"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "on": "on/off: 1"
|
||||||
|
status "off": "on/off: 0"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "zcl on-off on": "on/off: 1"
|
||||||
|
reply "zcl on-off off": "on/off: 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff"
|
||||||
|
state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "level", label: 'Level ${currentValue}%'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch", "level", "levelSliderControl", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
log.trace description
|
||||||
|
if (description?.startsWith("catchall:")) {
|
||||||
|
def msg = zigbee.parse(description)
|
||||||
|
log.trace msg
|
||||||
|
log.trace "data: $msg.data"
|
||||||
|
|
||||||
|
if(description?.endsWith("0100") ||description?.endsWith("1001"))
|
||||||
|
{
|
||||||
|
def result = createEvent(name: "switch", value: "on")
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
if(description?.endsWith("0000") || description?.endsWith("1000"))
|
||||||
|
{
|
||||||
|
def result = createEvent(name: "switch", value: "off")
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (description?.startsWith("read attr")) {
|
||||||
|
|
||||||
|
log.debug description[-2..-1]
|
||||||
|
def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 )
|
||||||
|
|
||||||
|
sendEvent( name: "level", value: i )
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
log.debug "on()"
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
|
||||||
|
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
log.debug "off()"
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
|
||||||
|
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
// Schedule poll every 1 min
|
||||||
|
//schedule("0 */1 * * * ?", poll)
|
||||||
|
//poll()
|
||||||
|
|
||||||
|
[
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
log.trace "setLevel($value)"
|
||||||
|
def cmds = []
|
||||||
|
|
||||||
|
if (value == 0) {
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {0000 0000}"
|
||||||
|
}
|
||||||
|
else if (device.latestValue("switch") == "off") {
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent(name: "level", value: value)
|
||||||
|
def level = hex(value * 255/100)
|
||||||
|
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
|
||||||
|
|
||||||
|
//log.debug cmds
|
||||||
|
cmds
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
|
||||||
|
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||||
|
log.debug "Confuguring Reporting and Bindings."
|
||||||
|
def configCmds = [
|
||||||
|
|
||||||
|
//Switch Reporting
|
||||||
|
"zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500",
|
||||||
|
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000",
|
||||||
|
|
||||||
|
//Level Control Reporting
|
||||||
|
"zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500",
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500",
|
||||||
|
]
|
||||||
|
return configCmds + refresh() // send refresh cmds as part of config
|
||||||
|
}
|
||||||
|
|
||||||
|
def uninstalled() {
|
||||||
|
|
||||||
|
log.debug "uninstalled()"
|
||||||
|
|
||||||
|
response("zcl rftd")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEndpointId() {
|
||||||
|
new BigInteger(device.endpointId, 16).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private hex(value, width=2) {
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() < width) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer convertHexToInt(hex) {
|
||||||
|
Integer.parseInt(hex,16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
int i = 0;
|
||||||
|
int j = array.length - 1;
|
||||||
|
byte tmp;
|
||||||
|
while (j > i) {
|
||||||
|
tmp = array[j];
|
||||||
|
array[j] = array[i];
|
||||||
|
array[i] = tmp;
|
||||||
|
j--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
@@ -0,0 +1,604 @@
|
|||||||
|
metadata {
|
||||||
|
// Automatically generated. Make future change here.
|
||||||
|
definition (name: "CT100 Thermostat", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
capability "Relative Humidity Measurement"
|
||||||
|
capability "Thermostat"
|
||||||
|
capability "Battery"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
attribute "thermostatFanState", "string"
|
||||||
|
|
||||||
|
command "switchMode"
|
||||||
|
command "switchFanMode"
|
||||||
|
command "quickSetCool"
|
||||||
|
command "quickSetHeat"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x08", inClusters: "0x43,0x40,0x44,0x31,0x80,0x85,0x60"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
status "off" : "command: 4003, payload: 00"
|
||||||
|
status "heat" : "command: 4003, payload: 01"
|
||||||
|
status "cool" : "command: 4003, payload: 02"
|
||||||
|
status "auto" : "command: 4003, payload: 03"
|
||||||
|
status "emergencyHeat" : "command: 4003, payload: 04"
|
||||||
|
|
||||||
|
status "fanAuto" : "command: 4403, payload: 00"
|
||||||
|
status "fanOn" : "command: 4403, payload: 01"
|
||||||
|
status "fanCirculate" : "command: 4403, payload: 06"
|
||||||
|
|
||||||
|
status "heat 60" : "command: 4303, payload: 01 09 3C"
|
||||||
|
status "heat 72" : "command: 4303, payload: 01 09 48"
|
||||||
|
|
||||||
|
status "cool 76" : "command: 4303, payload: 02 09 4C"
|
||||||
|
status "cool 80" : "command: 4303, payload: 02 09 50"
|
||||||
|
|
||||||
|
status "temp 58" : "command: 3105, payload: 01 2A 02 44"
|
||||||
|
status "temp 62" : "command: 3105, payload: 01 2A 02 6C"
|
||||||
|
status "temp 78" : "command: 3105, payload: 01 2A 03 0C"
|
||||||
|
status "temp 86" : "command: 3105, payload: 01 2A 03 34"
|
||||||
|
|
||||||
|
status "idle" : "command: 4203, payload: 00"
|
||||||
|
status "heating" : "command: 4203, payload: 01"
|
||||||
|
status "cooling" : "command: 4203, payload: 02"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "2502": "command: 2503, payload: FF"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||||
|
state("temperature", label:'${currentValue}°',
|
||||||
|
backgroundColors:[
|
||||||
|
[value: 32, color: "#153591"],
|
||||||
|
[value: 44, color: "#1e9cbb"],
|
||||||
|
[value: 59, color: "#90d2a7"],
|
||||||
|
[value: 74, color: "#44b621"],
|
||||||
|
[value: 84, color: "#f1d801"],
|
||||||
|
[value: 92, color: "#d04e00"],
|
||||||
|
[value: 98, color: "#bc2323"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "off", label:'${name}', action:"switchMode", nextState:"to_heat"
|
||||||
|
state "heat", label:'${name}', action:"switchMode", nextState:"to_cool"
|
||||||
|
state "cool", label:'${name}', action:"switchMode", nextState:"..."
|
||||||
|
state "auto", label:'${name}', action:"switchMode", nextState:"..."
|
||||||
|
state "emergency heat", label:'${name}', action:"switchMode", nextState:"..."
|
||||||
|
state "to_heat", label: "heat", action:"switchMode", nextState:"to_cool"
|
||||||
|
state "to_cool", label: "cool", action:"switchMode", nextState:"..."
|
||||||
|
state "...", label: "...", action:"off", nextState:"off"
|
||||||
|
}
|
||||||
|
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "fanAuto", label:'${name}', action:"switchFanMode"
|
||||||
|
state "fanOn", label:'${name}', action:"switchFanMode"
|
||||||
|
state "fanCirculate", label:'${name}', action:"switchFanMode"
|
||||||
|
}
|
||||||
|
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "setHeatingSetpoint", action:"quickSetHeat", backgroundColor:"#d04e00"
|
||||||
|
}
|
||||||
|
valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "heat", label:'${currentValue}° heat', backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "setCoolingSetpoint", action:"quickSetCool", backgroundColor: "#1e9cbb"
|
||||||
|
}
|
||||||
|
valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "cool", label:'${currentValue}° cool', backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
valueTile("humidity", "device.humidity", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "humidity", label:'${currentValue}% humidity', unit:""
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
main "temperature"
|
||||||
|
details(["temperature", "mode", "fanMode", "heatSliderControl", "heatingSetpoint", "coolSliderControl", "coolingSetpoint", "refresh", "humidity", "battery"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description)
|
||||||
|
{
|
||||||
|
def result = []
|
||||||
|
if (description == "updated") {
|
||||||
|
} else {
|
||||||
|
def zwcmd = zwave.parse(description, [0x42:2, 0x43:2, 0x31: 2, 0x60: 3])
|
||||||
|
if (zwcmd) {
|
||||||
|
result += zwaveEvent(zwcmd)
|
||||||
|
} else {
|
||||||
|
log.debug "$device.displayName couldn't parse $description"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!result) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (result.size() == 1 && (!state.lastbatt || now() - state.lastbatt > 48*60*60*1000)) {
|
||||||
|
result << response(zwave.batteryV1.batteryGet().format())
|
||||||
|
}
|
||||||
|
log.debug "$device.displayName parsed '$description' to $result"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
|
||||||
|
def result = null
|
||||||
|
def encapsulatedCommand = cmd.encapsulatedCommand([0x42:2, 0x43:2, 0x31: 2])
|
||||||
|
log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}")
|
||||||
|
if (encapsulatedCommand) {
|
||||||
|
result = zwaveEvent(encapsulatedCommand)
|
||||||
|
if (cmd.sourceEndPoint == 1) { // indicates a response to refresh() vs an unrequested update
|
||||||
|
def event = ([] + result)[0] // in case zwaveEvent returns a list
|
||||||
|
def resp = nextRefreshQuery(event?.name)
|
||||||
|
if (resp) {
|
||||||
|
log.debug("sending next refresh query: $resp")
|
||||||
|
result = [] + result + response(["delay 200", resp])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd)
|
||||||
|
{
|
||||||
|
def cmdScale = cmd.scale == 1 ? "F" : "C"
|
||||||
|
def temp = convertTemperatureIfNeeded(cmd.scaledValue, cmdScale, cmd.precision)
|
||||||
|
def unit = getTemperatureScale()
|
||||||
|
def map1 = [ value: temp, unit: unit, displayed: false ]
|
||||||
|
switch (cmd.setpointType) {
|
||||||
|
case 1:
|
||||||
|
map1.name = "heatingSetpoint"
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
map1.name = "coolingSetpoint"
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.debug "unknown setpointType $cmd.setpointType"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// So we can respond with same format
|
||||||
|
state.size = cmd.size
|
||||||
|
state.scale = cmd.scale
|
||||||
|
state.precision = cmd.precision
|
||||||
|
|
||||||
|
def mode = device.latestValue("thermostatMode")
|
||||||
|
if (mode && map1.name.startsWith(mode) || (mode == "emergency heat" && map1.name == "heatingSetpoint")) {
|
||||||
|
def map2 = [ name: "thermostatSetpoint", value: temp, unit: unit ]
|
||||||
|
[ createEvent(map1), createEvent(map2) ]
|
||||||
|
} else {
|
||||||
|
createEvent(map1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
if (cmd.sensorType == 1) {
|
||||||
|
map.name = "temperature"
|
||||||
|
map.unit = getTemperatureScale()
|
||||||
|
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision)
|
||||||
|
} else if (cmd.sensorType == 5) {
|
||||||
|
map.name = "humidity"
|
||||||
|
map.unit = "%"
|
||||||
|
map.value = cmd.scaledSensorValue
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport cmd)
|
||||||
|
{
|
||||||
|
def map = [name: "thermostatOperatingState" ]
|
||||||
|
switch (cmd.operatingState) {
|
||||||
|
case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_IDLE:
|
||||||
|
map.value = "idle"
|
||||||
|
break
|
||||||
|
case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_HEATING:
|
||||||
|
map.value = "heating"
|
||||||
|
break
|
||||||
|
case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_COOLING:
|
||||||
|
map.value = "cooling"
|
||||||
|
break
|
||||||
|
case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_FAN_ONLY:
|
||||||
|
map.value = "fan only"
|
||||||
|
break
|
||||||
|
case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_HEAT:
|
||||||
|
map.value = "pending heat"
|
||||||
|
break
|
||||||
|
case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_COOL:
|
||||||
|
map.value = "pending cool"
|
||||||
|
break
|
||||||
|
case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_VENT_ECONOMIZER:
|
||||||
|
map.value = "vent economizer"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
def result = createEvent(map)
|
||||||
|
if (result.isStateChange && device.latestValue("thermostatMode") == "auto" && (result.value == "heating" || result.value == "cooling")) {
|
||||||
|
def thermostatSetpoint = device.latestValue("${result.value}Setpoint")
|
||||||
|
result = [result, createEvent(name: "thermostatSetpoint", value: thermostatSetpoint, unit: getTemperatureScale())]
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanStateReport cmd) {
|
||||||
|
def map = [name: "thermostatFanState", unit: ""]
|
||||||
|
switch (cmd.fanOperatingState) {
|
||||||
|
case 0:
|
||||||
|
map.value = "idle"
|
||||||
|
break
|
||||||
|
case 1:
|
||||||
|
map.value = "running"
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
map.value = "running high"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) {
|
||||||
|
def map = [name: "thermostatMode"]
|
||||||
|
def thermostatSetpoint = null
|
||||||
|
switch (cmd.mode) {
|
||||||
|
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF:
|
||||||
|
map.value = "off"
|
||||||
|
break
|
||||||
|
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_HEAT:
|
||||||
|
map.value = "heat"
|
||||||
|
thermostatSetpoint = device.latestValue("heatingSetpoint")
|
||||||
|
break
|
||||||
|
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUXILIARY_HEAT:
|
||||||
|
map.value = "emergency heat"
|
||||||
|
thermostatSetpoint = device.latestValue("heatingSetpoint")
|
||||||
|
break
|
||||||
|
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_COOL:
|
||||||
|
map.value = "cool"
|
||||||
|
thermostatSetpoint = device.latestValue("coolingSetpoint")
|
||||||
|
break
|
||||||
|
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUTO:
|
||||||
|
map.value = "auto"
|
||||||
|
def temp = device.latestValue("temperature")
|
||||||
|
def heatingSetpoint = device.latestValue("heatingSetpoint")
|
||||||
|
def coolingSetpoint = device.latestValue("coolingSetpoint")
|
||||||
|
if (temp && heatingSetpoint && coolingSetpoint) {
|
||||||
|
if (temp < (heatingSetpoint + coolingSetpoint) / 2.0) {
|
||||||
|
thermostatSetpoint = heatingSetpoint
|
||||||
|
} else {
|
||||||
|
thermostatSetpoint = coolingSetpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
state.lastTriedMode = map.value
|
||||||
|
if (thermostatSetpoint) {
|
||||||
|
[ createEvent(map), createEvent(name: "thermostatSetpoint", value: thermostatSetpoint, unit: getTemperatureScale()) ]
|
||||||
|
} else {
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) {
|
||||||
|
def map = [name: "thermostatFanMode", displayed: false]
|
||||||
|
switch (cmd.fanMode) {
|
||||||
|
case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW:
|
||||||
|
map.value = "fanAuto"
|
||||||
|
break
|
||||||
|
case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_LOW:
|
||||||
|
map.value = "fanOn"
|
||||||
|
break
|
||||||
|
case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_CIRCULATION:
|
||||||
|
map.value = "fanCirculate"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
state.lastTriedFanMode = map.value
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) {
|
||||||
|
def supportedModes = ""
|
||||||
|
if(cmd.off) { supportedModes += "off " }
|
||||||
|
if(cmd.heat) { supportedModes += "heat " }
|
||||||
|
if(cmd.auxiliaryemergencyHeat) { supportedModes += "emergency heat " }
|
||||||
|
if(cmd.cool) { supportedModes += "cool " }
|
||||||
|
if(cmd.auto) { supportedModes += "auto " }
|
||||||
|
|
||||||
|
state.supportedModes = supportedModes
|
||||||
|
[ createEvent(name:"supportedModes", value: supportedModes, displayed: false),
|
||||||
|
response(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet()) ]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeSupportedReport cmd) {
|
||||||
|
def supportedFanModes = ""
|
||||||
|
if(cmd.auto) { supportedFanModes += "fanAuto " }
|
||||||
|
if(cmd.low) { supportedFanModes += "fanOn " }
|
||||||
|
if(cmd.circulation) { supportedFanModes += "fanCirculate " }
|
||||||
|
|
||||||
|
state.supportedFanModes = supportedFanModes
|
||||||
|
[ createEvent(name:"supportedFanModes", value: supportedModes, displayed: false),
|
||||||
|
response(refresh()) ]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||||
|
log.debug "Zwave event received: $cmd"
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||||
|
def map = [ name: "battery", unit: "%" ]
|
||||||
|
if (cmd.batteryLevel == 0xFF) {
|
||||||
|
map.value = 1
|
||||||
|
map.descriptionText = "${device.displayName} battery is low"
|
||||||
|
map.isStateChange = true
|
||||||
|
} else {
|
||||||
|
map.value = cmd.batteryLevel
|
||||||
|
}
|
||||||
|
state.lastbatt = now()
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
log.warn "Unexpected zwave command $cmd"
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
// Use encapsulation to differentiate refresh cmds from what the thermostat sends proactively on change
|
||||||
|
def cmd = zwave.sensorMultilevelV2.sensorMultilevelGet()
|
||||||
|
zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:1).encapsulate(cmd).format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def nextRefreshQuery(name) {
|
||||||
|
def cmd = null
|
||||||
|
switch (name) {
|
||||||
|
case "temperature":
|
||||||
|
cmd = zwave.thermostatModeV2.thermostatModeGet()
|
||||||
|
break
|
||||||
|
case "thermostatMode":
|
||||||
|
cmd = zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1)
|
||||||
|
break
|
||||||
|
case "heatingSetpoint":
|
||||||
|
cmd = zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2)
|
||||||
|
break
|
||||||
|
case "coolingSetpoint":
|
||||||
|
cmd = zwave.thermostatFanModeV3.thermostatFanModeGet()
|
||||||
|
break
|
||||||
|
case "thermostatFanMode":
|
||||||
|
cmd = zwave.thermostatOperatingStateV2.thermostatOperatingStateGet()
|
||||||
|
break
|
||||||
|
case "thermostatOperatingState":
|
||||||
|
// get humidity, multilevel sensor get to endpoint 2
|
||||||
|
cmd = zwave.sensorMultilevelV2.sensorMultilevelGet()
|
||||||
|
return zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:2).encapsulate(cmd).format()
|
||||||
|
default: return null
|
||||||
|
}
|
||||||
|
zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:1).encapsulate(cmd).format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def quickSetHeat(degrees) {
|
||||||
|
setHeatingSetpoint(degrees, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setHeatingSetpoint(degrees, delay = 30000) {
|
||||||
|
setHeatingSetpoint(degrees.toDouble(), delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setHeatingSetpoint(Double degrees, Integer delay = 30000) {
|
||||||
|
log.trace "setHeatingSetpoint($degrees, $delay)"
|
||||||
|
def deviceScale = state.scale ?: 1
|
||||||
|
def deviceScaleString = deviceScale == 2 ? "C" : "F"
|
||||||
|
def locationScale = getTemperatureScale()
|
||||||
|
def p = (state.precision == null) ? 1 : state.precision
|
||||||
|
|
||||||
|
def convertedDegrees
|
||||||
|
if (locationScale == "C" && deviceScaleString == "F") {
|
||||||
|
convertedDegrees = celsiusToFahrenheit(degrees)
|
||||||
|
} else if (locationScale == "F" && deviceScaleString == "C") {
|
||||||
|
convertedDegrees = fahrenheitToCelsius(degrees)
|
||||||
|
} else {
|
||||||
|
convertedDegrees = degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
delayBetween([
|
||||||
|
zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 1, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(),
|
||||||
|
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()
|
||||||
|
], delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def quickSetCool(degrees) {
|
||||||
|
setCoolingSetpoint(degrees, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setCoolingSetpoint(degrees, delay = 30000) {
|
||||||
|
setCoolingSetpoint(degrees.toDouble(), delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setCoolingSetpoint(Double degrees, Integer delay = 30000) {
|
||||||
|
log.trace "setCoolingSetpoint($degrees, $delay)"
|
||||||
|
def deviceScale = state.scale ?: 1
|
||||||
|
def deviceScaleString = deviceScale == 2 ? "C" : "F"
|
||||||
|
def locationScale = getTemperatureScale()
|
||||||
|
def p = (state.precision == null) ? 1 : state.precision
|
||||||
|
|
||||||
|
def convertedDegrees
|
||||||
|
if (locationScale == "C" && deviceScaleString == "F") {
|
||||||
|
convertedDegrees = celsiusToFahrenheit(degrees)
|
||||||
|
} else if (locationScale == "F" && deviceScaleString == "C") {
|
||||||
|
convertedDegrees = fahrenheitToCelsius(degrees)
|
||||||
|
} else {
|
||||||
|
convertedDegrees = degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
delayBetween([
|
||||||
|
zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 2, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(),
|
||||||
|
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()
|
||||||
|
], delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.thermostatModeV2.thermostatModeSupportedGet().format(),
|
||||||
|
], 2300)
|
||||||
|
}
|
||||||
|
|
||||||
|
def modes() {
|
||||||
|
["off", "heat", "cool", "auto", "emergency heat"]
|
||||||
|
}
|
||||||
|
|
||||||
|
def switchMode() {
|
||||||
|
def currentMode = device.currentState("thermostatMode")?.value
|
||||||
|
def lastTriedMode = state.lastTriedMode ?: currentMode ?: "off"
|
||||||
|
def supportedModes = getDataByName("supportedModes")
|
||||||
|
def modeOrder = modes()
|
||||||
|
def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
|
||||||
|
def nextMode = next(lastTriedMode)
|
||||||
|
if (supportedModes?.contains(currentMode)) {
|
||||||
|
while (!supportedModes.contains(nextMode) && nextMode != "off") {
|
||||||
|
nextMode = next(nextMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.lastTriedMode = nextMode
|
||||||
|
delayBetween([
|
||||||
|
zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[nextMode]).format(),
|
||||||
|
zwave.thermostatModeV2.thermostatModeGet().format()
|
||||||
|
], 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
def switchToMode(nextMode) {
|
||||||
|
def supportedModes = getDataByName("supportedModes")
|
||||||
|
if(supportedModes && !supportedModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported"
|
||||||
|
if (nextMode in modes()) {
|
||||||
|
state.lastTriedMode = nextMode
|
||||||
|
"$nextMode"()
|
||||||
|
} else {
|
||||||
|
log.debug("no mode method '$nextMode'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def switchFanMode() {
|
||||||
|
def currentMode = device.currentState("thermostatFanMode")?.value
|
||||||
|
def lastTriedMode = state.lastTriedFanMode ?: currentMode ?: "off"
|
||||||
|
def supportedModes = getDataByName("supportedFanModes") ?: "fanAuto fanOn"
|
||||||
|
def modeOrder = ["fanAuto", "fanCirculate", "fanOn"]
|
||||||
|
def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
|
||||||
|
def nextMode = next(lastTriedMode)
|
||||||
|
while (!supportedModes?.contains(nextMode) && nextMode != "fanAuto") {
|
||||||
|
nextMode = next(nextMode)
|
||||||
|
}
|
||||||
|
switchToFanMode(nextMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
def switchToFanMode(nextMode) {
|
||||||
|
def supportedFanModes = getDataByName("supportedFanModes")
|
||||||
|
if(supportedFanModes && !supportedFanModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported"
|
||||||
|
|
||||||
|
def returnCommand
|
||||||
|
if (nextMode == "fanAuto") {
|
||||||
|
returnCommand = fanAuto()
|
||||||
|
} else if (nextMode == "fanOn") {
|
||||||
|
returnCommand = fanOn()
|
||||||
|
} else if (nextMode == "fanCirculate") {
|
||||||
|
returnCommand = fanCirculate()
|
||||||
|
} else {
|
||||||
|
log.debug("no fan mode '$nextMode'")
|
||||||
|
}
|
||||||
|
if(returnCommand) state.lastTriedFanMode = nextMode
|
||||||
|
returnCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
def getDataByName(String name) {
|
||||||
|
state[name] ?: device.getDataValue(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
def getModeMap() { [
|
||||||
|
"off": 0,
|
||||||
|
"heat": 1,
|
||||||
|
"cool": 2,
|
||||||
|
"auto": 3,
|
||||||
|
"emergency heat": 4
|
||||||
|
]}
|
||||||
|
|
||||||
|
def setThermostatMode(String value) {
|
||||||
|
delayBetween([
|
||||||
|
zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[value]).format(),
|
||||||
|
zwave.thermostatModeV2.thermostatModeGet().format()
|
||||||
|
], standardDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def getFanModeMap() { [
|
||||||
|
"auto": 0,
|
||||||
|
"on": 1,
|
||||||
|
"circulate": 6
|
||||||
|
]}
|
||||||
|
|
||||||
|
def setThermostatFanMode(String value) {
|
||||||
|
delayBetween([
|
||||||
|
zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[value]).format(),
|
||||||
|
zwave.thermostatFanModeV3.thermostatFanModeGet().format()
|
||||||
|
], standardDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.thermostatModeV2.thermostatModeSet(mode: 0).format(),
|
||||||
|
zwave.thermostatModeV2.thermostatModeGet().format()
|
||||||
|
], standardDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def heat() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.thermostatModeV2.thermostatModeSet(mode: 1).format(),
|
||||||
|
zwave.thermostatModeV2.thermostatModeGet().format()
|
||||||
|
], standardDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def emergencyHeat() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.thermostatModeV2.thermostatModeSet(mode: 4).format(),
|
||||||
|
zwave.thermostatModeV2.thermostatModeGet().format()
|
||||||
|
], standardDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def cool() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.thermostatModeV2.thermostatModeSet(mode: 2).format(),
|
||||||
|
zwave.thermostatModeV2.thermostatModeGet().format()
|
||||||
|
], standardDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def auto() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.thermostatModeV2.thermostatModeSet(mode: 3).format(),
|
||||||
|
zwave.thermostatModeV2.thermostatModeGet().format()
|
||||||
|
], standardDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def fanOn() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 1).format(),
|
||||||
|
zwave.thermostatFanModeV3.thermostatFanModeGet().format()
|
||||||
|
], standardDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def fanAuto() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 0).format(),
|
||||||
|
zwave.thermostatFanModeV3.thermostatFanModeGet().format()
|
||||||
|
], standardDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def fanCirculate() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 6).format(),
|
||||||
|
zwave.thermostatFanModeV3.thermostatFanModeGet().format()
|
||||||
|
], standardDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStandardDelay() {
|
||||||
|
1000
|
||||||
|
}
|
||||||
|
|
||||||
244
devicetypes/smartthings/danalock.src/danalock.groovy
Normal file
244
devicetypes/smartthings/danalock.src/danalock.groovy
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition(name: "Danalock", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Lock"
|
||||||
|
capability "Battery"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint deviceId: '0x4002', inClusters: '0x72,0x80,0x86,0x98'
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "locked": "command: 9881, payload: 00 62 03 FF 00 07 FE FE"
|
||||||
|
status "unlocked": "command: 9881, payload: 00 62 03 00 FF 06 FE FE"
|
||||||
|
|
||||||
|
reply "9881006201FF,delay 4200,9881006202": "command: 9881, payload: 00 62 03 FF 00 07 FE FE"
|
||||||
|
reply "988100620100,delay 4200,9881006202": "command: 9881, payload: 00 62 03 00 00 06 FE FE"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("toggle", "device.lock", width: 2, height: 2) {
|
||||||
|
state "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821", nextState:"unlocking"
|
||||||
|
state "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking"
|
||||||
|
state "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking"
|
||||||
|
state "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#79b821"
|
||||||
|
state "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("lock", "device.lock", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked", nextState:"locking"
|
||||||
|
}
|
||||||
|
standardTile("unlock", "device.lock", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked", nextState:"unlocking"
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.lock", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "toggle"
|
||||||
|
details(["toggle", "lock", "unlock", "battery", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import physicalgraph.zwave.commands.doorlockv1.*
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def result = null
|
||||||
|
if (description.startsWith("Err")) {
|
||||||
|
if (state.sec) {
|
||||||
|
result = createEvent(descriptionText:description, displayed:false)
|
||||||
|
} else {
|
||||||
|
result = createEvent(
|
||||||
|
descriptionText: "This lock 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.",
|
||||||
|
eventType: "ALERT",
|
||||||
|
name: "secureInclusion",
|
||||||
|
value: "failed",
|
||||||
|
displayed: true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2, 0x85: 2, 0x8A: 1 ])
|
||||||
|
if (cmd) {
|
||||||
|
result = zwaveEvent(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug "\"$description\" parsed to ${result.inspect()}"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||||
|
def encapsulatedCommand = cmd.encapsulatedCommand([0x62: 1, 0x71: 2, 0x80: 1, 0x8A: 1, 0x85: 2, 0x98: 1])
|
||||||
|
// log.debug "encapsulated: $encapsulatedCommand"
|
||||||
|
if (encapsulatedCommand) {
|
||||||
|
zwaveEvent(encapsulatedCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) {
|
||||||
|
createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful")
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
|
||||||
|
state.sec = cmd.commandClassSupport.collect { String.format("%02X ", it) }.join()
|
||||||
|
if (cmd.commandClassControl) {
|
||||||
|
state.secCon = cmd.commandClassControl.collect { String.format("%02X ", it) }.join()
|
||||||
|
}
|
||||||
|
log.debug "Security command classes: $state.sec"
|
||||||
|
createEvent(name:"secureInclusion", value:"success", descriptionText:"$device.displayName is securely included")
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(DoorLockOperationReport cmd) {
|
||||||
|
def result = []
|
||||||
|
def map = [ name: "lock" ]
|
||||||
|
if (cmd.doorLockMode == 0xFF) {
|
||||||
|
map.value = "locked"
|
||||||
|
} else if (cmd.doorLockMode >= 0x40) {
|
||||||
|
map.value = "unknown"
|
||||||
|
} else if (cmd.doorLockMode & 1) {
|
||||||
|
map.value = "unlocked with timeout"
|
||||||
|
} else {
|
||||||
|
map.value = "unlocked"
|
||||||
|
}
|
||||||
|
result ? [createEvent(map), *result] : createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.timev1.TimeGet cmd) {
|
||||||
|
def result = []
|
||||||
|
def now = new Date().toCalendar()
|
||||||
|
if(location.timeZone) now.timeZone = location.timeZone
|
||||||
|
result << createEvent(descriptionText: "$device.displayName requested time update", displayed: false)
|
||||||
|
result << response(secure(zwave.timeV1.timeReport(
|
||||||
|
hourLocalTime: now.get(Calendar.HOUR_OF_DAY),
|
||||||
|
minuteLocalTime: now.get(Calendar.MINUTE),
|
||||||
|
secondLocalTime: now.get(Calendar.SECOND)))
|
||||||
|
)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||||
|
createEvent(name: "lock", value: cmd.value ? "unlocked" : "locked")
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
} else {
|
||||||
|
map.value = cmd.batteryLevel
|
||||||
|
}
|
||||||
|
state.lastbatt = new Date().time
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||||
|
def result = []
|
||||||
|
|
||||||
|
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
|
||||||
|
log.debug "msr: $msr"
|
||||||
|
updateDataValue("MSR", msr)
|
||||||
|
|
||||||
|
result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {
|
||||||
|
def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}"
|
||||||
|
updateDataValue("fw", fw)
|
||||||
|
def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}"
|
||||||
|
createEvent(descriptionText: text, isStateChange: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) {
|
||||||
|
def msg = cmd.status == 0 ? "try again later" :
|
||||||
|
cmd.status == 1 ? "try again in $cmd.waitTime seconds" :
|
||||||
|
cmd.status == 2 ? "request queued" : "sorry"
|
||||||
|
createEvent(displayed: false, descriptionText: "$device.displayName is busy, $msg")
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
createEvent(displayed: false, descriptionText: "$device.displayName: $cmd")
|
||||||
|
}
|
||||||
|
|
||||||
|
def lockAndCheck(doorLockMode) {
|
||||||
|
secureSequence([
|
||||||
|
zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode),
|
||||||
|
zwave.doorLockV1.doorLockOperationGet()
|
||||||
|
], 4200)
|
||||||
|
}
|
||||||
|
|
||||||
|
def lock() {
|
||||||
|
lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_SECURED)
|
||||||
|
}
|
||||||
|
|
||||||
|
def unlock() {
|
||||||
|
lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED)
|
||||||
|
}
|
||||||
|
|
||||||
|
def unlockwtimeout() {
|
||||||
|
lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED_WITH_TIMEOUT)
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
def cmds = [secure(zwave.doorLockV1.doorLockOperationGet())]
|
||||||
|
if (secondsPast(state.lastbatt, 60)) {
|
||||||
|
cmds << "delay 8000"
|
||||||
|
cmds << secure(zwave.batteryV1.batteryGet())
|
||||||
|
}
|
||||||
|
log.debug "refresh sending ${cmds.inspect()}"
|
||||||
|
cmds
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
def cmds = []
|
||||||
|
// Only check lock state if it changed recently or we haven't had an update in an hour
|
||||||
|
def latest = device.currentState("lock")?.date?.time
|
||||||
|
if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 67 * 60)) {
|
||||||
|
cmds << secure(zwave.doorLockV1.doorLockOperationGet())
|
||||||
|
state.lastPoll = (new Date()).time
|
||||||
|
} else if (secondsPast(state.lastbatt, 53 * 3600)) {
|
||||||
|
cmds << secure(zwave.batteryV1.batteryGet())
|
||||||
|
}
|
||||||
|
if(cmds) cmds << "delay 6000"
|
||||||
|
log.debug "poll is sending ${cmds.inspect()}, state: ${state.inspect()}"
|
||||||
|
sendEvent(descriptionText: "poll sent ${cmds ?: 'nothing'}", isStateChange: false) // workaround to keep polling from being shut off
|
||||||
|
cmds ?: null
|
||||||
|
}
|
||||||
|
|
||||||
|
private secure(physicalgraph.zwave.Command cmd) {
|
||||||
|
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||||
|
}
|
||||||
|
|
||||||
|
private secureSequence(commands, delay=4200) {
|
||||||
|
delayBetween(commands.collect{ secure(it) }, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean secondsPast(timestamp, seconds) {
|
||||||
|
if (!(timestamp instanceof Number)) {
|
||||||
|
if (timestamp instanceof Date) {
|
||||||
|
timestamp = timestamp.time
|
||||||
|
} else if ((timestamp instanceof String) && timestamp.isNumber()) {
|
||||||
|
timestamp = timestamp.toLong()
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (new Date().time - timestamp) > (seconds * 1000)
|
||||||
|
}
|
||||||
222
devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy
Normal file
222
devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Dimmer Switch", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Indicator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint inClusters: "0x26"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "on": "command: 2003, payload: FF"
|
||||||
|
status "off": "command: 2003, payload: 00"
|
||||||
|
status "09%": "command: 2003, payload: 09"
|
||||||
|
status "10%": "command: 2003, payload: 0A"
|
||||||
|
status "33%": "command: 2003, payload: 21"
|
||||||
|
status "66%": "command: 2003, payload: 42"
|
||||||
|
status "99%": "command: 2003, payload: 63"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "2001FF,delay 5000,2602": "command: 2603, payload: FF"
|
||||||
|
reply "200100,delay 5000,2602": "command: 2603, payload: 00"
|
||||||
|
reply "200119,delay 5000,2602": "command: 2603, payload: 19"
|
||||||
|
reply "200132,delay 5000,2602": "command: 2603, payload: 32"
|
||||||
|
reply "20014B,delay 5000,2602": "command: 2603, payload: 4B"
|
||||||
|
reply "200163,delay 5000,2602": "command: 2603, payload: 63"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
}
|
||||||
|
standardTile("indicator", "device.indicatorStatus", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "when off", action:"indicator.indicatorWhenOn", icon:"st.indicators.lit-when-off"
|
||||||
|
state "when on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on"
|
||||||
|
state "never", action:"indicator.indicatorWhenOff", icon:"st.indicators.never-lit"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch", "refresh", "indicator", "levelSliderControl"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def item1 = [
|
||||||
|
canBeCurrentState: false,
|
||||||
|
linkText: getLinkText(device),
|
||||||
|
isStateChange: false,
|
||||||
|
displayed: false,
|
||||||
|
descriptionText: description,
|
||||||
|
value: description
|
||||||
|
]
|
||||||
|
def result
|
||||||
|
def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1])
|
||||||
|
if (cmd) {
|
||||||
|
result = createEvent(cmd, item1)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
item1.displayed = displayed(description, item1.isStateChange)
|
||||||
|
result = [item1]
|
||||||
|
}
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) {
|
||||||
|
def result = doCreateEvent(cmd, item1)
|
||||||
|
for (int i = 0; i < result.size(); i++) {
|
||||||
|
result[i].type = "physical"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, Map item1) {
|
||||||
|
def result = doCreateEvent(cmd, item1)
|
||||||
|
for (int i = 0; i < result.size(); i++) {
|
||||||
|
result[i].type = "physical"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd, Map item1) {
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd, Map item1) {
|
||||||
|
[response(zwave.basicV1.basicGet())]
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd, Map item1) {
|
||||||
|
def result = doCreateEvent(cmd, item1)
|
||||||
|
for (int i = 0; i < result.size(); i++) {
|
||||||
|
result[i].type = "physical"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd, Map item1) {
|
||||||
|
def result = doCreateEvent(cmd, item1)
|
||||||
|
result[0].descriptionText = "${item1.linkText} is ${item1.value}"
|
||||||
|
result[0].handlerName = cmd.value ? "statusOn" : "statusOff"
|
||||||
|
for (int i = 0; i < result.size(); i++) {
|
||||||
|
result[i].type = "digital"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) {
|
||||||
|
def result = [item1]
|
||||||
|
|
||||||
|
item1.name = "switch"
|
||||||
|
item1.value = cmd.value ? "on" : "off"
|
||||||
|
item1.handlerName = item1.value
|
||||||
|
item1.descriptionText = "${item1.linkText} was turned ${item1.value}"
|
||||||
|
item1.canBeCurrentState = true
|
||||||
|
item1.isStateChange = isStateChange(device, item1.name, item1.value)
|
||||||
|
item1.displayed = item1.isStateChange
|
||||||
|
|
||||||
|
if (cmd.value >= 5) {
|
||||||
|
def item2 = new LinkedHashMap(item1)
|
||||||
|
item2.name = "level"
|
||||||
|
item2.value = cmd.value as String
|
||||||
|
item2.unit = "%"
|
||||||
|
item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %"
|
||||||
|
item2.canBeCurrentState = true
|
||||||
|
item2.isStateChange = isStateChange(device, item2.name, item2.value)
|
||||||
|
item2.displayed = false
|
||||||
|
result << item2
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
|
||||||
|
def value = "when off"
|
||||||
|
if (cmd.configurationValue[0] == 1) {value = "when on"}
|
||||||
|
if (cmd.configurationValue[0] == 2) {value = "never"}
|
||||||
|
[name: "indicatorStatus", value: value, display: false]
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.Command cmd, Map map) {
|
||||||
|
// Handles any Z-Wave commands we aren't interested in
|
||||||
|
log.debug "UNHANDLED COMMAND $cmd"
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
log.info "on"
|
||||||
|
delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
delayBetween ([zwave.basicV1.basicSet(value: 0x00).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
def valueaux = value as Integer
|
||||||
|
def level = Math.min(valueaux, 99)
|
||||||
|
delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value, duration) {
|
||||||
|
def valueaux = value as Integer
|
||||||
|
def level = Math.min(valueaux, 99)
|
||||||
|
def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60)
|
||||||
|
zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def indicatorWhenOn() {
|
||||||
|
sendEvent(name: "indicatorStatus", value: "when on", display: false)
|
||||||
|
zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def indicatorWhenOff() {
|
||||||
|
sendEvent(name: "indicatorStatus", value: "when off", display: false)
|
||||||
|
zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def indicatorNever() {
|
||||||
|
sendEvent(name: "indicatorStatus", value: "never", display: false)
|
||||||
|
zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def invertSwitch(invert=true) {
|
||||||
|
if (invert) {
|
||||||
|
zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 4, size: 1).format()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 4, size: 1).format()
|
||||||
|
}
|
||||||
|
}
|
||||||
50
devicetypes/smartthings/door-shield.src/door-shield.groovy
Normal file
50
devicetypes/smartthings/door-shield.src/door-shield.groovy
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Door Shield", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
|
command "open"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "ping": "catchall: 0104 0000 01 01 0040 00 6A67 00 00 0000 0A 00 0A70696E67"
|
||||||
|
status "response": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A4F4D4E4F4D4E4F4D4E4F4D"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("shield", "device.shield", width: 2, height: 2, canChangeBackground: true) {
|
||||||
|
state(name:"default", action:"open", icon:"st.shields.shields.door-shield", backgroundColor:"#ffffff")
|
||||||
|
}
|
||||||
|
|
||||||
|
main "shield"
|
||||||
|
details "shield"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
def value = zigbee.parse(description)?.text
|
||||||
|
def name = value && value != "ping" ? "response" : null
|
||||||
|
def result = createEvent(name: name, value: value)
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands sent to the device
|
||||||
|
def open() {
|
||||||
|
zigbee.smartShield(text: "open sesame").format()
|
||||||
|
}
|
||||||
@@ -0,0 +1,675 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* Ecobee Thermostat
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2013-06-13
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Ecobee Thermostat", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Thermostat"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Refresh"
|
||||||
|
|
||||||
|
command "generateEvent"
|
||||||
|
command "raiseSetpoint"
|
||||||
|
command "lowerSetpoint"
|
||||||
|
command "resumeProgram"
|
||||||
|
command "switchMode"
|
||||||
|
|
||||||
|
attribute "thermostatSetpoint","number"
|
||||||
|
attribute "thermostatStatus","string"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator { }
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||||
|
state("temperature", label:'${currentValue}°', unit:"F",
|
||||||
|
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"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "off", action:"switchMode", nextState: "updating", icon: "st.thermostat.heating-cooling-off"
|
||||||
|
state "heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.heat"
|
||||||
|
state "cool", action:"switchMode", nextState: "updating", icon: "st.thermostat.cool"
|
||||||
|
state "auto", action:"switchMode", nextState: "updating", icon: "st.thermostat.auto"
|
||||||
|
state "auxHeatOnly", action:"switchMode", icon: "st.thermostat.emergency-heat"
|
||||||
|
state "updating", label:"Working", icon: "st.secondary.secondary"
|
||||||
|
}
|
||||||
|
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "auto", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "on"
|
||||||
|
state "on", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "off"
|
||||||
|
state "off", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "circulate"
|
||||||
|
state "circulate", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "auto"
|
||||||
|
}
|
||||||
|
standardTile("upButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "setpoint", action:"raiseSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-up"
|
||||||
|
}
|
||||||
|
valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") {
|
||||||
|
state "thermostatSetpoint", label:'${currentValue}'
|
||||||
|
}
|
||||||
|
valueTile("currentStatus", "device.thermostatStatus", height: 1, width: 2, decoration: "flat") {
|
||||||
|
state "thermostatStatus", label:'${currentValue}', backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("downButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "setpoint", action:"lowerSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-down"
|
||||||
|
}
|
||||||
|
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00"
|
||||||
|
}
|
||||||
|
valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "heat", label:'${currentValue}° heat', unit:"F"
|
||||||
|
}
|
||||||
|
controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "setCoolingSetpoint", action:"thermostat.setCoolingSetpoint", backgroundColor: "#1e9cbb"
|
||||||
|
}
|
||||||
|
valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
standardTile("resumeProgram", "device.resumeProgram", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "resume", label:'Resume Program', action:"device.resumeProgram", icon:"st.sonos.play-icon"
|
||||||
|
}
|
||||||
|
main "temperature"
|
||||||
|
details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
input "highTemperature", "number", title: "Auto Mode High Temperature:", defaultValue: 80
|
||||||
|
input "lowTemperature", "number", title: "Auto Mode Low Temperature:", defaultValue: 70
|
||||||
|
input name: "holdType", type: "enum", title: "Hold Type", description: "When changing temperature, use Temporary or Permanent hold", required: true, options:["Temporary", "Permanent"]
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// parse events into attributes
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "Parsing '${description}'"
|
||||||
|
// TODO: handle '' attribute
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh()
|
||||||
|
{
|
||||||
|
log.debug "refresh called"
|
||||||
|
poll()
|
||||||
|
log.debug "refresh ended"
|
||||||
|
}
|
||||||
|
|
||||||
|
def go()
|
||||||
|
{
|
||||||
|
log.debug "before:go tile tapped"
|
||||||
|
poll()
|
||||||
|
log.debug "after"
|
||||||
|
}
|
||||||
|
|
||||||
|
void poll() {
|
||||||
|
log.debug "Executing 'poll' using parent SmartApp"
|
||||||
|
|
||||||
|
def results = parent.pollChild(this)
|
||||||
|
parseEventData(results)
|
||||||
|
generateStatusEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseEventData(Map results)
|
||||||
|
{
|
||||||
|
log.debug "parsing data $results"
|
||||||
|
if(results)
|
||||||
|
{
|
||||||
|
results.each { name, value ->
|
||||||
|
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def isChange = false
|
||||||
|
def isDisplayed = true
|
||||||
|
|
||||||
|
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
|
||||||
|
isChange = isTemperatureStateChange(device, name, value.toString())
|
||||||
|
isDisplayed = isChange
|
||||||
|
|
||||||
|
sendEvent(
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: "F",
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: getThermostatDescriptionText(name, value, linkText),
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isChange,
|
||||||
|
displayed: isDisplayed)
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
isChange = isStateChange(device, name, value.toString())
|
||||||
|
isDisplayed = isChange
|
||||||
|
|
||||||
|
sendEvent(
|
||||||
|
name: name,
|
||||||
|
value: value.toString(),
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: getThermostatDescriptionText(name, value, linkText),
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isChange,
|
||||||
|
displayed: isDisplayed)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generateSetpointEvent ()
|
||||||
|
generateStatusEvent ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void generateEvent(Map results)
|
||||||
|
{
|
||||||
|
log.debug "parsing data $results"
|
||||||
|
if(results)
|
||||||
|
{
|
||||||
|
results.each { name, value ->
|
||||||
|
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def isChange = false
|
||||||
|
def isDisplayed = true
|
||||||
|
|
||||||
|
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
|
||||||
|
isChange = isTemperatureStateChange(device, name, value.toString())
|
||||||
|
isDisplayed = isChange
|
||||||
|
|
||||||
|
sendEvent(
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: "F",
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: getThermostatDescriptionText(name, value, linkText),
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isChange,
|
||||||
|
displayed: isDisplayed)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
isChange = isStateChange(device, name, value.toString())
|
||||||
|
isDisplayed = isChange
|
||||||
|
|
||||||
|
sendEvent(
|
||||||
|
name: name,
|
||||||
|
value: value.toString(),
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: getThermostatDescriptionText(name, value, linkText),
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isChange,
|
||||||
|
displayed: isDisplayed)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generateSetpointEvent ()
|
||||||
|
generateStatusEvent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getThermostatDescriptionText(name, value, linkText)
|
||||||
|
{
|
||||||
|
if(name == "temperature")
|
||||||
|
{
|
||||||
|
return "$linkText was $value°F"
|
||||||
|
}
|
||||||
|
else if(name == "heatingSetpoint")
|
||||||
|
{
|
||||||
|
return "latest heating setpoint was $value°F"
|
||||||
|
}
|
||||||
|
else if(name == "coolingSetpoint")
|
||||||
|
{
|
||||||
|
return "latest cooling setpoint was $value°F"
|
||||||
|
}
|
||||||
|
else if (name == "thermostatMode")
|
||||||
|
{
|
||||||
|
return "thermostat mode is ${value}"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return "${name} = ${value}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void setHeatingSetpoint(degreesF) {
|
||||||
|
setHeatingSetpoint(degreesF.toDouble())
|
||||||
|
}
|
||||||
|
|
||||||
|
void setHeatingSetpoint(Double degreesF) {
|
||||||
|
log.debug "setHeatingSetpoint({$degreesF})"
|
||||||
|
sendEvent("name":"heatingSetpoint", "value":degreesF)
|
||||||
|
Double coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||||
|
log.debug "coolingSetpoint: $coolingSetpoint"
|
||||||
|
parent.setHold(this, degreesF, coolingSetpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCoolingSetpoint(degreesF) {
|
||||||
|
setCoolingSetpoint(degreesF.toDouble())
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCoolingSetpoint(Double degreesF) {
|
||||||
|
log.debug "setCoolingSetpoint({$degreesF})"
|
||||||
|
sendEvent("name":"coolingSetpoint", "value":degreesF)
|
||||||
|
Double heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||||
|
parent.setHold(this, heatingSetpoint, degreesF)
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def resumeProgram() {
|
||||||
|
parent.resumeProgram(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
def modes() {
|
||||||
|
if (state.modes) {
|
||||||
|
log.debug "Modes = ${state.modes}"
|
||||||
|
return state.modes
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
state.modes = parent.availableModes(this)
|
||||||
|
log.debug "Modes = ${state.modes}"
|
||||||
|
return state.modes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def fanModes() {
|
||||||
|
["off", "on", "auto", "circulate"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def switchMode() {
|
||||||
|
log.debug "in switchMode"
|
||||||
|
def currentMode = device.currentState("thermostatMode")?.value
|
||||||
|
def lastTriedMode = state.lastTriedMode ?: currentMode ?: "off"
|
||||||
|
def modeOrder = modes()
|
||||||
|
def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
|
||||||
|
def nextMode = next(lastTriedMode)
|
||||||
|
switchToMode(nextMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
def switchToMode(nextMode) {
|
||||||
|
log.debug "In switchToMode = ${nextMode}"
|
||||||
|
if (nextMode in modes()) {
|
||||||
|
state.lastTriedMode = nextMode
|
||||||
|
"$nextMode"()
|
||||||
|
} else {
|
||||||
|
log.debug("no mode method '$nextMode'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def switchFanMode() {
|
||||||
|
def currentFanMode = device.currentState("thermostatFanMode")?.value
|
||||||
|
log.debug "switching fan from current mode: $currentFanMode"
|
||||||
|
def returnCommand
|
||||||
|
|
||||||
|
switch (currentFanMode) {
|
||||||
|
case "fanAuto":
|
||||||
|
returnCommand = switchToFanMode("fanOn")
|
||||||
|
break
|
||||||
|
case "fanOn":
|
||||||
|
returnCommand = switchToFanMode("fanCirculate")
|
||||||
|
break
|
||||||
|
case "fanCirculate":
|
||||||
|
returnCommand = switchToFanMode("fanAuto")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if(!currentFanMode) { returnCommand = switchToFanMode("fanOn") }
|
||||||
|
returnCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
def switchToFanMode(nextMode) {
|
||||||
|
|
||||||
|
log.debug "switching to fan mode: $nextMode"
|
||||||
|
def returnCommand
|
||||||
|
|
||||||
|
if(nextMode == "fanAuto") {
|
||||||
|
if(!fanModes.contains("fanAuto")) {
|
||||||
|
returnCommand = fanAuto()
|
||||||
|
} else {
|
||||||
|
returnCommand = switchToFanMode("fanOn")
|
||||||
|
}
|
||||||
|
} else if(nextMode == "fanOn") {
|
||||||
|
if(!fanModes.contains("fanOn")) {
|
||||||
|
returnCommand = fanOn()
|
||||||
|
} else {
|
||||||
|
returnCommand = switchToFanMode("fanCirculate")
|
||||||
|
}
|
||||||
|
} else if(nextMode == "fanCirculate") {
|
||||||
|
if(!fanModes.contains("fanCirculate")) {
|
||||||
|
returnCommand = fanCirculate()
|
||||||
|
} else {
|
||||||
|
returnCommand = switchToFanMode("fanAuto")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
returnCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
def getDataByName(String name) {
|
||||||
|
state[name] ?: device.getDataValue(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setThermostatMode(String value) {
|
||||||
|
log.debug "setThermostatMode({$value})"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def setThermostatFanMode(String value) {
|
||||||
|
|
||||||
|
log.debug "setThermostatFanMode({$value})"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def generateModeEvent(mode) {
|
||||||
|
|
||||||
|
sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true, isStateChange: true)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def generateFanModeEvent(fanMode) {
|
||||||
|
|
||||||
|
sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${mode} mode", displayed: true, isStateChange: true)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def generateOperatingStateEvent(operatingState) {
|
||||||
|
|
||||||
|
sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true, isStateChange: true)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
log.debug "off"
|
||||||
|
generateModeEvent("off")
|
||||||
|
if (parent.setMode (this,"off"))
|
||||||
|
generateModeEvent("off")
|
||||||
|
else {
|
||||||
|
log.debug "Error setting new mode."
|
||||||
|
def currentMode = device.currentState("thermostatMode")?.value
|
||||||
|
generateModeEvent(currentMode) // reset the tile back
|
||||||
|
}
|
||||||
|
generateSetpointEvent()
|
||||||
|
generateStatusEvent()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def heat() {
|
||||||
|
log.debug "heat"
|
||||||
|
generateModeEvent("heat")
|
||||||
|
if (parent.setMode (this,"heat"))
|
||||||
|
generateModeEvent("heat")
|
||||||
|
else {
|
||||||
|
log.debug "Error setting new mode."
|
||||||
|
def currentMode = device.currentState("thermostatMode")?.value
|
||||||
|
generateModeEvent(currentMode) // reset the tile back
|
||||||
|
}
|
||||||
|
generateSetpointEvent()
|
||||||
|
generateStatusEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
def auxHeatOnly() {
|
||||||
|
log.debug "auxHeatOnly"
|
||||||
|
generateModeEvent("auxHeatOnly")
|
||||||
|
if (parent.setMode (this,"auxHeatOnly"))
|
||||||
|
generateModeEvent("auxHeatOnly")
|
||||||
|
else {
|
||||||
|
log.debug "Error setting new mode."
|
||||||
|
def currentMode = device.currentState("thermostatMode")?.value
|
||||||
|
generateModeEvent(currentMode) // reset the tile back
|
||||||
|
}
|
||||||
|
generateSetpointEvent()
|
||||||
|
generateStatusEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
def cool() {
|
||||||
|
log.debug "cool"
|
||||||
|
generateModeEvent("cool")
|
||||||
|
if (parent.setMode (this,"cool"))
|
||||||
|
generateModeEvent("cool")
|
||||||
|
else {
|
||||||
|
log.debug "Error setting new mode."
|
||||||
|
def currentMode = device.currentState("thermostatMode")?.value
|
||||||
|
generateModeEvent(currentMode) // reset the tile back
|
||||||
|
}
|
||||||
|
generateSetpointEvent()
|
||||||
|
generateStatusEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
def auto() {
|
||||||
|
log.debug "auto"
|
||||||
|
generateModeEvent("auto")
|
||||||
|
if (parent.setMode (this,"auto"))
|
||||||
|
generateModeEvent("auto")
|
||||||
|
else {
|
||||||
|
log.debug "Error setting new mode."
|
||||||
|
def currentMode = device.currentState("thermostatMode")?.value
|
||||||
|
generateModeEvent(currentMode) // reset the tile back
|
||||||
|
}
|
||||||
|
generateSetpointEvent()
|
||||||
|
generateStatusEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
def fanOn() {
|
||||||
|
log.debug "fanOn"
|
||||||
|
parent.setFanMode (this,"on")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def fanAuto() {
|
||||||
|
log.debug "fanAuto"
|
||||||
|
parent.setFanMode (this,"auto")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def fanCirculate() {
|
||||||
|
log.debug "fanCirculate"
|
||||||
|
parent.setFanMode (this,"circulate")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def fanOff() {
|
||||||
|
log.debug "fanOff"
|
||||||
|
parent.setFanMode (this,"off")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def generateSetpointEvent() {
|
||||||
|
|
||||||
|
log.debug "Generate SetPoint Event"
|
||||||
|
|
||||||
|
def mode = device.currentValue("thermostatMode")
|
||||||
|
log.debug "Current Mode = ${mode}"
|
||||||
|
|
||||||
|
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||||
|
log.debug "Heating Setpoint = ${heatingSetpoint}"
|
||||||
|
|
||||||
|
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||||
|
log.debug "Cooling Setpoint = ${coolingSetpoint}"
|
||||||
|
|
||||||
|
if (mode == "heat") {
|
||||||
|
|
||||||
|
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (mode == "cool") {
|
||||||
|
|
||||||
|
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°")
|
||||||
|
|
||||||
|
} else if (mode == "auto") {
|
||||||
|
|
||||||
|
sendEvent("name":"thermostatSetpoint", "value":"Auto")
|
||||||
|
|
||||||
|
} else if (mode == "off") {
|
||||||
|
|
||||||
|
sendEvent("name":"thermostatSetpoint", "value":"Off")
|
||||||
|
|
||||||
|
} else if (mode == "emergencyHeat") {
|
||||||
|
|
||||||
|
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void raiseSetpoint() {
|
||||||
|
|
||||||
|
log.debug "Raise SetPoint"
|
||||||
|
|
||||||
|
def mode = device.currentValue("thermostatMode")
|
||||||
|
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||||
|
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||||
|
|
||||||
|
log.debug "Current Mode = ${mode}"
|
||||||
|
|
||||||
|
if (mode == "heat") {
|
||||||
|
|
||||||
|
heatingSetpoint++
|
||||||
|
|
||||||
|
if (heatingSetpoint > 99)
|
||||||
|
heatingSetpoint = 99
|
||||||
|
|
||||||
|
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
|
||||||
|
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
||||||
|
|
||||||
|
parent.setHold (this, heatingSetpoint, coolingSetpoint)
|
||||||
|
|
||||||
|
log.debug "New Heating Setpoint = ${heatingSetpoint}"
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (mode == "cool") {
|
||||||
|
|
||||||
|
coolingSetpoint++
|
||||||
|
|
||||||
|
if (coolingSetpoint > 99)
|
||||||
|
coolingSetpoint = 99
|
||||||
|
|
||||||
|
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°")
|
||||||
|
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
||||||
|
|
||||||
|
parent.setHold (this, heatingSetpoint, coolingSetpoint)
|
||||||
|
|
||||||
|
log.debug "New Cooling Setpoint = ${coolingSetpoint}"
|
||||||
|
|
||||||
|
}
|
||||||
|
generateStatusEvent()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void lowerSetpoint() {
|
||||||
|
log.debug "Lower SetPoint"
|
||||||
|
|
||||||
|
def mode = device.currentValue("thermostatMode")
|
||||||
|
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||||
|
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||||
|
|
||||||
|
log.debug "Current Mode = ${mode}, Current Heating Setpoint = ${heatingSetpoint}, Current Cooling Setpoint = ${coolingSetpoint}"
|
||||||
|
|
||||||
|
if (mode == "heat" || mode == "emergencyHeat") {
|
||||||
|
|
||||||
|
heatingSetpoint--
|
||||||
|
|
||||||
|
if (heatingSetpoint < 32)
|
||||||
|
heatingSetpoint = 32
|
||||||
|
|
||||||
|
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
|
||||||
|
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
||||||
|
|
||||||
|
parent.setHold (this, heatingSetpoint, coolingSetpoint)
|
||||||
|
|
||||||
|
log.debug "New Heating Setpoint = ${heatingSetpoint}"
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (mode == "cool") {
|
||||||
|
|
||||||
|
coolingSetpoint--
|
||||||
|
|
||||||
|
if (coolingSetpoint < 32)
|
||||||
|
coolingSetpoint = 32
|
||||||
|
|
||||||
|
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°")
|
||||||
|
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
||||||
|
|
||||||
|
parent.setHold (this, heatingSetpoint, coolingSetpoint)
|
||||||
|
|
||||||
|
log.debug "New Cooling Setpoint = ${coolingSetpoint}"
|
||||||
|
|
||||||
|
}
|
||||||
|
generateStatusEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
def generateStatusEvent() {
|
||||||
|
|
||||||
|
def mode = device.currentValue("thermostatMode")
|
||||||
|
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||||
|
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||||
|
def temperature = device.currentValue("temperature").toInteger()
|
||||||
|
|
||||||
|
def statusText
|
||||||
|
|
||||||
|
log.debug "Generate Status Event for Mode = ${mode}"
|
||||||
|
log.debug "Temperature = ${temperature}"
|
||||||
|
log.debug "Heating set point = ${heatingSetpoint}"
|
||||||
|
log.debug "Cooling set point = ${coolingSetpoint}"
|
||||||
|
log.debug "HVAC Mode = ${mode}"
|
||||||
|
|
||||||
|
if (mode == "heat") {
|
||||||
|
|
||||||
|
if (temperature >= heatingSetpoint)
|
||||||
|
statusText = "Right Now: Idle"
|
||||||
|
else
|
||||||
|
statusText = "Heating to ${heatingSetpoint}° F"
|
||||||
|
|
||||||
|
} else if (mode == "cool") {
|
||||||
|
|
||||||
|
if (temperature <= coolingSetpoint)
|
||||||
|
statusText = "Right Now: Idle"
|
||||||
|
else
|
||||||
|
statusText = "Cooling to ${coolingSetpoint}° F"
|
||||||
|
|
||||||
|
} else if (mode == "auto") {
|
||||||
|
|
||||||
|
statusText = "Right Now: Auto"
|
||||||
|
|
||||||
|
} else if (mode == "off") {
|
||||||
|
|
||||||
|
statusText = "Right Now: Off"
|
||||||
|
|
||||||
|
} else if (mode == "emergencyHeat") {
|
||||||
|
|
||||||
|
statusText = "Emergency Heat"
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
statusText = "?"
|
||||||
|
|
||||||
|
}
|
||||||
|
log.debug "Generate Status Event = ${statusText}"
|
||||||
|
sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true, isStateChange: true)
|
||||||
|
}
|
||||||
|
|
||||||
213
devicetypes/smartthings/econet-vent.src/econet-vent.groovy
Normal file
213
devicetypes/smartthings/econet-vent.src/econet-vent.groovy
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
/**
|
||||||
|
* Econet EV100 Vent
|
||||||
|
*
|
||||||
|
* Copyright 2014 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
// Automatically generated. Make future change here.
|
||||||
|
definition (name: "EcoNet Vent", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Battery"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Configuration"
|
||||||
|
|
||||||
|
command "open"
|
||||||
|
command "close"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x1100", inClusters: "0x26,0x72,0x86,0x77,0x80,0x20"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "on": "command: 2603, payload: FF"
|
||||||
|
status "off": "command: 2603, payload: 00"
|
||||||
|
status "09%": "command: 2603, payload: 09"
|
||||||
|
status "10%": "command: 2603, payload: 0A"
|
||||||
|
status "33%": "command: 2603, payload: 21"
|
||||||
|
status "66%": "command: 2603, payload: 42"
|
||||||
|
status "99%": "command: 2603, payload: 63"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "2001FF,delay 100,2602": "command: 2603, payload: FF"
|
||||||
|
reply "200100,delay 100,2602": "command: 2603, payload: 00"
|
||||||
|
reply "200119,delay 100,2602": "command: 2603, payload: 19"
|
||||||
|
reply "200132,delay 100,2602": "command: 2603, payload: 32"
|
||||||
|
reply "20014B,delay 100,2602": "command: 2603, payload: 4B"
|
||||||
|
reply "200163,delay 100,2602": "command: 2603, payload: 63"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", action:"switch.off", icon:"st.vents.vent-open-text", backgroundColor:"#53a7c0"
|
||||||
|
state "off", action:"switch.on", icon:"st.vents.vent-closed", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch","battery","refresh","levelSliderControl"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def result = []
|
||||||
|
def cmd = zwave.parse(description, [0x20: 1, 0x26: 3, 0x70: 1, 0x32:3])
|
||||||
|
if (cmd) {
|
||||||
|
result = zwaveEvent(cmd)
|
||||||
|
log.debug("'$description' parsed to $result")
|
||||||
|
} else {
|
||||||
|
log.debug("Couldn't zwave.parse '$description'")
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
//send the command to stop polling
|
||||||
|
def updated() {
|
||||||
|
response("poll stop")
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||||
|
dimmerEvents(cmd) + [ response("poll stop") ] // we get a BasicReport when the hub starts polling
|
||||||
|
}
|
||||||
|
|
||||||
|
//parse manufacture name and store it
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||||
|
if (state.manufacturer != cmd.manufacturerName) {
|
||||||
|
updateDataValue("manufacturer", cmd.manufacturerName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||||
|
dimmerEvents(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) {
|
||||||
|
dimmerEvents(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) {
|
||||||
|
dimmerEvents(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
def dimmerEvents(physicalgraph.zwave.Command cmd) {
|
||||||
|
def text = "$device.displayName is ${cmd.value ? "open" : "closed"}"
|
||||||
|
def switchEvent = createEvent(name: "switch", value: (cmd.value ? "on" : "off"), descriptionText: text)
|
||||||
|
def levelEvent = createEvent(name:"level", value: cmd.value, unit:"%")
|
||||||
|
[switchEvent, levelEvent]
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
state.lastbat = new Date().time
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
def linkText = device.label ?: device.name
|
||||||
|
[linkText: linkText, descriptionText: "$linkText: $cmd", displayed: false]
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.basicV1.basicSet(value: 0xFF).format(),
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def open() {
|
||||||
|
on()
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.basicV1.basicSet(value: 0x00).format(),
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def close() {
|
||||||
|
off()
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
delayBetween([
|
||||||
|
zwave.basicV1.basicSet(value: value).format(),
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value, duration) {
|
||||||
|
setLevel(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet().format(),
|
||||||
|
zwave.batteryV1.batteryGet().format()
|
||||||
|
], 2200)
|
||||||
|
}
|
||||||
|
|
||||||
|
//poll for battery once a day
|
||||||
|
def poll() {
|
||||||
|
|
||||||
|
if (secondsPast(state.lastbatt, 36*60*60)) {
|
||||||
|
return zwave.batteryV1.batteryGet().format()
|
||||||
|
} else {
|
||||||
|
|
||||||
|
return zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def configure() {
|
||||||
|
[
|
||||||
|
zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
|
||||||
|
]
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
//check last message so battery poll doesn't happen all the time
|
||||||
|
private Boolean secondsPast(timestamp, seconds) {
|
||||||
|
if (!(timestamp instanceof Number)) {
|
||||||
|
if (timestamp instanceof Date) {
|
||||||
|
timestamp = timestamp.time
|
||||||
|
} else if ((timestamp instanceof String) && timestamp.isNumber()) {
|
||||||
|
timestamp = timestamp.toLong()
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (new Date().time - timestamp) > (seconds * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Warn message for unkown events
|
||||||
|
def createEvents(physicalgraph.zwave.Command cmd) {
|
||||||
|
log.warn "UNEXPECTED COMMAND: $cmd"
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Everspring Flood Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Water Sensor"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Battery"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0xA102", inClusters: "0x86,0x72,0x85,0x84,0x80,0x70,0x9C,0x20,0x71"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "dry": "command: 9C02, payload: 00 05 00 00 00"
|
||||||
|
status "wet": "command: 9C02, payload: 00 05 FF 00 00"
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i).incomingMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tiles {
|
||||||
|
standardTile("water", "device.water", width: 2, height: 2) {
|
||||||
|
state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff"
|
||||||
|
state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0"
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||||
|
}
|
||||||
|
main "water"
|
||||||
|
details(["water", "battery", "configure"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def result = null
|
||||||
|
def parsedZwEvent = zwave.parse(description, [0x9C: 1, 0x71: 1, 0x84: 2, 0x30: 1])
|
||||||
|
if (parsedZwEvent) {
|
||||||
|
result = zwaveEvent(parsedZwEvent)
|
||||||
|
}
|
||||||
|
log.debug "Parse '${description}' returned ${result}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd)
|
||||||
|
{
|
||||||
|
def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)]
|
||||||
|
def now = new Date().time
|
||||||
|
if (!state.battreq || now - state.battreq > 53*60*60*1000) {
|
||||||
|
state.battreq = now
|
||||||
|
result << response(zwave.batteryV1.batteryGet())
|
||||||
|
} else {
|
||||||
|
result << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
if (cmd.sensorType == 0x05) {
|
||||||
|
map.name = "water"
|
||||||
|
map.value = cmd.sensorState ? "wet" : "dry"
|
||||||
|
map.descriptionText = "${device.displayName} is ${map.value}"
|
||||||
|
} else {
|
||||||
|
map.descriptionText = "${device.displayName}: ${cmd}"
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
map.name = "water"
|
||||||
|
map.value = cmd.sensorValue ? "wet" : "dry"
|
||||||
|
map.descriptionText = "${device.displayName} is ${map.value}"
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.alarmv1.AlarmReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
if (cmd.alarmType == 1 && cmd.alarmLevel == 0xFF) {
|
||||||
|
map.name = "battery"
|
||||||
|
map.value = 1
|
||||||
|
map.unit = "%"
|
||||||
|
map.descriptionText = "${device.displayName} has a low battery"
|
||||||
|
map.displayed = true
|
||||||
|
map
|
||||||
|
} else if (cmd.alarmType == 2 && cmd.alarmLevel == 1) {
|
||||||
|
map.descriptionText = "${device.displayName} powered up"
|
||||||
|
map.displayed = false
|
||||||
|
map
|
||||||
|
} else {
|
||||||
|
map.descriptionText = "${device.displayName}: ${cmd}"
|
||||||
|
map.displayed = false
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||||
|
def map = [:]
|
||||||
|
if (cmd.batteryLevel == 0xFF) {
|
||||||
|
map.name = "battery"
|
||||||
|
map.value = 1
|
||||||
|
map.unit = "%"
|
||||||
|
map.descriptionText = "${device.displayName} has a low battery"
|
||||||
|
map.displayed = true
|
||||||
|
} else {
|
||||||
|
map.name = "battery"
|
||||||
|
map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1
|
||||||
|
map.unit = "%"
|
||||||
|
map.displayed = false
|
||||||
|
}
|
||||||
|
[createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd)
|
||||||
|
{
|
||||||
|
createEvent(descriptionText: "${device.displayName}: ${cmd}", displayed: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure()
|
||||||
|
{
|
||||||
|
if (!device.currentState("battery")) {
|
||||||
|
sendEvent(name: "battery", value:100, unit:"%", descriptionText:"(Default battery event)", displayed:false)
|
||||||
|
}
|
||||||
|
zwave.associationV1.associationSet(groupingIdentifier: 1, nodeId: [zwaveHubNodeId]).format()
|
||||||
|
}
|
||||||
334
devicetypes/smartthings/fibaro-dimmer.src/fibaro-dimmer.groovy
Normal file
334
devicetypes/smartthings/fibaro-dimmer.src/fibaro-dimmer.groovy
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
/**
|
||||||
|
* Device Type Definition File
|
||||||
|
*
|
||||||
|
* Device Type: Fibaro Dimmer
|
||||||
|
* File Name: fibaro-dimmer.groovy
|
||||||
|
* Initial Release: 2015-06-00
|
||||||
|
* Author: SmartThings
|
||||||
|
*
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
// Automatically generated. Make future change here.
|
||||||
|
definition (name: "Fibaro Dimmer", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
command "resetParams2StDefaults"
|
||||||
|
command "listCurrentParams"
|
||||||
|
command "updateZwaveParam"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x1101", inClusters: "0x72,0x86,0x70,0x85,0x8E,0x26,0x7A,0x27,0x73,0xEF,0x26,0x2B"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "on": "command: 2003, payload: FF"
|
||||||
|
status "off": "command: 2003, payload: 00"
|
||||||
|
status "09%": "command: 2003, payload: 09"
|
||||||
|
status "10%": "command: 2003, payload: 0A"
|
||||||
|
status "33%": "command: 2003, payload: 21"
|
||||||
|
status "66%": "command: 2003, payload: 42"
|
||||||
|
status "99%": "command: 2003, payload: 63"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "2001FF,delay 5000,2602": "command: 2603, payload: FF"
|
||||||
|
reply "200100,delay 5000,2602": "command: 2603, payload: 00"
|
||||||
|
reply "200119,delay 5000,2602": "command: 2603, payload: 19"
|
||||||
|
reply "200132,delay 5000,2602": "command: 2603, payload: 32"
|
||||||
|
reply "20014B,delay 5000,2602": "command: 2603, payload: 4B"
|
||||||
|
reply "200163,delay 5000,2602": "command: 2603, payload: 63"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch", "refresh", "levelSliderControl"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def item1 = [
|
||||||
|
canBeCurrentState: false,
|
||||||
|
linkText: getLinkText(device),
|
||||||
|
isStateChange: false,
|
||||||
|
displayed: false,
|
||||||
|
descriptionText: description,
|
||||||
|
value: description
|
||||||
|
]
|
||||||
|
def result
|
||||||
|
def cmd = zwave.parse(description, [0x26: 1, 0x70: 2, 072: 2])
|
||||||
|
//log.debug "cmd: ${cmd}"
|
||||||
|
|
||||||
|
if (cmd) {
|
||||||
|
result = createEvent(cmd, item1)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
item1.displayed = displayed(description, item1.isStateChange)
|
||||||
|
result = [item1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if(result?.descriptionText)
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
|
||||||
|
result
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) {
|
||||||
|
def result = doCreateEvent(cmd, item1)
|
||||||
|
for (int i = 0; i < result.size(); i++) {
|
||||||
|
result[i].type = "physical"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, Map item1) {
|
||||||
|
def result = doCreateEvent(cmd, item1)
|
||||||
|
for (int i = 0; i < result.size(); i++) {
|
||||||
|
result[i].type = "physical"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd, Map item1) {
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd, Map item1) {
|
||||||
|
[response(zwave.basicV1.basicGet())]
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd, Map item1) {
|
||||||
|
def result = doCreateEvent(cmd, item1)
|
||||||
|
for (int i = 0; i < result.size(); i++) {
|
||||||
|
result[i].type = "physical"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd, Map item1) {
|
||||||
|
def result = doCreateEvent(cmd, item1)
|
||||||
|
result[0].descriptionText = "${item1.linkText} is ${item1.value}"
|
||||||
|
result[0].handlerName = cmd.value ? "statusOn" : "statusOff"
|
||||||
|
for (int i = 0; i < result.size(); i++) {
|
||||||
|
result[i].type = "digital"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) {
|
||||||
|
def result = [item1]
|
||||||
|
|
||||||
|
item1.name = "switch"
|
||||||
|
item1.value = cmd.value ? "on" : "off"
|
||||||
|
item1.handlerName = item1.value
|
||||||
|
item1.descriptionText = "${item1.linkText} was turned ${item1.value}"
|
||||||
|
item1.canBeCurrentState = true
|
||||||
|
item1.isStateChange = isStateChange(device, item1.name, item1.value)
|
||||||
|
item1.displayed = item1.isStateChange
|
||||||
|
|
||||||
|
if (cmd.value >= 5) {
|
||||||
|
def item2 = new LinkedHashMap(item1)
|
||||||
|
item2.name = "level"
|
||||||
|
item2.value = cmd.value as String
|
||||||
|
item2.unit = "%"
|
||||||
|
item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %"
|
||||||
|
item2.canBeCurrentState = true
|
||||||
|
item2.isStateChange = isStateChange(device, item2.name, item2.value)
|
||||||
|
item2.displayed = false
|
||||||
|
result << item2
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.Command cmd, Map map) {
|
||||||
|
// Handles any Z-Wave commands we aren't interested in
|
||||||
|
log.debug "UNHANDLED COMMAND $cmd"
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
log.info "on"
|
||||||
|
delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
delayBetween ([zwave.basicV1.basicSet(value: 0x00).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
def level = Math.min(value as Integer, 99)
|
||||||
|
delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value, duration) {
|
||||||
|
def level = Math.min(value as Integer, 99)
|
||||||
|
def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60)
|
||||||
|
zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the device to settings needed by SmarthThings at device discovery time. Assumes
|
||||||
|
* device is already at default parameter settings.
|
||||||
|
*
|
||||||
|
* @param none
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def configure() {
|
||||||
|
log.debug "Configuring Device..."
|
||||||
|
def cmds = []
|
||||||
|
|
||||||
|
// send associate to group 3 to get sensor data reported only to hub
|
||||||
|
cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format()
|
||||||
|
|
||||||
|
|
||||||
|
delayBetween(cmds, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd, Map item1) {
|
||||||
|
|
||||||
|
log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will allow the user to update device parameters (behavior) from an app.
|
||||||
|
* the user can write his/her own app to envoke this method.
|
||||||
|
* No type or value checking is done to compare to what device capability or reaction.
|
||||||
|
* It is up to user to read OEM documentation prio to envoking this method.
|
||||||
|
*
|
||||||
|
* <p>THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION!
|
||||||
|
*
|
||||||
|
* @param List[paramNumber:80,value:10,size:1]
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def updateZwaveParam(params) {
|
||||||
|
if ( params ) {
|
||||||
|
def pNumber = params.paramNumber
|
||||||
|
def pSize = params.size
|
||||||
|
def pValue = [params.value]
|
||||||
|
log.debug "Make sure device is awake and in recieve mode"
|
||||||
|
log.debug "Updating ${device.displayName} parameter number '${pNumber}' with value '${pValue}' with size of '${pSize}'"
|
||||||
|
|
||||||
|
def cmds = []
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format()
|
||||||
|
delayBetween(cmds, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all of available Fibaro parameters back to the device defaults except for what
|
||||||
|
* SmartThings needs to support the stock functionality as released.
|
||||||
|
*
|
||||||
|
* <p>THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION!
|
||||||
|
*
|
||||||
|
* @param none
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def resetParams2StDefaults() {
|
||||||
|
log.debug "Resetting ${device.displayName} parameters to SmartThings compatible defaults"
|
||||||
|
def cmds = []
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 1, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 6, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 7, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 8, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [5], parameterNumber: 9, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 10, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 11, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [99], parameterNumber: 12, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 13, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 14, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 15, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 16, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 17, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 18, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 19, size: 1).format()
|
||||||
|
//Param 20 is different for 50Hz or 60 Hz uncomment the line that will reflect the power frequency used
|
||||||
|
//cmds << zwave.configurationV1.configurationSet(configurationValue: [101], parameterNumber: 20, size: 1).format() //60 Hz (US)
|
||||||
|
//cmds << zwave.configurationV1.configurationSet(configurationValue: [110], parameterNumber: 20, size: 1).format() //50 Hz (UK)
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [3], parameterNumber: 30, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [600], parameterNumber: 39, size: 1).format()
|
||||||
|
//Param 40 not needed by SmartThings
|
||||||
|
//cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 40, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 41, size: 1).format()
|
||||||
|
|
||||||
|
delayBetween(cmds, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists all of available Fibaro parameters and thier current settings out to the
|
||||||
|
* logging window in the IDE. This will be called from the "Fibaro Tweaker" or
|
||||||
|
* user's own app.
|
||||||
|
*
|
||||||
|
* <p>THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION!
|
||||||
|
*
|
||||||
|
* @param none
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def listCurrentParams() {
|
||||||
|
log.debug "Listing of current parameter settings of ${device.displayName}"
|
||||||
|
def cmds = []
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 6).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 7).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 8).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 9).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 10).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 11).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 13).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 14).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 15).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 16).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 17).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 18).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 19).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 20).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 30).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 39).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 40).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 41).format()
|
||||||
|
|
||||||
|
delayBetween(cmds, 500)
|
||||||
|
}
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
/**
|
||||||
|
* Device Type Definition File
|
||||||
|
*
|
||||||
|
* Device Type: Fibaro Door/Window Sensor
|
||||||
|
* File Name: fibaro-door-window-sensor.groovy
|
||||||
|
* Initial Release: 2014-12-10
|
||||||
|
* @author: Todd Wackford
|
||||||
|
* Email: todd@wackford.net
|
||||||
|
* @version: 1.0
|
||||||
|
*
|
||||||
|
* Copyright 2014 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up metadata, simulator info and tile definition. The tamper tile is setup, but
|
||||||
|
* not displayed to the user. We do this so we can receive events and display on device
|
||||||
|
* activity. If the user wants to display the tamper tile, adjust the tile display lines
|
||||||
|
* with the following:
|
||||||
|
* main(["contact", "temperature", "tamper"])
|
||||||
|
* details(["contact", "temperature", "battery", "tamper"])
|
||||||
|
*
|
||||||
|
* @param none
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Fibaro Door/Window Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
//capability "Temperature Measurement" //UNCOMMENT ME IF TEMP INSTALLED
|
||||||
|
capability "Contact Sensor"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Battery"
|
||||||
|
capability "Configuration"
|
||||||
|
|
||||||
|
command "resetParams2StDefaults"
|
||||||
|
command "listCurrentParams"
|
||||||
|
command "updateZwaveParam"
|
||||||
|
command "test"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x2001", inClusters: "0x30,0x9C,0x85,0x72,0x70,0x86,0x80,0x56,0x84,0x7A,0xEF,0x2B"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// messages the device returns in response to commands it receives
|
||||||
|
status "open" : "command: 2001, payload: FF"
|
||||||
|
status "closed": "command: 2001, payload: 00"
|
||||||
|
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "temperature ${i}F": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(
|
||||||
|
scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport(
|
||||||
|
batteryLevel: i).incomingMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("contact", "device.contact", width: 2, height: 2) {
|
||||||
|
state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e"
|
||||||
|
state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821"
|
||||||
|
}
|
||||||
|
valueTile("temperature", "device.temperature", inactiveLabel: false) {
|
||||||
|
state "temperature", label:'${currentValue}°',
|
||||||
|
backgroundColors:[
|
||||||
|
[value: "", color: "#ffffff"],
|
||||||
|
[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"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
standardTile("tamper", "device.alarm") {
|
||||||
|
state("secure", label:'secure', icon:"st.locks.lock.locked", backgroundColor:"#ffffff")
|
||||||
|
state("tampered", label:'tampered', icon:"st.locks.lock.unlocked", backgroundColor:"#53a7c0")
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||||
|
}
|
||||||
|
|
||||||
|
//this will display a temperature tile for the DS18B20 sensor
|
||||||
|
//main(["contact", "temperature"]) //COMMENT ME OUT IF NO TEMP INSTALLED
|
||||||
|
//details(["contact", "temperature", "battery"]) //COMMENT ME OUT IF NO TEMP INSTALLED
|
||||||
|
|
||||||
|
//this will hide the temperature tile if the DS18B20 sensor is not installed
|
||||||
|
main(["contact"]) //UNCOMMENT ME IF NO TEMP INSTALLED
|
||||||
|
details(["contact", "battery"]) //UNCOMMENT ME IF NO TEMP INSTALLED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description)
|
||||||
|
{
|
||||||
|
def result = []
|
||||||
|
def cmd = zwave.parse(description, [0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2, 0x80: 1, 0x72: 2, 0x56: 1, 0x60: 3])
|
||||||
|
if (cmd) {
|
||||||
|
result += zwaveEvent(cmd)
|
||||||
|
}
|
||||||
|
log.debug "parsed '$description' to ${result.inspect()}"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd)
|
||||||
|
{
|
||||||
|
def versions = [0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2, 0x80: 1, 0x72: 2, 0x60: 3]
|
||||||
|
// def encapsulatedCommand = cmd.encapsulatedCommand(versions)
|
||||||
|
def version = versions[cmd.commandClass as Integer]
|
||||||
|
def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)
|
||||||
|
def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)
|
||||||
|
if (!encapsulatedCommand) {
|
||||||
|
log.debug "Could not extract command from $cmd"
|
||||||
|
} else {
|
||||||
|
return zwaveEvent(encapsulatedCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
|
||||||
|
def encapsulatedCommand = cmd.encapsulatedCommand([0x30: 2, 0x31: 2]) // can specify command class versions here like in zwave.parse
|
||||||
|
log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}")
|
||||||
|
if (encapsulatedCommand) {
|
||||||
|
return zwaveEvent(encapsulatedCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
|
||||||
|
{
|
||||||
|
def event = createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)
|
||||||
|
def cmds = []
|
||||||
|
if (!state.lastbat || now() - state.lastbat > 24*60*60*1000) {
|
||||||
|
cmds << zwave.batteryV1.batteryGet().format()
|
||||||
|
} else {
|
||||||
|
cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
|
||||||
|
}
|
||||||
|
[event, response(cmds)]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
switch (cmd.sensorType) {
|
||||||
|
case 1:
|
||||||
|
// temperature
|
||||||
|
def cmdScale = cmd.scale == 1 ? "F" : "C"
|
||||||
|
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
|
||||||
|
map.unit = getTemperatureScale()
|
||||||
|
map.name = "temperature"
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
state.lastbat = now()
|
||||||
|
[createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) {
|
||||||
|
def map = [:]
|
||||||
|
map.value = cmd.sensorValue ? "open" : "closed"
|
||||||
|
map.name = "contact"
|
||||||
|
if (map.value == "closed") {
|
||||||
|
map.descriptionText = "$device.displayName is closed"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
map.descriptionText = "$device.displayName is open"
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
// added so UK (non-multichannel) and US device supported by same device file.
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
sensorValueEvent(cmd.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd)
|
||||||
|
{
|
||||||
|
sensorValueEvent(cmd.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
map.value = cmd.sensorState ? "tampered" : "secure"
|
||||||
|
map.name = "tamper"
|
||||||
|
if (map.value == "tampered") {
|
||||||
|
map.descriptionText = "$device.displayName has been tampered with"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
map.descriptionText = "$device.displayName is secure"
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
log.debug "Catchall reached for cmd: ${cmd.toString()}}"
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) {
|
||||||
|
def result = []
|
||||||
|
log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'"
|
||||||
|
|
||||||
|
if (cmd.parameterNumber == 15) {
|
||||||
|
if (cmd.configurationValue[0] == 1) { //error in temp probe
|
||||||
|
result << createEvent(name:"temperature", value:"-99")
|
||||||
|
} else if (cmd.configurationValue[0] == 255) { //no temp probe
|
||||||
|
result << createEvent(name:"temperature", value:"")
|
||||||
|
}
|
||||||
|
result += response(zwave.batteryV1.batteryGet().format()) // send this after configure() runs
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||||
|
def result = []
|
||||||
|
|
||||||
|
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
|
||||||
|
log.debug "msr: $msr"
|
||||||
|
device.updateDataValue(["MSR", msr])
|
||||||
|
|
||||||
|
result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the device to settings needed by SmarthThings at device discovery time.
|
||||||
|
*
|
||||||
|
* @param none
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def configure() {
|
||||||
|
log.debug "Configuring Device..."
|
||||||
|
def cmds = []
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 1, size: 2).format()
|
||||||
|
// send associate to group 3 to get sensor data reported only to hub
|
||||||
|
cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format()
|
||||||
|
|
||||||
|
// send associate to group 2 to get tamper alarm data reported
|
||||||
|
cmds << zwave.associationV2.associationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]).format()
|
||||||
|
|
||||||
|
// turn on the tamper alarm
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 10, size: 1).format()
|
||||||
|
//cmds << zwave.configurationV1.configurationGet(parameterNumber: 10).format()
|
||||||
|
|
||||||
|
// temperature change sensitivity
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [4], parameterNumber: 12, size: 1).format()
|
||||||
|
//cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format()
|
||||||
|
|
||||||
|
// remove group 1 association to stop redundant BasicSet
|
||||||
|
cmds << zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId).format()
|
||||||
|
|
||||||
|
// see if there is a temp probe on board and is it working
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 15).format()
|
||||||
|
|
||||||
|
delayBetween(cmds, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
//used to add "test" button for simulation of user changes to parameters
|
||||||
|
def test() {
|
||||||
|
def params = [paramNumber:10,value:1,size:1]
|
||||||
|
updateZwaveParam(params)
|
||||||
|
//zwave.wakeUpV1.wakeUpIntervalSet(seconds: 30, nodeid:zwaveHubNodeId).format()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will allow the user to update device parameters (behavior) from an app.
|
||||||
|
* A "Zwave Tweaker" app will be developed as an interface to do this. Or the user can
|
||||||
|
* write his/her own app to envoke this method. No type or value checking is done to
|
||||||
|
* compare to what device capability or reaction. It is up to user to read OEM
|
||||||
|
* documentation prio to envoking this method.
|
||||||
|
*
|
||||||
|
* <p>THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION!
|
||||||
|
*
|
||||||
|
* @param List[paramNumber:80,value:10,size:1]
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def updateZwaveParam(params) {
|
||||||
|
if ( params ) {
|
||||||
|
def pNumber = params.paramNumber
|
||||||
|
def pSize = params.size
|
||||||
|
def pValue = [params.value]
|
||||||
|
log.debug "Make sure device is awake and in recieve mode"
|
||||||
|
log.debug "Updating ${device.displayName} parameter number '${pNumber}' with value '${pValue}' with size of '${pSize}'"
|
||||||
|
|
||||||
|
def cmds = []
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format()
|
||||||
|
delayBetween(cmds, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all of available Fibaro parameters back to the device defaults except for what
|
||||||
|
* SmartThings needs to support the stock functionality as released. This will be
|
||||||
|
* called from the "Fibaro Tweaker" or user's app.
|
||||||
|
*
|
||||||
|
* <p>THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION!
|
||||||
|
*
|
||||||
|
* @param none
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def resetParams2StDefaults() {
|
||||||
|
log.debug "Resetting ${device.displayName} parameters to SmartThings compatible defaults"
|
||||||
|
def cmds = []
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 1, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 2, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 5, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 7, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 9, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 10, size: 1).format() //ST Custom
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [4], parameterNumber: 12, size: 1).format() //St Custom
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 13, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 14, size: 1).format()
|
||||||
|
|
||||||
|
delayBetween(cmds, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists all of available Fibaro parameters and thier current settings out to the
|
||||||
|
* logging window in the IDE. This will be called from the "Fibaro Tweaker" or
|
||||||
|
* user's own app.
|
||||||
|
*
|
||||||
|
* <p>THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION!
|
||||||
|
*
|
||||||
|
* @param none
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def listCurrentParams() {
|
||||||
|
log.debug "Listing of current parameter settings of ${device.displayName}"
|
||||||
|
def cmds = []
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 5).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 7).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 9).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 10).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 13).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 14).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 15).format()
|
||||||
|
|
||||||
|
delayBetween(cmds, 500)
|
||||||
|
}
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
/**
|
||||||
|
* Device Type Definition File
|
||||||
|
*
|
||||||
|
* Device Type: Fibaro Flood Sensor
|
||||||
|
* File Name: fibaro-flood-sensor.groovy
|
||||||
|
* Initial Release: 2014-12-10
|
||||||
|
* @author: Todd Wackford
|
||||||
|
* Email: todd@wackford.net
|
||||||
|
* @version: 1.0
|
||||||
|
*
|
||||||
|
* Copyright 2014 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up metadata, simulator info and tile definition. The tamper tile is setup, but
|
||||||
|
* not displayed to the user. We do this so we can receive events and display on device
|
||||||
|
* activity. If the user wants to display the tamper tile, adjust the tile display lines
|
||||||
|
* with the following:
|
||||||
|
* main(["water", "temperature", "tamper"])
|
||||||
|
* details(["water", "temperature", "battery", "tamper"])
|
||||||
|
*
|
||||||
|
* @param none
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Fibaro Flood Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Water Sensor"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Battery"
|
||||||
|
|
||||||
|
command "resetParams2StDefaults"
|
||||||
|
command "listCurrentParams"
|
||||||
|
command "updateZwaveParam"
|
||||||
|
command "test"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0xA102", inClusters: "0x30,0x9C,0x60,0x85,0x8E,0x72,0x70,0x86,0x80,0x84"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// messages the device returns in response to commands it receives
|
||||||
|
status "motion (basic)" : "command: 2001, payload: FF"
|
||||||
|
status "no motion (basic)" : "command: 2001, payload: 00"
|
||||||
|
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "temperature ${i}F": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(
|
||||||
|
scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 200; i <= 1000; i += 200) {
|
||||||
|
status "luminance ${i} lux": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(
|
||||||
|
scaledSensorValue: i, precision: 0, sensorType: 3).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport(
|
||||||
|
batteryLevel: i).incomingMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("water", "device.water", width: 2, height: 2) {
|
||||||
|
state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff"
|
||||||
|
state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0"
|
||||||
|
}
|
||||||
|
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"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
standardTile("tamper", "device.tamper") {
|
||||||
|
state("secure", label:"secure", icon:"st.locks.lock.locked", backgroundColor:"#ffffff")
|
||||||
|
state("tampered", label:"tampered", icon:"st.locks.lock.unlocked", backgroundColor:"#53a7c0")
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["water", "temperature"])
|
||||||
|
details(["water", "temperature", "battery", "configure"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description)
|
||||||
|
{
|
||||||
|
def result = []
|
||||||
|
|
||||||
|
if (description == "updated") {
|
||||||
|
if (!state.MSR) {
|
||||||
|
result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds: 60*60, nodeid:zwaveHubNodeId))
|
||||||
|
result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
def cmd = zwave.parse(description, [0x31: 2, 0x30: 1, 0x70: 2, 0x71: 1, 0x84: 1, 0x80: 1, 0x9C: 1, 0x72: 2, 0x56: 2, 0x60: 3])
|
||||||
|
|
||||||
|
if (cmd) {
|
||||||
|
result += zwaveEvent(cmd) //createEvent(zwaveEvent(cmd))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result << response(zwave.batteryV1.batteryGet().format())
|
||||||
|
|
||||||
|
if ( result[0] != null ) {
|
||||||
|
log.debug "Parse returned ${result}"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
|
||||||
|
def encapsulatedCommand = cmd.encapsulatedCommand([0x30: 2, 0x31: 2]) // can specify command class versions here like in zwave.parse
|
||||||
|
log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}")
|
||||||
|
if (encapsulatedCommand) {
|
||||||
|
return zwaveEvent(encapsulatedCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Generation
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) {
|
||||||
|
def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)]
|
||||||
|
if (!isConfigured()) {
|
||||||
|
// we're still in the process of configuring a newly joined device
|
||||||
|
result += lateConfigure(true)
|
||||||
|
} else {
|
||||||
|
result += response(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||||
|
log.debug "We're done with WakeUp!"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
|
||||||
|
switch (cmd.sensorType) {
|
||||||
|
case 1:
|
||||||
|
// temperature
|
||||||
|
def cmdScale = cmd.scale == 1 ? "F" : "C"
|
||||||
|
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
|
||||||
|
map.unit = getTemperatureScale()
|
||||||
|
map.name = "temperature"
|
||||||
|
break;
|
||||||
|
case 0:
|
||||||
|
// here's our tamper alarm = acceleration
|
||||||
|
map.value = cmd.sensorState == 255 ? "active" : "inactive"
|
||||||
|
map.name = "acceleration"
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||||
|
def map = [:]
|
||||||
|
map.name = "battery"
|
||||||
|
map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1
|
||||||
|
map.unit = "%"
|
||||||
|
map.displayed = false
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) {
|
||||||
|
def map = [:]
|
||||||
|
map.value = cmd.sensorValue ? "active" : "inactive"
|
||||||
|
map.name = "acceleration"
|
||||||
|
|
||||||
|
if (map.value == "active") {
|
||||||
|
map.descriptionText = "$device.displayName detected vibration"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
map.descriptionText = "$device.displayName vibration has stopped"
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) {
|
||||||
|
log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'"
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||||
|
log.debug "BasicSet with CMD = ${cmd}"
|
||||||
|
|
||||||
|
if (!isConfigured()) {
|
||||||
|
def result = []
|
||||||
|
def map = [:]
|
||||||
|
|
||||||
|
map.name = "water"
|
||||||
|
map.value = cmd.value ? "wet" : "dry"
|
||||||
|
map.descriptionText = "${device.displayName} is ${map.value}"
|
||||||
|
|
||||||
|
// If we are getting a BasicSet, and isConfigured == false, then we are likely NOT properly configured.
|
||||||
|
result += lateConfigure(true)
|
||||||
|
|
||||||
|
result << createEvent(map)
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
|
||||||
|
if (cmd.sensorType == 0x05) {
|
||||||
|
map.name = "water"
|
||||||
|
map.value = cmd.sensorState ? "wet" : "dry"
|
||||||
|
map.descriptionText = "${device.displayName} is ${map.value}"
|
||||||
|
|
||||||
|
log.debug "CMD = SensorAlarmReport: ${cmd}"
|
||||||
|
setConfigured()
|
||||||
|
} else if ( cmd.sensorType == 0) {
|
||||||
|
map.name = "tamper"
|
||||||
|
map.isStateChange = true
|
||||||
|
map.value = cmd.sensorState ? "tampered" : "secure"
|
||||||
|
map.descriptionText = "${device.displayName} has been tampered with"
|
||||||
|
runIn(30, "resetTamper") //device does not send alarm cancelation
|
||||||
|
|
||||||
|
} else if ( cmd.sensorType == 1) {
|
||||||
|
map.name = "tamper"
|
||||||
|
map.value = cmd.sensorState ? "tampered" : "secure"
|
||||||
|
map.descriptionText = "${device.displayName} has been tampered with"
|
||||||
|
runIn(30, "resetTamper") //device does not send alarm cancelation
|
||||||
|
|
||||||
|
} else {
|
||||||
|
map.descriptionText = "${device.displayName}: ${cmd}"
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def resetTamper() {
|
||||||
|
def map = [:]
|
||||||
|
map.name = "tamper"
|
||||||
|
map.value = "secure"
|
||||||
|
map.descriptionText = "$device.displayName is secure"
|
||||||
|
sendEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
log.debug "Catchall reached for cmd: ${cmd.toString()}}"
|
||||||
|
[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||||
|
def result = []
|
||||||
|
|
||||||
|
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
|
||||||
|
log.debug "msr: $msr"
|
||||||
|
device.updateDataValue(["MSR", msr])
|
||||||
|
|
||||||
|
if ( msr == "010F-0B00-2001" ) { //this is the msr and device type for the fibaro flood sensor
|
||||||
|
result += lateConfigure(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def setConfigured() {
|
||||||
|
device.updateDataValue("configured", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
def isConfigured() {
|
||||||
|
Boolean configured = device.getDataValue(["configured"]) as Boolean
|
||||||
|
|
||||||
|
return configured
|
||||||
|
}
|
||||||
|
|
||||||
|
def lateConfigure(setConf = False) {
|
||||||
|
def res = response(configure())
|
||||||
|
|
||||||
|
if (setConf)
|
||||||
|
setConfigured()
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the device to settings needed by SmarthThings at device discovery time.
|
||||||
|
*
|
||||||
|
* @param none
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def configure() {
|
||||||
|
log.debug "Configuring Device..."
|
||||||
|
def cmds = []
|
||||||
|
|
||||||
|
// send associate to group 2 to get alarm data
|
||||||
|
cmds << zwave.associationV2.associationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]).format()
|
||||||
|
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 5, size: 1).format()
|
||||||
|
|
||||||
|
// send associate to group 3 to get sensor data reported only to hub
|
||||||
|
cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format()
|
||||||
|
|
||||||
|
// temp hysteresis set to .5 degrees celcius
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,50], parameterNumber: 12, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format()
|
||||||
|
|
||||||
|
// reporting frequency of temps and battery set to one hour
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,60*60], parameterNumber: 10, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 10).format()
|
||||||
|
|
||||||
|
cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
|
||||||
|
|
||||||
|
delayBetween(cmds, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//used to add "test" button for simulation of user changes to parameters
|
||||||
|
def test() {
|
||||||
|
def params = [paramNumber:12,value:4,size:1]
|
||||||
|
updateZwaveParam(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will allow the user to update device parameters (behavior) from an app.
|
||||||
|
* A "Zwave Tweaker" app will be developed as an interface to do this. Or the user can
|
||||||
|
* write his/her own app to envoke this method. No type or value checking is done to
|
||||||
|
* compare to what device capability or reaction. It is up to user to read OEM
|
||||||
|
* documentation prio to envoking this method.
|
||||||
|
*
|
||||||
|
* <p>THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION!
|
||||||
|
*
|
||||||
|
* @param List[paramNumber:80,value:10,size:1]
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def updateZwaveParam(params) {
|
||||||
|
if ( params ) {
|
||||||
|
def pNumber = params.paramNumber
|
||||||
|
def pSize = params.size
|
||||||
|
def pValue = [params.value]
|
||||||
|
log.debug "Make sure device is awake and in recieve mode (triple-click?)"
|
||||||
|
log.debug "Updating ${device.displayName} parameter number '${pNumber}' with value '${pValue}' with size of '${pSize}'"
|
||||||
|
|
||||||
|
def cmds = []
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format()
|
||||||
|
delayBetween(cmds, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all of available Fibaro parameters back to the device defaults except for what
|
||||||
|
* SmartThings needs to support the stock functionality as released. This will be
|
||||||
|
* called from the "Fibaro Tweaker" or user's app.
|
||||||
|
*
|
||||||
|
* <p>THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION!
|
||||||
|
*
|
||||||
|
* @param none
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def resetParams2StDefaults() {
|
||||||
|
log.debug "Resetting ${device.displayName} parameters to SmartThings compatible defaults"
|
||||||
|
def cmds = []
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 1, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [3], parameterNumber: 2, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 5, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 7, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 9, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,60*60], parameterNumber: 10, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,50], parameterNumber: 12, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 13, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [5,220], parameterNumber: 50, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [13,172], parameterNumber: 51, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0,0,225], parameterNumber: 61, size: 4).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,255,0,0], parameterNumber: 62, size: 4).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 63, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 73, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 74, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 75, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 76, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 77, size: 1).format()
|
||||||
|
|
||||||
|
delayBetween(cmds, 1200)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists all of available Fibaro parameters and thier current settings out to the
|
||||||
|
* logging window in the IDE This will be called from the "Fibaro Tweaker" or
|
||||||
|
* user's own app.
|
||||||
|
*
|
||||||
|
* <p>THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION!
|
||||||
|
*
|
||||||
|
* @param none
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def listCurrentParams() {
|
||||||
|
log.debug "Listing of current parameter settings of ${device.displayName}"
|
||||||
|
def cmds = []
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 5).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 7).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 9).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 10).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 13).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 50).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 51).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 61).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 62).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 63).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 73).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 74).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 75).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 76).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 77).format()
|
||||||
|
|
||||||
|
delayBetween(cmds, 1200)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,435 @@
|
|||||||
|
/**
|
||||||
|
* Device Type Definition File
|
||||||
|
*
|
||||||
|
* Device Type: Fibaro Motion Sensor
|
||||||
|
* File Name: fibaro-motion-sensor.groovy
|
||||||
|
* Initial Release: 2014-12-10
|
||||||
|
* Author: Todd Wackford
|
||||||
|
* Email: todd@wackford.net
|
||||||
|
*
|
||||||
|
* Copyright 2014 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
***************************************************************************************
|
||||||
|
*
|
||||||
|
* Change Log:
|
||||||
|
*
|
||||||
|
* 1. 20150125 Todd Wackford
|
||||||
|
* Incorporated Crc16Encap function to support core code changes. Duncan figured it
|
||||||
|
* out as usual.
|
||||||
|
*
|
||||||
|
* 2. 20150125 Todd Wackford
|
||||||
|
* Leaned out parse and moved most device info getting into configuration method.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up metadata, simulator info and tile definition.
|
||||||
|
*
|
||||||
|
* @param none
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Fibaro Motion Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Motion Sensor"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
capability "Acceleration Sensor"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Illuminance Measurement"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Battery"
|
||||||
|
|
||||||
|
command "resetParams2StDefaults"
|
||||||
|
command "listCurrentParams"
|
||||||
|
command "updateZwaveParam"
|
||||||
|
command "test"
|
||||||
|
command "configure"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x2001", inClusters: "0x30,0x84,0x85,0x80,0x8F,0x56,0x72,0x86,0x70,0x8E,0x31,0x9C,0xEF,0x30,0x31,0x9C"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// messages the device returns in response to commands it receives
|
||||||
|
status "motion (basic)" : "command: 2001, payload: FF"
|
||||||
|
status "no motion (basic)" : "command: 2001, payload: 00"
|
||||||
|
status "motion (binary)" : "command: 3003, payload: FF"
|
||||||
|
status "no motion (binary)" : "command: 3003, payload: 00"
|
||||||
|
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "temperature ${i}F": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(
|
||||||
|
scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 200; i <= 1000; i += 200) {
|
||||||
|
status "luminance ${i} lux": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(
|
||||||
|
scaledSensorValue: i, precision: 0, sensorType: 3).incomingMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport(
|
||||||
|
batteryLevel: i).incomingMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("motion", "device.motion", width: 2, 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("illuminance", "device.illuminance", inactiveLabel: false) {
|
||||||
|
state "luminosity", label:'${currentValue} ${unit}', unit:"lux"
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||||
|
}
|
||||||
|
standardTile("acceleration", "device.acceleration") {
|
||||||
|
state("active", label:'vibration', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0")
|
||||||
|
state("inactive", label:'still', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
main(["motion", "temperature", "acceleration", "illuminance"])
|
||||||
|
details(["motion", "temperature", "acceleration", "battery", "illuminance", "configure"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the device to settings needed by SmarthThings at device discovery time.
|
||||||
|
*
|
||||||
|
* @param none
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def configure() {
|
||||||
|
log.debug "Configuring Device For SmartThings Use"
|
||||||
|
def cmds = []
|
||||||
|
|
||||||
|
// send associate to group 3 to get sensor data reported only to hub
|
||||||
|
cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format()
|
||||||
|
|
||||||
|
// turn on tamper sensor with active/inactive reports (use it as an acceleration sensor) default is 0, or off
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [4], parameterNumber: 24, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 24).format()
|
||||||
|
|
||||||
|
// temperature change report threshold (0-255 = 0.1 to 25.5C) default is 1.0 Celcius, setting to .5 Celcius
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [5], parameterNumber: 60, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 60).format()
|
||||||
|
|
||||||
|
cmds << response(zwave.batteryV1.batteryGet())
|
||||||
|
cmds << response(zwave.versionV1.versionGet().format())
|
||||||
|
cmds << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format())
|
||||||
|
cmds << response(zwave.firmwareUpdateMdV2.firmwareMdGet().format())
|
||||||
|
|
||||||
|
delayBetween(cmds, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description)
|
||||||
|
{
|
||||||
|
def result = []
|
||||||
|
def cmd = zwave.parse(description, [0x72: 2, 0x31: 2, 0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2, 0x80: 1, 0x86: 1, 0x7A: 1, 0x56: 1])
|
||||||
|
|
||||||
|
if (description == "updated") {
|
||||||
|
result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds: 7200, nodeid:zwaveHubNodeId))
|
||||||
|
result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd) {
|
||||||
|
if( cmd.CMD == "8407" ) {
|
||||||
|
result << response(zwave.batteryV1.batteryGet().format())
|
||||||
|
result << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format())
|
||||||
|
}
|
||||||
|
result << createEvent(zwaveEvent(cmd))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( result[0] != null ) {
|
||||||
|
log.debug "Parse returned ${result}"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd)
|
||||||
|
{
|
||||||
|
def versions = [0x31: 2, 0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2]
|
||||||
|
// def encapsulatedCommand = cmd.encapsulatedCommand(versions)
|
||||||
|
def version = versions[cmd.commandClass as Integer]
|
||||||
|
def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)
|
||||||
|
def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)
|
||||||
|
if (!encapsulatedCommand) {
|
||||||
|
log.debug "Could not extract command from $cmd"
|
||||||
|
} else {
|
||||||
|
zwaveEvent(encapsulatedCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd, Map item1) {
|
||||||
|
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||||
|
log.debug "manufacturerName: ${cmd.manufacturerName}"
|
||||||
|
log.debug "productId: ${cmd.productId}"
|
||||||
|
log.debug "productTypeId: ${cmd.productTypeId}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd, Map item1) {
|
||||||
|
updateDataValue("applicationVersion", "${cmd.applicationVersion}")
|
||||||
|
log.debug "applicationVersion: ${cmd.applicationVersion}"
|
||||||
|
log.debug "applicationSubVersion: ${cmd.applicationSubVersion}"
|
||||||
|
log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}"
|
||||||
|
log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}"
|
||||||
|
log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.firmwareupdatemdv1.FirmwareMdReport cmd, Map item1) {
|
||||||
|
log.debug "checksum: ${cmd.checksum}"
|
||||||
|
log.debug "firmwareId: ${cmd.firmwareId}"
|
||||||
|
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
map.name = "acceleration"
|
||||||
|
|
||||||
|
map.value = cmd.sensorState ? "active" : "inactive"
|
||||||
|
if (map.value == "active") {
|
||||||
|
map.descriptionText = "$device.displayName detected vibration"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
map.descriptionText = "$device.displayName vibration has stopped"
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Generation
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
|
||||||
|
{
|
||||||
|
[descriptionText: "${device.displayName} woke up", isStateChange: false]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
switch (cmd.sensorType) {
|
||||||
|
case 1:
|
||||||
|
// temperature
|
||||||
|
def cmdScale = cmd.scale == 1 ? "F" : "C"
|
||||||
|
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
|
||||||
|
map.unit = getTemperatureScale()
|
||||||
|
map.name = "temperature"
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// luminance
|
||||||
|
map.value = cmd.scaledSensorValue.toInteger().toString()
|
||||||
|
map.unit = "lux"
|
||||||
|
map.name = "illuminance"
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||||
|
log.debug cmd
|
||||||
|
def map = [:]
|
||||||
|
map.name = "battery"
|
||||||
|
map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1
|
||||||
|
map.unit = "%"
|
||||||
|
map.displayed = false
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) {
|
||||||
|
def map = [:]
|
||||||
|
map.value = cmd.sensorValue ? "active" : "inactive"
|
||||||
|
map.name = "motion"
|
||||||
|
if (map.value == "active") {
|
||||||
|
map.descriptionText = "$device.displayName detected motion"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
map.descriptionText = "$device.displayName motion has stopped"
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||||
|
def map = [:]
|
||||||
|
map.value = cmd.value ? "active" : "inactive"
|
||||||
|
map.name = "motion"
|
||||||
|
if (map.value == "active") {
|
||||||
|
map.descriptionText = "$device.displayName detected motion"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
map.descriptionText = "$device.displayName motion has stopped"
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
log.debug "Catchall reached for cmd: ${cmd.toString()}}"
|
||||||
|
[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) {
|
||||||
|
log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'"
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||||
|
def result = []
|
||||||
|
|
||||||
|
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
|
||||||
|
log.debug "msr: $msr"
|
||||||
|
updateDataValue("MSR", msr)
|
||||||
|
|
||||||
|
if ( msr == "010F-0800-2001" ) { //this is the msr and device type for the fibaro motion sensor
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
//used to add "test" button for simulation of user changes to parameters
|
||||||
|
def test() {
|
||||||
|
def params = [paramNumber:80,value:10,size:1]
|
||||||
|
updateZwaveParam(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will allow the user to update device parameters (behavior) from an app.
|
||||||
|
* A "Zwave Tweaker" app will be developed as an interface to do this. Or the user can
|
||||||
|
* write his/her own app to envoke this method. No type or value checking is done to
|
||||||
|
* compare to what device capability or reaction. It is up to user to read OEM
|
||||||
|
* documentation prio to envoking this method.
|
||||||
|
*
|
||||||
|
* <p>THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION!
|
||||||
|
*
|
||||||
|
* @param List[paramNumber:80,value:10,size:1]
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def updateZwaveParam(params) {
|
||||||
|
if ( params ) {
|
||||||
|
def pNumber = params.paramNumber
|
||||||
|
def pSize = params.size
|
||||||
|
def pValue = [params.value]
|
||||||
|
log.debug "Make sure device is awake and in recieve mode"
|
||||||
|
log.debug "Updating ${device.displayName} parameter number '${pNumber}' with value '${pValue}' with size of '${pSize}'"
|
||||||
|
|
||||||
|
def cmds = []
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format()
|
||||||
|
delayBetween(cmds, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all of available Fibaro parameters back to the device defaults except for what
|
||||||
|
* SmartThings needs to support the stock functionality as released. This will be
|
||||||
|
* called from the "Fibaro Tweaker" or user's app.
|
||||||
|
*
|
||||||
|
* <p>THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION!
|
||||||
|
*
|
||||||
|
* @param none
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def resetParams2StDefaults() {
|
||||||
|
log.debug "Resetting Sensor Parameters to SmartThings Compatible Defaults"
|
||||||
|
def cmds = []
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [10], parameterNumber: 1, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [15], parameterNumber: 2, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 4, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,30], parameterNumber: 6, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 8, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,200], parameterNumber: 9, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 12, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 16, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [15], parameterNumber: 20, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,30], parameterNumber: 22, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [4], parameterNumber: 24, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 26, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,200], parameterNumber: 40, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 42, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [5], parameterNumber: 60, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [3,132], parameterNumber: 62, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 64, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 66, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [10], parameterNumber: 80, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [50], parameterNumber: 81, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [0,100], parameterNumber: 82, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [3,232], parameterNumber: 83, size: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [18], parameterNumber: 86, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [28], parameterNumber: 87, size: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 89, size: 1).format()
|
||||||
|
|
||||||
|
delayBetween(cmds, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists all of available Fibaro parameters and thier current settings out to the
|
||||||
|
* logging window in the IDE This will be called from the "Fibaro Tweaker" or
|
||||||
|
* user's own app.
|
||||||
|
*
|
||||||
|
* <p>THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION!
|
||||||
|
*
|
||||||
|
* @param none
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def listCurrentParams() {
|
||||||
|
log.debug "Listing of current parameter settings of ${device.displayName}"
|
||||||
|
def cmds = []
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 1).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 2).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 4).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 6).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 8).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 9).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 14).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 16).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 20).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 22).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 24).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 26).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 40).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 42).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 60).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 62).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 64).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 66).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 80).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 81).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 82).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 83).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 86).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 87).format()
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: 89).format()
|
||||||
|
|
||||||
|
delayBetween(cmds, 500)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,864 @@
|
|||||||
|
/**
|
||||||
|
* Device Type Definition File
|
||||||
|
*
|
||||||
|
* Device Type: Fibaro RGBW Controller
|
||||||
|
* File Name: fibaro-rgbw-controller.groovy
|
||||||
|
* Initial Release: 2015-01-04
|
||||||
|
* Author: Todd Wackford
|
||||||
|
* Email: todd@wackford.net
|
||||||
|
*
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
|
||||||
|
definition (name: "Fibaro RGBW Controller", namespace: "smartthings", author: "Todd Wackford") {
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Color Control"
|
||||||
|
capability "Power Meter"
|
||||||
|
|
||||||
|
command "getDeviceData"
|
||||||
|
command "softwhite"
|
||||||
|
command "daylight"
|
||||||
|
command "warmwhite"
|
||||||
|
command "red"
|
||||||
|
command "green"
|
||||||
|
command "blue"
|
||||||
|
command "cyan"
|
||||||
|
command "magenta"
|
||||||
|
command "orange"
|
||||||
|
command "purple"
|
||||||
|
command "yellow"
|
||||||
|
command "white"
|
||||||
|
command "fireplace"
|
||||||
|
command "storm"
|
||||||
|
command "deepfade"
|
||||||
|
command "litefade"
|
||||||
|
command "police"
|
||||||
|
command "setAdjustedColor"
|
||||||
|
command "setWhiteLevel"
|
||||||
|
command "test"
|
||||||
|
|
||||||
|
attribute "whiteLevel", "string"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x1101", inClusters: "0x27,0x72,0x86,0x26,0x60,0x70,0x32,0x31,0x85,0x33"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "on": "command: 2003, payload: FF"
|
||||||
|
status "off": "command: 2003, payload: 00"
|
||||||
|
status "09%": "command: 2003, payload: 09"
|
||||||
|
status "10%": "command: 2003, payload: 0A"
|
||||||
|
status "33%": "command: 2003, payload: 21"
|
||||||
|
status "66%": "command: 2003, payload: 42"
|
||||||
|
status "99%": "command: 2003, payload: 63"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "2001FF,delay 5000,2602": "command: 2603, payload: FF"
|
||||||
|
reply "200100,delay 5000,2602": "command: 2603, payload: 00"
|
||||||
|
reply "200119,delay 5000,2602": "command: 2603, payload: 19"
|
||||||
|
reply "200132,delay 5000,2602": "command: 2603, payload: 32"
|
||||||
|
reply "20014B,delay 5000,2602": "command: 2603, payload: 4B"
|
||||||
|
reply "200163,delay 5000,2602": "command: 2603, payload: 63"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
|
||||||
|
state "color", action:"setAdjustedColor"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
controlTile("whiteSliderControl", "device.whiteLevel", "slider", height: 1, width: 3, inactiveLabel: false) {
|
||||||
|
state "whiteLevel", action:"setWhiteLevel", label:'White Level'
|
||||||
|
}
|
||||||
|
standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) {
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.illuminance.illuminance.bright", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.illuminance.illuminance.dark", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "turningOn", label:'${name}', icon:"st.illuminance.illuminance.bright", backgroundColor:"#79b821"
|
||||||
|
state "turningOff", label:'${name}', icon:"st.illuminance.illuminance.dark", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
valueTile("power", "device.power", decoration: "flat") {
|
||||||
|
state "power", label:'${currentValue} W'
|
||||||
|
}
|
||||||
|
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", height: 1, inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
standardTile("softwhite", "device.softwhite", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offsoftwhite", label:"soft white", action:"softwhite", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "onsoftwhite", label:"soft white", action:"softwhite", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFF1E0"
|
||||||
|
}
|
||||||
|
standardTile("daylight", "device.daylight", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offdaylight", label:"daylight", action:"daylight", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "ondaylight", label:"daylight", action:"daylight", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFB"
|
||||||
|
}
|
||||||
|
standardTile("warmwhite", "device.warmwhite", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offwarmwhite", label:"warm white", action:"warmwhite", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "onwarmwhite", label:"warm white", action:"warmwhite", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFF4E5"
|
||||||
|
}
|
||||||
|
standardTile("red", "device.red", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offred", label:"red", action:"red", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "onred", label:"red", action:"red", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF0000"
|
||||||
|
}
|
||||||
|
standardTile("green", "device.green", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offgreen", label:"green", action:"green", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "ongreen", label:"green", action:"green", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00FF00"
|
||||||
|
}
|
||||||
|
standardTile("blue", "device.blue", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offblue", label:"blue", action:"blue", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "onblue", label:"blue", action:"blue", icon:"st.illuminance.illuminance.bright", backgroundColor:"#0000FF"
|
||||||
|
}
|
||||||
|
standardTile("cyan", "device.cyan", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offcyan", label:"cyan", action:"cyan", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "oncyan", label:"cyan", action:"cyan", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00FFFF"
|
||||||
|
}
|
||||||
|
standardTile("magenta", "device.magenta", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offmagenta", label:"magenta", action:"magenta", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "onmagenta", label:"magenta", action:"magenta", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF00FF"
|
||||||
|
}
|
||||||
|
standardTile("orange", "device.orange", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offorange", label:"orange", action:"orange", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "onorange", label:"orange", action:"orange", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF6600"
|
||||||
|
}
|
||||||
|
standardTile("purple", "device.purple", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offpurple", label:"purple", action:"purple", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "onpurple", label:"purple", action:"purple", icon:"st.illuminance.illuminance.bright", backgroundColor:"#BF00FF"
|
||||||
|
}
|
||||||
|
standardTile("yellow", "device.yellow", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offyellow", label:"yellow", action:"yellow", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "onyellow", label:"yellow", action:"yellow", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFF00"
|
||||||
|
}
|
||||||
|
standardTile("white", "device.white", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offwhite", label:"White", action:"white", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "onwhite", label:"White", action:"white", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF"
|
||||||
|
}
|
||||||
|
standardTile("fireplace", "device.fireplace", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offfireplace", label:"Fire Place", action:"fireplace", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "onfireplace", label:"Fire Place", action:"fireplace", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF"
|
||||||
|
}
|
||||||
|
standardTile("storm", "device.storm", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offstorm", label:"storm", action:"storm", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "onstorm", label:"storm", action:"storm", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF"
|
||||||
|
}
|
||||||
|
standardTile("deepfade", "device.deepfade", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offdeepfade", label:"deep fade", action:"deepfade", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "ondeepfade", label:"deep fade", action:"deepfade", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF"
|
||||||
|
}
|
||||||
|
standardTile("litefade", "device.litefade", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offlitefade", label:"lite fade", action:"litefade", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "onlitefade", label:"lite fade", action:"litefade", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF"
|
||||||
|
}
|
||||||
|
standardTile("police", "device.police", height: 1, inactiveLabel: false, canChangeIcon: false) {
|
||||||
|
state "offpolice", label:"police", action:"police", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8"
|
||||||
|
state "onpolice", label:"police", action:"police", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF"
|
||||||
|
}
|
||||||
|
controlTile("saturationSliderControl", "device.saturation", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "saturation", action:"color control.setSaturation"
|
||||||
|
}
|
||||||
|
valueTile("saturation", "device.saturation", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "saturation", label: 'Sat ${currentValue} '
|
||||||
|
}
|
||||||
|
controlTile("hueSliderControl", "device.hue", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "hue", action:"color control.setHue"
|
||||||
|
}
|
||||||
|
valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "hue", label: 'Hue ${currentValue} '
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch",
|
||||||
|
"levelSliderControl",
|
||||||
|
"rgbSelector",
|
||||||
|
"whiteSliderControl",
|
||||||
|
/*"softwhite",
|
||||||
|
"daylight",
|
||||||
|
"warmwhite",
|
||||||
|
"red",
|
||||||
|
"green",
|
||||||
|
"blue",
|
||||||
|
"white",
|
||||||
|
"cyan",
|
||||||
|
"magenta",
|
||||||
|
"orange",
|
||||||
|
"purple",
|
||||||
|
"yellow",
|
||||||
|
"fireplace",
|
||||||
|
"storm",
|
||||||
|
"deepfade",
|
||||||
|
"litefade",
|
||||||
|
"police",
|
||||||
|
"power",
|
||||||
|
"configure",*/
|
||||||
|
"refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def setAdjustedColor(value) {
|
||||||
|
log.debug "setAdjustedColor: ${value}"
|
||||||
|
|
||||||
|
toggleTiles("off") //turn off the hard color tiles
|
||||||
|
|
||||||
|
def level = device.latestValue("level")
|
||||||
|
if(level == null)
|
||||||
|
level = 50
|
||||||
|
log.debug "level is: ${level}"
|
||||||
|
value.level = level
|
||||||
|
|
||||||
|
def c = hexToRgb(value.hex)
|
||||||
|
value.rh = hex(c.r * (level/100))
|
||||||
|
value.gh = hex(c.g * (level/100))
|
||||||
|
value.bh = hex(c.b * (level/100))
|
||||||
|
|
||||||
|
setColor(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setColor(value) {
|
||||||
|
log.debug "setColor: ${value}"
|
||||||
|
log.debug "hue is: ${value.hue}"
|
||||||
|
log.debug "saturation is: ${value.saturation}"
|
||||||
|
|
||||||
|
if (value.size() < 8)
|
||||||
|
toggleTiles("off")
|
||||||
|
|
||||||
|
if (( value.size() == 2) && (value.hue != null) && (value.saturation != null)) { //assuming we're being called from outside of device (App)
|
||||||
|
def rgb = hslToRGB(value.hue, value.saturation, 0.5)
|
||||||
|
value.hex = rgbToHex(rgb)
|
||||||
|
value.rh = hex(rgb.r)
|
||||||
|
value.gh = hex(rgb.g)
|
||||||
|
value.bh = hex(rgb.b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((value.size() == 3) && (value.hue != null) && (value.saturation != null) && (value.level)) { //user passed in a level value too from outside (App)
|
||||||
|
def rgb = hslToRGB(value.hue, value.saturation, 0.5)
|
||||||
|
value.hex = rgbToHex(rgb)
|
||||||
|
value.rh = hex(rgb.r * value.level/100)
|
||||||
|
value.gh = hex(rgb.g * value.level/100)
|
||||||
|
value.bh = hex(rgb.b * value.level/100)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (( value.size() == 1) && (value.hex)) { //being called from outside of device (App) with only hex
|
||||||
|
def rgbInt = hexToRgb(value.hex)
|
||||||
|
value.rh = hex(rgbInt.r)
|
||||||
|
value.gh = hex(rgbInt.g)
|
||||||
|
value.bh = hex(rgbInt.b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (( value.size() == 2) && (value.hex) && (value.level)) { //being called from outside of device (App) with only hex and level
|
||||||
|
|
||||||
|
def rgbInt = hexToRgb(value.hex)
|
||||||
|
value.rh = hex(rgbInt.r * value.level/100)
|
||||||
|
value.gh = hex(rgbInt.g * value.level/100)
|
||||||
|
value.bh = hex(rgbInt.b * value.level/100)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (( value.size() == 1) && (value.colorName)) { //being called from outside of device (App) with only color name
|
||||||
|
def colorData = getColorData(value.colorName)
|
||||||
|
value.rh = colorData.rh
|
||||||
|
value.gh = colorData.gh
|
||||||
|
value.bh = colorData.bh
|
||||||
|
value.hex = "#${value.rh}${value.gh}${value.bh}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (( value.size() == 2) && (value.colorName) && (value.level)) { //being called from outside of device (App) with only color name and level
|
||||||
|
def colorData = getColorData(value.colorName)
|
||||||
|
value.rh = hex(colorData.r * value.level/100)
|
||||||
|
value.gh = hex(colorData.g * value.level/100)
|
||||||
|
value.bh = hex(colorData.b * value.level/100)
|
||||||
|
value.hex = "#${hex(colorData.r)}${hex(colorData.g)}${hex(colorData.b)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (( value.size() == 3) && (value.red != null) && (value.green != null) && (value.blue != null)) { //being called from outside of device (App) with only color values (0-255)
|
||||||
|
value.rh = hex(value.red)
|
||||||
|
value.gh = hex(value.green)
|
||||||
|
value.bh = hex(value.blue)
|
||||||
|
value.hex = "#${value.rh}${value.gh}${value.bh}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (( value.size() == 4) && (value.red != null) && (value.green != null) && (value.blue != null) && (value.level)) { //being called from outside of device (App) with only color values (0-255) and level
|
||||||
|
value.rh = hex(value.red * value.level/100)
|
||||||
|
value.gh = hex(value.green * value.level/100)
|
||||||
|
value.bh = hex(value.blue * value.level/100)
|
||||||
|
value.hex = "#${hex(value.red)}${hex(value.green)}${hex(value.blue)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent(name: "hue", value: value.hue, displayed: false)
|
||||||
|
sendEvent(name: "saturation", value: value.saturation, displayed: false)
|
||||||
|
sendEvent(name: "color", value: value.hex, displayed: false)
|
||||||
|
if (value.level) {
|
||||||
|
sendEvent(name: "level", value: value.level)
|
||||||
|
}
|
||||||
|
if (value.switch) {
|
||||||
|
sendEvent(name: "switch", value: value.switch)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRGB(value.rh, value.gh, value.bh)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(level) {
|
||||||
|
log.debug "setLevel($level)"
|
||||||
|
|
||||||
|
if (level == 0) { off() }
|
||||||
|
else if (device.latestValue("switch") == "off") { on() }
|
||||||
|
|
||||||
|
def colorHex = device.latestValue("color")
|
||||||
|
if (colorHex == null)
|
||||||
|
colorHex = "#FFFFFF"
|
||||||
|
|
||||||
|
def c = hexToRgb(colorHex)
|
||||||
|
|
||||||
|
def r = hex(c.r * (level/100))
|
||||||
|
def g = hex(c.g * (level/100))
|
||||||
|
def b = hex(c.b * (level/100))
|
||||||
|
|
||||||
|
sendEvent(name: "level", value: level)
|
||||||
|
sendEvent(name: "setLevel", value: level, displayed: false)
|
||||||
|
sendRGB(r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def setWhiteLevel(value) {
|
||||||
|
log.debug "setWhiteLevel: ${value}"
|
||||||
|
def level = Math.min(value as Integer, 99)
|
||||||
|
level = 255 * level/99 as Integer
|
||||||
|
def channel = 0
|
||||||
|
|
||||||
|
if (device.latestValue("switch") == "off") { on() }
|
||||||
|
|
||||||
|
sendEvent(name: "whiteLevel", value: value)
|
||||||
|
sendWhite(channel, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def sendWhite(channel, value) {
|
||||||
|
def whiteLevel = hex(value)
|
||||||
|
def cmd = [String.format("3305010${channel}${whiteLevel}%02X", 50)]
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
def sendRGB(redHex, greenHex, blueHex) {
|
||||||
|
def cmd = [String.format("33050302${redHex}03${greenHex}04${blueHex}%02X", 100),]
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sendRGBW(redHex, greenHex, blueHex, whiteHex) {
|
||||||
|
def cmd = [String.format("33050400${whiteHex}02${redHex}03${greenHex}04${blueHex}%02X", 100),]
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
log.debug "Configuring Device For SmartThings Use"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def cmds = []
|
||||||
|
|
||||||
|
// send associate to group 3 to get sensor data reported only to hub
|
||||||
|
cmds << zwave.associationV2.associationSet(groupingIdentifier:5, nodeId:[zwaveHubNodeId]).format()
|
||||||
|
|
||||||
|
|
||||||
|
//cmds << sendEvent(name: "level", value: 50)
|
||||||
|
//cmds << on()
|
||||||
|
//cmds << doColorButton("Green")
|
||||||
|
delayBetween(cmds, 500)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
//log.debug "description: ${description}"
|
||||||
|
def item1 = [
|
||||||
|
canBeCurrentState: false,
|
||||||
|
linkText: getLinkText(device),
|
||||||
|
isStateChange: false,
|
||||||
|
displayed: false,
|
||||||
|
descriptionText: description,
|
||||||
|
value: description
|
||||||
|
]
|
||||||
|
def result
|
||||||
|
def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 2, 0x72: 2, 0x60: 3, 0x33: 2, 0x32: 3, 0x31:2, 0x30: 2, 0x86: 1, 0x7A: 1])
|
||||||
|
|
||||||
|
if (cmd) {
|
||||||
|
if ( cmd.CMD != "7006" ) {
|
||||||
|
result = createEvent(cmd, item1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
item1.displayed = displayed(description, item1.isStateChange)
|
||||||
|
result = [item1]
|
||||||
|
}
|
||||||
|
//log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def getDeviceData() {
|
||||||
|
def cmd = []
|
||||||
|
|
||||||
|
cmd << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
|
||||||
|
cmd << response(zwave.versionV1.versionGet())
|
||||||
|
cmd << response(zwave.firmwareUpdateMdV1.firmwareMdGet())
|
||||||
|
|
||||||
|
delayBetween(cmd, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd, Map item1) {
|
||||||
|
log.debug "manufacturerName: ${cmd.manufacturerName}"
|
||||||
|
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||||
|
log.debug "productId: ${cmd.productId}"
|
||||||
|
log.debug "productTypeId: ${cmd.productTypeId}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd, Map item1) {
|
||||||
|
updateDataValue("applicationVersion", "${cmd.applicationVersion}")
|
||||||
|
log.debug "applicationVersion: ${cmd.applicationVersion}"
|
||||||
|
log.debug "applicationSubVersion: ${cmd.applicationSubVersion}"
|
||||||
|
log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}"
|
||||||
|
log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}"
|
||||||
|
log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.firmwareupdatemdv1.FirmwareMdReport cmd, Map item1) {
|
||||||
|
log.debug "checksum: ${cmd.checksum}"
|
||||||
|
log.debug "firmwareId: ${cmd.firmwareId}"
|
||||||
|
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.colorcontrolv1.CapabilityReport cmd, Map item1) {
|
||||||
|
|
||||||
|
log.debug "In CapabilityReport"
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, Map item1) {
|
||||||
|
def encapsulatedCommand = cmd.encapsulatedCommand([0x26: 1, 0x30: 2, 0x32: 2, 0x33: 2]) // can specify command class versions here like in zwave.parse
|
||||||
|
//log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}")
|
||||||
|
if ((cmd.sourceEndPoint >= 1) && (cmd.sourceEndPoint <= 5)) { // we don't need color report
|
||||||
|
//don't do anything
|
||||||
|
} else {
|
||||||
|
if (encapsulatedCommand) {
|
||||||
|
zwaveEvent(encapsulatedCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) {
|
||||||
|
def result = doCreateEvent(cmd, item1)
|
||||||
|
for (int i = 0; i < result.size(); i++) {
|
||||||
|
result[i].type = "physical"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, Map item1) {
|
||||||
|
def result = doCreateEvent(cmd, item1)
|
||||||
|
for (int i = 0; i < result.size(); i++) {
|
||||||
|
result[i].type = "physical"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd, Map item1) {
|
||||||
|
def result = [:]
|
||||||
|
if ( cmd.sensorType == 4 ) { //power level comming in
|
||||||
|
result.name = "power"
|
||||||
|
result.value = cmd.scaledSensorValue
|
||||||
|
result.descriptionText = "$device.displayName power usage is ${result.value} watt(s)"
|
||||||
|
result.isStateChange
|
||||||
|
sendEvent(name: result.name, value: result.value, displayed: false)
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd, Map item1) {
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd, Map item1) {
|
||||||
|
[response(zwave.basicV1.basicGet())]
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelSet cmd, Map item1) {
|
||||||
|
def result = doCreateEvent(cmd, item1)
|
||||||
|
for (int i = 0; i < result.size(); i++) {
|
||||||
|
result[i].type = "physical"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd, Map item1) {
|
||||||
|
def result = doCreateEvent(cmd, item1)
|
||||||
|
result[0].descriptionText = "${item1.linkText} is ${item1.value}"
|
||||||
|
result[0].handlerName = cmd.value ? "statusOn" : "statusOff"
|
||||||
|
for (int i = 0; i < result.size(); i++) {
|
||||||
|
result[i].type = "digital"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) {
|
||||||
|
def result = [item1]
|
||||||
|
|
||||||
|
item1.name = "switch"
|
||||||
|
item1.value = cmd.value ? "on" : "off"
|
||||||
|
item1.handlerName = item1.value
|
||||||
|
item1.descriptionText = "${item1.linkText} was turned ${item1.value}"
|
||||||
|
item1.canBeCurrentState = true
|
||||||
|
item1.isStateChange = isStateChange(device, item1.name, item1.value)
|
||||||
|
item1.displayed = item1.isStateChange
|
||||||
|
|
||||||
|
if (cmd.value >= 5) {
|
||||||
|
def item2 = new LinkedHashMap(item1)
|
||||||
|
item2.name = "level"
|
||||||
|
item2.value = cmd.value as String
|
||||||
|
item2.unit = "%"
|
||||||
|
item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %"
|
||||||
|
item2.canBeCurrentState = true
|
||||||
|
item2.isStateChange = isStateChange(device, item2.name, item2.value)
|
||||||
|
item2.displayed = false
|
||||||
|
result << item2
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd, item1) {
|
||||||
|
log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'"
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
|
||||||
|
log.debug "Report: $cmd"
|
||||||
|
def value = "when off"
|
||||||
|
if (cmd.configurationValue[0] == 1) {value = "when on"}
|
||||||
|
if (cmd.configurationValue[0] == 2) {value = "never"}
|
||||||
|
[name: "indicatorStatus", value: value, display: false]
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
def createEvent(physicalgraph.zwave.Command cmd, Map map) {
|
||||||
|
// Handles any Z-Wave commands we aren't interested in
|
||||||
|
log.debug "UNHANDLED COMMAND $cmd"
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
log.debug "on()"
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(),
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
log.debug "off()"
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
delayBetween ([zwave.basicV1.basicSet(value: 0x00).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
def cmd = []
|
||||||
|
cmd << response(zwave.switchMultilevelV1.switchMultilevelGet().format())
|
||||||
|
delayBetween(cmd, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will allow the user to update device parameters (behavior) from an app.
|
||||||
|
* A "Zwave Tweaker" app will be developed as an interface to do this. Or the user can
|
||||||
|
* write his/her own app to envoke this method. No type or value checking is done to
|
||||||
|
* compare to what device capability or reaction. It is up to user to read OEM
|
||||||
|
* documentation prio to envoking this method.
|
||||||
|
*
|
||||||
|
* <p>THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION!
|
||||||
|
*
|
||||||
|
* @param List[paramNumber:80,value:10,size:1]
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @return none
|
||||||
|
*/
|
||||||
|
def updateZwaveParam(params) {
|
||||||
|
if ( params ) {
|
||||||
|
def pNumber = params.paramNumber
|
||||||
|
def pSize = params.size
|
||||||
|
def pValue = [params.value]
|
||||||
|
log.debug "Updating ${device.displayName} parameter number '${pNumber}' with value '${pValue}' with size of '${pSize}'"
|
||||||
|
|
||||||
|
def cmds = []
|
||||||
|
cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format()
|
||||||
|
|
||||||
|
cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format()
|
||||||
|
delayBetween(cmds, 1500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def test() {
|
||||||
|
//def value = [:]
|
||||||
|
//value = [hue: 0, saturation: 100, level: 5]
|
||||||
|
//value = [red: 255, green: 0, blue: 255, level: 60]
|
||||||
|
//setColor(value)
|
||||||
|
|
||||||
|
def cmd = []
|
||||||
|
|
||||||
|
if ( !state.cnt ) {
|
||||||
|
state.cnt = 6
|
||||||
|
} else {
|
||||||
|
state.cnt = state.cnt + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( state.cnt > 10 )
|
||||||
|
state.cnt = 6
|
||||||
|
|
||||||
|
// run programmed light show
|
||||||
|
cmd << zwave.configurationV1.configurationSet(configurationValue: [state.cnt], parameterNumber: 72, size: 1).format()
|
||||||
|
cmd << zwave.configurationV1.configurationGet(parameterNumber: 72).format()
|
||||||
|
|
||||||
|
delayBetween(cmd, 500)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def colorNameToRgb(color) {
|
||||||
|
|
||||||
|
final colors = [
|
||||||
|
[name:"Soft White", r: 255, g: 241, b: 224 ],
|
||||||
|
[name:"Daylight", r: 255, g: 255, b: 251 ],
|
||||||
|
[name:"Warm White", r: 255, g: 244, b: 229 ],
|
||||||
|
|
||||||
|
[name:"Red", r: 255, g: 0, b: 0 ],
|
||||||
|
[name:"Green", r: 0, g: 255, b: 0 ],
|
||||||
|
[name:"Blue", r: 0, g: 0, b: 255 ],
|
||||||
|
|
||||||
|
[name:"Cyan", r: 0, g: 255, b: 255 ],
|
||||||
|
[name:"Magenta", r: 255, g: 0, b: 33 ],
|
||||||
|
[name:"Orange", r: 255, g: 102, b: 0 ],
|
||||||
|
|
||||||
|
[name:"Purple", r: 170, g: 0, b: 255 ],
|
||||||
|
[name:"Yellow", r: 255, g: 255, b: 0 ],
|
||||||
|
[name:"White", r: 255, g: 255, b: 255 ]
|
||||||
|
]
|
||||||
|
|
||||||
|
def colorData = [:]
|
||||||
|
colorData = colors.find { it.name == color }
|
||||||
|
|
||||||
|
colorData
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value, width=2) {
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() < width) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
def hexToRgb(colorHex) {
|
||||||
|
def rrInt = Integer.parseInt(colorHex.substring(1,3),16)
|
||||||
|
def ggInt = Integer.parseInt(colorHex.substring(3,5),16)
|
||||||
|
def bbInt = Integer.parseInt(colorHex.substring(5,7),16)
|
||||||
|
|
||||||
|
def colorData = [:]
|
||||||
|
colorData = [r: rrInt, g: ggInt, b: bbInt]
|
||||||
|
colorData
|
||||||
|
}
|
||||||
|
|
||||||
|
def rgbToHex(rgb) {
|
||||||
|
def r = hex(rgb.r)
|
||||||
|
def g = hex(rgb.g)
|
||||||
|
def b = hex(rgb.b)
|
||||||
|
def hexColor = "#${r}${g}${b}"
|
||||||
|
|
||||||
|
hexColor
|
||||||
|
}
|
||||||
|
|
||||||
|
def hslToRGB(float var_h, float var_s, float var_l) {
|
||||||
|
float h = var_h / 100
|
||||||
|
float s = var_s / 100
|
||||||
|
float l = var_l
|
||||||
|
|
||||||
|
def r = 0
|
||||||
|
def g = 0
|
||||||
|
def b = 0
|
||||||
|
|
||||||
|
if (s == 0) {
|
||||||
|
r = l * 255
|
||||||
|
g = l * 255
|
||||||
|
b = l * 255
|
||||||
|
} else {
|
||||||
|
float var_2 = 0
|
||||||
|
if (l < 0.5) {
|
||||||
|
var_2 = l * (1 + s)
|
||||||
|
} else {
|
||||||
|
var_2 = (l + s) - (s * l)
|
||||||
|
}
|
||||||
|
|
||||||
|
float var_1 = 2 * l - var_2
|
||||||
|
|
||||||
|
r = 255 * hueToRgb(var_1, var_2, h + (1 / 3))
|
||||||
|
g = 255 * hueToRgb(var_1, var_2, h)
|
||||||
|
b = 255 * hueToRgb(var_1, var_2, h - (1 / 3))
|
||||||
|
}
|
||||||
|
|
||||||
|
def rgb = [:]
|
||||||
|
rgb = [r: r, g: g, b: b]
|
||||||
|
|
||||||
|
rgb
|
||||||
|
}
|
||||||
|
|
||||||
|
def hueToRgb(v1, v2, vh) {
|
||||||
|
if (vh < 0) { vh += 1 }
|
||||||
|
if (vh > 1) { vh -= 1 }
|
||||||
|
if ((6 * vh) < 1) { return (v1 + (v2 - v1) * 6 * vh) }
|
||||||
|
if ((2 * vh) < 1) { return (v2) }
|
||||||
|
if ((3 * vh) < 2) { return (v1 + (v2 - $v1) * ((2 / 3 - vh) * 6)) }
|
||||||
|
return (v1)
|
||||||
|
}
|
||||||
|
|
||||||
|
def rgbToHSL(rgb) {
|
||||||
|
def r = rgb.r / 255
|
||||||
|
def g = rgb.g / 255
|
||||||
|
def b = rgb.b / 255
|
||||||
|
def h = 0
|
||||||
|
def s = 0
|
||||||
|
def l = 0
|
||||||
|
|
||||||
|
def var_min = [r,g,b].min()
|
||||||
|
def var_max = [r,g,b].max()
|
||||||
|
def del_max = var_max - var_min
|
||||||
|
|
||||||
|
l = (var_max + var_min) / 2
|
||||||
|
|
||||||
|
if (del_max == 0) {
|
||||||
|
h = 0
|
||||||
|
s = 0
|
||||||
|
} else {
|
||||||
|
if (l < 0.5) { s = del_max / (var_max + var_min) }
|
||||||
|
else { s = del_max / (2 - var_max - var_min) }
|
||||||
|
|
||||||
|
def del_r = (((var_max - r) / 6) + (del_max / 2)) / del_max
|
||||||
|
def del_g = (((var_max - g) / 6) + (del_max / 2)) / del_max
|
||||||
|
def del_b = (((var_max - b) / 6) + (del_max / 2)) / del_max
|
||||||
|
|
||||||
|
if (r == var_max) { h = del_b - del_g }
|
||||||
|
else if (g == var_max) { h = (1 / 3) + del_r - del_b }
|
||||||
|
else if (b == var_max) { h = (2 / 3) + del_g - del_r }
|
||||||
|
|
||||||
|
if (h < 0) { h += 1 }
|
||||||
|
if (h > 1) { h -= 1 }
|
||||||
|
}
|
||||||
|
def hsl = [:]
|
||||||
|
hsl = [h: h * 100, s: s * 100, l: l]
|
||||||
|
|
||||||
|
hsl
|
||||||
|
}
|
||||||
|
|
||||||
|
def getColorData(colorName) {
|
||||||
|
log.debug "getColorData: ${colorName}"
|
||||||
|
|
||||||
|
def colorRGB = colorNameToRgb(colorName)
|
||||||
|
def colorHex = rgbToHex(colorRGB)
|
||||||
|
def colorHSL = rgbToHSL(colorRGB)
|
||||||
|
|
||||||
|
def colorData = [:]
|
||||||
|
colorData = [h: colorHSL.h,
|
||||||
|
s: colorHSL.s,
|
||||||
|
l: device.latestValue("level"),
|
||||||
|
r: colorRGB.r,
|
||||||
|
g: colorRGB.g,
|
||||||
|
b: colorRGB.b,
|
||||||
|
rh: hex(colorRGB.r),
|
||||||
|
gh: hex(colorRGB.g),
|
||||||
|
bh: hex(colorRGB.b),
|
||||||
|
hex: colorHex,
|
||||||
|
alpha: 1]
|
||||||
|
|
||||||
|
colorData
|
||||||
|
}
|
||||||
|
|
||||||
|
def doColorButton(colorName) {
|
||||||
|
log.debug "doColorButton: '${colorName}()'"
|
||||||
|
|
||||||
|
if (device.latestValue("switch") == "off") { on() }
|
||||||
|
|
||||||
|
def level = device.latestValue("level")
|
||||||
|
def maxLevel = hex(99)
|
||||||
|
|
||||||
|
toggleTiles(colorName.toLowerCase().replaceAll("\\s",""))
|
||||||
|
|
||||||
|
if ( colorName == "Fire Place" ) { updateZwaveParam([paramNumber:72, value:6, size:1]) }
|
||||||
|
else if ( colorName == "Storm" ) { updateZwaveParam([paramNumber:72, value:7, size:1]) }
|
||||||
|
else if ( colorName == "Deep Fade" ) { updateZwaveParam([paramNumber:72, value:8, size:1]) }
|
||||||
|
else if ( colorName == "Lite Fade" ) { updateZwaveParam([paramNumber:72, value:9, size:1]) }
|
||||||
|
else if ( colorName == "Police" ) { updateZwaveParam([paramNumber:72, value:10, size:1]) }
|
||||||
|
else if ( colorName == "White" ) { String.format("33050400${maxLevel}02${hex(0)}03${hex(0)}04${hex(0)}%02X", 100) }
|
||||||
|
else if ( colorName == "Daylight" ) { String.format("33050400${maxLevel}02${maxLevel}03${maxLevel}04${maxLevel}%02X", 100) }
|
||||||
|
else {
|
||||||
|
def c = getColorData(colorName)
|
||||||
|
def newValue = ["hue": c.h, "saturation": c.s, "level": level, "red": c.r, "green": c.g, "blue": c.b, "hex": c.hex, "alpha": c.alpha]
|
||||||
|
setColor(newValue)
|
||||||
|
def r = hex(c.r * (level/100))
|
||||||
|
def g = hex(c.g * (level/100))
|
||||||
|
def b = hex(c.b * (level/100))
|
||||||
|
def w = hex(0) //to turn off white channel with toggling tiles
|
||||||
|
sendRGBW(r, g, b, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def toggleTiles(color) {
|
||||||
|
state.colorTiles = []
|
||||||
|
if ( !state.colorTiles ) {
|
||||||
|
state.colorTiles = ["softwhite","daylight","warmwhite","red","green","blue","cyan","magenta","orange","purple","yellow","white","fireplace","storm","deepfade","litefade","police"]
|
||||||
|
}
|
||||||
|
|
||||||
|
def cmds = []
|
||||||
|
|
||||||
|
state.colorTiles.each({
|
||||||
|
if ( it == color ) {
|
||||||
|
log.debug "Turning ${it} on"
|
||||||
|
cmds << sendEvent(name: it, value: "on${it}", display: True, descriptionText: "${device.displayName} ${color} is 'ON'", isStateChange: true)
|
||||||
|
} else {
|
||||||
|
//log.debug "Turning ${it} off"
|
||||||
|
cmds << sendEvent(name: it, value: "off${it}", displayed: false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
delayBetween(cmds, 2500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rows of buttons
|
||||||
|
def softwhite() { doColorButton("Soft White") }
|
||||||
|
def daylight() { doColorButton("Daylight") }
|
||||||
|
def warmwhite() { doColorButton("Warm White") }
|
||||||
|
|
||||||
|
def red() { doColorButton("Red") }
|
||||||
|
def green() { doColorButton("Green") }
|
||||||
|
def blue() { doColorButton("Blue") }
|
||||||
|
|
||||||
|
def cyan() { doColorButton("Cyan") }
|
||||||
|
def magenta() { doColorButton("Magenta") }
|
||||||
|
def orange() { doColorButton("Orange") }
|
||||||
|
|
||||||
|
def purple() { doColorButton("Purple") }
|
||||||
|
def yellow() { doColorButton("Yellow") }
|
||||||
|
def white() { doColorButton("White") }
|
||||||
|
|
||||||
|
def fireplace() { doColorButton("Fire Place") }
|
||||||
|
def storm() { doColorButton("Storm") }
|
||||||
|
def deepfade() { doColorButton("Deep Fade") }
|
||||||
|
|
||||||
|
def litefade() { doColorButton("Lite Fade") }
|
||||||
|
def police() { doColorButton("Police") }
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Fortrezz Water Valve", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Valve"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x1000", inClusters: "0x25,0x72,0x86,0x71,0x22,0x70"
|
||||||
|
fingerprint deviceId: "0x1006", inClusters: "0x25"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
status "close": "command: 2503, payload: FF"
|
||||||
|
status "open": "command: 2503, payload: 00"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "2001FF": "command: 2503, payload: FF"
|
||||||
|
reply "200100": "command: 2503, payload: 00"
|
||||||
|
}
|
||||||
|
|
||||||
|
// tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("contact", "device.contact", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#53a7c0", nextState:"closing"
|
||||||
|
state "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#e86d13", nextState:"opening"
|
||||||
|
state "opening", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#ffe71e"
|
||||||
|
state "closing", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffe71e"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "contact"
|
||||||
|
details(["contact","refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
log.trace description
|
||||||
|
def result = null
|
||||||
|
def cmd = zwave.parse(description)
|
||||||
|
if (cmd) {
|
||||||
|
result = createEvent(zwaveEvent(cmd))
|
||||||
|
}
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
|
||||||
|
def value = cmd.value ? "closed" : "open"
|
||||||
|
[name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
[:] // Handles all Z-Wave commands we aren't interested in
|
||||||
|
}
|
||||||
|
|
||||||
|
def open() {
|
||||||
|
zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00).format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def close() {
|
||||||
|
zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF).format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||||
|
}
|
||||||
192
devicetypes/smartthings/foscam.src/foscam.groovy
Normal file
192
devicetypes/smartthings/foscam.src/foscam.groovy
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* Foscam
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2014-02-04
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Foscam", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Image Capture"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO:encrypt these settings and make them required:true
|
||||||
|
preferences {
|
||||||
|
input "username", "text", title: "Username", description: "Your Foscam Username", required: false
|
||||||
|
input "password", "password", title: "Password", description: "Your Foscam Password", required: false
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("image", "device.image", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: true) {
|
||||||
|
state "default", label: "", action: "", icon: "st.camera.dropcam-centered", backgroundColor: "#FFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
carouselTile("cameraDetails", "device.image", width: 3, height: 2) { }
|
||||||
|
|
||||||
|
standardTile("take", "device.image", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
|
||||||
|
state "take", label: "Take", action: "Image Capture.take", icon: "st.camera.dropcam", backgroundColor: "#FFFFFF", nextState:"taking"
|
||||||
|
state "taking", label:'Taking', action: "", icon: "st.camera.dropcam", backgroundColor: "#53a7c0"
|
||||||
|
state "image", label: "Take", action: "Image Capture.take", icon: "st.camera.dropcam", backgroundColor: "#FFFFFF", nextState:"taking"
|
||||||
|
}
|
||||||
|
|
||||||
|
/*standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"getDeviceInfo", icon:"st.secondary.refresh"
|
||||||
|
}*/
|
||||||
|
|
||||||
|
main "image"
|
||||||
|
details(["cameraDetails", "take"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse events into attributes
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "Parsing '${description}'"
|
||||||
|
|
||||||
|
def map = stringToMap(description)
|
||||||
|
log.debug map
|
||||||
|
|
||||||
|
def result = []
|
||||||
|
|
||||||
|
if (map.bucket && map.key)
|
||||||
|
{ //got a s3 pointer
|
||||||
|
putImageInS3(map)
|
||||||
|
}
|
||||||
|
else if (map.headers && map.body)
|
||||||
|
{ //got device info response
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO:need to figure out a way to reliable know which end the snapshot should be taken at.
|
||||||
|
Current theory is that 8xxx series cameras are at /snapshot.cgi and 9xxx series are at /cgi-bin/CGIProxy.fcgi
|
||||||
|
*/
|
||||||
|
|
||||||
|
def headerString = new String(map.headers.decodeBase64())
|
||||||
|
if (headerString.contains("404 Not Found")) {
|
||||||
|
state.snapshot = "/snapshot.cgi"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map.body) {
|
||||||
|
def bodyString = new String(map.body.decodeBase64())
|
||||||
|
def body = new XmlSlurper().parseText(bodyString)
|
||||||
|
def productName = body?.productName?.text()
|
||||||
|
if (productName)
|
||||||
|
{
|
||||||
|
log.trace "Got Foscam Product Name: $productName"
|
||||||
|
state.snapshot = "/cgi-bin/CGIProxy.fcgi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def putImageInS3(map) {
|
||||||
|
|
||||||
|
def s3ObjectContent
|
||||||
|
|
||||||
|
try {
|
||||||
|
def imageBytes = getS3Object(map.bucket, map.key + ".jpg")
|
||||||
|
|
||||||
|
if(imageBytes)
|
||||||
|
{
|
||||||
|
s3ObjectContent = imageBytes.getObjectContent()
|
||||||
|
def bytes = new ByteArrayInputStream(s3ObjectContent.bytes)
|
||||||
|
storeImage(getPictureName(), bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e) {
|
||||||
|
log.error e
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
//explicitly close the stream
|
||||||
|
if (s3ObjectContent) { s3ObjectContent.close() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle commands
|
||||||
|
def take() {
|
||||||
|
log.debug "Executing 'take'"
|
||||||
|
//Snapshot uri depends on model number:
|
||||||
|
//because 8 series uses user and 9 series uses usr -
|
||||||
|
//try based on port since issuing a GET with usr to 8 series causes it throw 401 until you reauthorize using basic digest authentication
|
||||||
|
|
||||||
|
def host = getHostAddress()
|
||||||
|
def port = host.split(":")[1]
|
||||||
|
def path = (port == "80") ? "/snapshot.cgi?user=${getUsername()}&pwd=${getPassword()}" : "/cgi-bin/CGIProxy.fcgi?usr=${getUsername()}&pwd=${getPassword()}&cmd=snapPicture2"
|
||||||
|
|
||||||
|
|
||||||
|
def hubAction = new physicalgraph.device.HubAction(
|
||||||
|
method: "GET",
|
||||||
|
path: path,
|
||||||
|
headers: [HOST:host]
|
||||||
|
)
|
||||||
|
hubAction.options = [outputMsgToS3:true]
|
||||||
|
hubAction
|
||||||
|
}
|
||||||
|
|
||||||
|
/*def getDeviceInfo() {
|
||||||
|
log.debug "Executing 'getDeviceInfo'"
|
||||||
|
def path = "/cgi-bin/CGIProxy.fcgi"
|
||||||
|
def hubAction = new physicalgraph.device.HubAction(
|
||||||
|
method: "GET",
|
||||||
|
path: path,
|
||||||
|
headers: [HOST:getHostAddress()],
|
||||||
|
query:[cmd:"getDevInfo", usr:getUsername(), pwd:getPassword()]
|
||||||
|
)
|
||||||
|
}*/
|
||||||
|
|
||||||
|
//helper methods
|
||||||
|
private getPictureName() {
|
||||||
|
def pictureUuid = java.util.UUID.randomUUID().toString().replaceAll('-', '')
|
||||||
|
return device.deviceNetworkId + "_$pictureUuid" + ".jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUsername() {
|
||||||
|
settings.username
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPassword() {
|
||||||
|
settings.password
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer convertHexToInt(hex) {
|
||||||
|
Integer.parseInt(hex,16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private String convertHexToIP(hex) {
|
||||||
|
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHostAddress() {
|
||||||
|
def parts = device.deviceNetworkId.split(":")
|
||||||
|
def ip = convertHexToIP(parts[0])
|
||||||
|
def port = convertHexToInt(parts[1])
|
||||||
|
return ip + ":" + port
|
||||||
|
}
|
||||||
|
|
||||||
|
private hashMD5(String somethingToHash) {
|
||||||
|
java.security.MessageDigest.getInstance("MD5").digest(somethingToHash.getBytes("UTF-8")).encodeHex().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private calcDigestAuth(String method, String uri) {
|
||||||
|
def HA1 = hashMD5("${getUsername}::${getPassword}")
|
||||||
|
def HA2 = hashMD5("${method}:${uri}")
|
||||||
|
def response = hashMD5("${HA1}::::auth:${HA2}")
|
||||||
|
|
||||||
|
'Digest username="'+ getUsername() + '", realm="", nonce="", uri="'+ uri +'", qop=auth, nc=, cnonce="", response="' + response + '", opaque=""'
|
||||||
|
}
|
||||||
379
devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy
Normal file
379
devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
/**
|
||||||
|
* GE Link Bulb
|
||||||
|
*
|
||||||
|
* Copyright 2014 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* Thanks to Chad Monroe @cmonroe and Patrick Stuart @pstuart, and others
|
||||||
|
*
|
||||||
|
******************************************************************************
|
||||||
|
* Changes
|
||||||
|
******************************************************************************
|
||||||
|
*
|
||||||
|
* Change 1: 2014-10-10 (wackford)
|
||||||
|
* Added setLevel event so subscriptions to the event will work
|
||||||
|
* Change 2: 2014-12-10 (jscgs350 using Sticks18's code and effort!)
|
||||||
|
* Modified parse section to properly identify bulb status in the app when manually turned on by a physical switch
|
||||||
|
* Change 3: 2014-12-12 (jscgs350, Sticks18's)
|
||||||
|
* Modified to ensure dimming was smoother, and added fix for dimming below 7
|
||||||
|
* Change 4: 2014-12-14 Part 1 (Sticks18)
|
||||||
|
* Modified to ignore unnecessary level change responses to prevent level skips
|
||||||
|
* Change 5: 2014-12-14 Part 2 (Sticks18, jscgs350)
|
||||||
|
* Modified to clean up trace&debug logging, added new code from @sticks18 for parsing "on/off" to determine if the bulb is manually turned on and immediately update the app
|
||||||
|
* Change 6: 2015-01-02 (Sticks18)
|
||||||
|
* Modified to allow dim rate in Preferences. Added ability to dim during On/Off commands and included this option in Preferences. Defaults are "Normal" and no dim for On/Off.
|
||||||
|
* Change 7: 2015-01-09 (tslagle13)
|
||||||
|
* dimOnOff is was boolean, and switched to enum. Properly update "rampOn" and "rampOff" when refreshed or a polled (dim transition for On/Off commands)
|
||||||
|
* Change 8: 2015-03-06 (Juan Risso)
|
||||||
|
* Slider range from 0..100
|
||||||
|
* Change 9: 2015-03-06 (Juan Risso)
|
||||||
|
* Setlevel -> value to integer (to prevent smartapp calling this function from not working).
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "GE Link Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Polling"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState:"turningOff"
|
||||||
|
state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState:"turningOn"
|
||||||
|
state "turningOn", label:'${name}', action: "switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "turningOff", label:'${name}', action: "switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "level", label: 'Level ${currentValue}%'
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch", "level", "levelSliderControl", "refresh"])
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
|
||||||
|
input("dimRate", "enum", title: "Dim Rate", options: ["Instant", "Normal", "Slow", "Very Slow"], defaultValue: "Normal", required: false, displayDuringSetup: true)
|
||||||
|
input("dimOnOff", "enum", title: "Dim transition for On/Off commands?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: true)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
log.trace description
|
||||||
|
|
||||||
|
if (description?.startsWith("on/off:")) {
|
||||||
|
log.debug "The bulb was sent a command to do something just now..."
|
||||||
|
if (description[-1] == "1") {
|
||||||
|
def result = createEvent(name: "switch", value: "on")
|
||||||
|
log.debug "On command was sent maybe from manually turning on? : Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
} else if (description[-1] == "0") {
|
||||||
|
def result = createEvent(name: "switch", value: "off")
|
||||||
|
log.debug "Off command was sent : Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def msg = zigbee.parse(description)
|
||||||
|
|
||||||
|
if (description?.startsWith("catchall:")) {
|
||||||
|
// log.trace msg
|
||||||
|
// log.trace "data: $msg.data"
|
||||||
|
|
||||||
|
def x = description[-4..-1]
|
||||||
|
// log.debug x
|
||||||
|
|
||||||
|
switch (x)
|
||||||
|
{
|
||||||
|
|
||||||
|
case "0000":
|
||||||
|
|
||||||
|
def result = createEvent(name: "switch", value: "off")
|
||||||
|
log.debug "${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
break
|
||||||
|
|
||||||
|
case "1000":
|
||||||
|
|
||||||
|
def result = createEvent(name: "switch", value: "off")
|
||||||
|
log.debug "${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
break
|
||||||
|
|
||||||
|
case "0100":
|
||||||
|
|
||||||
|
def result = createEvent(name: "switch", value: "on")
|
||||||
|
log.debug "${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
break
|
||||||
|
|
||||||
|
case "1001":
|
||||||
|
|
||||||
|
def result = createEvent(name: "switch", value: "on")
|
||||||
|
log.debug "${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description?.startsWith("read attr")) {
|
||||||
|
|
||||||
|
// log.trace description[27..28]
|
||||||
|
// log.trace description[-2..-1]
|
||||||
|
|
||||||
|
if (description[27..28] == "0A") {
|
||||||
|
|
||||||
|
// log.debug description[-2..-1]
|
||||||
|
def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 )
|
||||||
|
sendEvent( name: "level", value: i )
|
||||||
|
sendEvent( name: "switch.setLevel", value: i) //added to help subscribers
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
|
||||||
|
if (description[-2..-1] == "00" && state.trigger == "setLevel") {
|
||||||
|
// log.debug description[-2..-1]
|
||||||
|
def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 )
|
||||||
|
sendEvent( name: "level", value: i )
|
||||||
|
sendEvent( name: "switch.setLevel", value: i) //added to help subscribers
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description[-2..-1] == state.lvl) {
|
||||||
|
// log.debug description[-2..-1]
|
||||||
|
def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 )
|
||||||
|
sendEvent( name: "level", value: i )
|
||||||
|
sendEvent( name: "switch.setLevel", value: i) //added to help subscribers
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
|
||||||
|
[
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 8 0", "delay 500",
|
||||||
|
"st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}"
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
|
||||||
|
state.dOnOff = "0000"
|
||||||
|
|
||||||
|
if (dimRate) {
|
||||||
|
|
||||||
|
switch (dimRate)
|
||||||
|
{
|
||||||
|
|
||||||
|
case "Instant":
|
||||||
|
|
||||||
|
state.rate = "0000"
|
||||||
|
if (dimOnOff) { state.dOnOff = "0000"}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "Normal":
|
||||||
|
|
||||||
|
state.rate = "1500"
|
||||||
|
if (dimOnOff) { state.dOnOff = "0015"}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "Slow":
|
||||||
|
|
||||||
|
state.rate = "2500"
|
||||||
|
if (dimOnOff) { state.dOnOff = "0025"}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "Very Slow":
|
||||||
|
|
||||||
|
state.rate = "3500"
|
||||||
|
if (dimOnOff) { state.dOnOff = "0035"}
|
||||||
|
break
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
|
||||||
|
state.rate = "1500"
|
||||||
|
state.dOnOff = "0000"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dimOnOff == "Yes"){
|
||||||
|
switch (dimOnOff){
|
||||||
|
case "InstantOnOff":
|
||||||
|
|
||||||
|
state.rate = "0000"
|
||||||
|
if (state.rate == "0000") { state.dOnOff = "0000"}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "NormalOnOff":
|
||||||
|
|
||||||
|
state.rate = "1500"
|
||||||
|
if (state.rate == "1500") { state.dOnOff = "0015"}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "SlowOnOff":
|
||||||
|
|
||||||
|
state.rate = "2500"
|
||||||
|
if (state.rate == "2500") { state.dOnOff = "0025"}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "Very SlowOnOff":
|
||||||
|
|
||||||
|
state.rate = "3500"
|
||||||
|
if (state.rate == "3500") { state.dOnOff = "0035"}
|
||||||
|
break
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
state.dOnOff = "0000"
|
||||||
|
}
|
||||||
|
|
||||||
|
"st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state.dOnOff}}"
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
state.lvl = "00"
|
||||||
|
state.trigger = "on/off"
|
||||||
|
|
||||||
|
// log.debug "on()"
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
state.lvl = "00"
|
||||||
|
state.trigger = "on/off"
|
||||||
|
|
||||||
|
// log.debug "off()"
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
|
||||||
|
[
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 8 0", "delay 500",
|
||||||
|
"st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}"
|
||||||
|
]
|
||||||
|
poll()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
|
||||||
|
def cmds = []
|
||||||
|
value = value as Integer
|
||||||
|
if (value == 0) {
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
cmds << "st cmd 0x${device.deviceNetworkId} 1 8 0 {0000 ${state.rate}}"
|
||||||
|
}
|
||||||
|
else if (device.latestValue("switch") == "off") {
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent(name: "level", value: value)
|
||||||
|
value = (value * 255 / 100)
|
||||||
|
def level = hex(value);
|
||||||
|
|
||||||
|
state.trigger = "setLevel"
|
||||||
|
state.lvl = "${level}"
|
||||||
|
|
||||||
|
if (dimRate) {
|
||||||
|
cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${state.rate}}"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 1500}"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug cmds
|
||||||
|
cmds
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
|
||||||
|
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||||
|
log.debug "Confuguring Reporting and Bindings."
|
||||||
|
def configCmds = [
|
||||||
|
|
||||||
|
//Switch Reporting
|
||||||
|
"zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
|
||||||
|
|
||||||
|
//Level Control Reporting
|
||||||
|
"zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 500",
|
||||||
|
]
|
||||||
|
return configCmds + refresh() // send refresh cmds as part of config
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value, width=2) {
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() < width) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer convertHexToInt(hex) {
|
||||||
|
Integer.parseInt(hex,16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
int i = 0;
|
||||||
|
int j = array.length - 1;
|
||||||
|
byte tmp;
|
||||||
|
while (j > i) {
|
||||||
|
tmp = array[j];
|
||||||
|
array[j] = array[i];
|
||||||
|
array[i] = tmp;
|
||||||
|
j--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* GE/Jasco ZigBee Dimmer
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2015-07-01
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "GE ZigBee Dimmer", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Switch"
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Power Meter"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45852"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45857"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "on": "on/off: 1"
|
||||||
|
status "off": "on/off: 0"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "zcl on-off on": "on/off: 1"
|
||||||
|
reply "zcl on-off off": "on/off: 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "level", label: 'Level ${currentValue}%'
|
||||||
|
}
|
||||||
|
valueTile("power", "device.power", decoration: "flat") {
|
||||||
|
state "power", label:'${currentValue} W'
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
main "switch"
|
||||||
|
details(["switch", "level", "power","levelSliderControl","refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "description is $description"
|
||||||
|
|
||||||
|
def finalResult = isKnownDescription(description)
|
||||||
|
if (finalResult != "false") {
|
||||||
|
log.info finalResult
|
||||||
|
if (finalResult.type == "update") {
|
||||||
|
log.info "$device updates: ${finalResult.value}"
|
||||||
|
}
|
||||||
|
else if (finalResult.type == "power") {
|
||||||
|
def powerValue = (finalResult.value as Integer)/10
|
||||||
|
sendEvent(name: "power", value: powerValue)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10
|
||||||
|
|
||||||
|
power level is an integer. The exact power level with correct units needs to be handled in the device type
|
||||||
|
to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sendEvent(name: finalResult.type, value: finalResult.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||||
|
log.debug parseDescriptionAsMap(description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands to device
|
||||||
|
def zigbeeCommand(cluster, attribute){
|
||||||
|
["st cmd 0x${device.deviceNetworkId} ${endpointId} ${cluster} ${attribute} {}"]
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
zigbeeCommand("6", "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
zigbeeCommand("6", "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
value = value as Integer
|
||||||
|
if (value == 0) {
|
||||||
|
off()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sendEvent(name: "level", value: value)
|
||||||
|
setLevelWithRate(value, "0000") + ["delay 1000"] + on() //value is between 0 to 100; GE does NOT switch on if OFF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
[
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0702 0x0400", "delay 500"
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
onOffConfig() + levelConfig() + powerConfig() + refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private getEndpointId() {
|
||||||
|
new BigInteger(device.endpointId, 16).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value, width=2) {
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() < width) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer convertHexToInt(hex) {
|
||||||
|
Integer.parseInt(hex,16)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Need to reverse array of size 2
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
byte tmp;
|
||||||
|
tmp = array[1];
|
||||||
|
array[1] = array[0];
|
||||||
|
array[0] = tmp;
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseDescriptionAsMap(description) {
|
||||||
|
if (description?.startsWith("read attr -")) {
|
||||||
|
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (description?.startsWith("catchall: ")) {
|
||||||
|
def seg = (description - "catchall: ").split(" ")
|
||||||
|
def zigbeeMap = [:]
|
||||||
|
zigbeeMap += [raw: (description - "catchall: ")]
|
||||||
|
zigbeeMap += [profileId: seg[0]]
|
||||||
|
zigbeeMap += [clusterId: seg[1]]
|
||||||
|
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||||
|
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||||
|
zigbeeMap += [options: seg[4]]
|
||||||
|
zigbeeMap += [messageType: seg[5]]
|
||||||
|
zigbeeMap += [dni: seg[6]]
|
||||||
|
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||||
|
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||||
|
zigbeeMap += [manufacturerId: seg[9]]
|
||||||
|
zigbeeMap += [command: seg[10]]
|
||||||
|
zigbeeMap += [direction: seg[11]]
|
||||||
|
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||||
|
it.join('')
|
||||||
|
} : []]
|
||||||
|
|
||||||
|
zigbeeMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def isKnownDescription(description) {
|
||||||
|
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||||
|
def descMap = parseDescriptionAsMap(description)
|
||||||
|
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||||
|
isDescriptionOnOff(descMap)
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "0008" || descMap.clusterId == "0008"){
|
||||||
|
isDescriptionLevel(descMap)
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "0702" || descMap.clusterId == "0702"){
|
||||||
|
isDescriptionPower(descMap)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(description?.startsWith("on/off:")) {
|
||||||
|
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||||
|
return [type: "switch", value : switchValue]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def isDescriptionOnOff(descMap) {
|
||||||
|
def switchValue = "undefined"
|
||||||
|
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||||
|
value = descMap.value
|
||||||
|
if (value == "01"){
|
||||||
|
switchValue = "on"
|
||||||
|
}
|
||||||
|
else if (value == "00"){
|
||||||
|
switchValue = "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (descMap.clusterId == "0006") {
|
||||||
|
//cluster info from catch all
|
||||||
|
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||||
|
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||||
|
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||||
|
switchValue = "on"
|
||||||
|
}
|
||||||
|
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||||
|
switchValue = "off"
|
||||||
|
}
|
||||||
|
else if(descMap.command=="07"){
|
||||||
|
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (switchValue != "undefined"){
|
||||||
|
return [type: "switch", value : switchValue]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//@return - false or "success" or level [0-100]
|
||||||
|
def isDescriptionLevel(descMap) {
|
||||||
|
def dimmerValue = -1
|
||||||
|
if (descMap.cluster == "0008"){
|
||||||
|
//TODO: the message returned with catchall is command 0B with clusterId 0008. That is just a confirmation message
|
||||||
|
def value = convertHexToInt(descMap.value)
|
||||||
|
dimmerValue = Math.round(value * 100 / 255)
|
||||||
|
if(dimmerValue==0 && value > 0) {
|
||||||
|
dimmerValue = 1 //handling for non-zero hex value less than 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(descMap.clusterId == "0008") {
|
||||||
|
if(descMap.command=="0B"){
|
||||||
|
return [type: "update", value : "level updated successfully"] //device updating the level change was successful. no value sent.
|
||||||
|
}
|
||||||
|
else if(descMap.command=="07"){
|
||||||
|
return [type: "update", value : "level (0008) capability configured successfully"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dimmerValue != -1){
|
||||||
|
return [type: "level", value : dimmerValue]
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def isDescriptionPower(descMap) {
|
||||||
|
def powerValue = "undefined"
|
||||||
|
if (descMap.cluster == "0702") {
|
||||||
|
if (descMap.attrId == "0400") {
|
||||||
|
powerValue = convertHexToInt(descMap.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (descMap.clusterId == "0702") {
|
||||||
|
if(descMap.command=="07"){
|
||||||
|
return [type: "update", value : "power (0702) capability configured successfully"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (powerValue != "undefined"){
|
||||||
|
return [type: "power", value : powerValue]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def onOffConfig() {
|
||||||
|
[
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 6 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl global send-me-a-report 6 0 0x10 0 600 {01}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
//level config for devices with min reporting interval as 5 seconds and reporting interval if no activity as 1hour (3600s)
|
||||||
|
//min level change is 01
|
||||||
|
def levelConfig() {
|
||||||
|
[
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 8 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl global send-me-a-report 8 0 0x20 1 3600 {01}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s)
|
||||||
|
//min change in value is 05
|
||||||
|
def powerConfig() {
|
||||||
|
[
|
||||||
|
//Meter (Power) Reporting
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0702 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl global send-me-a-report 0x0702 0x0400 0x2A 1 600 {05}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevelWithRate(level, rate) {
|
||||||
|
if(rate == null){
|
||||||
|
rate = "0000"
|
||||||
|
}
|
||||||
|
level = convertToHexString(level * 255 / 100) //Converting the 0-100 range to 0-FF range in hex
|
||||||
|
["st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {$level $rate}"]
|
||||||
|
}
|
||||||
|
|
||||||
|
String convertToHexString(value, width=2) {
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() < width) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* GE/Jasco ZigBee Switch
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2015-07-01
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
// Automatically generated. Make future change here.
|
||||||
|
definition (name: "GE ZigBee Switch", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Switch"
|
||||||
|
capability "Power Meter"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "0003, 000A,0019", manufacturer: "Jasco Products", model: "45853"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45856"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "on": "on/off: 1"
|
||||||
|
status "off": "on/off: 0"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "zcl on-off on": "on/off: 1"
|
||||||
|
reply "zcl on-off off": "on/off: 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
valueTile("power", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "power", label:'${currentValue} Watts'
|
||||||
|
}
|
||||||
|
main "switch"
|
||||||
|
details(["switch", "power", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "description is $description"
|
||||||
|
|
||||||
|
def finalResult = isKnownDescription(description)
|
||||||
|
if (finalResult != "false") {
|
||||||
|
log.info finalResult
|
||||||
|
if (finalResult.type == "update") {
|
||||||
|
log.info "$device updates: ${finalResult.value}"
|
||||||
|
}
|
||||||
|
else if (finalResult.type == "power") {
|
||||||
|
def powerValue = (finalResult.value as Integer)/10
|
||||||
|
sendEvent(name: "power", value: powerValue)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10
|
||||||
|
|
||||||
|
power level is an integer. The exact power level with correct units needs to be handled in the device type
|
||||||
|
to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sendEvent(name: finalResult.type, value: finalResult.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||||
|
log.debug parseDescriptionAsMap(description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands to device
|
||||||
|
def zigbeeCommand(cluster, attribute){
|
||||||
|
"st cmd 0x${device.deviceNetworkId} ${endpointId} ${cluster} ${attribute} {}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
zigbeeCommand("6", "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
zigbeeCommand("6", "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
[
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0702 0x0400", "delay 500"
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
onOffConfig() + powerConfig() + refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private getEndpointId() {
|
||||||
|
new BigInteger(device.endpointId, 16).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value, width=2) {
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() < width) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer convertHexToInt(hex) {
|
||||||
|
Integer.parseInt(hex,16)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Need to reverse array of size 2
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
byte tmp;
|
||||||
|
tmp = array[1];
|
||||||
|
array[1] = array[0];
|
||||||
|
array[0] = tmp;
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseDescriptionAsMap(description) {
|
||||||
|
if (description?.startsWith("read attr -")) {
|
||||||
|
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (description?.startsWith("catchall: ")) {
|
||||||
|
def seg = (description - "catchall: ").split(" ")
|
||||||
|
def zigbeeMap = [:]
|
||||||
|
zigbeeMap += [raw: (description - "catchall: ")]
|
||||||
|
zigbeeMap += [profileId: seg[0]]
|
||||||
|
zigbeeMap += [clusterId: seg[1]]
|
||||||
|
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||||
|
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||||
|
zigbeeMap += [options: seg[4]]
|
||||||
|
zigbeeMap += [messageType: seg[5]]
|
||||||
|
zigbeeMap += [dni: seg[6]]
|
||||||
|
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||||
|
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||||
|
zigbeeMap += [manufacturerId: seg[9]]
|
||||||
|
zigbeeMap += [command: seg[10]]
|
||||||
|
zigbeeMap += [direction: seg[11]]
|
||||||
|
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||||
|
it.join('')
|
||||||
|
} : []]
|
||||||
|
|
||||||
|
zigbeeMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def isKnownDescription(description) {
|
||||||
|
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||||
|
def descMap = parseDescriptionAsMap(description)
|
||||||
|
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||||
|
isDescriptionOnOff(descMap)
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "0702" || descMap.clusterId == "0702"){
|
||||||
|
isDescriptionPower(descMap)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(description?.startsWith("on/off:")) {
|
||||||
|
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||||
|
return [type: "switch", value : switchValue]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def isDescriptionOnOff(descMap) {
|
||||||
|
def switchValue = "undefined"
|
||||||
|
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||||
|
value = descMap.value
|
||||||
|
if (value == "01"){
|
||||||
|
switchValue = "on"
|
||||||
|
}
|
||||||
|
else if (value == "00"){
|
||||||
|
switchValue = "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (descMap.clusterId == "0006") {
|
||||||
|
//cluster info from catch all
|
||||||
|
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||||
|
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||||
|
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||||
|
switchValue = "on"
|
||||||
|
}
|
||||||
|
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||||
|
switchValue = "off"
|
||||||
|
}
|
||||||
|
else if(descMap.command=="07"){
|
||||||
|
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (switchValue != "undefined"){
|
||||||
|
return [type: "switch", value : switchValue]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def isDescriptionPower(descMap) {
|
||||||
|
def powerValue = "undefined"
|
||||||
|
if (descMap.cluster == "0702") {
|
||||||
|
if (descMap.attrId == "0400") {
|
||||||
|
powerValue = convertHexToInt(descMap.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (descMap.clusterId == "0702") {
|
||||||
|
if(descMap.command=="07"){
|
||||||
|
return [type: "update", value : "power (0702) capability configured successfully"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (powerValue != "undefined"){
|
||||||
|
return [type: "power", value : powerValue]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def onOffConfig() {
|
||||||
|
[
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 6 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl global send-me-a-report 6 0 0x10 0 600 {01}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s)
|
||||||
|
//min change in value is 05
|
||||||
|
def powerConfig() {
|
||||||
|
[
|
||||||
|
//Meter (Power) Reporting
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0702 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl global send-me-a-report 0x0702 0x0400 0x2A 1 600 {05}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
String convertToHexString(value, width=2) {
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() < width) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Home Energy Meter", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Energy Meter"
|
||||||
|
capability "Power Meter"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
command "reset"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x3103", inClusters: "0x32"
|
||||||
|
fingerprint inClusters: "0x32"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
for (int i = 0; i <= 10000; i += 1000) {
|
||||||
|
status "power ${i} W": new physicalgraph.zwave.Zwave().meterV1.meterReport(
|
||||||
|
scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage()
|
||||||
|
}
|
||||||
|
for (int i = 0; i <= 100; i += 10) {
|
||||||
|
status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport(
|
||||||
|
scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tile definitions
|
||||||
|
tiles {
|
||||||
|
valueTile("power", "device.power", width: 2, height: 2) {
|
||||||
|
state "default", label:'${currentValue} W'
|
||||||
|
}
|
||||||
|
valueTile("energy", "device.energy") {
|
||||||
|
state "default", label:'${currentValue} kWh'
|
||||||
|
}
|
||||||
|
standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'reset kWh', action:"reset"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main (["power","energy"])
|
||||||
|
details(["power","energy", "reset", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
response(refresh())
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def result = null
|
||||||
|
def cmd = zwave.parse(description, [0x31: 1, 0x32: 1, 0x60: 3])
|
||||||
|
if (cmd) {
|
||||||
|
result = createEvent(zwaveEvent(cmd))
|
||||||
|
}
|
||||||
|
log.debug "Parsed '$description' to $result"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.meterv1.MeterReport cmd) {
|
||||||
|
if (cmd.scale == 0) {
|
||||||
|
[name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]
|
||||||
|
} else if (cmd.scale == 1) {
|
||||||
|
[name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
log.debug "$device.displayName: $cmd"
|
||||||
|
[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.meterV2.meterGet(scale: 0).format(),
|
||||||
|
zwave.meterV2.meterGet(scale: 2).format()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.meterV2.meterReset().format(),
|
||||||
|
zwave.meterV2.meterGet(scale: 0).format()
|
||||||
|
], 1000)
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "HomeSeer Multisensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Motion Sensor"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Illuminance Measurement"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Battery"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x2101", inClusters: "0x60,0x31,0x70,0x84,0x85,0x80,0x72,0x77,0x86"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// messages the device returns in response to commands it receives
|
||||||
|
status "motion (basic)" : "command: 2001, payload: FF"
|
||||||
|
status "no motion (basic)" : "command: 2001, payload: 00"
|
||||||
|
status "76.7 deg F" : "command: 6006, payload: 03 31 05 01 2A 02 FF"
|
||||||
|
status "80.6 deg F" : "command: 6006, payload: 03 31 05 01 2A 03 26"
|
||||||
|
status "2 lux" : "command: 6006, payload: 02 31 05 03 01 02"
|
||||||
|
status "80 lux" : "command: 6006, payload: 02 31 05 03 01 50"
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport(
|
||||||
|
batteryLevel: i).incomingMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("motion", "device.motion", width: 2, 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("illuminance", "device.illuminance", inactiveLabel: false) {
|
||||||
|
state "luminosity", label:'${currentValue} ${unit}', unit:"lux"
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["motion", "temperature", "illuminance"])
|
||||||
|
details(["motion", "temperature", "illuminance", "battery"])
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
input "intervalMins", "number", title: "Multisensor report (minutes)", description: "Minutes between temperature/illuminance readings", defaultValue: 20, required: false, displayDuringSetup: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
def result = null
|
||||||
|
def cmd = zwave.parse(description, [0x31: 1, 0x84: 2, 0x60: 1, 0x85: 1, 0x70: 1])
|
||||||
|
if (cmd) {
|
||||||
|
result = zwaveEvent(cmd)
|
||||||
|
}
|
||||||
|
// log.debug "Parsed ${description.inspect()} to ${result.inspect()}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.multiinstancev1.MultiInstanceCmdEncap cmd) {
|
||||||
|
def encapsulated = null
|
||||||
|
if (cmd.respondsTo("encapsulatedCommand")) {
|
||||||
|
encapsulated = cmd.encapsulatedCommand()
|
||||||
|
} else {
|
||||||
|
def hex1 = { n -> String.format("%02X", n) }
|
||||||
|
def sorry = "command: ${hex1(cmd.commandClass)}${hex1(cmd.command)}, payload: " + cmd.parameter.collect{ hex1(it) }.join(" ")
|
||||||
|
encapsulated = zwave.parse(sorry, [0x31: 1, 0x84: 2, 0x60: 1, 0x85: 1, 0x70: 1])
|
||||||
|
}
|
||||||
|
return encapsulated ? zwaveEvent(encapsulated) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) {
|
||||||
|
def results = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)]
|
||||||
|
|
||||||
|
if (state.config) {
|
||||||
|
state.config = false
|
||||||
|
results << response(configure())
|
||||||
|
}
|
||||||
|
def prevBattery = device.currentState("battery")
|
||||||
|
if (!prevBattery || (new Date().time - prevBattery.date.time)/60000 >= 60 * 53) {
|
||||||
|
results << response(zwave.batteryV1.batteryGet().format())
|
||||||
|
}
|
||||||
|
results << response(temperatureGetCmd().format())
|
||||||
|
results << response(illuminanceGetCmd().format())
|
||||||
|
|
||||||
|
results << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format())
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv1.SensorMultilevelReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
switch (cmd.sensorType) {
|
||||||
|
case 1:
|
||||||
|
// temperature
|
||||||
|
def cmdScale = cmd.scale == 1 ? "F" : "C"
|
||||||
|
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
|
||||||
|
map.unit = getTemperatureScale()
|
||||||
|
map.name = "temperature"
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// luminance
|
||||||
|
map.value = cmd.scaledSensorValue.toInteger().toString()
|
||||||
|
map.unit = "lux"
|
||||||
|
map.name = "illuminance"
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
// humidity
|
||||||
|
map.value = cmd.scaledSensorValue.toInteger().toString()
|
||||||
|
map.unit = "%"
|
||||||
|
map.name = "humidity"
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
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!"
|
||||||
|
} else {
|
||||||
|
map.value = cmd.batteryLevel
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) {
|
||||||
|
def map = [:]
|
||||||
|
map.value = cmd.sensorValue ? "active" : "inactive"
|
||||||
|
map.name = "motion"
|
||||||
|
if (map.value == "active") {
|
||||||
|
map.descriptionText = "$device.displayName detected motion"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
map.descriptionText = "$device.displayName motion has stopped"
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||||
|
def map = [:]
|
||||||
|
map.value = cmd.value ? "active" : "inactive"
|
||||||
|
map.name = "motion"
|
||||||
|
if (map.value == "active") {
|
||||||
|
map.descriptionText = "$device.displayName detected motion"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
map.descriptionText = "$device.displayName motion has stopped"
|
||||||
|
}
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
createEvent(displayed: false, descriptionText: "$device.displayName: $cmd")
|
||||||
|
}
|
||||||
|
|
||||||
|
def temperatureGetCmd() {
|
||||||
|
zwave.multiInstanceV1.multiInstanceCmdEncap(instance:3, commandClass:0x31, command:0x04)
|
||||||
|
}
|
||||||
|
|
||||||
|
def illuminanceGetCmd() {
|
||||||
|
zwave.multiInstanceV1.multiInstanceCmdEncap(instance:2, commandClass:0x31, command:0x04)
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
log.debug "Will configure wakeup interval (${60 * (settings.intervalMins ?: 20).toInteger()} seconds)"
|
||||||
|
state.config = true
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
zwave.wakeUpV2.wakeUpIntervalSet(seconds: 60 * (settings.intervalMins ?: 20).toInteger(), nodeid: zwaveHubNodeId).format()
|
||||||
|
}
|
||||||
78
devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy
Normal file
78
devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Hue Bridge
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
*/
|
||||||
|
// for the UI
|
||||||
|
metadata {
|
||||||
|
// Automatically generated. Make future change here.
|
||||||
|
definition (name: "Hue Bridge", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
attribute "serialNumber", "string"
|
||||||
|
attribute "networkAddress", "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
|
||||||
|
state "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#FFFFFF"
|
||||||
|
}
|
||||||
|
valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "default", label:'SN: ${currentValue}'
|
||||||
|
}
|
||||||
|
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
|
||||||
|
}
|
||||||
|
|
||||||
|
main (["icon"])
|
||||||
|
details(["networkAddress","serialNumber"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse events into attributes
|
||||||
|
def parse(description) {
|
||||||
|
log.debug "Parsing '${description}'"
|
||||||
|
def results = []
|
||||||
|
def result = parent.parse(this, description)
|
||||||
|
|
||||||
|
if (result instanceof physicalgraph.device.HubAction){
|
||||||
|
log.trace "HUE BRIDGE HubAction received -- DOES THIS EVER HAPPEN?"
|
||||||
|
results << result
|
||||||
|
} else if (description == "updated") {
|
||||||
|
//do nothing
|
||||||
|
log.trace "HUE BRIDGE was updated"
|
||||||
|
} else {
|
||||||
|
log.trace "HUE BRIDGE, OTHER"
|
||||||
|
def map = description
|
||||||
|
if (description instanceof String) {
|
||||||
|
map = stringToMap(description)
|
||||||
|
}
|
||||||
|
if (map?.name && map?.value) {
|
||||||
|
log.trace "HUE BRIDGE, GENERATING EVENT: $map.name: $map.value"
|
||||||
|
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.trace "HUE BRIDGE, OTHER"
|
||||||
|
def msg = parseLanMessage(description)
|
||||||
|
if (msg.body) {
|
||||||
|
def contentType = msg.headers["Content-Type"]
|
||||||
|
if (contentType?.contains("json")) {
|
||||||
|
def bulbs = new groovy.json.JsonSlurper().parseText(msg.body)
|
||||||
|
if (bulbs.state) {
|
||||||
|
log.warn "NOT PROCESSED: $msg.body"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.debug "HUE BRIDGE, GENERATING BULB LIST EVENT: $bulbs"
|
||||||
|
sendEvent(name: "bulbList", value: device.hub.id, isStateChange: true, data: bulbs, displayed: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (contentType?.contains("xml")) {
|
||||||
|
log.debug "HUE BRIDGE, SWALLOWING BRIDGE DESCRIPTION RESPONSE -- BRIDGE ALREADY PRESENT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results
|
||||||
|
}
|
||||||
166
devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy
Normal file
166
devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
/**
|
||||||
|
* Hue Bulb
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
*/
|
||||||
|
// for the UI
|
||||||
|
metadata {
|
||||||
|
// Automatically generated. Make future change here.
|
||||||
|
definition (name: "Hue Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Color Control"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
command "setAdjustedColor"
|
||||||
|
command "reset"
|
||||||
|
command "refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821"
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"Color Reset", action:"reset", icon:"st.lights.philips.hue-single"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
|
||||||
|
state "color", action:"setAdjustedColor"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "level", label: 'Level ${currentValue}%'
|
||||||
|
}
|
||||||
|
controlTile("saturationSliderControl", "device.saturation", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "saturation", action:"color control.setSaturation"
|
||||||
|
}
|
||||||
|
valueTile("saturation", "device.saturation", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "saturation", label: 'Sat ${currentValue} '
|
||||||
|
}
|
||||||
|
controlTile("hueSliderControl", "device.hue", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "hue", action:"color control.setHue"
|
||||||
|
}
|
||||||
|
valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "hue", label: 'Hue ${currentValue} '
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch", "levelSliderControl", "rgbSelector", "refresh", "reset"])
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse events into attributes
|
||||||
|
def parse(description) {
|
||||||
|
log.debug "parse() - $description"
|
||||||
|
def results = []
|
||||||
|
def map = description
|
||||||
|
if (description instanceof String) {
|
||||||
|
log.debug "Hue Bulb stringToMap - ${map}"
|
||||||
|
map = stringToMap(description)
|
||||||
|
}
|
||||||
|
if (map?.name && map?.value) {
|
||||||
|
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
||||||
|
}
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle commands
|
||||||
|
def on(transition = "4") {
|
||||||
|
log.trace parent.on(this,transition)
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
}
|
||||||
|
|
||||||
|
def off(transition = "4") {
|
||||||
|
log.trace parent.off(this,transition)
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
}
|
||||||
|
|
||||||
|
def nextLevel() {
|
||||||
|
def level = device.latestValue("level") as Integer ?: 0
|
||||||
|
if (level <= 100) {
|
||||||
|
level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
level = 25
|
||||||
|
}
|
||||||
|
setLevel(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(percent) {
|
||||||
|
log.debug "Executing 'setLevel'"
|
||||||
|
parent.setLevel(this, percent)
|
||||||
|
sendEvent(name: "level", value: percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setSaturation(percent) {
|
||||||
|
log.debug "Executing 'setSaturation'"
|
||||||
|
parent.setSaturation(this, percent)
|
||||||
|
sendEvent(name: "saturation", value: percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setHue(percent) {
|
||||||
|
log.debug "Executing 'setHue'"
|
||||||
|
parent.setHue(this, percent)
|
||||||
|
sendEvent(name: "hue", value: percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setColor(value,alert = "none",transition = 4) {
|
||||||
|
log.debug "setColor: ${value}, $this"
|
||||||
|
parent.setColor(this, value, alert, transition)
|
||||||
|
if (value.hue) { sendEvent(name: "hue", value: value.hue)}
|
||||||
|
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)}
|
||||||
|
if (value.hex) { sendEvent(name: "color", value: value.hex)}
|
||||||
|
if (value.level) { sendEvent(name: "level", value: value.level)}
|
||||||
|
if (value.switch) { sendEvent(name: "switch", value: value.switch)}
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset() {
|
||||||
|
log.debug "Executing 'reset'"
|
||||||
|
def value = [level:100, hex:"#90C638", saturation:56, hue:23]
|
||||||
|
setAdjustedColor(value)
|
||||||
|
parent.poll()
|
||||||
|
}
|
||||||
|
|
||||||
|
def setAdjustedColor(value) {
|
||||||
|
if (value) {
|
||||||
|
log.trace "setAdjustedColor: ${value}"
|
||||||
|
def adjusted = value + [:]
|
||||||
|
adjusted.hue = adjustOutgoingHue(value.hue)
|
||||||
|
// Needed because color picker always sends 100
|
||||||
|
adjusted.level = null
|
||||||
|
setColor(adjusted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
log.debug "Executing 'refresh'"
|
||||||
|
parent.manualRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
def adjustOutgoingHue(percent) {
|
||||||
|
def adjusted = percent
|
||||||
|
if (percent > 31) {
|
||||||
|
if (percent < 63.0) {
|
||||||
|
adjusted = percent + (7 * (percent -30 ) / 32)
|
||||||
|
}
|
||||||
|
else if (percent < 73.0) {
|
||||||
|
adjusted = 69 + (5 * (percent - 62) / 10)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
adjusted = percent + (2 * (100 - percent) / 28)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info "percent: $percent, adjusted: $adjusted"
|
||||||
|
adjusted
|
||||||
|
}
|
||||||
79
devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy
Normal file
79
devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* Hue Lux Bulb
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
*/
|
||||||
|
// for the UI
|
||||||
|
metadata {
|
||||||
|
// Automatically generated. Make future change here.
|
||||||
|
definition (name: "Hue Lux Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
command "refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821"
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "level", label: 'Level ${currentValue}%'
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch", "levelSliderControl", "refresh"])
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse events into attributes
|
||||||
|
def parse(description) {
|
||||||
|
log.debug "parse() - $description"
|
||||||
|
def results = []
|
||||||
|
|
||||||
|
def map = description
|
||||||
|
if (description instanceof String) {
|
||||||
|
log.debug "Hue Bulb stringToMap - ${map}"
|
||||||
|
map = stringToMap(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map?.name && map?.value) {
|
||||||
|
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
||||||
|
}
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle commands
|
||||||
|
def on() {
|
||||||
|
parent.on(this)
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
parent.off(this)
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(percent) {
|
||||||
|
log.debug "Executing 'setLevel'"
|
||||||
|
parent.setLevel(this, percent)
|
||||||
|
sendEvent(name: "level", value: percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
log.debug "Executing 'refresh'"
|
||||||
|
parent.manualRefresh()
|
||||||
|
}
|
||||||
91
devicetypes/smartthings/life360-user.src/life360-user.groovy
Normal file
91
devicetypes/smartthings/life360-user.src/life360-user.groovy
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* Life360-User
|
||||||
|
*
|
||||||
|
* Author: jeff
|
||||||
|
* Date: 2013-08-15
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "Life360 User", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Presence Sensor"
|
||||||
|
capability "Sensor"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "present": "presence: 1"
|
||||||
|
status "not present": "presence: 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) {
|
||||||
|
state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#53a7c0")
|
||||||
|
state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ffffff")
|
||||||
|
}
|
||||||
|
|
||||||
|
main "presence"
|
||||||
|
details "presence"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def generatePresenceEvent(boolean present) {
|
||||||
|
log.debug "Here in generatePresenceEvent!"
|
||||||
|
def value = formatValue(present)
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = formatDescriptionText(linkText, present)
|
||||||
|
def handlerName = getState(present)
|
||||||
|
|
||||||
|
def results = [
|
||||||
|
name: "presence",
|
||||||
|
value: value,
|
||||||
|
unit: null,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: handlerName
|
||||||
|
]
|
||||||
|
log.debug "Generating Event: ${results}"
|
||||||
|
sendEvent (results)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setMemberId (String memberId) {
|
||||||
|
log.debug "MemberId = ${memberId}"
|
||||||
|
state.life360MemberId = memberId
|
||||||
|
}
|
||||||
|
|
||||||
|
def getMemberId () {
|
||||||
|
|
||||||
|
log.debug "MemberId = ${state.life360MemberId}"
|
||||||
|
|
||||||
|
return(state.life360MemberId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatValue(boolean present) {
|
||||||
|
if (present)
|
||||||
|
return "present"
|
||||||
|
else
|
||||||
|
return "not present"
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDescriptionText(String linkText, boolean present) {
|
||||||
|
if (present)
|
||||||
|
return "Life360 User $linkText has arrived"
|
||||||
|
else
|
||||||
|
return "Life360 User $linkText has left"
|
||||||
|
}
|
||||||
|
|
||||||
|
private getState(boolean present) {
|
||||||
|
if (present)
|
||||||
|
return "arrived"
|
||||||
|
else
|
||||||
|
return "left"
|
||||||
|
}
|
||||||
61
devicetypes/smartthings/light-sensor.src/light-sensor.groovy
Normal file
61
devicetypes/smartthings/light-sensor.src/light-sensor.groovy
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Light Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Illuminance Measurement"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", deviceId: "0106", inClusters: "0000,0001,0003,0009,0400"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
status "dark": "illuminance: 8"
|
||||||
|
status "light": "illuminance: 300"
|
||||||
|
status "bright": "illuminance: 1000"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
valueTile("illuminance", "device.illuminance", width: 2, height: 2) {
|
||||||
|
state("illuminance", label:'${currentValue}', unit:"lux",
|
||||||
|
backgroundColors:[
|
||||||
|
[value: 9, color: "#767676"],
|
||||||
|
[value: 315, color: "#ffa81e"],
|
||||||
|
[value: 1000, color: "#fbd41b"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
main "illuminance"
|
||||||
|
details "illuminance"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
def result
|
||||||
|
if (description?.startsWith("illuminance: ")) {
|
||||||
|
def raw = description - "illuminance: "
|
||||||
|
if (raw.isNumber()) {
|
||||||
|
result = createEvent(
|
||||||
|
name: "illuminance",
|
||||||
|
value: Math.round(zigbee.lux(raw as Integer)).toString(),
|
||||||
|
unit: "lux"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Logitech Harmony Hub
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Logitech Harmony Hub C2C", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Media Controller"
|
||||||
|
capability "Refresh"
|
||||||
|
|
||||||
|
command "activityoff"
|
||||||
|
command "alloff"
|
||||||
|
command "refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
|
||||||
|
state "default", label: "Harmony", action: "", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#FFFFFF"
|
||||||
|
}
|
||||||
|
valueTile("currentActivity", "device.currentActivity", decoration: "flat", height: 1, width: 3, inactiveLabel: false) {
|
||||||
|
state "default", label:'${currentValue}'
|
||||||
|
}
|
||||||
|
standardTile("huboff", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'End Activity', action:"activityoff", icon:"st.harmony.harmony-hub-icon"
|
||||||
|
}
|
||||||
|
standardTile("alloff", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'All Activities', action:"alloff", icon:"st.secondary.off"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main (["icon"])
|
||||||
|
details(["currentActivity", "huboff", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def startActivity(String activityId) {
|
||||||
|
log.debug "Executing 'Start Activity'"
|
||||||
|
log.trace parent.activity("$device.deviceNetworkId-$activityId","start")
|
||||||
|
}
|
||||||
|
|
||||||
|
def activityoff() {
|
||||||
|
log.debug "Executing 'Activity Off'"
|
||||||
|
log.trace parent.activity(device.deviceNetworkId,"hub")
|
||||||
|
}
|
||||||
|
|
||||||
|
def alloff() {
|
||||||
|
log.debug "Executing 'All Off'"
|
||||||
|
log.trace parent.activity("all","end")
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
log.debug "Executing 'Poll'"
|
||||||
|
log.trace parent.poll()
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
log.debug "Executing 'Refresh'"
|
||||||
|
log.trace parent.poll()
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* MimoLite Garage Door Controller
|
||||||
|
*
|
||||||
|
* Copyright 2014 Todd Wackford
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Note: This device type is based on the work of Jit Jack (cesaldar) as posted on the SmartThings Community website.
|
||||||
|
*
|
||||||
|
* This device type file will configure a Fortrezz MimoLite Wireless Interface/Bridge Module as a Garage Door
|
||||||
|
* Controller. The Garage Door must be physically configured per the following diagram:
|
||||||
|
* "http://www.fortrezz.com/index.php/component/jdownloads/finish/4/17?Itemid=0"
|
||||||
|
* for all functionality to work correctly.
|
||||||
|
*
|
||||||
|
* This device type will also set the atttibute "powered" to "powerOn" or "powerOff" accordingly. This uses
|
||||||
|
* the alarm capability of the MimoLite and the status will be displayed to the user on a secondary tile. User
|
||||||
|
* can subscribe to the status of this atttribute to be notified when power drops out.
|
||||||
|
*
|
||||||
|
* This device type implements a "Configure" action tile which will set the momentary switch timeout to 25ms and
|
||||||
|
* turn on the powerout alarm.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
// Automatically generated. Make future change here.
|
||||||
|
definition (name: "MimoLite Garage Door Controller", namespace: "smartthings", author: "Todd Wackford") {
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Contact Sensor"
|
||||||
|
|
||||||
|
attribute "powered", "string"
|
||||||
|
|
||||||
|
command "on"
|
||||||
|
command "off"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x1000", inClusters: "0x72,0x86,0x71,0x30,0x31,0x35,0x70,0x85,0x25,0x03"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// Simulator stuff
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "doorClosed", label: "Closed", action: "on", icon: "st.doors.garage.garage-closed", backgroundColor: "#79b821"
|
||||||
|
state "doorOpen", label: "Open", action: "on", icon: "st.doors.garage.garage-open", backgroundColor: "#ffa81e"
|
||||||
|
state "doorOpening", label: "Opening", action: "on", icon: "st.doors.garage.garage-opening", backgroundColor: "#ffa81e"
|
||||||
|
state "doorClosing", label: "Closing", action: "on", icon: "st.doors.garage.garage-closing", backgroundColor: "#ffa81e"
|
||||||
|
state "on", label: "Actuate", action: "off", icon: "st.doors.garage.garage-closed", backgroundColor: "#53a7c0"
|
||||||
|
state "off", label: '${name}', action: "on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("contact", "device.contact", inactiveLabel: false) {
|
||||||
|
state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e"
|
||||||
|
state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
standardTile("powered", "device.powered", inactiveLabel: false) {
|
||||||
|
state "powerOn", label: "Power On", icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||||
|
state "powerOff", label: "Power Off", icon: "st.switches.switch.off", backgroundColor: "#ffa81e"
|
||||||
|
}
|
||||||
|
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||||
|
}
|
||||||
|
main (["switch", "contact"])
|
||||||
|
details(["switch", "powered", "refresh", "configure"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "description is: ${description}"
|
||||||
|
|
||||||
|
def result = null
|
||||||
|
def cmd = zwave.parse(description, [0x20: 1, 0x84: 1, 0x30: 1, 0x70: 1])
|
||||||
|
|
||||||
|
log.debug "command value is: $cmd.CMD"
|
||||||
|
|
||||||
|
if (cmd.CMD == "7105") { //Mimo sent a power loss report
|
||||||
|
log.debug "Device lost power"
|
||||||
|
sendEvent(name: "powered", value: "powerOff", descriptionText: "$device.displayName lost power")
|
||||||
|
} else {
|
||||||
|
sendEvent(name: "powered", value: "powerOn", descriptionText: "$device.displayName regained power")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd) {
|
||||||
|
result = createEvent(zwaveEvent(cmd))
|
||||||
|
}
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def sensorValueEvent(Short value) {
|
||||||
|
if (value) {
|
||||||
|
sendEvent(name: "contact", value: "open")
|
||||||
|
sendEvent(name: "switch", value: "doorOpen")
|
||||||
|
} else {
|
||||||
|
sendEvent(name: "contact", value: "closed")
|
||||||
|
sendEvent(name: "switch", value: "doorClosed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||||
|
[name: "switch", value: cmd.value ? "on" : "off", type: "physical"]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd)
|
||||||
|
{
|
||||||
|
sensorValueEvent(cmd.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
|
||||||
|
def doorState = device.currentValue('contact')
|
||||||
|
if ( doorState == "closed")
|
||||||
|
[name: "switch", value: cmd.value ? "on" : "doorOpening", type: "digital"]
|
||||||
|
else
|
||||||
|
[name: "switch", value: cmd.value ? "on" : "doorClosing", type: "digital"]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd)
|
||||||
|
{
|
||||||
|
sensorValueEvent(cmd.sensorValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.alarmv1.AlarmReport cmd)
|
||||||
|
{
|
||||||
|
log.debug "We lost power" //we caught this up in the parse method. This method not used.
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
// Handles all Z-Wave commands we aren't interested in
|
||||||
|
[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
log.debug "Configuring...." //setting up to monitor power alarm and actuator duration
|
||||||
|
delayBetween([
|
||||||
|
zwave.associationV1.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format(),
|
||||||
|
zwave.configurationV1.configurationSet(configurationValue: [25], parameterNumber: 11, size: 1).format(),
|
||||||
|
zwave.configurationV1.configurationGet(parameterNumber: 11).format()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.basicV1.basicSet(value: 0xFF).format(),
|
||||||
|
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
delayBetween([
|
||||||
|
zwave.basicV1.basicSet(value: 0x00).format(),
|
||||||
|
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Mobile Presence", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Presence Sensor"
|
||||||
|
capability "Sensor"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "present": "presence: 1"
|
||||||
|
status "not present": "presence: 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) {
|
||||||
|
state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#53a7c0")
|
||||||
|
state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ffffff")
|
||||||
|
}
|
||||||
|
main "presence"
|
||||||
|
details "presence"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def name = parseName(description)
|
||||||
|
def value = parseValue(description)
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = parseDescriptionText(linkText, value, description)
|
||||||
|
def handlerName = getState(value)
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
def results = [
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: null,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: handlerName,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: displayed(description, isStateChange)
|
||||||
|
]
|
||||||
|
log.debug "Parse returned $results.descriptionText"
|
||||||
|
return results
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseName(String description) {
|
||||||
|
if (description?.startsWith("presence: ")) {
|
||||||
|
return "presence"
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseValue(String description) {
|
||||||
|
switch(description) {
|
||||||
|
case "presence: 1": return "present"
|
||||||
|
case "presence: 0": return "not present"
|
||||||
|
default: return description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseDescriptionText(String linkText, String value, String description) {
|
||||||
|
switch(value) {
|
||||||
|
case "present": return "$linkText has arrived"
|
||||||
|
case "not present": return "$linkText has left"
|
||||||
|
default: return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getState(String value) {
|
||||||
|
switch(value) {
|
||||||
|
case "present": return "arrived"
|
||||||
|
case "not present": return "left"
|
||||||
|
default: return value
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* Momentary Button Tile
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
*
|
||||||
|
* Date: 2013-05-01
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Momentary Button Tile", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Momentary"
|
||||||
|
capability "Sensor"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "off", label: 'Push', action: "momentary.push", backgroundColor: "#ffffff", nextState: "on"
|
||||||
|
state "on", label: 'Push', action: "momentary.push", backgroundColor: "#53a7c0"
|
||||||
|
}
|
||||||
|
main "switch"
|
||||||
|
details "switch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
}
|
||||||
|
|
||||||
|
def push() {
|
||||||
|
sendEvent(name: "switch", value: "on", isStateChange: true, display: false)
|
||||||
|
sendEvent(name: "switch", value: "off", isStateChange: true, display: false)
|
||||||
|
sendEvent(name: "momentary", value: "pushed", isStateChange: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
push()
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
push()
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Motion Detector", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Motion Sensor"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0009,0500"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
status "active": "zone report :: type: 19 value: 0031"
|
||||||
|
status "inactive": "zone report :: type: 19 value: 0030"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("motion", "device.motion", width: 2, 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
main "motion"
|
||||||
|
details "motion"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
def name = null
|
||||||
|
def value = description
|
||||||
|
def descriptionText = null
|
||||||
|
if (zigbee.isZoneType19(description)) {
|
||||||
|
name = "motion"
|
||||||
|
def isActive = zigbee.translateStatusZoneType19(description)
|
||||||
|
value = isActive ? "active" : "inactive"
|
||||||
|
descriptionText = isActive ? "${device.displayName} detected motion" : "${device.displayName} motion has stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
def result = createEvent(
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
descriptionText: descriptionText
|
||||||
|
)
|
||||||
|
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
/**
|
||||||
|
* NYCE Motion Sensor
|
||||||
|
*
|
||||||
|
* Copyright 2014 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "NYCE Motion Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Motion Sensor"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Battery"
|
||||||
|
capability "Refresh"
|
||||||
|
|
||||||
|
command "enrollResponse"
|
||||||
|
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3041"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3043"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("motion", "device.motion", width: 2, 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("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery'
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main (["motion"])
|
||||||
|
details(["motion","battery","refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "description: $description"
|
||||||
|
|
||||||
|
Map map = [:]
|
||||||
|
if (description?.startsWith('catchall:')) {
|
||||||
|
map = parseCatchAllMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('read attr -')) {
|
||||||
|
map = parseReportAttributeMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('zone status')) {
|
||||||
|
map = parseIasMessage(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug "Parse returned $map"
|
||||||
|
def result = map ? createEvent(map) : null
|
||||||
|
|
||||||
|
if (description?.startsWith('enroll request')) {
|
||||||
|
List cmds = enrollResponse()
|
||||||
|
log.debug "enroll response: ${cmds}"
|
||||||
|
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseCatchAllMessage(String description) {
|
||||||
|
Map resultMap = [:]
|
||||||
|
def cluster = zigbee.parse(description)
|
||||||
|
if (shouldProcessMessage(cluster)) {
|
||||||
|
switch(cluster.clusterId) {
|
||||||
|
case 0x0001:
|
||||||
|
log.debug 'Battery'
|
||||||
|
resultMap.name = 'battery'
|
||||||
|
resultMap.value = getBatteryPercentage(cluster.data.last())
|
||||||
|
break
|
||||||
|
|
||||||
|
case 0x0406:
|
||||||
|
log.debug 'motion'
|
||||||
|
resultMap.name = 'motion'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldProcessMessage(cluster) {
|
||||||
|
// 0x0B is default response indicating message got through
|
||||||
|
// 0x07 is bind message
|
||||||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
|
cluster.command == 0x0B ||
|
||||||
|
cluster.command == 0x07 ||
|
||||||
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
|
return !ignoredMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getBatteryPercentage(int value) {
|
||||||
|
def minVolts = 2.1
|
||||||
|
def maxVolts = 3.0
|
||||||
|
def volts = value / 10
|
||||||
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
|
if(pct>1)
|
||||||
|
pct=1 //if battery is overrated, decreasing battery value to 100%
|
||||||
|
return (int) pct * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseDescriptionAsMap(description) {
|
||||||
|
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseReportAttributeMessage(String description) {
|
||||||
|
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
log.debug "Desc Map: $descMap"
|
||||||
|
|
||||||
|
Map resultMap = [:]
|
||||||
|
if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||||
|
log.debug "Battery"
|
||||||
|
resultMap.name = "battery"
|
||||||
|
resultMap.value = getBatteryPercentage(Integer.parseInt(descMap.value, 16))
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
|
||||||
|
log.debug "motion"
|
||||||
|
resultMap.name = "motion"
|
||||||
|
resultMap.value = descMap.value.endsWith("01") ? "active" : "inactive"
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Map parseIasMessage(String description) {
|
||||||
|
List parsedMsg = description.split(' ')
|
||||||
|
String msgCode = parsedMsg[2]
|
||||||
|
|
||||||
|
Map resultMap = [:]
|
||||||
|
switch(msgCode) {
|
||||||
|
case '0x0030': // Closed/No Motion/Dry
|
||||||
|
log.debug 'no motion'
|
||||||
|
resultMap.name = 'motion'
|
||||||
|
resultMap.value = 'inactive'
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0032': // Open/Motion/Wet
|
||||||
|
log.debug 'motion'
|
||||||
|
resultMap.name = 'motion'
|
||||||
|
resultMap.value = 'active'
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0032': // Tamper Alarm
|
||||||
|
log.debug 'motion with tamper alarm'
|
||||||
|
resultMap.name = 'motion'
|
||||||
|
resultMap.value = 'active'
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0033': // Battery Alarm
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0034': // Supervision Report
|
||||||
|
log.debug 'no motion with tamper alarm'
|
||||||
|
resultMap.name = 'motion'
|
||||||
|
resultMap.value = 'inactive'
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0035': // Restore Report
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0036': // Trouble/Failure
|
||||||
|
log.debug 'motion with failure alarm'
|
||||||
|
resultMap.name = 'motion'
|
||||||
|
resultMap.value = 'active'
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0038': // Test Mode
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh()
|
||||||
|
{
|
||||||
|
log.debug "refresh called"
|
||||||
|
[
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
|
||||||
|
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||||
|
log.debug "Confuguring Reporting, IAS CIE, and Bindings."
|
||||||
|
def configCmds = [
|
||||||
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
"zcl global send-me-a-report 1 0x20 0x20 0x3600 0x3600 {01}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x001 {${device.zigbeeId}} {}", "delay 1500",
|
||||||
|
|
||||||
|
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
]
|
||||||
|
return configCmds + refresh() + enrollResponse() // send refresh cmds as part of config
|
||||||
|
}
|
||||||
|
|
||||||
|
def enrollResponse() {
|
||||||
|
log.debug "Sending enroll response"
|
||||||
|
[
|
||||||
|
|
||||||
|
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1"
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value) {
|
||||||
|
new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
int i = 0;
|
||||||
|
int j = array.length - 1;
|
||||||
|
byte tmp;
|
||||||
|
while (j > i) {
|
||||||
|
tmp = array[j];
|
||||||
|
array[j] = array[i];
|
||||||
|
array[i] = tmp;
|
||||||
|
j--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
/**
|
||||||
|
* NYCE Open/Close Sensor
|
||||||
|
*
|
||||||
|
* Copyright 2015 NYCE Sensors Inc.
|
||||||
|
*
|
||||||
|
* 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: "NYCE Open/Closed Sensor", namespace: "smartthings", author: "NYCE") {
|
||||||
|
capability "Battery"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Contact Sensor"
|
||||||
|
capability "Refresh"
|
||||||
|
|
||||||
|
command "enrollResponse"
|
||||||
|
|
||||||
|
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("contact", "device.contact", width: 2, height: 2) {
|
||||||
|
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||||
|
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main (["contact"])
|
||||||
|
details(["contact","battery","refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
Map map = [:]
|
||||||
|
|
||||||
|
List listMap = []
|
||||||
|
List listResult = []
|
||||||
|
|
||||||
|
log.debug "parse: Parse message: ${description}"
|
||||||
|
|
||||||
|
if (description?.startsWith("enroll request")) {
|
||||||
|
List cmds = enrollResponse()
|
||||||
|
|
||||||
|
log.debug "parse: enrollResponse() ${cmds}"
|
||||||
|
listResult = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (description?.startsWith("zone status")) {
|
||||||
|
listMap = parseIasMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith("read attr -")) {
|
||||||
|
map = parseReportAttributeMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith("catchall:")) {
|
||||||
|
map = parseCatchAllMessage(description)
|
||||||
|
}
|
||||||
|
// this condition is temperary to accomodate some unknown issue caused by SmartThings
|
||||||
|
// once they fix the bug, this condition is not needed
|
||||||
|
// The issue is most of the time when a device is removed thru the app, it takes couple
|
||||||
|
// times to pair again successfully
|
||||||
|
else if (description?.startsWith("updated")) {
|
||||||
|
List cmds = configure()
|
||||||
|
listResult = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create events from map or list of maps, whichever was returned
|
||||||
|
if (listMap) {
|
||||||
|
for (msg in listMap) {
|
||||||
|
listResult << createEvent(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (map) {
|
||||||
|
listResult << createEvent(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug "parse: listResult ${listResult}"
|
||||||
|
return listResult
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseCatchAllMessage(String description) {
|
||||||
|
Map resultMap = [:]
|
||||||
|
def cluster = zigbee.parse(description)
|
||||||
|
|
||||||
|
if (shouldProcessMessage(cluster)) {
|
||||||
|
def msgStatus = cluster.data[2]
|
||||||
|
|
||||||
|
log.debug "parseCatchAllMessage: msgStatus: ${msgStatus}"
|
||||||
|
if (msgStatus == 0) {
|
||||||
|
switch(cluster.clusterId) {
|
||||||
|
case 0x0001:
|
||||||
|
log.debug 'Battery'
|
||||||
|
resultMap.name = 'battery'
|
||||||
|
log.info "in parse catch all"
|
||||||
|
log.debug "battery value: ${cluster.data.last()}"
|
||||||
|
resultMap.value = getBatteryPercentage(cluster.data.last())
|
||||||
|
break
|
||||||
|
case 0x0402: // temperature cluster
|
||||||
|
if (cluster.command == 0x01) {
|
||||||
|
if(cluster.data[3] == 0x29) {
|
||||||
|
def tempC = Integer.parseInt(cluster.data[-2..-1].reverse().collect{cluster.hex1(it)}.join(), 16) / 100
|
||||||
|
resultMap = getTemperatureResult(getConvertedTemperature(tempC))
|
||||||
|
log.debug "parseCatchAllMessage: Temp resultMap: ${resultMap}"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.debug "parseCatchAllMessage: Temperature cluster Wrong data type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.debug "parseCatchAllMessage: Unhandled Temperature cluster command ${cluster.command}"
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 0x0405: // humidity cluster
|
||||||
|
if (cluster.command == 0x01) {
|
||||||
|
if(cluster.data[3] == 0x21) {
|
||||||
|
def hum = Integer.parseInt(cluster.data[-2..-1].reverse().collect{cluster.hex1(it)}.join(), 16) / 100
|
||||||
|
resultMap = getHumidityResult(hum)
|
||||||
|
log.debug "parseCatchAllMessage: Hum resultMap: ${resultMap}"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.debug "parseCatchAllMessage: Humidity cluster wrong data type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.debug "parseCatchAllMessage: Unhandled Humidity cluster command ${cluster.command}"
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.debug "parseCatchAllMessage: Message error code: Error code: ${msgStatus} ClusterID: ${cluster.clusterId} Command: ${cluster.command}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getBatteryPercentage(int value) {
|
||||||
|
def minVolts = 2.3
|
||||||
|
def maxVolts = 3.1
|
||||||
|
def volts = value / 10
|
||||||
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
|
|
||||||
|
//for battery that may have a higher voltage than 3.1V
|
||||||
|
if( pct > 1 )
|
||||||
|
{
|
||||||
|
pct = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
//the device actual shut off voltage is 2.25. When it drops to 2.3, there
|
||||||
|
//is actually still 0.05V, which is about 6% of juice left.
|
||||||
|
//setting the percentage to 6% so a battery low warning is issued
|
||||||
|
if( pct <= 0 )
|
||||||
|
{
|
||||||
|
pct = 0.06
|
||||||
|
}
|
||||||
|
return (int) pct * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldProcessMessage(cluster) {
|
||||||
|
// 0x0B is default response indicating message got through
|
||||||
|
// 0x07 is bind message
|
||||||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
|
cluster.command == 0x0B ||
|
||||||
|
cluster.command == 0x07 ||
|
||||||
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
|
|
||||||
|
return !ignoredMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseReportAttributeMessage(String description) {
|
||||||
|
Map descMap = (description - "read attr - ").split(",").inject([:]) {
|
||||||
|
map, param -> def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
Map resultMap = [:]
|
||||||
|
|
||||||
|
log.debug "parseReportAttributeMessage: descMap ${descMap}"
|
||||||
|
|
||||||
|
switch(descMap.cluster) {
|
||||||
|
case "0001":
|
||||||
|
log.debug 'Battery'
|
||||||
|
resultMap.name = 'battery'
|
||||||
|
resultMap.value = getBatteryPercentage(convertHexToInt(descMap.value))
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
log.info descMap.cluster
|
||||||
|
log.info "cluster1"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseIasMessage(String description) {
|
||||||
|
List parsedMsg = description.split(" ")
|
||||||
|
String msgCode = parsedMsg[2]
|
||||||
|
|
||||||
|
List resultListMap = []
|
||||||
|
Map resultMap_battery = [:]
|
||||||
|
Map resultMap_battery_state = [:]
|
||||||
|
Map resultMap_sensor = [:]
|
||||||
|
|
||||||
|
// Relevant bit field definitions from ZigBee spec
|
||||||
|
def BATTERY_BIT = ( 1 << 3 )
|
||||||
|
def TROUBLE_BIT = ( 1 << 6 )
|
||||||
|
def SENSOR_BIT = ( 1 << 0 ) // it's ALARM1 bit from the ZCL spec
|
||||||
|
|
||||||
|
// Convert hex string to integer
|
||||||
|
def zoneStatus = Integer.parseInt(msgCode[-4..-1],16)
|
||||||
|
|
||||||
|
log.debug "parseIasMessage: zoneStatus: ${zoneStatus}"
|
||||||
|
|
||||||
|
// Check each relevant bit, create map for it, and add to list
|
||||||
|
log.debug "parseIasMessage: Battery Status ${zoneStatus & BATTERY_BIT}"
|
||||||
|
log.debug "parseIasMessage: Trouble Status ${zoneStatus & TROUBLE_BIT}"
|
||||||
|
log.debug "parseIasMessage: Sensor Status ${zoneStatus & SENSOR_BIT}"
|
||||||
|
|
||||||
|
/* Comment out this path to check the battery state to avoid overwriting the
|
||||||
|
battery value (Change log #2), but keep these conditions for later use
|
||||||
|
resultMap_battery_state.name = "battery_state"
|
||||||
|
if (zoneStatus & TROUBLE_BIT) {
|
||||||
|
resultMap_battery_state.value = "failed"
|
||||||
|
|
||||||
|
resultMap_battery.name = "battery"
|
||||||
|
resultMap_battery.value = 0
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (zoneStatus & BATTERY_BIT) {
|
||||||
|
resultMap_battery_state.value = "low"
|
||||||
|
|
||||||
|
// to generate low battery notification by the platform
|
||||||
|
resultMap_battery.name = "battery"
|
||||||
|
resultMap_battery.value = 15
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resultMap_battery_state.value = "ok"
|
||||||
|
|
||||||
|
// to clear the low battery state stored in the platform
|
||||||
|
// otherwise, there is no notification sent again
|
||||||
|
resultMap_battery.name = "battery"
|
||||||
|
resultMap_battery.value = 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
resultMap_sensor.name = "contact"
|
||||||
|
resultMap_sensor.value = (zoneStatus & SENSOR_BIT) ? "open" : "closed"
|
||||||
|
|
||||||
|
resultListMap << resultMap_battery_state
|
||||||
|
resultListMap << resultMap_battery
|
||||||
|
resultListMap << resultMap_sensor
|
||||||
|
|
||||||
|
return resultListMap
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||||
|
|
||||||
|
def configCmds = [
|
||||||
|
//battery reporting and heartbeat
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500",
|
||||||
|
|
||||||
|
|
||||||
|
// Writes CIE attribute on end device to direct reports to the hub's EUID
|
||||||
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||||
|
]
|
||||||
|
|
||||||
|
log.debug "configure: Write IAS CIE"
|
||||||
|
return configCmds
|
||||||
|
}
|
||||||
|
|
||||||
|
def enrollResponse() {
|
||||||
|
[
|
||||||
|
// Enrolling device into the IAS Zone
|
||||||
|
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value) {
|
||||||
|
new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
int i = 0;
|
||||||
|
int j = array.length - 1;
|
||||||
|
byte tmp;
|
||||||
|
|
||||||
|
while (j > i) {
|
||||||
|
tmp = array[j];
|
||||||
|
array[j] = array[i];
|
||||||
|
array[i] = tmp;
|
||||||
|
j--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEndpointId() {
|
||||||
|
new BigInteger(device.endpointId, 16).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer convertHexToInt(hex) {
|
||||||
|
Integer.parseInt(hex,16)
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
log.debug "Refreshing Battery"
|
||||||
|
[
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 1 0x20", "delay 200"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* On/Off Button Tile
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
*
|
||||||
|
* Date: 2013-05-01
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "On/Off Button Tile", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Sensor"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("button", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "off", label: 'Off', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "on"
|
||||||
|
state "on", label: 'On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "off"
|
||||||
|
}
|
||||||
|
main "button"
|
||||||
|
details "button"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "On/Off Shield", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Sensor"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulator metadata
|
||||||
|
simulator {
|
||||||
|
status "on": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A6F6E"
|
||||||
|
status "off": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A6F6666"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "raw 0x0 { 00 00 0a 0a 6f 6e }": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A6F6E"
|
||||||
|
reply "raw 0x0 { 00 00 0a 0a 6f 66 66 }": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A6F6666"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true) {
|
||||||
|
state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||||
|
state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "switch"
|
||||||
|
details "switch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
def value = zigbee.parse(description)?.text
|
||||||
|
def name = value in ["on","off"] ? "switch" : null
|
||||||
|
def result = createEvent(name: name, value: value)
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands sent to the device
|
||||||
|
def on() {
|
||||||
|
zigbee.smartShield(text: "on").format()
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
zigbee.smartShield(text: "off").format()
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Contact Sensor"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0009,0500", outClusters: "0000"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "open": "zone report :: type: 19 value: 0031"
|
||||||
|
status "closed": "zone report :: type: 19 value: 0030"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("contact", "device.contact", width: 2, height: 2) {
|
||||||
|
state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e"
|
||||||
|
state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "contact"
|
||||||
|
details "contact"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
def name = null
|
||||||
|
def value = description
|
||||||
|
if (zigbee.isZoneType19(description)) {
|
||||||
|
name = "contact"
|
||||||
|
value = zigbee.translateStatusZoneType19(description) ? "open" : "closed"
|
||||||
|
}
|
||||||
|
|
||||||
|
def result = createEvent(name: name, value: value)
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
/*
|
||||||
|
Osram Lightify Gardenspot Mini RGB
|
||||||
|
|
||||||
|
Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling
|
||||||
|
that issue by using state variables
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "OSRAM LIGHTIFY Gardenspot mini RGB", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
|
capability "Color Temperature"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Color Control"
|
||||||
|
|
||||||
|
attribute "colorName", "string"
|
||||||
|
|
||||||
|
command "setAdjustedColor"
|
||||||
|
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "on": "on/off: 1"
|
||||||
|
status "off": "on/off: 0"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "zcl on-off on": "on/off: 1"
|
||||||
|
reply "zcl on-off off": "on/off: 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821"
|
||||||
|
state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
|
||||||
|
state "color", action:"setAdjustedColor"
|
||||||
|
}
|
||||||
|
valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "colorName", label: '${currentValue}'
|
||||||
|
}
|
||||||
|
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "level", label: 'Level ${currentValue}%'
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch", "refresh", "colorName", "levelSliderControl", "level", "rgbSelector"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
//log.info "description is $description"
|
||||||
|
if (description?.startsWith("catchall:")) {
|
||||||
|
if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1"))
|
||||||
|
{
|
||||||
|
def result = createEvent(name: "switch", value: "on")
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0"))
|
||||||
|
{
|
||||||
|
if(!(description?.startsWith("catchall: 0104 0300"))){
|
||||||
|
def result = createEvent(name: "switch", value: "off")
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (description?.startsWith("read attr -")) {
|
||||||
|
def descMap = parseDescriptionAsMap(description)
|
||||||
|
log.trace "descMap : $descMap"
|
||||||
|
|
||||||
|
if (descMap.cluster == "0300") {
|
||||||
|
if(descMap.attrId == "0000"){ //Hue Attribute
|
||||||
|
def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 360)
|
||||||
|
log.debug "Hue value returned is $hueValue"
|
||||||
|
sendEvent(name: "hue", value: hueValue, displayed:false)
|
||||||
|
}
|
||||||
|
else if(descMap.attrId == "0001"){ //Saturation Attribute
|
||||||
|
def saturationValue = Math.round(convertHexToInt(descMap.value) / 255 * 100)
|
||||||
|
log.debug "Saturation from refresh is $saturationValue"
|
||||||
|
sendEvent(name: "saturation", value: saturationValue, displayed:false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(descMap.cluster == "0008"){
|
||||||
|
def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255)
|
||||||
|
log.debug "dimmer value is $dimmerValue"
|
||||||
|
sendEvent(name: "level", value: dimmerValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
def name = description?.startsWith("on/off: ") ? "switch" : null
|
||||||
|
def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null
|
||||||
|
def result = createEvent(name: name, value: value)
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
log.debug "on()"
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
setLevel(state?.levelValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zigbeeOff() {
|
||||||
|
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
log.debug "off()"
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
zigbeeOff()
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
[
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 1"
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
state.levelValue = 100
|
||||||
|
log.debug "Configuring Reporting and Bindings."
|
||||||
|
def configCmds = [
|
||||||
|
|
||||||
|
//Switch Reporting
|
||||||
|
"zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500",
|
||||||
|
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000",
|
||||||
|
|
||||||
|
//Level Control Reporting
|
||||||
|
"zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500",
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500"
|
||||||
|
]
|
||||||
|
return configCmds + refresh() // send refresh cmds as part of config
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseDescriptionAsMap(description) {
|
||||||
|
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll(){
|
||||||
|
log.debug "Poll is calling refresh"
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
def zigbeeSetLevel(level) {
|
||||||
|
"st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
state.levelValue = (value==null) ? 100 : value
|
||||||
|
log.trace "setLevel($value)"
|
||||||
|
def cmds = []
|
||||||
|
|
||||||
|
if (value == 0) {
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
cmds << zigbeeOff()
|
||||||
|
}
|
||||||
|
else if (device.latestValue("switch") == "off") {
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent(name: "level", value: state.levelValue)
|
||||||
|
def level = hex(state.levelValue * 255 / 100)
|
||||||
|
cmds << zigbeeSetLevel(level)
|
||||||
|
|
||||||
|
//log.debug cmds
|
||||||
|
cmds
|
||||||
|
}
|
||||||
|
|
||||||
|
//input Hue Integer values; returns color name for saturation 100%
|
||||||
|
private getColorName(hueValue){
|
||||||
|
if(hueValue>360 || hueValue<0)
|
||||||
|
return
|
||||||
|
|
||||||
|
hueValue = Math.round(hueValue / 100 * 360)
|
||||||
|
|
||||||
|
log.debug "hue value is $hueValue"
|
||||||
|
|
||||||
|
def colorName = "Color Mode"
|
||||||
|
if(hueValue>=0 && hueValue <= 4){
|
||||||
|
colorName = "Red"
|
||||||
|
}
|
||||||
|
else if (hueValue>=5 && hueValue <=21 ){
|
||||||
|
colorName = "Brick Red"
|
||||||
|
}
|
||||||
|
else if (hueValue>=22 && hueValue <=30 ){
|
||||||
|
colorName = "Safety Orange"
|
||||||
|
}
|
||||||
|
else if (hueValue>=31 && hueValue <=40 ){
|
||||||
|
colorName = "Dark Orange"
|
||||||
|
}
|
||||||
|
else if (hueValue>=41 && hueValue <=49 ){
|
||||||
|
colorName = "Amber"
|
||||||
|
}
|
||||||
|
else if (hueValue>=50 && hueValue <=56 ){
|
||||||
|
colorName = "Gold"
|
||||||
|
}
|
||||||
|
else if (hueValue>=57 && hueValue <=65 ){
|
||||||
|
colorName = "Yellow"
|
||||||
|
}
|
||||||
|
else if (hueValue>=66 && hueValue <=83 ){
|
||||||
|
colorName = "Electric Lime"
|
||||||
|
}
|
||||||
|
else if (hueValue>=84 && hueValue <=93 ){
|
||||||
|
colorName = "Lawn Green"
|
||||||
|
}
|
||||||
|
else if (hueValue>=94 && hueValue <=112 ){
|
||||||
|
colorName = "Bright Green"
|
||||||
|
}
|
||||||
|
else if (hueValue>=113 && hueValue <=135 ){
|
||||||
|
colorName = "Lime"
|
||||||
|
}
|
||||||
|
else if (hueValue>=136 && hueValue <=166 ){
|
||||||
|
colorName = "Spring Green"
|
||||||
|
}
|
||||||
|
else if (hueValue>=167 && hueValue <=171 ){
|
||||||
|
colorName = "Turquoise"
|
||||||
|
}
|
||||||
|
else if (hueValue>=172 && hueValue <=187 ){
|
||||||
|
colorName = "Aqua"
|
||||||
|
}
|
||||||
|
else if (hueValue>=188 && hueValue <=203 ){
|
||||||
|
colorName = "Sky Blue"
|
||||||
|
}
|
||||||
|
else if (hueValue>=204 && hueValue <=217 ){
|
||||||
|
colorName = "Dodger Blue"
|
||||||
|
}
|
||||||
|
else if (hueValue>=218 && hueValue <=223 ){
|
||||||
|
colorName = "Navy Blue"
|
||||||
|
}
|
||||||
|
else if (hueValue>=224 && hueValue <=251 ){
|
||||||
|
colorName = "Blue"
|
||||||
|
}
|
||||||
|
else if (hueValue>=252 && hueValue <=256 ){
|
||||||
|
colorName = "Han Purple"
|
||||||
|
}
|
||||||
|
else if (hueValue>=257 && hueValue <=274 ){
|
||||||
|
colorName = "Electric Indigo"
|
||||||
|
}
|
||||||
|
else if (hueValue>=275 && hueValue <=289 ){
|
||||||
|
colorName = "Electric Purple"
|
||||||
|
}
|
||||||
|
else if (hueValue>=290 && hueValue <=300 ){
|
||||||
|
colorName = "Orchid Purple"
|
||||||
|
}
|
||||||
|
else if (hueValue>=301 && hueValue <=315 ){
|
||||||
|
colorName = "Magenta"
|
||||||
|
}
|
||||||
|
else if (hueValue>=316 && hueValue <=326 ){
|
||||||
|
colorName = "Hot Pink"
|
||||||
|
}
|
||||||
|
else if (hueValue>=327 && hueValue <=335 ){
|
||||||
|
colorName = "Deep Pink"
|
||||||
|
}
|
||||||
|
else if (hueValue>=336 && hueValue <=339 ){
|
||||||
|
colorName = "Raspberry"
|
||||||
|
}
|
||||||
|
else if (hueValue>=340 && hueValue <=352 ){
|
||||||
|
colorName = "Crimson"
|
||||||
|
}
|
||||||
|
else if (hueValue>=353 && hueValue <=360 ){
|
||||||
|
colorName = "Red"
|
||||||
|
}
|
||||||
|
|
||||||
|
colorName
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEndpointId() {
|
||||||
|
new BigInteger(device.endpointId, 16).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value, width=2) {
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() < width) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
private evenHex(value){
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() % 2 != 0) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer convertHexToInt(hex) {
|
||||||
|
Integer.parseInt(hex,16)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Need to reverse array of size 2
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
byte tmp;
|
||||||
|
tmp = array[1];
|
||||||
|
array[1] = array[0];
|
||||||
|
array[0] = tmp;
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
def setAdjustedColor(value) {
|
||||||
|
log.debug "setAdjustedColor: ${value}"
|
||||||
|
def adjusted = value + [:]
|
||||||
|
adjusted.level = null // needed because color picker always sends 100
|
||||||
|
setColor(adjusted)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setColor(value){
|
||||||
|
log.trace "setColor($value)"
|
||||||
|
def max = 0xfe
|
||||||
|
|
||||||
|
if (value.hex) { sendEvent(name: "color", value: value.hex, displayed:false)}
|
||||||
|
|
||||||
|
def colorName = getColorName(value.hue)
|
||||||
|
sendEvent(name: "colorName", value: colorName)
|
||||||
|
|
||||||
|
log.debug "color name is : $colorName"
|
||||||
|
sendEvent(name: "hue", value: value.hue, displayed:false)
|
||||||
|
sendEvent(name: "saturation", value: value.saturation, displayed:false)
|
||||||
|
def scaledHueValue = evenHex(Math.round(value.hue * max / 100.0))
|
||||||
|
def scaledSatValue = evenHex(Math.round(value.saturation * max / 100.0))
|
||||||
|
|
||||||
|
def cmd = []
|
||||||
|
if (value.switch != "off" && device.latestValue("switch") == "off") {
|
||||||
|
cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
|
||||||
|
cmd << "delay 150"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${scaledHueValue} 00 0000}"
|
||||||
|
cmd << "delay 150"
|
||||||
|
cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${scaledSatValue} 0000}"
|
||||||
|
|
||||||
|
if (value.level) {
|
||||||
|
state.levelValue = value.level
|
||||||
|
sendEvent(name: "level", value: value.level)
|
||||||
|
def level = hex(value.level * 255 / 100)
|
||||||
|
cmd << zigbeeSetLevel(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.switch == "off") {
|
||||||
|
cmd << "delay 150"
|
||||||
|
cmd << off()
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
/*
|
||||||
|
Osram Flex RGBW Light Strip
|
||||||
|
|
||||||
|
Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling
|
||||||
|
that issue by using state variables
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "OSRAM LIGHTIFY LED Flexible Strip RGBW", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
|
capability "Color Temperature"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Color Control"
|
||||||
|
|
||||||
|
attribute "colorName", "string"
|
||||||
|
|
||||||
|
command "setAdjustedColor"
|
||||||
|
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "on": "on/off: 1"
|
||||||
|
status "off": "on/off: 0"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "zcl on-off on": "on/off: 1"
|
||||||
|
reply "zcl on-off off": "on/off: 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821"
|
||||||
|
state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||||
|
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||||
|
}
|
||||||
|
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "colorTemperature", label: '${currentValue} K'
|
||||||
|
}
|
||||||
|
valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "colorName", label: '${currentValue}'
|
||||||
|
}
|
||||||
|
|
||||||
|
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
|
||||||
|
state "color", action:"setAdjustedColor"
|
||||||
|
}
|
||||||
|
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "level", label: 'Level ${currentValue}%'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch", "refresh", "colorName", "levelSliderControl", "level", "colorTempSliderControl", "colorTemp", "rgbSelector"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
//log.info "description is $description"
|
||||||
|
if (description?.startsWith("catchall:")) {
|
||||||
|
if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1"))
|
||||||
|
{
|
||||||
|
def result = createEvent(name: "switch", value: "on")
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0"))
|
||||||
|
{
|
||||||
|
if(!(description?.startsWith("catchall: 0104 0300"))){
|
||||||
|
def result = createEvent(name: "switch", value: "off")
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (description?.startsWith("read attr -")) { //for values returned after hitting refresh
|
||||||
|
def descMap = parseDescriptionAsMap(description)
|
||||||
|
log.trace "descMap : $descMap"
|
||||||
|
|
||||||
|
if (descMap.cluster == "0300") {
|
||||||
|
if(descMap.attrId == "0007"){
|
||||||
|
log.debug "in read attr"
|
||||||
|
log.debug descMap.value
|
||||||
|
def tempInMired = convertHexToInt(descMap.value)
|
||||||
|
def tempInKelvin = Math.round(1000000/tempInMired)
|
||||||
|
log.trace "temp in kelvin: $tempInKelvin"
|
||||||
|
sendEvent(name: "colorTemperature", value: tempInKelvin, displayed:false)
|
||||||
|
}
|
||||||
|
else if(descMap.attrId == "0008"){ //Color mode attribute
|
||||||
|
if(descMap.value == "00"){
|
||||||
|
state.colorType = "rgb"
|
||||||
|
}else if(descMap.value == "02"){
|
||||||
|
state.colorType = "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(descMap.attrId == "0000"){ //Hue Attribute
|
||||||
|
def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 360)
|
||||||
|
log.debug "Hue value returned is $hueValue"
|
||||||
|
sendEvent(name: "hue", value: hueValue, displayed:false)
|
||||||
|
}
|
||||||
|
else if(descMap.attrId == "0001"){ //Saturation Attribute
|
||||||
|
def saturationValue = Math.round(convertHexToInt(descMap.value) / 255 * 100)
|
||||||
|
log.debug "Saturation from refresh is $saturationValue"
|
||||||
|
sendEvent(name: "saturation", value: saturationValue, displayed:false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(descMap.cluster == "0008"){
|
||||||
|
def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255)
|
||||||
|
log.debug "dimmer value is $dimmerValue"
|
||||||
|
sendEvent(name: "level", value: dimmerValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
def name = description?.startsWith("on/off: ") ? "switch" : null
|
||||||
|
def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null
|
||||||
|
def result = createEvent(name: name, value: value)
|
||||||
|
log.debug "description is $description"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
log.debug "on()"
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
setLevel(state?.levelValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zigbeeOff() {
|
||||||
|
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
log.debug "off()"
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
zigbeeOff()
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
[
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 1", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 7", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 8"
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
state.levelValue = 100
|
||||||
|
state.colorType = "white"
|
||||||
|
log.debug "Configuring Reporting and Bindings."
|
||||||
|
def configCmds = [
|
||||||
|
|
||||||
|
//Switch Reporting
|
||||||
|
"zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500",
|
||||||
|
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000",
|
||||||
|
|
||||||
|
//Level Control Reporting
|
||||||
|
"zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500",
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500"
|
||||||
|
]
|
||||||
|
return configCmds + refresh() // send refresh cmds as part of config
|
||||||
|
}
|
||||||
|
|
||||||
|
def setColorTemperature(value) {
|
||||||
|
state?.colorType = "white"
|
||||||
|
if(value<101){
|
||||||
|
value = (value*38) + 2700 //Calculation of mapping 0-100 to 2700-6500
|
||||||
|
}
|
||||||
|
|
||||||
|
def tempInMired = Math.round(1000000/value)
|
||||||
|
def finalHex = swapEndianHex(hex(tempInMired, 4))
|
||||||
|
def genericName = getGenericName(value)
|
||||||
|
log.debug "generic name is : $genericName"
|
||||||
|
|
||||||
|
def cmds = []
|
||||||
|
sendEvent(name: "colorTemperature", value: value, displayed:false)
|
||||||
|
sendEvent(name: "colorName", value: genericName)
|
||||||
|
|
||||||
|
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0300 0x0a {${finalHex} 2000}"
|
||||||
|
|
||||||
|
cmds
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseDescriptionAsMap(description) {
|
||||||
|
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll(){
|
||||||
|
log.debug "Poll is calling refresh"
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
def zigbeeSetLevel(level) {
|
||||||
|
"st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
state.levelValue = (value==null) ? 100 : value
|
||||||
|
log.trace "setLevel($value)"
|
||||||
|
def cmds = []
|
||||||
|
|
||||||
|
if (value == 0) {
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
cmds << zigbeeOff()
|
||||||
|
}
|
||||||
|
else if (device.latestValue("switch") == "off") {
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent(name: "level", value: state.levelValue)
|
||||||
|
def level = hex(state.levelValue * 255 / 100)
|
||||||
|
cmds << zigbeeSetLevel(level)
|
||||||
|
|
||||||
|
//log.debug cmds
|
||||||
|
cmds
|
||||||
|
}
|
||||||
|
|
||||||
|
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
|
||||||
|
private getGenericName(value){
|
||||||
|
def genericName = "White"
|
||||||
|
if(state?.colorType == "rgb"){
|
||||||
|
genericName = "Color Mode"
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
if(value < 3300){
|
||||||
|
genericName = "Soft White"
|
||||||
|
} else if(value < 4150){
|
||||||
|
genericName = "Moonlight"
|
||||||
|
} else if(value < 5000){
|
||||||
|
genericName = "Cool White"
|
||||||
|
} else if(value <= 6500){
|
||||||
|
genericName = "Daylight"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
genericName
|
||||||
|
}
|
||||||
|
|
||||||
|
//input Hue Integer values; returns color name for saturation 100%
|
||||||
|
private getColorName(hueValue){
|
||||||
|
if(hueValue>360 || hueValue<0)
|
||||||
|
return
|
||||||
|
|
||||||
|
hueValue = Math.round(hueValue / 100 * 360)
|
||||||
|
|
||||||
|
log.debug "hue value is $hueValue"
|
||||||
|
|
||||||
|
def colorName = "Color Mode"
|
||||||
|
if(hueValue>=0 && hueValue <= 4){
|
||||||
|
colorName = "Red"
|
||||||
|
}
|
||||||
|
else if (hueValue>=5 && hueValue <=21 ){
|
||||||
|
colorName = "Brick Red"
|
||||||
|
}
|
||||||
|
else if (hueValue>=22 && hueValue <=30 ){
|
||||||
|
colorName = "Safety Orange"
|
||||||
|
}
|
||||||
|
else if (hueValue>=31 && hueValue <=40 ){
|
||||||
|
colorName = "Dark Orange"
|
||||||
|
}
|
||||||
|
else if (hueValue>=41 && hueValue <=49 ){
|
||||||
|
colorName = "Amber"
|
||||||
|
}
|
||||||
|
else if (hueValue>=50 && hueValue <=56 ){
|
||||||
|
colorName = "Gold"
|
||||||
|
}
|
||||||
|
else if (hueValue>=57 && hueValue <=65 ){
|
||||||
|
colorName = "Yellow"
|
||||||
|
}
|
||||||
|
else if (hueValue>=66 && hueValue <=83 ){
|
||||||
|
colorName = "Electric Lime"
|
||||||
|
}
|
||||||
|
else if (hueValue>=84 && hueValue <=93 ){
|
||||||
|
colorName = "Lawn Green"
|
||||||
|
}
|
||||||
|
else if (hueValue>=94 && hueValue <=112 ){
|
||||||
|
colorName = "Bright Green"
|
||||||
|
}
|
||||||
|
else if (hueValue>=113 && hueValue <=135 ){
|
||||||
|
colorName = "Lime"
|
||||||
|
}
|
||||||
|
else if (hueValue>=136 && hueValue <=166 ){
|
||||||
|
colorName = "Spring Green"
|
||||||
|
}
|
||||||
|
else if (hueValue>=167 && hueValue <=171 ){
|
||||||
|
colorName = "Turquoise"
|
||||||
|
}
|
||||||
|
else if (hueValue>=172 && hueValue <=187 ){
|
||||||
|
colorName = "Aqua"
|
||||||
|
}
|
||||||
|
else if (hueValue>=188 && hueValue <=203 ){
|
||||||
|
colorName = "Sky Blue"
|
||||||
|
}
|
||||||
|
else if (hueValue>=204 && hueValue <=217 ){
|
||||||
|
colorName = "Dodger Blue"
|
||||||
|
}
|
||||||
|
else if (hueValue>=218 && hueValue <=223 ){
|
||||||
|
colorName = "Navy Blue"
|
||||||
|
}
|
||||||
|
else if (hueValue>=224 && hueValue <=251 ){
|
||||||
|
colorName = "Blue"
|
||||||
|
}
|
||||||
|
else if (hueValue>=252 && hueValue <=256 ){
|
||||||
|
colorName = "Han Purple"
|
||||||
|
}
|
||||||
|
else if (hueValue>=257 && hueValue <=274 ){
|
||||||
|
colorName = "Electric Indigo"
|
||||||
|
}
|
||||||
|
else if (hueValue>=275 && hueValue <=289 ){
|
||||||
|
colorName = "Electric Purple"
|
||||||
|
}
|
||||||
|
else if (hueValue>=290 && hueValue <=300 ){
|
||||||
|
colorName = "Orchid Purple"
|
||||||
|
}
|
||||||
|
else if (hueValue>=301 && hueValue <=315 ){
|
||||||
|
colorName = "Magenta"
|
||||||
|
}
|
||||||
|
else if (hueValue>=316 && hueValue <=326 ){
|
||||||
|
colorName = "Hot Pink"
|
||||||
|
}
|
||||||
|
else if (hueValue>=327 && hueValue <=335 ){
|
||||||
|
colorName = "Deep Pink"
|
||||||
|
}
|
||||||
|
else if (hueValue>=336 && hueValue <=339 ){
|
||||||
|
colorName = "Raspberry"
|
||||||
|
}
|
||||||
|
else if (hueValue>=340 && hueValue <=352 ){
|
||||||
|
colorName = "Crimson"
|
||||||
|
}
|
||||||
|
else if (hueValue>=353 && hueValue <=360 ){
|
||||||
|
colorName = "Red"
|
||||||
|
}
|
||||||
|
|
||||||
|
colorName
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private getEndpointId() {
|
||||||
|
new BigInteger(device.endpointId, 16).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value, width=2) {
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() < width) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
private evenHex(value){
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() % 2 != 0) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer convertHexToInt(hex) {
|
||||||
|
Integer.parseInt(hex,16)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Need to reverse array of size 2
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
byte tmp;
|
||||||
|
tmp = array[1];
|
||||||
|
array[1] = array[0];
|
||||||
|
array[0] = tmp;
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
def setAdjustedColor(value) {
|
||||||
|
log.debug "setAdjustedColor: ${value}"
|
||||||
|
def adjusted = value + [:]
|
||||||
|
adjusted.level = null // needed because color picker always sends 100
|
||||||
|
setColor(adjusted)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setColor(value){
|
||||||
|
state?.colorType = "rgb"
|
||||||
|
log.trace "setColor($value)"
|
||||||
|
def max = 0xfe
|
||||||
|
|
||||||
|
if (value.hex) { sendEvent(name: "color", value: value.hex, displayed:false)}
|
||||||
|
|
||||||
|
def colorName = getColorName(value.hue)
|
||||||
|
log.debug "color name is : $colorName"
|
||||||
|
sendEvent(name: "colorName", value: colorName)
|
||||||
|
sendEvent(name: "colorTemperature", value: "--", displayed:false)
|
||||||
|
|
||||||
|
|
||||||
|
sendEvent(name: "hue", value: value.hue, displayed:false)
|
||||||
|
sendEvent(name: "saturation", value: value.saturation, displayed:false)
|
||||||
|
def scaledHueValue = evenHex(Math.round(value.hue * max / 100.0))
|
||||||
|
def scaledSatValue = evenHex(Math.round(value.saturation * max / 100.0))
|
||||||
|
|
||||||
|
def cmd = []
|
||||||
|
if (value.switch != "off" && device.latestValue("switch") == "off") {
|
||||||
|
cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
|
||||||
|
cmd << "delay 150"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${scaledHueValue} 00 0000}"
|
||||||
|
cmd << "delay 150"
|
||||||
|
cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${scaledSatValue} 0000}"
|
||||||
|
|
||||||
|
if (value.level) {
|
||||||
|
state.levelValue = value.level
|
||||||
|
sendEvent(name: "level", value: value.level)
|
||||||
|
def level = hex(value.level * 255 / 100)
|
||||||
|
cmd << zigbeeSetLevel(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.switch == "off") {
|
||||||
|
cmd << "delay 150"
|
||||||
|
cmd << off()
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
/*
|
||||||
|
Osram Tunable White 60 A19 bulb
|
||||||
|
|
||||||
|
Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling
|
||||||
|
that issue by using state variables
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "OSRAM LIGHTIFY LED Tunable White 60W", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
|
capability "Color Temperature"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
attribute "colorName", "string"
|
||||||
|
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "on": "on/off: 1"
|
||||||
|
status "off": "on/off: 0"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "zcl on-off on": "on/off: 1"
|
||||||
|
reply "zcl on-off off": "on/off: 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821"
|
||||||
|
state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||||
|
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||||
|
}
|
||||||
|
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "colorTemperature", label: '${currentValue} K'
|
||||||
|
}
|
||||||
|
valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "colorName", label: '${currentValue}'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "level", label: 'Level ${currentValue}%'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch", "refresh", "colorName", "levelSliderControl", "level", "colorTempSliderControl", "colorTemp"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
//log.trace description
|
||||||
|
if (description?.startsWith("catchall:")) {
|
||||||
|
if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1"))
|
||||||
|
{
|
||||||
|
def result = createEvent(name: "switch", value: "on")
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0"))
|
||||||
|
{
|
||||||
|
def result = createEvent(name: "switch", value: "off")
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (description?.startsWith("read attr -")) {
|
||||||
|
def descMap = parseDescriptionAsMap(description)
|
||||||
|
log.trace "descMap : $descMap"
|
||||||
|
|
||||||
|
if (descMap.cluster == "0300") {
|
||||||
|
log.debug descMap.value
|
||||||
|
def tempInMired = convertHexToInt(descMap.value)
|
||||||
|
def tempInKelvin = Math.round(1000000/tempInMired)
|
||||||
|
log.trace "temp in kelvin: $tempInKelvin"
|
||||||
|
sendEvent(name: "colorTemperature", value: tempInKelvin, displayed:false)
|
||||||
|
}
|
||||||
|
else if(descMap.cluster == "0008"){
|
||||||
|
def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255)
|
||||||
|
log.debug "dimmer value is $dimmerValue"
|
||||||
|
sendEvent(name: "level", value: dimmerValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
def name = description?.startsWith("on/off: ") ? "switch" : null
|
||||||
|
def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null
|
||||||
|
def result = createEvent(name: name, value: value)
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
log.debug "on()"
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
setLevel(state?.levelValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
log.debug "off()"
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
[
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 7"
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
state.levelValue = 100
|
||||||
|
log.debug "Confuguring Reporting and Bindings."
|
||||||
|
def configCmds = [
|
||||||
|
|
||||||
|
//Switch Reporting
|
||||||
|
"zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500",
|
||||||
|
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000",
|
||||||
|
|
||||||
|
//Level Control Reporting
|
||||||
|
"zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500",
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500"
|
||||||
|
]
|
||||||
|
return configCmds + refresh() // send refresh cmds as part of config
|
||||||
|
}
|
||||||
|
|
||||||
|
def setColorTemperature(value) {
|
||||||
|
if(value<101){
|
||||||
|
value = (value*38) + 2700 //Calculation of mapping 0-100 to 2700-6500
|
||||||
|
}
|
||||||
|
|
||||||
|
def tempInMired = Math.round(1000000/value)
|
||||||
|
def finalHex = swapEndianHex(hex(tempInMired, 4))
|
||||||
|
def genericName = getGenericName(value)
|
||||||
|
log.debug "generic name is : $genericName"
|
||||||
|
|
||||||
|
def cmds = []
|
||||||
|
sendEvent(name: "colorTemperature", value: value, displayed:false)
|
||||||
|
sendEvent(name: "colorName", value: genericName)
|
||||||
|
|
||||||
|
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0300 0x0a {${finalHex} 2000}"
|
||||||
|
|
||||||
|
cmds
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseDescriptionAsMap(description) {
|
||||||
|
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll(){
|
||||||
|
log.debug "Poll is calling refresh"
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
state.levelValue = (value==null) ? 100 : value
|
||||||
|
log.trace "setLevel($value)"
|
||||||
|
def cmds = []
|
||||||
|
|
||||||
|
if (value == 0) {
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||||
|
}
|
||||||
|
else if (device.latestValue("switch") == "off") {
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent(name: "level", value: state.levelValue)
|
||||||
|
def level = hex(state.levelValue * 254 / 100)
|
||||||
|
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
|
||||||
|
|
||||||
|
//log.debug cmds
|
||||||
|
cmds
|
||||||
|
}
|
||||||
|
|
||||||
|
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
|
||||||
|
private getGenericName(value){
|
||||||
|
def genericName = "White"
|
||||||
|
if(value < 3300){
|
||||||
|
genericName = "Soft White"
|
||||||
|
} else if(value < 4150){
|
||||||
|
genericName = "Moonlight"
|
||||||
|
} else if(value < 5000){
|
||||||
|
genericName = "Cool White"
|
||||||
|
} else if(value <= 6500){
|
||||||
|
genericName = "Daylight"
|
||||||
|
}
|
||||||
|
|
||||||
|
genericName
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEndpointId() {
|
||||||
|
new BigInteger(device.endpointId, 16).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value, width=2) {
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() < width) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer convertHexToInt(hex) {
|
||||||
|
Integer.parseInt(hex,16)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Need to reverse array of size 2
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
byte tmp;
|
||||||
|
tmp = array[1];
|
||||||
|
array[1] = array[0];
|
||||||
|
array[0] = tmp;
|
||||||
|
return array
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Particulate Detector", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// TBD
|
||||||
|
}
|
||||||
|
|
||||||
|
// tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("particulate", "device.particulate", width: 2, height: 2) {
|
||||||
|
state "default", icon: "st.particulate.particulate.particulate", backgroundColor: "#ffffff"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "particulate"
|
||||||
|
details "particulate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
// TBD
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Pet Feeder Shield", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
|
command "feed"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "ping": "catchall: 0104 0000 01 01 0040 00 6A67 00 00 0000 0A 00 0A70696E67"
|
||||||
|
status "response": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A4F4D4E4F4D4E4F4D4E4F4D"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "raw 0x0 { 00 00 0a 0a 6f 6e }": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A6F6E"
|
||||||
|
reply "raw 0x0 { 00 00 0a 0a 6f 66 66 }": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A6F6666"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("feeder", "device.petFeederShield", width: 2, height: 2, canChangeBackground: true) {
|
||||||
|
state "default", action: "feed", icon: "st.shields.shields.pet-feeder", backgroundColor: "#ffffff"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "feeder"
|
||||||
|
details "feeder"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
def value = zigbee.parse(description)?.text
|
||||||
|
def name = value && value != "ping" ? "response" : null
|
||||||
|
def result = createEvent(name: name, value: value)
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands sent to the device
|
||||||
|
def feed() {
|
||||||
|
zigbee.smartShield(text: "feed" ).format()
|
||||||
|
}
|
||||||
103
devicetypes/smartthings/plant-link.src/plant-link.groovy
Normal file
103
devicetypes/smartthings/plant-link.src/plant-link.groovy
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* PlantLink
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2013-12-17
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
|
||||||
|
definition (name: "Plant Link", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Relative Humidity Measurement"
|
||||||
|
capability "Battery"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0405,FC08", outClusters: "0003"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
valueTile("humidity", "device.humidity", width: 2, height: 2) {
|
||||||
|
state("humidity", label:'${currentValue}%', unit:"",
|
||||||
|
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") {
|
||||||
|
state "battery", label:'${currentValue}%', unit:""
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["humidity", "battery"])
|
||||||
|
details(["humidity", "battery"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "Parse description $description"
|
||||||
|
def map = [:]
|
||||||
|
if (description?.startsWith("read attr -")) {
|
||||||
|
def descMap = parseDescriptionAsMap(description)
|
||||||
|
log.debug "Desc Map: $descMap"
|
||||||
|
if (descMap.cluster == "0405" && descMap.attrId == "0000") {
|
||||||
|
log.debug "Humidity"
|
||||||
|
map.name = "humidity"
|
||||||
|
map.value = calculateHumidity(descMap.value)
|
||||||
|
} else if (descMap.cluster == "0001" && descMap.attrId == "0000") {
|
||||||
|
log.debug "Battery"
|
||||||
|
map.name = "battery"
|
||||||
|
map.value = calculateBattery(descMap.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def result = null
|
||||||
|
if (map) {
|
||||||
|
result = createEvent(map)
|
||||||
|
}
|
||||||
|
log.debug "Parse returned $map"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseDescriptionAsMap(description) {
|
||||||
|
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateHumidity(value) {
|
||||||
|
//adc reading of 0x1ec0 produces a plant fuel level near 0
|
||||||
|
//adc reading of 0x2100 produces a plant fuel level near 100%
|
||||||
|
def range = 576 //8448 - 7872
|
||||||
|
def percent = (Integer.parseInt(value, 16) / range) * 100
|
||||||
|
percent = Math.max(0.0, Math.min(percent, 100.0))
|
||||||
|
percent
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateBattery(value) {
|
||||||
|
def min = 2300
|
||||||
|
def percent = (Integer.parseInt(value, 16) - min) / 10
|
||||||
|
// Make sure our percentage is between 0 - 100
|
||||||
|
percent = Math.max(0.0, Math.min(percent, 100.0))
|
||||||
|
percent
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value) {
|
||||||
|
new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
}
|
||||||
261
devicetypes/smartthings/rgbw-light.src/rgbw-light.groovy
Normal file
261
devicetypes/smartthings/rgbw-light.src/rgbw-light.groovy
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* Z-Wave RGBW Light
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2015-7-12
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "RGBW Light", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Color Control"
|
||||||
|
capability "Color Temperature"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
command "reset"
|
||||||
|
|
||||||
|
fingerprint inClusters: "0x26,0x33"
|
||||||
|
fingerprint deviceId: "0x1102", inClusters: "0x26,0x33"
|
||||||
|
fingerprint inClusters: "0x33"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) {
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
}
|
||||||
|
standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
|
||||||
|
state "color", action:"setColor"
|
||||||
|
}
|
||||||
|
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "level", label: 'Level ${currentValue}%'
|
||||||
|
}
|
||||||
|
controlTile("colorTempControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "colorTemperature", action:"setColorTemperature"
|
||||||
|
}
|
||||||
|
valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "hue", label: 'Hue ${currentValue} '
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch", "levelSliderControl", "rgbSelector", "reset", "colorTempControl", "refresh"])
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
response(refresh())
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(description) {
|
||||||
|
def result = null
|
||||||
|
if (description != "updated") {
|
||||||
|
def cmd = zwave.parse(description, [0x20: 1, 0x26: 3, 0x70: 1, 0x33:3])
|
||||||
|
if (cmd) {
|
||||||
|
result = zwaveEvent(cmd)
|
||||||
|
log.debug("'$description' parsed to $result")
|
||||||
|
} else {
|
||||||
|
log.debug("Couldn't zwave.parse '$description'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||||
|
dimmerEvents(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||||
|
dimmerEvents(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) {
|
||||||
|
dimmerEvents(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
private dimmerEvents(physicalgraph.zwave.Command cmd) {
|
||||||
|
def value = (cmd.value ? "on" : "off")
|
||||||
|
def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")]
|
||||||
|
if (cmd.value) {
|
||||||
|
result << createEvent(name: "level", value: cmd.value, unit: "%")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) {
|
||||||
|
response(command(zwave.switchMultilevelV1.switchMultilevelGet()))
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||||
|
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x84: 1])
|
||||||
|
if (encapsulatedCommand) {
|
||||||
|
state.sec = 1
|
||||||
|
def result = zwaveEvent(encapsulatedCommand)
|
||||||
|
result = result.collect {
|
||||||
|
if (it instanceof physicalgraph.device.HubAction && !it.toString().startsWith("9881")) {
|
||||||
|
response(cmd.CMD + "00" + it.toString())
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
def linkText = device.label ?: device.name
|
||||||
|
[linkText: linkText, descriptionText: "$linkText: $cmd", displayed: false]
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
commands([
|
||||||
|
zwave.basicV1.basicSet(value: 0xFF),
|
||||||
|
zwave.switchMultilevelV3.switchMultilevelGet(),
|
||||||
|
], 3500)
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
commands([
|
||||||
|
zwave.basicV1.basicSet(value: 0x00),
|
||||||
|
zwave.switchMultilevelV3.switchMultilevelGet(),
|
||||||
|
], 3500)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(level) {
|
||||||
|
setLevel(level, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(level, duration) {
|
||||||
|
if(level > 99) level = 99
|
||||||
|
commands([
|
||||||
|
zwave.switchMultilevelV3.switchMultilevelSet(value: level, dimmingDuration: duration),
|
||||||
|
zwave.switchMultilevelV3.switchMultilevelGet(),
|
||||||
|
], (duration && duration < 12) ? (duration * 1000) : 3500)
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
commands([
|
||||||
|
zwave.switchMultilevelV3.switchMultilevelGet(),
|
||||||
|
], 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setSaturation(percent) {
|
||||||
|
log.debug "setSaturation($percent)"
|
||||||
|
setColor(saturation: percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setHue(value) {
|
||||||
|
log.debug "setHue($value)"
|
||||||
|
setColor(hue: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setColor(value) {
|
||||||
|
def result = []
|
||||||
|
log.debug "setColor: ${value}"
|
||||||
|
if (value.hex) {
|
||||||
|
def c = value.hex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) }
|
||||||
|
result << zwave.switchColorV3.switchColorSet(red:c[0], green:c[1], blue:c[2], warmWhite:0, coldWhite:0)
|
||||||
|
} else {
|
||||||
|
def hue = value.hue ?: device.currentValue("hue")
|
||||||
|
def saturation = value.saturation ?: device.currentValue("saturation")
|
||||||
|
if(hue == null) hue = 13
|
||||||
|
if(saturation == null) saturation = 13
|
||||||
|
def rgb = huesatToRGB(hue, saturation)
|
||||||
|
result << zwave.switchColorV3.switchColorSet(red: rgb[0], green: rgb[1], blue: rgb[2], warmWhite:0, coldWhite:0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(value.hue) sendEvent(name: "hue", value: value.hue)
|
||||||
|
if(value.hex) sendEvent(name: "color", value: value.hex)
|
||||||
|
if(value.switch) sendEvent(name: "switch", value: value.switch)
|
||||||
|
if(value.saturation) sendEvent(name: "saturation", value: value.saturation)
|
||||||
|
|
||||||
|
commands(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setColorTemperature(percent) {
|
||||||
|
if(percent > 99) percent = 99
|
||||||
|
int warmValue = percent * 255 / 99
|
||||||
|
command(zwave.switchColorV3.switchColorSet(red:0, green:0, blue:0, warmWhite:warmValue, coldWhite:(255 - warmValue)))
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset() {
|
||||||
|
log.debug "reset()"
|
||||||
|
sendEvent(name: "color", value: "#ffffff")
|
||||||
|
setColorTemperature(99)
|
||||||
|
}
|
||||||
|
|
||||||
|
private command(physicalgraph.zwave.Command cmd) {
|
||||||
|
if (state.sec) {
|
||||||
|
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||||
|
} else {
|
||||||
|
cmd.format()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private commands(commands, delay=200) {
|
||||||
|
delayBetween(commands.collect{ command(it) }, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def rgbToHSV(red, green, blue) {
|
||||||
|
float r = red / 255f
|
||||||
|
float g = green / 255f
|
||||||
|
float b = blue / 255f
|
||||||
|
float max = [r, g, b].max()
|
||||||
|
float delta = max - [r, g, b].min()
|
||||||
|
def hue = 13
|
||||||
|
def saturation = 0
|
||||||
|
if (max && delta) {
|
||||||
|
saturation = 100 * delta / max
|
||||||
|
if (r == max) {
|
||||||
|
hue = ((g - b) / delta) * 100 / 6
|
||||||
|
} else if (g == max) {
|
||||||
|
hue = (2 + (b - r) / delta) * 100 / 6
|
||||||
|
} else {
|
||||||
|
hue = (4 + (r - g) / delta) * 100 / 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[hue: hue, saturation: saturation, value: max * 100]
|
||||||
|
}
|
||||||
|
|
||||||
|
def huesatToRGB(float hue, float sat) {
|
||||||
|
while(hue >= 100) hue -= 100
|
||||||
|
int h = (int)(hue / 100 * 6)
|
||||||
|
float f = hue / 100 * 6 - h
|
||||||
|
int p = Math.round(255 * (1 - (sat / 100)))
|
||||||
|
int q = Math.round(255 * (1 - (sat / 100) * f))
|
||||||
|
int t = Math.round(255 * (1 - (sat / 100) * (1 - f)))
|
||||||
|
switch (h) {
|
||||||
|
case 0: return [255, t, p]
|
||||||
|
case 1: return [q, 255, p]
|
||||||
|
case 2: return [p, 255, t]
|
||||||
|
case 3: return [p, q, 255]
|
||||||
|
case 4: return [t, p, 255]
|
||||||
|
case 5: return [255, p, q]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* Samsung TV
|
||||||
|
*
|
||||||
|
* Author: SmartThings (juano23@gmail.com)
|
||||||
|
* Date: 2015-01-08
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "Samsung Smart TV", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "switch"
|
||||||
|
|
||||||
|
command "mute"
|
||||||
|
command "source"
|
||||||
|
command "menu"
|
||||||
|
command "tools"
|
||||||
|
command "HDMI"
|
||||||
|
command "Sleep"
|
||||||
|
command "Up"
|
||||||
|
command "Down"
|
||||||
|
command "Left"
|
||||||
|
command "Right"
|
||||||
|
command "chup"
|
||||||
|
command "chdown"
|
||||||
|
command "prech"
|
||||||
|
command "volup"
|
||||||
|
command "voldown"
|
||||||
|
command "Enter"
|
||||||
|
command "Return"
|
||||||
|
command "Exit"
|
||||||
|
command "Info"
|
||||||
|
command "Size"
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) {
|
||||||
|
state "default", label:'TV', action:"switch.off", icon:"st.Electronics.electronics15", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("power", "device.switch", width: 1, height: 1, canChangeIcon: false) {
|
||||||
|
state "default", label:'', action:"switch.off", decoration: "flat", icon:"st.thermostat.heating-cooling-off", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("mute", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Mute', action:"mute", icon:"st.custom.sonos.muted", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
standardTile("source", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Source', action:"source", icon:"st.Electronics.electronics15"
|
||||||
|
}
|
||||||
|
standardTile("tools", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Tools', action:"tools", icon:"st.secondary.tools"
|
||||||
|
}
|
||||||
|
standardTile("menu", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Menu', action:"menu", icon:"st.vents.vent"
|
||||||
|
}
|
||||||
|
standardTile("HDMI", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Source', action:"HDMI", icon:"st.Electronics.electronics15"
|
||||||
|
}
|
||||||
|
standardTile("Sleep", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Sleep', action:"Sleep", icon:"st.Bedroom.bedroom10"
|
||||||
|
}
|
||||||
|
standardTile("Up", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Up', action:"Up", icon:"st.thermostat.thermostat-up"
|
||||||
|
}
|
||||||
|
standardTile("Down", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Down', action:"Down", icon:"st.thermostat.thermostat-down"
|
||||||
|
}
|
||||||
|
standardTile("Left", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Left', action:"Left", icon:"st.thermostat.thermostat-left"
|
||||||
|
}
|
||||||
|
standardTile("Right", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Right', action:"Right", icon:"st.thermostat.thermostat-right"
|
||||||
|
}
|
||||||
|
standardTile("chup", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'CH Up', action:"chup", icon:"st.thermostat.thermostat-up"
|
||||||
|
}
|
||||||
|
standardTile("chdown", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'CH Down', action:"chdown", icon:"st.thermostat.thermostat-down"
|
||||||
|
}
|
||||||
|
standardTile("prech", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Pre CH', action:"prech", icon:"st.secondary.refresh-icon"
|
||||||
|
}
|
||||||
|
standardTile("volup", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Vol Up', action:"volup", icon:"st.thermostat.thermostat-up"
|
||||||
|
}
|
||||||
|
standardTile("voldown", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Vol Down', action:"voldown", icon:"st.thermostat.thermostat-down"
|
||||||
|
}
|
||||||
|
standardTile("Enter", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Enter', action:"Enter", icon:"st.illuminance.illuminance.dark"
|
||||||
|
}
|
||||||
|
standardTile("Return", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Return', action:"Return", icon:"st.secondary.refresh-icon"
|
||||||
|
}
|
||||||
|
standardTile("Exit", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Exit', action:"Exit", icon:"st.locks.lock.unlocked"
|
||||||
|
}
|
||||||
|
standardTile("Info", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Info', action:"Info", icon:"st.motion.acceleration.active"
|
||||||
|
}
|
||||||
|
standardTile("Size", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||||
|
state "default", label:'Picture Size', action:"Size", icon:"st.contact.contact.open"
|
||||||
|
}
|
||||||
|
main "switch"
|
||||||
|
details (["power","HDMI","Sleep","chup","prech","volup","chdown","mute","voldown", "menu", "Up", "tools", "Left", "Enter", "Right", "Return", "Down", "Exit", "Info","Size"])
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
log.debug "Turning TV OFF"
|
||||||
|
parent.tvAction("POWEROFF",device.deviceNetworkId)
|
||||||
|
sendEvent(name:"Command", value: "Power Off", displayed: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def mute() {
|
||||||
|
log.trace "MUTE pressed"
|
||||||
|
parent.tvAction("MUTE",device.deviceNetworkId)
|
||||||
|
sendEvent(name:"Command", value: "Mute", displayed: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def source() {
|
||||||
|
log.debug "SOURCE pressed"
|
||||||
|
parent.tvAction("SOURCE",device.deviceNetworkId)
|
||||||
|
sendEvent(name:"Command", value: "Source", displayed: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def menu() {
|
||||||
|
log.debug "MENU pressed"
|
||||||
|
parent.tvAction("MENU",device.deviceNetworkId)
|
||||||
|
}
|
||||||
|
|
||||||
|
def tools() {
|
||||||
|
log.debug "TOOLS pressed"
|
||||||
|
parent.tvAction("TOOLS",device.deviceNetworkId)
|
||||||
|
sendEvent(name:"Command", value: "Tools", displayed: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def HDMI() {
|
||||||
|
log.debug "HDMI pressed"
|
||||||
|
parent.tvAction("HDMI",device.deviceNetworkId)
|
||||||
|
sendEvent(name:"Command sent", value: "Source", displayed: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def Sleep() {
|
||||||
|
log.debug "SLEEP pressed"
|
||||||
|
parent.tvAction("SLEEP",device.deviceNetworkId)
|
||||||
|
sendEvent(name:"Command", value: "Sleep", displayed: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def Up() {
|
||||||
|
log.debug "UP pressed"
|
||||||
|
parent.tvAction("UP",device.deviceNetworkId)
|
||||||
|
}
|
||||||
|
|
||||||
|
def Down() {
|
||||||
|
log.debug "DOWN pressed"
|
||||||
|
parent.tvAction("DOWN",device.deviceNetworkId)
|
||||||
|
}
|
||||||
|
|
||||||
|
def Left() {
|
||||||
|
log.debug "LEFT pressed"
|
||||||
|
parent.tvAction("LEFT",device.deviceNetworkId)
|
||||||
|
}
|
||||||
|
|
||||||
|
def Right() {
|
||||||
|
log.debug "RIGHT pressed"
|
||||||
|
parent.tvAction("RIGHT",device.deviceNetworkId)
|
||||||
|
}
|
||||||
|
|
||||||
|
def chup() {
|
||||||
|
log.debug "CHUP pressed"
|
||||||
|
parent.tvAction("CHUP",device.deviceNetworkId)
|
||||||
|
sendEvent(name:"Command", value: "Channel Up", displayed: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def chdown() {
|
||||||
|
log.debug "CHDOWN pressed"
|
||||||
|
parent.tvAction("CHDOWN",device.deviceNetworkId)
|
||||||
|
sendEvent(name:"Command", value: "Channel Down", displayed: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def prech() {
|
||||||
|
log.debug "PRECH pressed"
|
||||||
|
parent.tvAction("PRECH",device.deviceNetworkId)
|
||||||
|
sendEvent(name:"Command", value: "Prev Channel", displayed: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def Exit() {
|
||||||
|
log.debug "EXIT pressed"
|
||||||
|
parent.tvAction("EXIT",device.deviceNetworkId)
|
||||||
|
}
|
||||||
|
|
||||||
|
def volup() {
|
||||||
|
log.debug "VOLUP pressed"
|
||||||
|
parent.tvAction("VOLUP",device.deviceNetworkId)
|
||||||
|
sendEvent(name:"Command", value: "Volume Up", displayed: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def voldown() {
|
||||||
|
log.debug "VOLDOWN pressed"
|
||||||
|
parent.tvAction("VOLDOWN",device.deviceNetworkId)
|
||||||
|
sendEvent(name:"Command", value: "Volume Down", displayed: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def Enter() {
|
||||||
|
log.debug "ENTER pressed"
|
||||||
|
parent.tvAction("ENTER",device.deviceNetworkId)
|
||||||
|
}
|
||||||
|
|
||||||
|
def Return() {
|
||||||
|
log.debug "RETURN pressed"
|
||||||
|
parent.tvAction("RETURN",device.deviceNetworkId)
|
||||||
|
}
|
||||||
|
|
||||||
|
def Info() {
|
||||||
|
log.debug "INFO pressed"
|
||||||
|
parent.tvAction("INFO",device.deviceNetworkId)
|
||||||
|
sendEvent(name:"Command", value: "Info", displayed: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def Size() {
|
||||||
|
log.debug "PICTURE_SIZE pressed"
|
||||||
|
parent.tvAction("PICTURE_SIZE",device.deviceNetworkId)
|
||||||
|
sendEvent(name:"Command", value: "Picture Size", displayed: true)
|
||||||
|
}
|
||||||
172
devicetypes/smartthings/secure-dimmer.src/secure-dimmer.groovy
Normal file
172
devicetypes/smartthings/secure-dimmer.src/secure-dimmer.groovy
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Secure Dimmer", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x11", inClusters: "0x98"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "on": "command: 9881, payload: 002603FF"
|
||||||
|
status "off": "command: 9881, payload: 00260300"
|
||||||
|
status "09%": "command: 9881, payload: 00260309"
|
||||||
|
status "10%": "command: 9881, payload: 0026030A"
|
||||||
|
status "33%": "command: 9881, payload: 00260321"
|
||||||
|
status "66%": "command: 9881, payload: 00260342"
|
||||||
|
status "99%": "command: 9881, payload: 00260363"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "9881002001FF,delay 100,9881002602": "command: 9881, payload: 002603FF"
|
||||||
|
reply "988100200100,delay 100,9881002602": "command: 9881, payload: 00260300"
|
||||||
|
reply "988100200119,delay 100,9881002602": "command: 9881, payload: 00260319"
|
||||||
|
reply "988100200132,delay 100,9881002602": "command: 9881, payload: 00260332"
|
||||||
|
reply "98810020014B,delay 100,9881002602": "command: 9881, payload: 0026034B"
|
||||||
|
reply "988100200163,delay 100,9881002602": "command: 9881, payload: 00260363"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch", "levelSliderControl", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
if (description.startsWith("Err 106")) {
|
||||||
|
state.sec = 0
|
||||||
|
createEvent(descriptionText: description, isStateChange: true)
|
||||||
|
} else {
|
||||||
|
def cmd = zwave.parse(description, [0x20: 1, 0x26: 3, 0x70: 1, 0x32:3, 0x98: 1])
|
||||||
|
if (cmd) {
|
||||||
|
zwaveEvent(cmd)
|
||||||
|
} else {
|
||||||
|
log.debug("Couldn't zwave.parse '$description'")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||||
|
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x26: 3, 0x32: 3])
|
||||||
|
state.sec = 1
|
||||||
|
if (encapsulatedCommand) {
|
||||||
|
zwaveEvent(encapsulatedCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||||
|
dimmerEvents(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) {
|
||||||
|
dimmerEvents(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) {
|
||||||
|
dimmerEvents(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
private dimmerEvents(physicalgraph.zwave.Command cmd) {
|
||||||
|
def value = (cmd.value ? "on" : "off")
|
||||||
|
def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")]
|
||||||
|
if (cmd.value) {
|
||||||
|
result << createEvent(name: "level", value: cmd.value, unit: "%")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) {
|
||||||
|
def map = [:]
|
||||||
|
if (cmd.meterType == 1) {
|
||||||
|
if (cmd.scale == 0) {
|
||||||
|
map = [name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]
|
||||||
|
} else if (cmd.scale == 1) {
|
||||||
|
map = [name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"]
|
||||||
|
} else if (cmd.scale == 2) {
|
||||||
|
map = [name: "power", value: cmd.scaledMeterValue, unit: "W"]
|
||||||
|
} else {
|
||||||
|
map = [name: "electric", value: cmd.scaledMeterValue, unit: ["pulses", "V", "A", "R/Z", ""][cmd.scale - 3]]
|
||||||
|
}
|
||||||
|
} else if (cmd.meterType == 2) {
|
||||||
|
map = [name: "gas", value: cmd.scaledMeterValue, unit: ["m^3", "ft^3", "", "pulses", ""][cmd.scale]]
|
||||||
|
} else if (cmd.meterType == 3) {
|
||||||
|
map = [name: "water", value: cmd.scaledMeterValue, unit: ["m^3", "ft^3", "gal"][cmd.scale]]
|
||||||
|
}
|
||||||
|
map.isStateChange = true // just show in activity
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
def linkText = device.label ?: device.name
|
||||||
|
[linkText: linkText, descriptionText: "$linkText: $cmd", displayed: false]
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
secureSequence([
|
||||||
|
zwave.basicV1.basicSet(value: 0xFF),
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet()
|
||||||
|
], 3500)
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
secureSequence([
|
||||||
|
zwave.basicV1.basicSet(value: 0x00),
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet()
|
||||||
|
], 3500)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
secureSequence([
|
||||||
|
zwave.basicV1.basicSet(value: value),
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value, duration) {
|
||||||
|
def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60)
|
||||||
|
secure(zwave.switchMultilevelV2.switchMultilevelSet(value: value, dimmingDuration: dimmingDuration))
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
secure(zwave.switchMultilevelV1.switchMultilevelGet())
|
||||||
|
}
|
||||||
|
|
||||||
|
private secure(physicalgraph.zwave.Command cmd) {
|
||||||
|
if (state.sec) {
|
||||||
|
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||||
|
} else {
|
||||||
|
cmd.format()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private secureSequence(Collection commands, ...delayBetweenArgs) {
|
||||||
|
delayBetween(commands.collect{ secure(it) }, *delayBetweenArgs)
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* Smart Body Analyzer
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2013-09-27
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Smart Body Analyzer", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Polling"
|
||||||
|
|
||||||
|
attribute "weight", "string"
|
||||||
|
attribute "leanMass", "string"
|
||||||
|
attribute "fatRatio", "string"
|
||||||
|
attribute "fatMass", "string"
|
||||||
|
attribute "pulse", "string"
|
||||||
|
|
||||||
|
command "storeGraphImage"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
|
||||||
|
state "default", label: "Withings", action: "", icon: "st.Bath.bath2", backgroundColor: "#FFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
plotTile("weightGraph", "device.weight", "scatter", width:2, height:1, content:"weightPoints")
|
||||||
|
valueTile("weight", "device.weight", width: 1, height: 1) {
|
||||||
|
state("weight", label:'${currentValue} lbs', unit:"lbs", inactiveLabel: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
plotTile("fatGraph", "device.fatRatio", "scatter", width:2, height:1, content:"fatPoints")
|
||||||
|
valueTile("fatRatio", "device.fatRatio", width: 1, height: 1) {
|
||||||
|
state("fatRatio", label:'${currentValue}% bodyfat', unit:"bodyfat", inactiveLabel: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
plotTile("pulseGraph", "device.pulse", "scatter", width:2, height:1, content:"pulsePoints")
|
||||||
|
valueTile("pulse", "device.pulse", width: 1, height: 1, inactiveLabel: false) {
|
||||||
|
state("pulse", label:'${currentValue} bpm', unit:"bpm",
|
||||||
|
backgroundColors:[
|
||||||
|
[value: 30, color: "#153591"],
|
||||||
|
[value: 60, color: "#1e9cbb"],
|
||||||
|
[value: 80, color: "#f1d801"],
|
||||||
|
[value: 100, color: "#d04e00"],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("refresh", "command.refresh", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"polling.poll", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "icon"
|
||||||
|
details(["weightGraph", "weight", "fatGraph", "fatRatio", "pulseGraph", "pulse", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mappings {
|
||||||
|
path("/weightPoints") {
|
||||||
|
action: [
|
||||||
|
GET: "weightPoints"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/fatPoints") {
|
||||||
|
action: [
|
||||||
|
GET: "fatPoints"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/pulsePoints") {
|
||||||
|
action: [
|
||||||
|
GET: "pulsePoints"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "Parsing '${description}'"
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(Map event) {
|
||||||
|
log.debug "Parsing '${event}'"
|
||||||
|
|
||||||
|
if(event.status == 200 && event.data)
|
||||||
|
{
|
||||||
|
parent.parse(event)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
else if(["weight", "leanMass", "fatRatio", "fatMass", "pulse"].contains(event.name) && event.date)
|
||||||
|
{
|
||||||
|
def dateString = event.date
|
||||||
|
data["measure.${event.name}.$dateString"] = event.value
|
||||||
|
|
||||||
|
def old = "measure.${event.name}." + (new Date() - 30).format('yyyy-MM-dd')
|
||||||
|
data.findAll { it.key.startsWith("measure.${event.name}.") && it.key < old }.collect { it.key }.each { state.remove(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
def storeGraphImage(String name, ByteArrayInputStream is, String contentType) {
|
||||||
|
storeImage(name, is, contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
log.debug "Executing 'poll'"
|
||||||
|
parent.poll()
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
log.debug "Executing 'refresh'"
|
||||||
|
sendEvent(name:"refreshing", description:"Refreshing Withings data", displayed:true)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
def measurementPoints(name) {
|
||||||
|
def points = []
|
||||||
|
|
||||||
|
def wdata = normalizeMeasurementPoints(name)
|
||||||
|
log.debug "data: ${wdata}"
|
||||||
|
def allValues = wdata.collect { it.y }
|
||||||
|
log.debug "allValues: ${allValues}"
|
||||||
|
|
||||||
|
def min = allValues.min()
|
||||||
|
def max = allValues.max()
|
||||||
|
|
||||||
|
def minMax = [:]
|
||||||
|
minMax[min] = min
|
||||||
|
minMax[max] = max
|
||||||
|
log.debug "minMax: $minMax"
|
||||||
|
|
||||||
|
wdata.reverse().each { it ->
|
||||||
|
points << plotPoint(it.x, it.y, minMax)
|
||||||
|
}
|
||||||
|
log.debug "points: ${points}"
|
||||||
|
|
||||||
|
return points.reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeMeasurementPoints(name) {
|
||||||
|
def measurementData = data.findAll { it.key.startsWith("measure.${name}.") }
|
||||||
|
log.debug "measurementData: ${measurementData}"
|
||||||
|
|
||||||
|
def normalizedData = []
|
||||||
|
measurementData.each { k, v ->
|
||||||
|
def d = Date.parse('yyyy-MM-dd', k - "measure.${name}.")
|
||||||
|
Calendar cal = Calendar.getInstance();
|
||||||
|
cal.setTime(d)
|
||||||
|
// BUG: DOES NOT HANDLE NEW YEAR PROPERLY
|
||||||
|
// Should concat YEAR + (PAD_LEFT(DAY_OF_YEAR))
|
||||||
|
// 2013365 == Dec. 31, 2013
|
||||||
|
// 2014001 == Jan. 1, 2014
|
||||||
|
normalizedData << [x:cal.get(Calendar.DAY_OF_YEAR), y:v]
|
||||||
|
}
|
||||||
|
log.debug "normalizedData: ${normalizedData}"
|
||||||
|
|
||||||
|
normalizedData.sort{ it.x }
|
||||||
|
}
|
||||||
|
|
||||||
|
private plotPoint(x, y, minMax) {
|
||||||
|
def removed = minMax.remove(y) != null
|
||||||
|
|
||||||
|
return [
|
||||||
|
color:"",
|
||||||
|
fillColor: removed ? "" : "#f3f3f3",
|
||||||
|
symbolStyle:"elipse",
|
||||||
|
point:y,
|
||||||
|
label: removed ? "${y}" : "",
|
||||||
|
x:x,
|
||||||
|
y:y
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def weightPoints() {
|
||||||
|
return [
|
||||||
|
"title":"My Weight",
|
||||||
|
"plots":[
|
||||||
|
"weight":[
|
||||||
|
"points":measurementPoints("weight")
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def pulsePoints() {
|
||||||
|
return [
|
||||||
|
"title":"My Pulse",
|
||||||
|
"plots":[
|
||||||
|
"pulse":[
|
||||||
|
"points":measurementPoints("pulse")
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def fatPoints() {
|
||||||
|
return [
|
||||||
|
"title":"Bodyfat %",
|
||||||
|
"plots":[
|
||||||
|
"fat":[
|
||||||
|
"points":measurementPoints("fatRatio")
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* SmartAlert Siren
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2013-03-05
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "SmartAlert Siren", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Alarm"
|
||||||
|
|
||||||
|
command "test"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x1100", inClusters: "0x26,0x71"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// reply messages
|
||||||
|
reply "2001FF,2002": "command: 2003, payload: FF"
|
||||||
|
reply "200100,2002": "command: 2003, payload: 00"
|
||||||
|
reply "200121,2002": "command: 2003, payload: 21"
|
||||||
|
reply "200142,2002": "command: 2003, payload: 42"
|
||||||
|
reply "2001FF,delay 3000,200100,2002": "command: 2003, payload: 00"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("alarm", "device.alarm", width: 2, height: 2) {
|
||||||
|
state "off", label:'off', action:'alarm.strobe', icon:"st.alarm.alarm.alarm", backgroundColor:"#ffffff"
|
||||||
|
state "strobe", label:'strobe!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13"
|
||||||
|
state "siren", label:'siren!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13"
|
||||||
|
state "both", label:'alarm!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13"
|
||||||
|
}
|
||||||
|
standardTile("strobe", "device.alarm", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "off", label:'', action:"alarm.strobe", icon:"st.secondary.strobe", backgroundColor:"#cccccc"
|
||||||
|
state "siren", label:'', action:"alarm.strobe", icon:"st.secondary.strobe", backgroundColor:"#cccccc"
|
||||||
|
state "strobe", label:'', action:'alarm.strobe', icon:"st.secondary.strobe", backgroundColor:"#e86d13"
|
||||||
|
state "both", label:'', action:'alarm.strobe', icon:"st.secondary.strobe", backgroundColor:"#e86d13"
|
||||||
|
}
|
||||||
|
standardTile("siren", "device.alarm", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "off", label:'', action:"alarm.siren", icon:"st.secondary.siren", backgroundColor:"#cccccc"
|
||||||
|
state "strobe", label:'', action:"alarm.siren", icon:"st.secondary.siren", backgroundColor:"#cccccc"
|
||||||
|
state "siren", label:'', action:'alarm.siren', icon:"st.secondary.siren", backgroundColor:"#e86d13"
|
||||||
|
state "both", label:'', action:'alarm.siren', icon:"st.secondary.siren", backgroundColor:"#e86d13"
|
||||||
|
}
|
||||||
|
standardTile("test", "device.alarm", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"test", icon:"st.secondary.test"
|
||||||
|
}
|
||||||
|
standardTile("off", "device.alarm", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"alarm.off", icon:"st.secondary.off"
|
||||||
|
}
|
||||||
|
main "alarm"
|
||||||
|
details(["alarm","strobe","siren","test","off"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
[
|
||||||
|
zwave.basicV1.basicSet(value: 0xFF).format(),
|
||||||
|
zwave.basicV1.basicGet().format()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
[
|
||||||
|
zwave.basicV1.basicSet(value: 0x00).format(),
|
||||||
|
zwave.basicV1.basicGet().format()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def test() {
|
||||||
|
[
|
||||||
|
zwave.basicV1.basicSet(value: 0xFF).format(),
|
||||||
|
"delay 3000",
|
||||||
|
zwave.basicV1.basicSet(value: 0x00).format(),
|
||||||
|
zwave.basicV1.basicGet().format()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def strobe() {
|
||||||
|
[
|
||||||
|
zwave.basicV1.basicSet(value: 0x21).format(),
|
||||||
|
zwave.basicV1.basicGet().format()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def siren() {
|
||||||
|
[
|
||||||
|
zwave.basicV1.basicSet(value: 0x42).format(),
|
||||||
|
zwave.basicV1.basicGet().format()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def both() {
|
||||||
|
[
|
||||||
|
zwave.basicV1.basicSet(value: 0xFF).format(),
|
||||||
|
zwave.basicV1.basicGet().format()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "parse($description)"
|
||||||
|
def result = null
|
||||||
|
def cmd = zwave.parse(description, [0x20: 1])
|
||||||
|
if (cmd) {
|
||||||
|
result = createEvents(cmd)
|
||||||
|
}
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def createEvents(physicalgraph.zwave.commands.basicv1.BasicReport cmd)
|
||||||
|
{
|
||||||
|
def switchValue = cmd.value ? "on" : "off"
|
||||||
|
def alarmValue
|
||||||
|
if (cmd.value == 0) {
|
||||||
|
alarmValue = "off"
|
||||||
|
}
|
||||||
|
else if (cmd.value <= 33) {
|
||||||
|
alarmValue = "strobe"
|
||||||
|
}
|
||||||
|
else if (cmd.value <= 66) {
|
||||||
|
alarmValue = "siren"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alarmValue = "both"
|
||||||
|
}
|
||||||
|
[
|
||||||
|
createEvent([name: "switch", value: switchValue, type: "digital", displayed: false]),
|
||||||
|
createEvent([name: "alarm", value: alarmValue, type: "digital"])
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
log.warn "UNEXPECTED COMMAND: $cmd"
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* SmartPower Dimming Outlet (CentraLite)
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2013-12-04
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "SmartPower Dimming Outlet", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Switch"
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Power Meter"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "4257050-ZHAC"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "on": "on/off: 1"
|
||||||
|
status "off": "on/off: 0"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "zcl on-off on": "on/off: 1"
|
||||||
|
reply "zcl on-off off": "on/off: 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
}
|
||||||
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
|
state "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "level", label: 'Level ${currentValue}%'
|
||||||
|
}
|
||||||
|
valueTile("power", "device.power", decoration: "flat") {
|
||||||
|
state "power", label:'${currentValue} W'
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
main "switch"
|
||||||
|
details(["switch", "level", "power","refresh","levelSliderControl"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "description is $description"
|
||||||
|
|
||||||
|
def finalResult = isKnownDescription(description)
|
||||||
|
if (finalResult != "false") {
|
||||||
|
log.info finalResult
|
||||||
|
if (finalResult.type == "update") {
|
||||||
|
log.info "$device updates: ${finalResult.value}"
|
||||||
|
}
|
||||||
|
else if (finalResult.type == "power") {
|
||||||
|
def powerValue = (finalResult.value as Integer)/10
|
||||||
|
sendEvent(name: "power", value: powerValue)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10
|
||||||
|
|
||||||
|
power level is an integer. The exact power level with correct units needs to be handled in the device type
|
||||||
|
to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sendEvent(name: finalResult.type, value: finalResult.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||||
|
log.debug parseDescriptionAsMap(description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands to device
|
||||||
|
def zigbeeCommand(cluster, attribute){
|
||||||
|
"st cmd 0x${device.deviceNetworkId} ${endpointId} ${cluster} ${attribute} {}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
zigbeeCommand("6", "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
zigbeeCommand("6", "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
value = value as Integer
|
||||||
|
if (value == 0) {
|
||||||
|
off()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (device.latestValue("switch") == "off") {
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
}
|
||||||
|
sendEvent(name: "level", value: value)
|
||||||
|
setLevelWithRate(value, "0000") //value is between 0 to 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
[
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0B04 0x050B", "delay 500"
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
onOffConfig() + levelConfig() + powerConfig() + refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private getEndpointId() {
|
||||||
|
new BigInteger(device.endpointId, 16).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value, width=2) {
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() < width) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer convertHexToInt(hex) {
|
||||||
|
Integer.parseInt(hex,16)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Need to reverse array of size 2
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
byte tmp;
|
||||||
|
tmp = array[1];
|
||||||
|
array[1] = array[0];
|
||||||
|
array[0] = tmp;
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseDescriptionAsMap(description) {
|
||||||
|
if (description?.startsWith("read attr -")) {
|
||||||
|
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (description?.startsWith("catchall: ")) {
|
||||||
|
def seg = (description - "catchall: ").split(" ")
|
||||||
|
def zigbeeMap = [:]
|
||||||
|
zigbeeMap += [raw: (description - "catchall: ")]
|
||||||
|
zigbeeMap += [profileId: seg[0]]
|
||||||
|
zigbeeMap += [clusterId: seg[1]]
|
||||||
|
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||||
|
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||||
|
zigbeeMap += [options: seg[4]]
|
||||||
|
zigbeeMap += [messageType: seg[5]]
|
||||||
|
zigbeeMap += [dni: seg[6]]
|
||||||
|
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||||
|
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||||
|
zigbeeMap += [manufacturerId: seg[9]]
|
||||||
|
zigbeeMap += [command: seg[10]]
|
||||||
|
zigbeeMap += [direction: seg[11]]
|
||||||
|
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||||
|
it.join('')
|
||||||
|
} : []]
|
||||||
|
|
||||||
|
zigbeeMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def isKnownDescription(description) {
|
||||||
|
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||||
|
def descMap = parseDescriptionAsMap(description)
|
||||||
|
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||||
|
isDescriptionOnOff(descMap)
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "0008" || descMap.clusterId == "0008"){
|
||||||
|
isDescriptionLevel(descMap)
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "0B04" || descMap.clusterId == "0B04"){
|
||||||
|
isDescriptionPower(descMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(description?.startsWith("on/off:")) {
|
||||||
|
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||||
|
return [type: "switch", value : switchValue]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def isDescriptionOnOff(descMap) {
|
||||||
|
def switchValue = "undefined"
|
||||||
|
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||||
|
value = descMap.value
|
||||||
|
if (value == "01"){
|
||||||
|
switchValue = "on"
|
||||||
|
}
|
||||||
|
else if (value == "00"){
|
||||||
|
switchValue = "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (descMap.clusterId == "0006") {
|
||||||
|
//cluster info from catch all
|
||||||
|
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||||
|
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||||
|
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||||
|
switchValue = "on"
|
||||||
|
}
|
||||||
|
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||||
|
switchValue = "off"
|
||||||
|
}
|
||||||
|
else if(descMap.command=="07"){
|
||||||
|
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (switchValue != "undefined"){
|
||||||
|
return [type: "switch", value : switchValue]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//@return - false or "success" or level [0-100]
|
||||||
|
def isDescriptionLevel(descMap) {
|
||||||
|
def dimmerValue = -1
|
||||||
|
if (descMap.cluster == "0008"){
|
||||||
|
//TODO: the message returned with catchall is command 0B with clusterId 0008. That is just a confirmation message
|
||||||
|
def value = convertHexToInt(descMap.value)
|
||||||
|
dimmerValue = Math.round(value * 100 / 255)
|
||||||
|
if(dimmerValue==0 && value > 0) {
|
||||||
|
dimmerValue = 1 //handling for non-zero hex value less than 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(descMap.clusterId == "0008") {
|
||||||
|
if(descMap.command=="0B"){
|
||||||
|
return [type: "update", value : "level updated successfully"] //device updating the level change was successful. no value sent.
|
||||||
|
}
|
||||||
|
else if(descMap.command=="07"){
|
||||||
|
return [type: "update", value : "level (0008) capability configured successfully"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dimmerValue != -1){
|
||||||
|
return [type: "level", value : dimmerValue]
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def isDescriptionPower(descMap) {
|
||||||
|
def powerValue = "undefined"
|
||||||
|
if (descMap.cluster == "0B04") {
|
||||||
|
if (descMap.attrId == "050b") {
|
||||||
|
powerValue = convertHexToInt(descMap.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (descMap.clusterId == "0B04") {
|
||||||
|
if(descMap.command=="07"){
|
||||||
|
return [type: "update", value : "power (0B04) capability configured successfully"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (powerValue != "undefined"){
|
||||||
|
return [type: "power", value : powerValue]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def onOffConfig() {
|
||||||
|
[
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 6 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl global send-me-a-report 6 0 0x10 0 600 {01}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
//level config for devices with min reporting interval as 5 seconds and reporting interval if no activity as 1hour (3600s)
|
||||||
|
//min level change is 01
|
||||||
|
def levelConfig() {
|
||||||
|
[
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 8 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl global send-me-a-report 8 0 0x20 5 3600 {01}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s)
|
||||||
|
//min change in value is 05
|
||||||
|
def powerConfig() {
|
||||||
|
[
|
||||||
|
//Meter (Power) Reporting
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0B04 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl global send-me-a-report 0x0B04 0x050B 0x2A 1 600 {05}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevelWithRate(level, rate) {
|
||||||
|
if(rate == null){
|
||||||
|
rate = "0000"
|
||||||
|
}
|
||||||
|
level = convertToHexString(level * 255 / 100) //Converting the 0-100 range to 0-FF range in hex
|
||||||
|
"st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {$level $rate}"
|
||||||
|
}
|
||||||
|
|
||||||
|
String convertToHexString(value, width=2) {
|
||||||
|
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
while (s.size() < width) {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
metadata {
|
||||||
|
// Automatically generated. Make future change here.
|
||||||
|
definition (name: "SmartPower Outlet V1", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0006", outClusters: "0019"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "on": "on/off: 1"
|
||||||
|
status "off": "on/off: 0"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "zcl on-off on": "on/off: 1"
|
||||||
|
reply "zcl on-off off": "on/off: 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
|
||||||
|
state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "switch"
|
||||||
|
details "switch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
if (description?.startsWith("catchall: 0104 000A")) {
|
||||||
|
log.debug "Dropping catchall for SmartPower Outlet"
|
||||||
|
return []
|
||||||
|
} else {
|
||||||
|
def name = description?.startsWith("on/off: ") ? "switch" : null
|
||||||
|
def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null
|
||||||
|
def result = createEvent(name: name, value: value)
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands to device
|
||||||
|
def on() {
|
||||||
|
'zcl on-off on'
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
'zcl on-off off'
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* CentraLite Switch
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2013-12-02
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
// Automatically generated. Make future change here.
|
||||||
|
definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Power Meter"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
// status messages
|
||||||
|
status "on": "on/off: 1"
|
||||||
|
status "off": "on/off: 0"
|
||||||
|
|
||||||
|
// reply messages
|
||||||
|
reply "zcl on-off on": "on/off: 1"
|
||||||
|
reply "zcl on-off off": "on/off: 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
|
||||||
|
state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||||
|
}
|
||||||
|
valueTile("power", "device.power", decoration: "flat") {
|
||||||
|
state "power", label:'${currentValue} W'
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "switch"
|
||||||
|
details(["switch","power","refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "Parse description $description"
|
||||||
|
def name = null
|
||||||
|
def value = null
|
||||||
|
if (description?.startsWith("read attr -")) {
|
||||||
|
def descMap = parseDescriptionAsMap(description)
|
||||||
|
log.debug "Read attr: $description"
|
||||||
|
if (descMap.cluster == "0006" && descMap.attrId == "0000") {
|
||||||
|
name = "switch"
|
||||||
|
value = descMap.value.endsWith("01") ? "on" : "off"
|
||||||
|
} else {
|
||||||
|
def reportValue = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim()
|
||||||
|
name = "power"
|
||||||
|
// assume 16 bit signed for encoding and power divisor is 10
|
||||||
|
value = Integer.parseInt(reportValue, 16) / 10
|
||||||
|
}
|
||||||
|
} else if (description?.startsWith("on/off:")) {
|
||||||
|
log.debug "Switch command"
|
||||||
|
name = "switch"
|
||||||
|
value = description?.endsWith(" 1") ? "on" : "off"
|
||||||
|
}
|
||||||
|
|
||||||
|
def result = createEvent(name: name, value: value)
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseDescriptionAsMap(description) {
|
||||||
|
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands to device
|
||||||
|
def on() {
|
||||||
|
'zcl on-off on'
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
'zcl on-off off'
|
||||||
|
}
|
||||||
|
|
||||||
|
def meter() {
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B"
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B"
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
[
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0xB04 {${device.zigbeeId}} {}"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* SmartSense Garage Door Multi
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2013-03-09
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "SmartSense Garage Door Multi", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Three Axis"
|
||||||
|
capability "Contact Sensor"
|
||||||
|
capability "Acceleration Sensor"
|
||||||
|
capability "Signal Strength"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Battery"
|
||||||
|
|
||||||
|
attribute "status", "string"
|
||||||
|
attribute "door", "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "open": "zone report :: type: 19 value: 0031"
|
||||||
|
status "closed": "zone report :: type: 19 value: 0030"
|
||||||
|
|
||||||
|
status "acceleration": "acceleration: 1, rssi: 0, lqi: 0"
|
||||||
|
status "no acceleration": "acceleration: 0, rssi: 0, lqi: 0"
|
||||||
|
|
||||||
|
for (int i = 20; i <= 100; i += 10) {
|
||||||
|
status "${i}F": "contactState: 0, accelerationState: 0, temp: $i F, battery: 100, rssi: 100, lqi: 255"
|
||||||
|
}
|
||||||
|
|
||||||
|
// kinda hacky because it depends on how it is installed
|
||||||
|
status "x,y,z: 0,0,0": "x: 0, y: 0, z: 0, rssi: 100, lqi: 255"
|
||||||
|
status "x,y,z: 1000,0,0": "x: 1000, y: 0, z: 0, rssi: 100, lqi: 255"
|
||||||
|
status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0, rssi: 100, lqi: 255"
|
||||||
|
status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000, rssi: 100, lqi: 255"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("status", "device.status", width: 2, height: 2) {
|
||||||
|
state("closed", label:'${name}', icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821", nextState:"opening")
|
||||||
|
state("open", label:'${name}', icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e", nextState:"closing")
|
||||||
|
state("opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#ffe71e")
|
||||||
|
state("closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#ffe71e")
|
||||||
|
}
|
||||||
|
standardTile("contact", "device.contact") {
|
||||||
|
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||||
|
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||||
|
}
|
||||||
|
standardTile("acceleration", "device.acceleration", decoration: "flat") {
|
||||||
|
state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0")
|
||||||
|
state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff")
|
||||||
|
}
|
||||||
|
valueTile("temperature", "device.temperature", decoration: "flat") {
|
||||||
|
state("temperature", label:'${currentValue}°')
|
||||||
|
}
|
||||||
|
valueTile("3axis", "device.threeAxis", decoration: "flat", wordWrap: false) {
|
||||||
|
state("threeAxis", label:'${currentValue}', unit:"")
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""/*, backgroundColors:[
|
||||||
|
[value: 5, color: "#BC2323"],
|
||||||
|
[value: 10, color: "#D04E00"],
|
||||||
|
[value: 15, color: "#F1D801"],
|
||||||
|
[value: 16, color: "#FFFFFF"]
|
||||||
|
]*/
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
valueTile("lqi", "device.lqi", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "lqi", label:'${currentValue}% signal', unit:""
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
main(["status","contact", "acceleration"])
|
||||||
|
details(["status","contact", "acceleration", "temperature", "3axis", "battery"/*, "lqi"*/])
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||||
|
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "parse($description)"
|
||||||
|
def results = null
|
||||||
|
|
||||||
|
if (!isSupportedDescription(description) || zigbee.isZoneType19(description)) {
|
||||||
|
// Ignore this in favor of orientation-based state
|
||||||
|
// results = parseSingleMessage(description)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
results = parseMultiSensorMessage(description)
|
||||||
|
}
|
||||||
|
log.debug "Parse returned ${results?.descriptionText}"
|
||||||
|
return results
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
log.debug "UPDATED"
|
||||||
|
def threeAxis = device.currentState("threeAxis")
|
||||||
|
if (threeAxis) {
|
||||||
|
def xyz = threeAxis.xyzValue
|
||||||
|
def value = Math.round(xyz.z) > 925 ? "open" : "closed"
|
||||||
|
sendEvent(name: "contact", value: value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def actuate() {
|
||||||
|
log.debug "Sending button press event"
|
||||||
|
sendEvent(name: "buttonPress", value: "true", isStateChange: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseMultiSensorMessage(description) {
|
||||||
|
def results = []
|
||||||
|
if (isAccelerationMessage(description)) {
|
||||||
|
results = parseAccelerationMessage(description)
|
||||||
|
}
|
||||||
|
else if (isContactMessage(description)) {
|
||||||
|
results = parseContactMessage(description)
|
||||||
|
}
|
||||||
|
else if (isRssiLqiMessage(description)) {
|
||||||
|
results = parseRssiLqiMessage(description)
|
||||||
|
}
|
||||||
|
else if (isOrientationMessage(description)) {
|
||||||
|
results = parseOrientationMessage(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseAccelerationMessage(String description) {
|
||||||
|
def results = []
|
||||||
|
def parts = description.split(',')
|
||||||
|
parts.each { part ->
|
||||||
|
part = part.trim()
|
||||||
|
if (part.startsWith('acceleration:')) {
|
||||||
|
def event = getAccelerationResult(part, description)
|
||||||
|
results << event
|
||||||
|
}
|
||||||
|
else if (part.startsWith('rssi:')) {
|
||||||
|
results << getRssiResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lqi:')) {
|
||||||
|
results << getLqiResult(part, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseContactMessage(String description) {
|
||||||
|
def results = []
|
||||||
|
def parts = description.split(',')
|
||||||
|
parts.each { part ->
|
||||||
|
part = part.trim()
|
||||||
|
if (part.startsWith('accelerationState:')) {
|
||||||
|
results << getAccelerationResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('temp:')) {
|
||||||
|
results << getTempResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('battery:')) {
|
||||||
|
results << getBatteryResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('rssi:')) {
|
||||||
|
results << getRssiResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lqi:')) {
|
||||||
|
results << getLqiResult(part, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseOrientationMessage(String description) {
|
||||||
|
def results = []
|
||||||
|
def xyzResults = [x: 0, y: 0, z: 0]
|
||||||
|
def parts = description.split(',')
|
||||||
|
parts.each { part ->
|
||||||
|
part = part.trim()
|
||||||
|
if (part.startsWith('x:')) {
|
||||||
|
def unsignedX = part.split(":")[1].trim().toInteger()
|
||||||
|
def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX
|
||||||
|
xyzResults.x = signedX
|
||||||
|
}
|
||||||
|
else if (part.startsWith('y:')) {
|
||||||
|
def unsignedY = part.split(":")[1].trim().toInteger()
|
||||||
|
def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY
|
||||||
|
xyzResults.y = signedY
|
||||||
|
}
|
||||||
|
else if (part.startsWith('z:')) {
|
||||||
|
def unsignedZ = part.split(":")[1].trim().toInteger()
|
||||||
|
def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ
|
||||||
|
xyzResults.z = signedZ
|
||||||
|
}
|
||||||
|
else if (part.startsWith('rssi:')) {
|
||||||
|
results << getRssiResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lqi:')) {
|
||||||
|
results << getLqiResult(part, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def xyz = getXyzResult(xyzResults, description)
|
||||||
|
results << xyz
|
||||||
|
|
||||||
|
// Looks for Z-axis orientation as virtual contact state
|
||||||
|
def a = xyz.value.split(',').collect{it.toInteger()}
|
||||||
|
def absValueXY = Math.max(Math.abs(a[0]), Math.abs(a[1]))
|
||||||
|
def absValueZ = Math.abs(a[2])
|
||||||
|
log.debug "absValueXY: $absValueXY, absValueZ: $absValueZ"
|
||||||
|
|
||||||
|
|
||||||
|
if (absValueZ > 825 && absValueXY < 175) {
|
||||||
|
results << createEvent(name: "contact", value: "open", unit: "")
|
||||||
|
results << createEvent(name: "status", value: "open", unit: "")
|
||||||
|
results << createEvent(name: "door", value: "open", unit: "")
|
||||||
|
log.debug "STATUS: open"
|
||||||
|
}
|
||||||
|
else if (absValueZ < 75 && absValueXY > 825) {
|
||||||
|
results << createEvent(name: "contact", value: "closed", unit: "")
|
||||||
|
results << createEvent(name: "status", value: "closed", unit: "")
|
||||||
|
results << createEvent(name: "door", value: "closed", unit: "")
|
||||||
|
log.debug "STATUS: closed"
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseRssiLqiMessage(String description) {
|
||||||
|
def results = []
|
||||||
|
// "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255"
|
||||||
|
def parts = description.split(',')
|
||||||
|
parts.each { part ->
|
||||||
|
part = part.trim()
|
||||||
|
if (part.startsWith('lastHopRssi:')) {
|
||||||
|
results << getRssiResult(part, description, true)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lastHopLqi:')) {
|
||||||
|
results << getLqiResult(part, description, true)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('rssi:')) {
|
||||||
|
results << getRssiResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lqi:')) {
|
||||||
|
results << getLqiResult(part, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAccelerationResult(part, description) {
|
||||||
|
def name = "acceleration"
|
||||||
|
def value = part.endsWith("1") ? "active" : "inactive"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText ${name} was $value"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: null,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: value,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: displayed(description, isStateChange)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTempResult(part, description) {
|
||||||
|
def name = "temperature"
|
||||||
|
def temperatureScale = getTemperatureScale()
|
||||||
|
def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale)
|
||||||
|
if (tempOffset) {
|
||||||
|
def offset = tempOffset as int
|
||||||
|
def v = value as int
|
||||||
|
value = v + offset
|
||||||
|
}
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText was $value°$temperatureScale"
|
||||||
|
def isStateChange = isTemperatureStateChange(device, name, value.toString())
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: temperatureScale,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: displayed(description, isStateChange)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getXyzResult(results, description) {
|
||||||
|
def name = "threeAxis"
|
||||||
|
def value = "${results.x},${results.y},${results.z}"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText ${name} was $value"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: null,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBatteryResult(part, description) {
|
||||||
|
def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null
|
||||||
|
def name = "battery"
|
||||||
|
def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor)
|
||||||
|
def unit = "%"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText ${name} was ${value}${unit}"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: unit,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRssiResult(part, description, lastHop=false) {
|
||||||
|
def name = lastHop ? "lastHopRssi" : "rssi"
|
||||||
|
def valueString = part.split(":")[1].trim()
|
||||||
|
def value = (Integer.parseInt(valueString) - 128).toString()
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText ${name} was $value dBm"
|
||||||
|
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: "dBm",
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: null,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use LQI (Link Quality Indicator) as a measure of signal strength. The values
|
||||||
|
* are 0 to 255 (0x00 to 0xFF) and higher values represent higher signal
|
||||||
|
* strength. Return as a percentage of 255.
|
||||||
|
*
|
||||||
|
* Note: To make the signal strength indicator more accurate, we could combine
|
||||||
|
* LQI with RSSI.
|
||||||
|
*/
|
||||||
|
private getLqiResult(part, description, lastHop=false) {
|
||||||
|
def name = lastHop ? "lastHopLqi" : "lqi"
|
||||||
|
def valueString = part.split(":")[1].trim()
|
||||||
|
def percentageOf = 255
|
||||||
|
def value = Math.round((Integer.parseInt(valueString) / percentageOf * 100)).toString()
|
||||||
|
def unit = "%"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText ${name} was: ${value}${unit}"
|
||||||
|
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: unit,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: null,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean isAccelerationMessage(String description) {
|
||||||
|
// "acceleration: 1, rssi: 91, lqi: 255"
|
||||||
|
description ==~ /acceleration:.*rssi:.*lqi:.*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean isContactMessage(String description) {
|
||||||
|
// "contactState: 1, accelerationState: 0, temp: 14.4 C, battery: 28, rssi: 59, lqi: 255"
|
||||||
|
description ==~ /contactState:.*accelerationState:.*temp:.*battery:.*rssi:.*lqi:.*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean isRssiLqiMessage(String description) {
|
||||||
|
// "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255"
|
||||||
|
description ==~ /lastHopRssi:.*lastHopLqi:.*rssi:.*lqi:.*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean isOrientationMessage(String description) {
|
||||||
|
// "x: 0, y: 33, z: 1017, rssi: 102, lqi: 255"
|
||||||
|
description ==~ /x:.*y:.*z:.*rssi:.*lqi:.*/
|
||||||
|
}
|
||||||
@@ -0,0 +1,432 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* SmartSense Garage Door Sensor Button
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2013-03-09
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "SmartSense Garage Door Sensor Button", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Three Axis"
|
||||||
|
capability "Garage Door Control"
|
||||||
|
capability "Contact Sensor"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Acceleration Sensor"
|
||||||
|
capability "Signal Strength"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Battery"
|
||||||
|
|
||||||
|
attribute "status", "string"
|
||||||
|
attribute "buttonPress", "string"
|
||||||
|
|
||||||
|
command "actuate"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "acceleration": "acceleration: 1, rssi: 0, lqi: 0"
|
||||||
|
status "no acceleration": "acceleration: 0, rssi: 0, lqi: 0"
|
||||||
|
|
||||||
|
for (int i = 20; i <= 100; i += 10) {
|
||||||
|
status "${i}F": "contactState: 0, accelerationState: 0, temp: $i F, battery: 100, rssi: 100, lqi: 255"
|
||||||
|
}
|
||||||
|
|
||||||
|
// kinda hacky because it depends on how it is installed
|
||||||
|
status "x,y,z: 0,0,0": "x: 0, y: 0, z: 0, rssi: 100, lqi: 255"
|
||||||
|
status "x,y,z: 1000,0,0": "x: 1000, y: 0, z: 0, rssi: 100, lqi: 255"
|
||||||
|
status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0, rssi: 100, lqi: 255"
|
||||||
|
status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000, rssi: 100, lqi: 255"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("status", "device.status", width: 2, height: 2) {
|
||||||
|
state("closed", label:'${name}', icon:"st.doors.garage.garage-closed", action: "actuate", backgroundColor:"#79b821", nextState:"opening")
|
||||||
|
state("open", label:'${name}', icon:"st.doors.garage.garage-open", action: "actuate", backgroundColor:"#ffa81e", nextState:"closing")
|
||||||
|
state("opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#ffe71e")
|
||||||
|
state("closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#ffe71e")
|
||||||
|
}
|
||||||
|
standardTile("contact", "device.contact") {
|
||||||
|
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||||
|
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||||
|
}
|
||||||
|
standardTile("acceleration", "device.acceleration", decoration: "flat") {
|
||||||
|
state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0")
|
||||||
|
state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff")
|
||||||
|
}
|
||||||
|
valueTile("temperature", "device.temperature", decoration: "flat") {
|
||||||
|
state("temperature", label:'${currentValue}°')
|
||||||
|
}
|
||||||
|
valueTile("3axis", "device.threeAxis", decoration: "flat", wordWrap: false) {
|
||||||
|
state("threeAxis", label:'${currentValue}', unit:"")
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""/*, backgroundColors:[
|
||||||
|
[value: 5, color: "#BC2323"],
|
||||||
|
[value: 10, color: "#D04E00"],
|
||||||
|
[value: 15, color: "#F1D801"],
|
||||||
|
[value: 16, color: "#FFFFFF"]
|
||||||
|
]*/
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
valueTile("lqi", "device.lqi", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "lqi", label:'${currentValue}% signal', unit:""
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
main(["status","contact", "acceleration"])
|
||||||
|
details(["status","contact", "acceleration", "temperature", "3axis", "battery"/*, "lqi"*/])
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||||
|
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def open() {
|
||||||
|
if (device.currentValue("status") != "open") {
|
||||||
|
log.debug "Sending button press event to open door"
|
||||||
|
sendEvent(name: "buttonPress", value: "true", isStateChange: true, unit: "")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.debug "Not opening door since it is already open"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def close() {
|
||||||
|
if (device.currentValue("status") != "closed") {
|
||||||
|
log.debug "Sending button press event to close door"
|
||||||
|
sendEvent(name: "buttonPress", value: "true", isStateChange: true, unit: "")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.debug "Not closing door since it is already closed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "parse($description)"
|
||||||
|
def results = null
|
||||||
|
|
||||||
|
if (!isSupportedDescription(description) || zigbee.isZoneType19(description)) {
|
||||||
|
// Ignore this in favor of orientation-based state
|
||||||
|
// results = parseSingleMessage(description)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
results = parseMultiSensorMessage(description)
|
||||||
|
}
|
||||||
|
log.debug "Parse returned ${results?.descriptionText}"
|
||||||
|
return results
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def actuate() {
|
||||||
|
log.debug "Sending button press event"
|
||||||
|
sendEvent(name: "buttonPress", value: "true", isStateChange: true, unit: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseMultiSensorMessage(description) {
|
||||||
|
def results = []
|
||||||
|
if (isAccelerationMessage(description)) {
|
||||||
|
results = parseAccelerationMessage(description)
|
||||||
|
}
|
||||||
|
else if (isContactMessage(description)) {
|
||||||
|
results = parseContactMessage(description)
|
||||||
|
}
|
||||||
|
else if (isRssiLqiMessage(description)) {
|
||||||
|
results = parseRssiLqiMessage(description)
|
||||||
|
}
|
||||||
|
else if (isOrientationMessage(description)) {
|
||||||
|
results = parseOrientationMessage(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseAccelerationMessage(String description) {
|
||||||
|
def results = []
|
||||||
|
def parts = description.split(',')
|
||||||
|
parts.each { part ->
|
||||||
|
part = part.trim()
|
||||||
|
if (part.startsWith('acceleration:')) {
|
||||||
|
def event = getAccelerationResult(part, description)
|
||||||
|
results << event
|
||||||
|
}
|
||||||
|
else if (part.startsWith('rssi:')) {
|
||||||
|
results << getRssiResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lqi:')) {
|
||||||
|
results << getLqiResult(part, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseContactMessage(String description) {
|
||||||
|
def results = []
|
||||||
|
def parts = description.split(',')
|
||||||
|
parts.each { part ->
|
||||||
|
part = part.trim()
|
||||||
|
if (part.startsWith('accelerationState:')) {
|
||||||
|
results << getAccelerationResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('temp:')) {
|
||||||
|
results << getTempResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('battery:')) {
|
||||||
|
results << getBatteryResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('rssi:')) {
|
||||||
|
results << getRssiResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lqi:')) {
|
||||||
|
results << getLqiResult(part, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseOrientationMessage(String description) {
|
||||||
|
def results = []
|
||||||
|
def xyzResults = [x: 0, y: 0, z: 0]
|
||||||
|
def parts = description.split(',')
|
||||||
|
parts.each { part ->
|
||||||
|
part = part.trim()
|
||||||
|
if (part.startsWith('x:')) {
|
||||||
|
def unsignedX = part.split(":")[1].trim().toInteger()
|
||||||
|
def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX
|
||||||
|
xyzResults.x = signedX
|
||||||
|
}
|
||||||
|
else if (part.startsWith('y:')) {
|
||||||
|
def unsignedY = part.split(":")[1].trim().toInteger()
|
||||||
|
def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY
|
||||||
|
xyzResults.y = signedY
|
||||||
|
}
|
||||||
|
else if (part.startsWith('z:')) {
|
||||||
|
def unsignedZ = part.split(":")[1].trim().toInteger()
|
||||||
|
def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ
|
||||||
|
xyzResults.z = signedZ
|
||||||
|
}
|
||||||
|
else if (part.startsWith('rssi:')) {
|
||||||
|
results << getRssiResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lqi:')) {
|
||||||
|
results << getLqiResult(part, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def xyz = getXyzResult(xyzResults, description)
|
||||||
|
results << xyz
|
||||||
|
|
||||||
|
// Looks for Z-axis orientation as virtual contact state
|
||||||
|
def a = xyz.value.split(',').collect{it.toInteger()}
|
||||||
|
def absValueXY = Math.max(Math.abs(a[0]), Math.abs(a[1]))
|
||||||
|
def absValueZ = Math.abs(a[2])
|
||||||
|
log.debug "absValueXY: $absValueXY, absValueZ: $absValueZ"
|
||||||
|
|
||||||
|
|
||||||
|
if (absValueZ > 825 && absValueXY < 175) {
|
||||||
|
results << createEvent(name: "contact", value: "open", unit: "")
|
||||||
|
results << createEvent(name: "status", value: "open", unit: "")
|
||||||
|
results << createEvent(name: "door", value: "open", unit: "")
|
||||||
|
log.debug "STATUS: open"
|
||||||
|
}
|
||||||
|
else if (absValueZ < 75 && absValueXY > 825) {
|
||||||
|
results << createEvent(name: "contact", value: "closed", unit: "")
|
||||||
|
results << createEvent(name: "status", value: "closed", unit: "")
|
||||||
|
results << createEvent(name: "door", value: "closed", unit: "")
|
||||||
|
log.debug "STATUS: closed"
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseRssiLqiMessage(String description) {
|
||||||
|
def results = []
|
||||||
|
// "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255"
|
||||||
|
def parts = description.split(',')
|
||||||
|
parts.each { part ->
|
||||||
|
part = part.trim()
|
||||||
|
if (part.startsWith('lastHopRssi:')) {
|
||||||
|
results << getRssiResult(part, description, true)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lastHopLqi:')) {
|
||||||
|
results << getLqiResult(part, description, true)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('rssi:')) {
|
||||||
|
results << getRssiResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lqi:')) {
|
||||||
|
results << getLqiResult(part, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAccelerationResult(part, description) {
|
||||||
|
def name = "acceleration"
|
||||||
|
def value = part.endsWith("1") ? "active" : "inactive"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText ${name} was $value"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: null,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: value,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: displayed(description, isStateChange)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTempResult(part, description) {
|
||||||
|
def name = "temperature"
|
||||||
|
def temperatureScale = getTemperatureScale()
|
||||||
|
def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale)
|
||||||
|
if (tempOffset) {
|
||||||
|
def offset = tempOffset as int
|
||||||
|
def v = value as int
|
||||||
|
value = v + offset
|
||||||
|
}
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText was $value°$temperatureScale"
|
||||||
|
def isStateChange = isTemperatureStateChange(device, name, value.toString())
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: temperatureScale,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: displayed(description, isStateChange)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getXyzResult(results, description) {
|
||||||
|
def name = "threeAxis"
|
||||||
|
def value = "${results.x},${results.y},${results.z}"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText ${name} was $value"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: null,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBatteryResult(part, description) {
|
||||||
|
def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null
|
||||||
|
def name = "battery"
|
||||||
|
def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor)
|
||||||
|
def unit = "%"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText ${name} was ${value}${unit}"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: unit,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRssiResult(part, description, lastHop=false) {
|
||||||
|
def name = lastHop ? "lastHopRssi" : "rssi"
|
||||||
|
def valueString = part.split(":")[1].trim()
|
||||||
|
def value = (Integer.parseInt(valueString) - 128).toString()
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText ${name} was $value dBm"
|
||||||
|
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: "dBm",
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: null,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use LQI (Link Quality Indicator) as a measure of signal strength. The values
|
||||||
|
* are 0 to 255 (0x00 to 0xFF) and higher values represent higher signal
|
||||||
|
* strength. Return as a percentage of 255.
|
||||||
|
*
|
||||||
|
* Note: To make the signal strength indicator more accurate, we could combine
|
||||||
|
* LQI with RSSI.
|
||||||
|
*/
|
||||||
|
private getLqiResult(part, description, lastHop=false) {
|
||||||
|
def name = lastHop ? "lastHopLqi" : "lqi"
|
||||||
|
def valueString = part.split(":")[1].trim()
|
||||||
|
def percentageOf = 255
|
||||||
|
def value = Math.round((Integer.parseInt(valueString) / percentageOf * 100)).toString()
|
||||||
|
def unit = "%"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText ${name} was: ${value}${unit}"
|
||||||
|
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: unit,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: null,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean isAccelerationMessage(String description) {
|
||||||
|
// "acceleration: 1, rssi: 91, lqi: 255"
|
||||||
|
description ==~ /acceleration:.*rssi:.*lqi:.*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean isContactMessage(String description) {
|
||||||
|
// "contactState: 1, accelerationState: 0, temp: 14.4 C, battery: 28, rssi: 59, lqi: 255"
|
||||||
|
description ==~ /contactState:.*accelerationState:.*temp:.*battery:.*rssi:.*lqi:.*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean isRssiLqiMessage(String description) {
|
||||||
|
// "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255"
|
||||||
|
description ==~ /lastHopRssi:.*lastHopLqi:.*rssi:.*lqi:.*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean isOrientationMessage(String description) {
|
||||||
|
// "x: 0, y: 33, z: 1017, rssi: 102, lqi: 255"
|
||||||
|
description ==~ /x:.*y:.*z:.*rssi:.*lqi:.*/
|
||||||
|
}
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
/**
|
||||||
|
* SmartSense Moisture Sensor
|
||||||
|
*
|
||||||
|
* Copyright 2014 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Battery"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
capability "Water Sensor"
|
||||||
|
|
||||||
|
command "enrollResponse"
|
||||||
|
|
||||||
|
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||||
|
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("water", "device.water", width: 2, height: 2) {
|
||||||
|
state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff"
|
||||||
|
state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0"
|
||||||
|
}
|
||||||
|
|
||||||
|
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", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery'
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "water", "temperature"
|
||||||
|
details(["water", "temperature", "battery", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "description: $description"
|
||||||
|
|
||||||
|
Map map = [:]
|
||||||
|
if (description?.startsWith('catchall:')) {
|
||||||
|
map = parseCatchAllMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('read attr -')) {
|
||||||
|
map = parseReportAttributeMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('temperature: ')) {
|
||||||
|
map = parseCustomMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('zone status')) {
|
||||||
|
map = parseIasMessage(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug "Parse returned $map"
|
||||||
|
def result = map ? createEvent(map) : null
|
||||||
|
|
||||||
|
if (description?.startsWith('enroll request')) {
|
||||||
|
List cmds = enrollResponse()
|
||||||
|
log.debug "enroll response: ${cmds}"
|
||||||
|
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseCatchAllMessage(String description) {
|
||||||
|
Map resultMap = [:]
|
||||||
|
def cluster = zigbee.parse(description)
|
||||||
|
if (shouldProcessMessage(cluster)) {
|
||||||
|
switch(cluster.clusterId) {
|
||||||
|
case 0x0001:
|
||||||
|
resultMap = getBatteryResult(cluster.data.last())
|
||||||
|
break
|
||||||
|
|
||||||
|
case 0x0402:
|
||||||
|
// temp is last 2 data values. reverse to swap endian
|
||||||
|
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||||
|
def value = getTemperature(temp)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldProcessMessage(cluster) {
|
||||||
|
// 0x0B is default response indicating message got through
|
||||||
|
// 0x07 is bind message
|
||||||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
|
cluster.command == 0x0B ||
|
||||||
|
cluster.command == 0x07 ||
|
||||||
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
|
return !ignoredMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseReportAttributeMessage(String description) {
|
||||||
|
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
log.debug "Desc Map: $descMap"
|
||||||
|
|
||||||
|
Map resultMap = [:]
|
||||||
|
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||||
|
def value = getTemperature(descMap.value)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||||
|
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseCustomMessage(String description) {
|
||||||
|
Map resultMap = [:]
|
||||||
|
if (description?.startsWith('temperature: ')) {
|
||||||
|
def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseIasMessage(String description) {
|
||||||
|
List parsedMsg = description.split(' ')
|
||||||
|
String msgCode = parsedMsg[2]
|
||||||
|
|
||||||
|
Map resultMap = [:]
|
||||||
|
switch(msgCode) {
|
||||||
|
case '0x0020': // Closed/No Motion/Dry
|
||||||
|
resultMap = getMoistureResult('dry')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0021': // Open/Motion/Wet
|
||||||
|
resultMap = getMoistureResult('wet')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0022': // Tamper Alarm
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0023': // Battery Alarm
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0024': // Supervision Report
|
||||||
|
log.debug 'dry with tamper alarm'
|
||||||
|
resultMap = getMoistureResult('dry')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0025': // Restore Report
|
||||||
|
log.debug 'water with tamper alarm'
|
||||||
|
resultMap = getMoistureResult('wet')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0026': // Trouble/Failure
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0028': // Test Mode
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
def getTemperature(value) {
|
||||||
|
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||||
|
if(getTemperatureScale() == "C"){
|
||||||
|
return celsius
|
||||||
|
} else {
|
||||||
|
return celsiusToFahrenheit(celsius) as Integer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getBatteryResult(rawValue) {
|
||||||
|
log.debug 'Battery'
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
|
||||||
|
def result = [
|
||||||
|
name: 'battery'
|
||||||
|
]
|
||||||
|
|
||||||
|
def volts = rawValue / 10
|
||||||
|
def descriptionText
|
||||||
|
if (volts > 3.5) {
|
||||||
|
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
def minVolts = 2.1
|
||||||
|
def maxVolts = 3.0
|
||||||
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
|
result.value = Math.min(100, (int) pct * 100)
|
||||||
|
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getTemperatureResult(value) {
|
||||||
|
log.debug 'TEMP'
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
if (tempOffset) {
|
||||||
|
def offset = tempOffset as int
|
||||||
|
def v = value as int
|
||||||
|
value = v + offset
|
||||||
|
}
|
||||||
|
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
||||||
|
return [
|
||||||
|
name: 'temperature',
|
||||||
|
value: value,
|
||||||
|
descriptionText: descriptionText
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getMoistureResult(value) {
|
||||||
|
log.debug 'water'
|
||||||
|
String descriptionText = "${device.displayName} is ${value}"
|
||||||
|
return [
|
||||||
|
name: 'water',
|
||||||
|
value: value,
|
||||||
|
descriptionText: descriptionText
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh()
|
||||||
|
{
|
||||||
|
log.debug "Refreshing Temperature and Battery"
|
||||||
|
[
|
||||||
|
|
||||||
|
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
|
||||||
|
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||||
|
log.debug "Confuguring Reporting, IAS CIE, and Bindings."
|
||||||
|
def configCmds = [
|
||||||
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
"zcl global send-me-a-report 1 0x20 0x20 300 0600 {01}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x001 {${device.zigbeeId}} {}", "delay 1000",
|
||||||
|
|
||||||
|
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
|
||||||
|
]
|
||||||
|
return configCmds + refresh() // send refresh cmds as part of config
|
||||||
|
}
|
||||||
|
|
||||||
|
def enrollResponse() {
|
||||||
|
log.debug "Sending enroll response"
|
||||||
|
[
|
||||||
|
|
||||||
|
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1"
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value) {
|
||||||
|
new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
int i = 0;
|
||||||
|
int j = array.length - 1;
|
||||||
|
byte tmp;
|
||||||
|
while (j > i) {
|
||||||
|
tmp = array[j];
|
||||||
|
array[j] = array[i];
|
||||||
|
array[i] = tmp;
|
||||||
|
j--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "SmartSense Moisture", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Water Sensor"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Battery"
|
||||||
|
|
||||||
|
fingerprint deviceId: "0x2001", inClusters: "0x30,0x9C,0x9D,0x85,0x80,0x72,0x31,0x84,0x86"
|
||||||
|
fingerprint deviceId: "0x2101", inClusters: "0x71,0x70,0x85,0x80,0x72,0x31,0x84,0x86"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "dry": "command: 7105, payload: 00 00 00 FF 05 FE 00 00"
|
||||||
|
status "wet": "command: 7105, payload: 00 FF 00 FF 05 02 00 00"
|
||||||
|
status "overheated": "command: 7105, payload: 00 00 00 FF 04 02 00 00"
|
||||||
|
status "freezing": "command: 7105, payload: 00 00 00 FF 04 05 00 00"
|
||||||
|
status "normal": "command: 7105, payload: 00 00 00 FF 04 FE 00 00"
|
||||||
|
for (int i = 0; i <= 100; i += 20) {
|
||||||
|
status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i).incomingMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tiles {
|
||||||
|
standardTile("water", "device.water", width: 2, height: 2) {
|
||||||
|
state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff"
|
||||||
|
state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0"
|
||||||
|
}
|
||||||
|
standardTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||||
|
state "normal", icon:"st.alarm.temperature.normal", backgroundColor:"#ffffff"
|
||||||
|
state "freezing", icon:"st.alarm.temperature.freeze", backgroundColor:"#53a7c0"
|
||||||
|
state "overheated", icon:"st.alarm.temperature.overheat", backgroundColor:"#F80000"
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""/*, backgroundColors:[
|
||||||
|
[value: 5, color: "#BC2323"],
|
||||||
|
[value: 10, color: "#D04E00"],
|
||||||
|
[value: 15, color: "#F1D801"],
|
||||||
|
[value: 16, color: "#FFFFFF"]
|
||||||
|
]*/
|
||||||
|
}
|
||||||
|
main (["water", "temperature"])
|
||||||
|
details(["water", "temperature", "battery"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def result = []
|
||||||
|
def parsedZwEvent = zwave.parse(description, [0x30: 1, 0x71: 2, 0x84: 1])
|
||||||
|
|
||||||
|
if(parsedZwEvent) {
|
||||||
|
if(parsedZwEvent.CMD == "8407") {
|
||||||
|
def lastStatus = device.currentState("battery")
|
||||||
|
def ageInMinutes = lastStatus ? (new Date().time - lastStatus.date.time)/60000 : 600
|
||||||
|
log.debug "Battery status was last checked ${ageInMinutes} minutes ago"
|
||||||
|
|
||||||
|
if (ageInMinutes >= 600) {
|
||||||
|
log.debug "Battery status is outdated, requesting battery report"
|
||||||
|
result << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format())
|
||||||
|
}
|
||||||
|
result << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format())
|
||||||
|
}
|
||||||
|
result << createEvent( zwaveEvent(parsedZwEvent) )
|
||||||
|
}
|
||||||
|
if(!result) result = [ descriptionText: parsedZwEvent, displayed: false ]
|
||||||
|
log.debug "Parse returned ${result}"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
|
||||||
|
{
|
||||||
|
[descriptionText: "${device.displayName} woke up", isStateChange: false]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
map.name = "water"
|
||||||
|
map.value = cmd.sensorValue ? "wet" : "dry"
|
||||||
|
map.descriptionText = "${device.displayName} is ${map.value}"
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||||
|
def map = [:]
|
||||||
|
if(cmd.batteryLevel == 0xFF) {
|
||||||
|
map.name = "battery"
|
||||||
|
map.value = 1
|
||||||
|
map.descriptionText = "${device.displayName} has a low battery"
|
||||||
|
map.displayed = true
|
||||||
|
} else {
|
||||||
|
map.name = "battery"
|
||||||
|
map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1
|
||||||
|
map.unit = "%"
|
||||||
|
map.displayed = false
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd)
|
||||||
|
{
|
||||||
|
def map = [:]
|
||||||
|
if (cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_WATER) {
|
||||||
|
map.name = "water"
|
||||||
|
map.value = cmd.alarmLevel ? "wet" : "dry"
|
||||||
|
map.descriptionText = "${device.displayName} is ${map.value}"
|
||||||
|
}
|
||||||
|
if(cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_HEAT) {
|
||||||
|
map.name = "temperature"
|
||||||
|
if(cmd.zwaveAlarmEvent == 1) { map.value = "overheated"}
|
||||||
|
if(cmd.zwaveAlarmEvent == 2) { map.value = "overheated"}
|
||||||
|
if(cmd.zwaveAlarmEvent == 3) { map.value = "changing temperature rapidly"}
|
||||||
|
if(cmd.zwaveAlarmEvent == 4) { map.value = "changing temperature rapidly"}
|
||||||
|
if(cmd.zwaveAlarmEvent == 5) { map.value = "freezing"}
|
||||||
|
if(cmd.zwaveAlarmEvent == 6) { map.value = "freezing"}
|
||||||
|
if(cmd.zwaveAlarmEvent == 254) { map.value = "normal"}
|
||||||
|
map.descriptionText = "${device.displayName} is ${map.value}"
|
||||||
|
}
|
||||||
|
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd)
|
||||||
|
{
|
||||||
|
log.debug "COMMAND CLASS: $cmd"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
/**
|
||||||
|
* SmartSense Motion/Temp Sensor
|
||||||
|
*
|
||||||
|
* Copyright 2014 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Motion Sensor"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Battery"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
capability "Refresh"
|
||||||
|
|
||||||
|
command "enrollResponse"
|
||||||
|
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "active": "zone report :: type: 19 value: 0031"
|
||||||
|
status "inactive": "zone report :: type: 19 value: 0030"
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||||
|
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("motion", "device.motion", width: 2, 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") {
|
||||||
|
state("temperature", label:'${currentValue}°', unit:"F",
|
||||||
|
backgroundColors:[
|
||||||
|
[value: 31, color: "#153591"],
|
||||||
|
[value: 44, color: "#1e9cbb"],
|
||||||
|
[value: 59, color: "#90d2a7"],
|
||||||
|
[value: 74, color: "#44b621"],
|
||||||
|
[value: 84, color: "#f1d801"],
|
||||||
|
[value: 95, color: "#d04e00"],
|
||||||
|
[value: 96, color: "#bc2323"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery'
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main (["motion","temperature"])
|
||||||
|
details(["motion","temperature","battery","refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "description: $description"
|
||||||
|
|
||||||
|
Map map = [:]
|
||||||
|
if (description?.startsWith('catchall:')) {
|
||||||
|
map = parseCatchAllMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('read attr -')) {
|
||||||
|
map = parseReportAttributeMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('temperature: ')) {
|
||||||
|
map = parseCustomMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('zone status')) {
|
||||||
|
map = parseIasMessage(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug "Parse returned $map"
|
||||||
|
def result = map ? createEvent(map) : null
|
||||||
|
|
||||||
|
if (description?.startsWith('enroll request')) {
|
||||||
|
List cmds = enrollResponse()
|
||||||
|
log.debug "enroll response: ${cmds}"
|
||||||
|
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseCatchAllMessage(String description) {
|
||||||
|
Map resultMap = [:]
|
||||||
|
def cluster = zigbee.parse(description)
|
||||||
|
if (shouldProcessMessage(cluster)) {
|
||||||
|
switch(cluster.clusterId) {
|
||||||
|
case 0x0001:
|
||||||
|
resultMap = getBatteryResult(cluster.data.last())
|
||||||
|
break
|
||||||
|
|
||||||
|
case 0x0402:
|
||||||
|
// temp is last 2 data values. reverse to swap endian
|
||||||
|
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||||
|
def value = getTemperature(temp)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 0x0406:
|
||||||
|
log.debug 'motion'
|
||||||
|
resultMap.name = 'motion'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldProcessMessage(cluster) {
|
||||||
|
// 0x0B is default response indicating message got through
|
||||||
|
// 0x07 is bind message
|
||||||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
|
cluster.command == 0x0B ||
|
||||||
|
cluster.command == 0x07 ||
|
||||||
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
|
return !ignoredMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getHumidity(value) {
|
||||||
|
return Math.round(Double.parseDouble(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseReportAttributeMessage(String description) {
|
||||||
|
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
log.debug "Desc Map: $descMap"
|
||||||
|
|
||||||
|
Map resultMap = [:]
|
||||||
|
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||||
|
def value = getTemperature(descMap.value)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||||
|
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
|
||||||
|
def value = descMap.value.endsWith("01") ? "active" : "inactive"
|
||||||
|
resultMap = getMotionResult(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseCustomMessage(String description) {
|
||||||
|
Map resultMap = [:]
|
||||||
|
if (description?.startsWith('temperature: ')) {
|
||||||
|
def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseIasMessage(String description) {
|
||||||
|
List parsedMsg = description.split(' ')
|
||||||
|
String msgCode = parsedMsg[2]
|
||||||
|
|
||||||
|
Map resultMap = [:]
|
||||||
|
switch(msgCode) {
|
||||||
|
case '0x0020': // Closed/No Motion/Dry
|
||||||
|
resultMap = getMotionResult('inactive')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0021': // Open/Motion/Wet
|
||||||
|
resultMap = getMotionResult('active')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0022': // Tamper Alarm
|
||||||
|
log.debug 'motion with tamper alarm'
|
||||||
|
resultMap = getMotionResult('active')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0023': // Battery Alarm
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0024': // Supervision Report
|
||||||
|
log.debug 'no motion with tamper alarm'
|
||||||
|
resultMap = getMotionResult('inactive')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0025': // Restore Report
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0026': // Trouble/Failure
|
||||||
|
log.debug 'motion with failure alarm'
|
||||||
|
resultMap = getMotionResult('active')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0028': // Test Mode
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
def getTemperature(value) {
|
||||||
|
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||||
|
if(getTemperatureScale() == "C"){
|
||||||
|
return celsius
|
||||||
|
} else {
|
||||||
|
return celsiusToFahrenheit(celsius) as Integer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getBatteryResult(rawValue) {
|
||||||
|
log.debug 'Battery'
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
|
||||||
|
log.debug rawValue
|
||||||
|
|
||||||
|
def result = [
|
||||||
|
name: 'battery',
|
||||||
|
value: '--'
|
||||||
|
]
|
||||||
|
|
||||||
|
def volts = rawValue / 10
|
||||||
|
def descriptionText
|
||||||
|
|
||||||
|
if (rawValue == 0) {}
|
||||||
|
else {
|
||||||
|
if (volts > 3.5) {
|
||||||
|
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||||
|
}
|
||||||
|
else if (volts > 0){
|
||||||
|
def minVolts = 2.1
|
||||||
|
def maxVolts = 3.0
|
||||||
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
|
result.value = Math.min(100, (int) pct * 100)
|
||||||
|
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||||
|
}}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getTemperatureResult(value) {
|
||||||
|
log.debug 'TEMP'
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
if (tempOffset) {
|
||||||
|
def offset = tempOffset as int
|
||||||
|
def v = value as int
|
||||||
|
value = v + offset
|
||||||
|
}
|
||||||
|
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
||||||
|
return [
|
||||||
|
name: 'temperature',
|
||||||
|
value: value,
|
||||||
|
descriptionText: descriptionText
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getMotionResult(value) {
|
||||||
|
log.debug 'motion'
|
||||||
|
String linkText = getLinkText(device)
|
||||||
|
String descriptionText = value == 'active' ? "${linkText} detected motion" : "${linkText} motion has stopped"
|
||||||
|
return [
|
||||||
|
name: 'motion',
|
||||||
|
value: value,
|
||||||
|
descriptionText: descriptionText
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh()
|
||||||
|
{
|
||||||
|
log.debug "refresh called"
|
||||||
|
[
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
|
||||||
|
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||||
|
log.debug "Confuguring Reporting, IAS CIE, and Bindings."
|
||||||
|
def configCmds = [
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500",
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500",
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x001 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}"
|
||||||
|
|
||||||
|
|
||||||
|
]
|
||||||
|
return configCmds + enrollResponse() + refresh() // send refresh cmds as part of config
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEndpointId() {
|
||||||
|
new BigInteger(device.endpointId, 16).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
def enrollResponse() {
|
||||||
|
log.debug "Sending enroll response"
|
||||||
|
[
|
||||||
|
|
||||||
|
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}"
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value) {
|
||||||
|
new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
int i = 0;
|
||||||
|
int j = array.length - 1;
|
||||||
|
byte tmp;
|
||||||
|
while (j > i) {
|
||||||
|
tmp = array[j];
|
||||||
|
array[j] = array[i];
|
||||||
|
array[i] = tmp;
|
||||||
|
j--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
/**
|
||||||
|
* SmartSense Motion/Temp Sensor
|
||||||
|
*
|
||||||
|
* Copyright 2014 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "SmartSense Motion/Temp Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Motion Sensor"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Battery"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
capability "Refresh"
|
||||||
|
|
||||||
|
command "enrollResponse"
|
||||||
|
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "active": "zone report :: type: 19 value: 0031"
|
||||||
|
status "inactive": "zone report :: type: 19 value: 0030"
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||||
|
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("motion", "device.motion", width: 2, 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") {
|
||||||
|
state("temperature", label:'${currentValue}°', unit:"F",
|
||||||
|
backgroundColors:[
|
||||||
|
[value: 31, color: "#153591"],
|
||||||
|
[value: 44, color: "#1e9cbb"],
|
||||||
|
[value: 59, color: "#90d2a7"],
|
||||||
|
[value: 74, color: "#44b621"],
|
||||||
|
[value: 84, color: "#f1d801"],
|
||||||
|
[value: 95, color: "#d04e00"],
|
||||||
|
[value: 96, color: "#bc2323"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery'
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main (["motion","temperature"])
|
||||||
|
details(["motion","temperature","battery","refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "description: $description"
|
||||||
|
|
||||||
|
Map map = [:]
|
||||||
|
if (description?.startsWith('catchall:')) {
|
||||||
|
map = parseCatchAllMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('read attr -')) {
|
||||||
|
map = parseReportAttributeMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('temperature: ')) {
|
||||||
|
map = parseCustomMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('zone status')) {
|
||||||
|
map = parseIasMessage(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug "Parse returned $map"
|
||||||
|
def result = map ? createEvent(map) : null
|
||||||
|
|
||||||
|
if (description?.startsWith('enroll request')) {
|
||||||
|
List cmds = enrollResponse()
|
||||||
|
log.debug "enroll response: ${cmds}"
|
||||||
|
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseCatchAllMessage(String description) {
|
||||||
|
Map resultMap = [:]
|
||||||
|
def cluster = zigbee.parse(description)
|
||||||
|
if (shouldProcessMessage(cluster)) {
|
||||||
|
switch(cluster.clusterId) {
|
||||||
|
case 0x0001:
|
||||||
|
resultMap = getBatteryResult(cluster.data.last())
|
||||||
|
break
|
||||||
|
|
||||||
|
case 0x0402:
|
||||||
|
// temp is last 2 data values. reverse to swap endian
|
||||||
|
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||||
|
def value = getTemperature(temp)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 0x0406:
|
||||||
|
log.debug 'motion'
|
||||||
|
resultMap.name = 'motion'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldProcessMessage(cluster) {
|
||||||
|
// 0x0B is default response indicating message got through
|
||||||
|
// 0x07 is bind message
|
||||||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
|
cluster.command == 0x0B ||
|
||||||
|
cluster.command == 0x07 ||
|
||||||
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
|
return !ignoredMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getHumidity(value) {
|
||||||
|
return Math.round(Double.parseDouble(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseReportAttributeMessage(String description) {
|
||||||
|
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
log.debug "Desc Map: $descMap"
|
||||||
|
|
||||||
|
Map resultMap = [:]
|
||||||
|
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||||
|
def value = getTemperature(descMap.value)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||||
|
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
|
||||||
|
def value = descMap.value.endsWith("01") ? "active" : "inactive"
|
||||||
|
resultMap = getMotionResult(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseCustomMessage(String description) {
|
||||||
|
Map resultMap = [:]
|
||||||
|
if (description?.startsWith('temperature: ')) {
|
||||||
|
def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseIasMessage(String description) {
|
||||||
|
List parsedMsg = description.split(' ')
|
||||||
|
String msgCode = parsedMsg[2]
|
||||||
|
|
||||||
|
Map resultMap = [:]
|
||||||
|
switch(msgCode) {
|
||||||
|
case '0x0020': // Closed/No Motion/Dry
|
||||||
|
resultMap = getMotionResult('inactive')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0021': // Open/Motion/Wet
|
||||||
|
resultMap = getMotionResult('active')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0022': // Tamper Alarm
|
||||||
|
log.debug 'motion with tamper alarm'
|
||||||
|
resultMap = getMotionResult('active')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0023': // Battery Alarm
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0024': // Supervision Report
|
||||||
|
log.debug 'no motion with tamper alarm'
|
||||||
|
resultMap = getMotionResult('inactive')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0025': // Restore Report
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0026': // Trouble/Failure
|
||||||
|
log.debug 'motion with failure alarm'
|
||||||
|
resultMap = getMotionResult('active')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0028': // Test Mode
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
def getTemperature(value) {
|
||||||
|
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||||
|
if(getTemperatureScale() == "C"){
|
||||||
|
return celsius
|
||||||
|
} else {
|
||||||
|
return celsiusToFahrenheit(celsius) as Integer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getBatteryResult(rawValue) {
|
||||||
|
log.debug 'Battery'
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
|
||||||
|
def result = [
|
||||||
|
name: 'battery'
|
||||||
|
]
|
||||||
|
|
||||||
|
def volts = rawValue / 10
|
||||||
|
def descriptionText
|
||||||
|
if (volts > 3.5) {
|
||||||
|
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
def minVolts = 2.1
|
||||||
|
def maxVolts = 3.0
|
||||||
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
|
result.value = Math.min(100, (int) pct * 100)
|
||||||
|
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getTemperatureResult(value) {
|
||||||
|
log.debug 'TEMP'
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
if (tempOffset) {
|
||||||
|
def offset = tempOffset as int
|
||||||
|
def v = value as int
|
||||||
|
value = v + offset
|
||||||
|
}
|
||||||
|
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
||||||
|
return [
|
||||||
|
name: 'temperature',
|
||||||
|
value: value,
|
||||||
|
descriptionText: descriptionText
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getMotionResult(value) {
|
||||||
|
log.debug 'motion'
|
||||||
|
String linkText = getLinkText(device)
|
||||||
|
String descriptionText = value == 'active' ? "${linkText} detected motion" : "${linkText} motion has stopped"
|
||||||
|
return [
|
||||||
|
name: 'motion',
|
||||||
|
value: value,
|
||||||
|
descriptionText: descriptionText
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh()
|
||||||
|
{
|
||||||
|
log.debug "refresh called"
|
||||||
|
[
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
|
||||||
|
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||||
|
log.debug "Confuguring Reporting, IAS CIE, and Bindings."
|
||||||
|
def configCmds = [
|
||||||
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x001 {${device.zigbeeId}} {}", "delay 1500",
|
||||||
|
|
||||||
|
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
]
|
||||||
|
return configCmds + refresh() // send refresh cmds as part of config
|
||||||
|
}
|
||||||
|
|
||||||
|
def enrollResponse() {
|
||||||
|
log.debug "Sending enroll response"
|
||||||
|
[
|
||||||
|
|
||||||
|
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1"
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value) {
|
||||||
|
new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
int i = 0;
|
||||||
|
int j = array.length - 1;
|
||||||
|
byte tmp;
|
||||||
|
while (j > i) {
|
||||||
|
tmp = array[j];
|
||||||
|
array[j] = array[i];
|
||||||
|
array[i] = tmp;
|
||||||
|
j--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "SmartSense Motion", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Signal Strength"
|
||||||
|
capability "Motion Sensor"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Battery"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", deviceId: "013A", inClusters: "0000", outClusters: "0006"
|
||||||
|
fingerprint profileId: "FC01", deviceId: "013A"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "active": "zone report :: type: 19 value: 0031"
|
||||||
|
status "inactive": "zone report :: type: 19 value: 0030"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("motion", "device.motion", width: 2, 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("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""/*, backgroundColors:[
|
||||||
|
[value: 5, color: "#BC2323"],
|
||||||
|
[value: 10, color: "#D04E00"],
|
||||||
|
[value: 15, color: "#F1D801"],
|
||||||
|
[value: 16, color: "#FFFFFF"]
|
||||||
|
]*/
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
valueTile("lqi", "device.lqi", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "lqi", label:'${currentValue}% signal', unit:""
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
main "motion"
|
||||||
|
details(["motion", "battery"/*, "lqi"*/])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def results
|
||||||
|
if (isZoneType19(description) || !isSupportedDescription(description)) {
|
||||||
|
results = parseBasicMessage(description)
|
||||||
|
}
|
||||||
|
else if (isMotionStatusMessage(description)){
|
||||||
|
results = parseMotionStatusMessage(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseBasicMessage(description) {
|
||||||
|
def name = parseName(description)
|
||||||
|
def value = parseValue(description)
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = parseDescriptionText(linkText, value, description)
|
||||||
|
def handlerName = value
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
def results = [
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: handlerName,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: displayed(description, isStateChange)
|
||||||
|
]
|
||||||
|
log.debug "Parse returned $results.descriptionText"
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseName(String description) {
|
||||||
|
if (isSupportedDescription(description)) {
|
||||||
|
return "motion"
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseValue(String description) {
|
||||||
|
if (isZoneType19(description)) {
|
||||||
|
if (translateStatusZoneType19(description)) {
|
||||||
|
return "active"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "inactive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
description
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseDescriptionText(String linkText, String value, String description) {
|
||||||
|
switch(value) {
|
||||||
|
case "active": return "$linkText detected motion"
|
||||||
|
case "inactive": return "$linkText motion has stopped"
|
||||||
|
default: return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean isMotionStatusMessage(String description) {
|
||||||
|
// "raw:7D360000001D55FF, dni:7D36, motion:00, battery:00, powerSource:00, rssi:1D, lqi:55, other:FF" - old (incorrect dev-conn parse)
|
||||||
|
// "raw:7D360000001D55FF, dni:7D36, motion:00, powerSource:0000, battery:00, rssi:1D, lqi:55, other:FF" - old (correct dev-conn parse)
|
||||||
|
// "raw:7D360000001D55FF, dni:7D36, motion:00, powerSource:00, battery:00, batteryDivisor:00, rssi:1D, lqi:55, other:FF" - new
|
||||||
|
description ==~ /raw:.*dni:.*motion:.*battery:.*powerSource:.*rssi:.*lqi:.*/ || description ==~ /raw:.*dni:.*motion:.*powerSource:.*battery:.*rssi:.*lqi:.*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseMotionStatusMessage(String description) {
|
||||||
|
def results = []
|
||||||
|
def parts = description.split(',')
|
||||||
|
parts.each { part ->
|
||||||
|
part = part.trim()
|
||||||
|
if (part.startsWith('motion:')) {
|
||||||
|
def motionResult = getMotionResult(part, description)
|
||||||
|
if (motionResult) {
|
||||||
|
results << motionResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (part.startsWith('powerSource:')) {
|
||||||
|
def powerSourceResult = getPowerSourceResult(part, description)
|
||||||
|
if (powerSourceResult) {
|
||||||
|
results << powerSourceResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (part.startsWith('battery:')) {
|
||||||
|
def batteryResult = getBatteryResult(part, description)
|
||||||
|
if (batteryResult) {
|
||||||
|
results << batteryResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (part.startsWith('rssi:')) {
|
||||||
|
def rssiResult = getRssiResult(part, description)
|
||||||
|
if (rssiResult) {
|
||||||
|
results << rssiResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lqi:')) {
|
||||||
|
def lqiResult = getLqiResult(part, description)
|
||||||
|
if (lqiResult) {
|
||||||
|
results << lqiResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMotionResult(part, description) {
|
||||||
|
def name = "motion"
|
||||||
|
def valueString = part.split(":")[1].trim()
|
||||||
|
def valueInt = Integer.parseInt(valueString, 16)
|
||||||
|
def value = valueInt == 0 ? "inactive" : "active"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = parseDescriptionText(linkText, value, description)
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: value,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: displayed(description, isStateChange)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPowerSourceResult(part, description) {
|
||||||
|
def name = "powerSource"
|
||||||
|
def valueString = part.split(":")[1].trim()
|
||||||
|
def valueInt = Integer.parseInt(valueString, 16)
|
||||||
|
def value = valueInt == 0 ? "battery" : "powered"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText
|
||||||
|
if (value == "battery") {
|
||||||
|
descriptionText = "$linkText is ${value} powered"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
descriptionText = "$linkText is plugged in"
|
||||||
|
}
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBatteryResult(part, description) {
|
||||||
|
def name = "battery"
|
||||||
|
def valueString = part.split(":")[1].trim()
|
||||||
|
def valueInt = Integer.parseInt(valueString, 16)
|
||||||
|
|
||||||
|
// Temporarily disallow zero as a valid result b/c the current firmware has a bug where zero is only value being sent
|
||||||
|
// This effectively disables battery reporting for this device, so needs to be removed once FW is updated
|
||||||
|
if (valueInt == 0) return null
|
||||||
|
|
||||||
|
|
||||||
|
def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null
|
||||||
|
def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor)
|
||||||
|
def unit = "%"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText battery was ${value}${unit}"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: unit,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRssiResult(part, description) {
|
||||||
|
def name = "rssi"
|
||||||
|
def parts = part.split(":")
|
||||||
|
if (parts.size() != 2) return null
|
||||||
|
|
||||||
|
def valueString = parts[1].trim()
|
||||||
|
def valueInt = Integer.parseInt(valueString, 16)
|
||||||
|
def value = (valueInt - 128).toString()
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText was $value dBm"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: "dBm",
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: null,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use LQI (Link Quality Indicator) as a measure of signal strength. The values
|
||||||
|
* are 0 to 255 (0x00 to 0xFF) and higher values represent higher signal
|
||||||
|
* strength. Return as a percentage of 255.
|
||||||
|
*
|
||||||
|
* Note: To make the signal strength indicator more accurate, we could combine
|
||||||
|
* LQI with RSSI.
|
||||||
|
*/
|
||||||
|
private getLqiResult(part, description) {
|
||||||
|
def name = "lqi"
|
||||||
|
def parts = part.split(":")
|
||||||
|
if (parts.size() != 2) return null
|
||||||
|
|
||||||
|
def valueString = parts[1].trim()
|
||||||
|
def valueInt = Integer.parseInt(valueString, 16)
|
||||||
|
def percentageOf = 255
|
||||||
|
def value = Math.round((valueInt / percentageOf * 100)).toString()
|
||||||
|
def unit = "%"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText Signal (LQI) was ${value}${unit}"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: unit,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: null,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,485 @@
|
|||||||
|
/**
|
||||||
|
* SmartSense Multi
|
||||||
|
*
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
|
capability "Three Axis"
|
||||||
|
capability "Battery"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Contact Sensor"
|
||||||
|
capability "Acceleration Sensor"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
|
||||||
|
command "enrollResponse"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "open": "zone report :: type: 19 value: 0031"
|
||||||
|
status "closed": "zone report :: type: 19 value: 0030"
|
||||||
|
|
||||||
|
status "acceleration": "acceleration: 1"
|
||||||
|
status "no acceleration": "acceleration: 0"
|
||||||
|
|
||||||
|
for (int i = 10; i <= 50; i += 10) {
|
||||||
|
status "temp ${i}C": "contactState: 0, accelerationState: 0, temp: $i C, battery: 100"
|
||||||
|
}
|
||||||
|
|
||||||
|
// kinda hacky because it depends on how it is installed
|
||||||
|
status "x,y,z: 0,0,0": "x: 0, y: 0, z: 0"
|
||||||
|
status "x,y,z: 1000,0,0": "x: 1000, y: 0, z: 0"
|
||||||
|
status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0"
|
||||||
|
status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000"
|
||||||
|
}
|
||||||
|
preferences {
|
||||||
|
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||||
|
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("contact", "device.contact", width: 2, height: 2) {
|
||||||
|
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||||
|
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||||
|
}
|
||||||
|
standardTile("acceleration", "device.acceleration") {
|
||||||
|
state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff")
|
||||||
|
state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0")
|
||||||
|
}
|
||||||
|
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("3axis", "device.threeAxis", decoration: "flat", wordWrap: false) {
|
||||||
|
state("threeAxis", label:'${currentValue}', unit:"", backgroundColor:"#ffffff")
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
main (["contact", "acceleration", "temperature"])
|
||||||
|
details(["contact","acceleration", "temperature", "3axis", "battery", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
|
||||||
|
Map map = [:]
|
||||||
|
if (description?.startsWith('catchall:')) {
|
||||||
|
map = parseCatchAllMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('read attr -')) {
|
||||||
|
map = parseReportAttributeMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('temperature: ')) {
|
||||||
|
map = parseCustomMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('zone status')) {
|
||||||
|
map = parseIasMessage(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
def result = map ? createEvent(map) : null
|
||||||
|
|
||||||
|
if (description?.startsWith('enroll request')) {
|
||||||
|
List cmds = enrollResponse()
|
||||||
|
log.debug "enroll response: ${cmds}"
|
||||||
|
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseCatchAllMessage(String description) {
|
||||||
|
Map resultMap = [:]
|
||||||
|
def cluster = zigbee.parse(description)
|
||||||
|
log.debug cluster
|
||||||
|
if (shouldProcessMessage(cluster)) {
|
||||||
|
switch(cluster.clusterId) {
|
||||||
|
case 0x0001:
|
||||||
|
resultMap = getBatteryResult(cluster.data.last())
|
||||||
|
break
|
||||||
|
|
||||||
|
case 0xFC02:
|
||||||
|
log.debug 'ACCELERATION'
|
||||||
|
break
|
||||||
|
|
||||||
|
case 0x0402:
|
||||||
|
log.debug 'TEMP'
|
||||||
|
// temp is last 2 data values. reverse to swap endian
|
||||||
|
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||||
|
def value = getTemperature(temp)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldProcessMessage(cluster) {
|
||||||
|
// 0x0B is default response indicating message got through
|
||||||
|
// 0x07 is bind message
|
||||||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
|
cluster.command == 0x0B ||
|
||||||
|
cluster.command == 0x07 ||
|
||||||
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
|
return !ignoredMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getHumidity(value) {
|
||||||
|
return Math.round(Double.parseDouble(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseReportAttributeMessage(String description) {
|
||||||
|
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
|
||||||
|
Map resultMap = [:]
|
||||||
|
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||||
|
def value = getTemperature(descMap.value)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "FC02" && descMap.attrId == "0010") {
|
||||||
|
resultMap = getAccelerationResult(descMap.value)
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "FC02" && descMap.attrId == "0012") {
|
||||||
|
resultMap = parseAxis(descMap.value)
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||||
|
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseCustomMessage(String description) {
|
||||||
|
Map resultMap = [:]
|
||||||
|
if (description?.startsWith('temperature: ')) {
|
||||||
|
def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseIasMessage(String description) {
|
||||||
|
List parsedMsg = description.split(' ')
|
||||||
|
String msgCode = parsedMsg[2]
|
||||||
|
|
||||||
|
Map resultMap = [:]
|
||||||
|
switch(msgCode) {
|
||||||
|
case '0x0020': // Closed/No Motion/Dry
|
||||||
|
resultMap = getContactResult('closed')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0021': // Open/Motion/Wet
|
||||||
|
resultMap = getContactResult('open')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0022': // Tamper Alarm
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0023': // Battery Alarm
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0024': // Supervision Report
|
||||||
|
resultMap = getContactResult('closed')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0025': // Restore Report
|
||||||
|
resultMap = getContactResult('open')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0026': // Trouble/Failure
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0028': // Test Mode
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
def getTemperature(value) {
|
||||||
|
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||||
|
if(getTemperatureScale() == "C"){
|
||||||
|
return celsius
|
||||||
|
} else {
|
||||||
|
return celsiusToFahrenheit(celsius) as Integer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getBatteryResult(rawValue) {
|
||||||
|
log.debug "Battery"
|
||||||
|
log.debug rawValue
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
|
||||||
|
def result = [
|
||||||
|
name: 'battery',
|
||||||
|
value: '--'
|
||||||
|
]
|
||||||
|
|
||||||
|
def volts = rawValue / 10
|
||||||
|
def descriptionText
|
||||||
|
|
||||||
|
if (rawValue == 255) {}
|
||||||
|
else {
|
||||||
|
|
||||||
|
if (volts > 3.5) {
|
||||||
|
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
def minVolts = 2.1
|
||||||
|
def maxVolts = 3.0
|
||||||
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
|
result.value = Math.min(100, (int) pct * 100)
|
||||||
|
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||||
|
}}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getTemperatureResult(value) {
|
||||||
|
log.debug "Temperature"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
if (tempOffset) {
|
||||||
|
def offset = tempOffset as int
|
||||||
|
def v = value as int
|
||||||
|
value = v + offset
|
||||||
|
}
|
||||||
|
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
||||||
|
return [
|
||||||
|
name: 'temperature',
|
||||||
|
value: value,
|
||||||
|
descriptionText: descriptionText
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getContactResult(value) {
|
||||||
|
log.debug "Contact"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
|
||||||
|
return [
|
||||||
|
name: 'contact',
|
||||||
|
value: value,
|
||||||
|
descriptionText: descriptionText
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAccelerationResult(numValue) {
|
||||||
|
log.debug "Acceleration"
|
||||||
|
def name = "acceleration"
|
||||||
|
def value = numValue.endsWith("1") ? "active" : "inactive"
|
||||||
|
//def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "was $value"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
isStateChange: isStateChange
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh()
|
||||||
|
{
|
||||||
|
log.debug "Refreshing Values "
|
||||||
|
[
|
||||||
|
|
||||||
|
/* sensitivity - default value (8) */
|
||||||
|
|
||||||
|
"zcl mfg-code 0x104E", "delay 200",
|
||||||
|
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||||
|
|
||||||
|
"zcl mfg-code 0x104E", "delay 200",
|
||||||
|
"zcl global read 0xFC02 0x0000", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
||||||
|
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
|
||||||
|
|
||||||
|
"zcl mfg-code 0x104E", "delay 200",
|
||||||
|
"zcl global read 0xFC02 0x0010", "delay 100",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
||||||
|
|
||||||
|
"zcl mfg-code 0x104E", "delay 200",
|
||||||
|
"zcl global read 0xFC02 0x0012", "delay 100",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
||||||
|
|
||||||
|
"zcl mfg-code 0x104E", "delay 200",
|
||||||
|
"zcl global read 0xFC02 0x0013", "delay 100",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
||||||
|
|
||||||
|
"zcl mfg-code 0x104E", "delay 200",
|
||||||
|
"zcl global read 0xFC02 0x0014", "delay 100",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1"
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
|
||||||
|
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||||
|
log.debug "Configuring Reporting"
|
||||||
|
|
||||||
|
def configCmds = [
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0xFC02 {${device.zigbeeId}} {}", "delay 200",
|
||||||
|
"zcl mfg-code 0x104E",
|
||||||
|
"zcl global send-me-a-report 0xFC02 0x0010 0x18 300 3600 {01}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
|
"zcl mfg-code 0x104E",
|
||||||
|
"zcl global send-me-a-report 0xFC02 0x0012 0x29 300 3600 {01}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
|
"zcl mfg-code 0x104E",
|
||||||
|
"zcl global send-me-a-report 0xFC02 0x0013 0x29 300 3600 {01}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
|
"zcl mfg-code 0x104E",
|
||||||
|
"zcl global send-me-a-report 0xFC02 0x0014 0x29 300 3600 {01}",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
return configCmds + refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEndpointId() {
|
||||||
|
new BigInteger(device.endpointId, 16).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
def enrollResponse() {
|
||||||
|
log.debug "Sending enroll response"
|
||||||
|
[
|
||||||
|
|
||||||
|
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1"
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Map parseAxis(String description) {
|
||||||
|
log.debug "parseAxis"
|
||||||
|
def xyzResults = [x: 0, y: 0, z: 0]
|
||||||
|
def parts = description.split("2900")
|
||||||
|
parts[0] = "12" + parts[0]
|
||||||
|
parts.each { part ->
|
||||||
|
part = part.trim()
|
||||||
|
if (part.startsWith("12")) {
|
||||||
|
def unsignedX = hexToInt(part.split("12")[1].trim())
|
||||||
|
def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX
|
||||||
|
xyzResults.x = signedX
|
||||||
|
log.debug "X Part: ${signedX}"
|
||||||
|
}
|
||||||
|
else if (part.startsWith("13")) {
|
||||||
|
def unsignedY = hexToInt(part.split("13")[1].trim())
|
||||||
|
def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY
|
||||||
|
xyzResults.y = signedY
|
||||||
|
log.debug "Y Part: ${signedY}"
|
||||||
|
}
|
||||||
|
else if (part.startsWith("14")) {
|
||||||
|
def unsignedZ = hexToInt(part.split("14")[1].trim())
|
||||||
|
def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ
|
||||||
|
xyzResults.z = signedZ
|
||||||
|
log.debug "Z Part: ${signedZ}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getXyzResult(xyzResults, description)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private Map getXyzResult(results, description) {
|
||||||
|
def name = "threeAxis"
|
||||||
|
def value = "${results.x},${results.y},${results.z}"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText was $value"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: null,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private hexToInt(value) {
|
||||||
|
new BigInteger(value, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private hex(value) {
|
||||||
|
new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
int i = 0;
|
||||||
|
int j = array.length - 1;
|
||||||
|
byte tmp;
|
||||||
|
while (j > i) {
|
||||||
|
tmp = array[j];
|
||||||
|
array[j] = array[i];
|
||||||
|
array[i] = tmp;
|
||||||
|
j--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,487 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "SmartSense Multi", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Three Axis"
|
||||||
|
capability "Contact Sensor"
|
||||||
|
capability "Acceleration Sensor"
|
||||||
|
capability "Signal Strength"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Battery"
|
||||||
|
|
||||||
|
fingerprint profileId: "FC01", deviceId: "0139"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "open": "zone report :: type: 19 value: 0031"
|
||||||
|
status "closed": "zone report :: type: 19 value: 0030"
|
||||||
|
|
||||||
|
status "acceleration": "acceleration: 1, rssi: 0, lqi: 0"
|
||||||
|
status "no acceleration": "acceleration: 0, rssi: 0, lqi: 0"
|
||||||
|
|
||||||
|
for (int i = 10; i <= 50; i += 10) {
|
||||||
|
status "temp ${i}C": "contactState: 0, accelerationState: 0, temp: $i C, battery: 100, rssi: 100, lqi: 255"
|
||||||
|
}
|
||||||
|
|
||||||
|
// kinda hacky because it depends on how it is installed
|
||||||
|
status "x,y,z: 0,0,0": "x: 0, y: 0, z: 0, rssi: 100, lqi: 255"
|
||||||
|
status "x,y,z: 1000,0,0": "x: 1000, y: 0, z: 0, rssi: 100, lqi: 255"
|
||||||
|
status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0, rssi: 100, lqi: 255"
|
||||||
|
status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000, rssi: 100, lqi: 255"
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||||
|
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("contact", "device.contact", width: 2, height: 2) {
|
||||||
|
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||||
|
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||||
|
}
|
||||||
|
standardTile("acceleration", "device.acceleration") {
|
||||||
|
state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0")
|
||||||
|
state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff")
|
||||||
|
}
|
||||||
|
valueTile("temperature", "device.temperature") {
|
||||||
|
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("3axis", "device.threeAxis", decoration: "flat", wordWrap: false) {
|
||||||
|
state("threeAxis", label:'${currentValue}', unit:"", backgroundColor:"#ffffff")
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""/*, backgroundColors:[
|
||||||
|
[value: 5, color: "#BC2323"],
|
||||||
|
[value: 10, color: "#D04E00"],
|
||||||
|
[value: 15, color: "#F1D801"],
|
||||||
|
[value: 16, color: "#FFFFFF"]
|
||||||
|
]*/
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
valueTile("lqi", "device.lqi", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "lqi", label:'${currentValue}% signal', unit:""
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
main(["contact", "acceleration", "temperature"])
|
||||||
|
details(["contact", "acceleration", "temperature", "3axis", "battery"/*, "lqi"*/])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
def results
|
||||||
|
|
||||||
|
if (!isSupportedDescription(description) || zigbee.isZoneType19(description)) {
|
||||||
|
results = parseSingleMessage(description)
|
||||||
|
}
|
||||||
|
else if (description == 'updated') {
|
||||||
|
//TODO is there a better way to handle this like the other device types?
|
||||||
|
results = parseOtherMessage(description)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
results = parseMultiSensorMessage(description)
|
||||||
|
}
|
||||||
|
log.debug "Parse returned $results.descriptionText"
|
||||||
|
return results
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseSingleMessage(description) {
|
||||||
|
|
||||||
|
def name = parseName(description)
|
||||||
|
def value = parseValue(description)
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = parseDescriptionText(linkText, value, description)
|
||||||
|
def handlerName = value == 'open' ? 'opened' : value
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
def results = [
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: null,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: handlerName,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: displayed(description, isStateChange)
|
||||||
|
]
|
||||||
|
log.debug "Parse results for $device: $results"
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO this just to handle 'updated' for now - investigate better way to do this
|
||||||
|
private Map parseOtherMessage(description) {
|
||||||
|
def name = null
|
||||||
|
def value = description
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = description
|
||||||
|
def handlerName = description
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
def results = [
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: null,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: handlerName,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: displayed(description, isStateChange)
|
||||||
|
]
|
||||||
|
log.debug "Parse results for $device: $results"
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseMultiSensorMessage(description) {
|
||||||
|
def results = []
|
||||||
|
if (isAccelerationMessage(description)) {
|
||||||
|
results = parseAccelerationMessage(description)
|
||||||
|
}
|
||||||
|
else if (isContactMessage(description)) {
|
||||||
|
results = parseContactMessage(description)
|
||||||
|
}
|
||||||
|
else if (isRssiLqiMessage(description)) {
|
||||||
|
results = parseRssiLqiMessage(description)
|
||||||
|
}
|
||||||
|
else if (isOrientationMessage(description)) {
|
||||||
|
results = parseOrientationMessage(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseAccelerationMessage(String description) {
|
||||||
|
def results = []
|
||||||
|
def parts = description.split(',')
|
||||||
|
parts.each { part ->
|
||||||
|
part = part.trim()
|
||||||
|
if (part.startsWith('acceleration:')) {
|
||||||
|
results << getAccelerationResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('rssi:')) {
|
||||||
|
results << getRssiResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lqi:')) {
|
||||||
|
results << getLqiResult(part, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseContactMessage(String description) {
|
||||||
|
def results = []
|
||||||
|
def parts = description.split(',')
|
||||||
|
parts.each { part ->
|
||||||
|
part = part.trim()
|
||||||
|
if (part.startsWith('contactState:')) {
|
||||||
|
results << getContactResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('accelerationState:')) {
|
||||||
|
results << getAccelerationResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('temp:')) {
|
||||||
|
results << getTempResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('battery:')) {
|
||||||
|
results << getBatteryResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('rssi:')) {
|
||||||
|
results << getRssiResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lqi:')) {
|
||||||
|
results << getLqiResult(part, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseOrientationMessage(String description) {
|
||||||
|
def results = []
|
||||||
|
def xyzResults = [x: 0, y: 0, z: 0]
|
||||||
|
def parts = description.split(',')
|
||||||
|
parts.each { part ->
|
||||||
|
part = part.trim()
|
||||||
|
if (part.startsWith('x:')) {
|
||||||
|
def unsignedX = part.split(":")[1].trim().toInteger()
|
||||||
|
def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX
|
||||||
|
xyzResults.x = signedX
|
||||||
|
}
|
||||||
|
else if (part.startsWith('y:')) {
|
||||||
|
def unsignedY = part.split(":")[1].trim().toInteger()
|
||||||
|
def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY
|
||||||
|
xyzResults.y = signedY
|
||||||
|
}
|
||||||
|
else if (part.startsWith('z:')) {
|
||||||
|
def unsignedZ = part.split(":")[1].trim().toInteger()
|
||||||
|
def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ
|
||||||
|
xyzResults.z = signedZ
|
||||||
|
}
|
||||||
|
else if (part.startsWith('rssi:')) {
|
||||||
|
results << getRssiResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lqi:')) {
|
||||||
|
results << getLqiResult(part, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results << getXyzResult(xyzResults, description)
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseRssiLqiMessage(String description) {
|
||||||
|
def results = []
|
||||||
|
// "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255"
|
||||||
|
def parts = description.split(',')
|
||||||
|
parts.each { part ->
|
||||||
|
part = part.trim()
|
||||||
|
if (part.startsWith('lastHopRssi:')) {
|
||||||
|
results << getRssiResult(part, description, true)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lastHopLqi:')) {
|
||||||
|
results << getLqiResult(part, description, true)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('rssi:')) {
|
||||||
|
results << getRssiResult(part, description)
|
||||||
|
}
|
||||||
|
else if (part.startsWith('lqi:')) {
|
||||||
|
results << getLqiResult(part, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private getContactResult(part, description) {
|
||||||
|
def name = "contact"
|
||||||
|
def value = part.endsWith("1") ? "open" : "closed"
|
||||||
|
def handlerName = value == 'open' ? 'opened' : value
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText was $handlerName"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: null,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: handlerName,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: displayed(description, isStateChange)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAccelerationResult(part, description) {
|
||||||
|
def name = "acceleration"
|
||||||
|
def value = part.endsWith("1") ? "active" : "inactive"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText was $value"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: null,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: value,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: displayed(description, isStateChange)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTempResult(part, description) {
|
||||||
|
def name = "temperature"
|
||||||
|
def temperatureScale = getTemperatureScale()
|
||||||
|
def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale)
|
||||||
|
if (tempOffset) {
|
||||||
|
def offset = tempOffset as int
|
||||||
|
def v = value as int
|
||||||
|
value = v + offset
|
||||||
|
}
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText was $value°$temperatureScale"
|
||||||
|
def isStateChange = isTemperatureStateChange(device, name, value.toString())
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: temperatureScale,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: displayed(description, isStateChange)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getXyzResult(results, description) {
|
||||||
|
def name = "threeAxis"
|
||||||
|
def value = "${results.x},${results.y},${results.z}"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText was $value"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: null,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBatteryResult(part, description) {
|
||||||
|
def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null
|
||||||
|
def name = "battery"
|
||||||
|
def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor)
|
||||||
|
def unit = "%"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText Battery was ${value}${unit}"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: unit,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: name,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRssiResult(part, description, lastHop=false) {
|
||||||
|
def name = lastHop ? "lastHopRssi" : "rssi"
|
||||||
|
def valueString = part.split(":")[1].trim()
|
||||||
|
def value = (Integer.parseInt(valueString) - 128).toString()
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText was $value dBm"
|
||||||
|
if (lastHop) {
|
||||||
|
descriptionText += " on the last hop"
|
||||||
|
}
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: "dBm",
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: null,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use LQI (Link Quality Indicator) as a measure of signal strength. The values
|
||||||
|
* are 0 to 255 (0x00 to 0xFF) and higher values represent higher signal
|
||||||
|
* strength. Return as a percentage of 255.
|
||||||
|
*
|
||||||
|
* Note: To make the signal strength indicator more accurate, we could combine
|
||||||
|
* LQI with RSSI.
|
||||||
|
*/
|
||||||
|
private getLqiResult(part, description, lastHop=false) {
|
||||||
|
def name = lastHop ? "lastHopLqi" : "lqi"
|
||||||
|
def valueString = part.split(":")[1].trim()
|
||||||
|
def percentageOf = 255
|
||||||
|
def value = Math.round((Integer.parseInt(valueString) / percentageOf * 100)).toString()
|
||||||
|
def unit = "%"
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText Signal (LQI) was: ${value}${unit}"
|
||||||
|
if (lastHop) {
|
||||||
|
descriptionText += " on the last hop"
|
||||||
|
}
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
unit: unit,
|
||||||
|
linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
handlerName: null,
|
||||||
|
isStateChange: isStateChange,
|
||||||
|
displayed: false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean isAccelerationMessage(String description) {
|
||||||
|
// "acceleration: 1, rssi: 91, lqi: 255"
|
||||||
|
description ==~ /acceleration:.*rssi:.*lqi:.*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean isContactMessage(String description) {
|
||||||
|
// "contactState: 1, accelerationState: 0, temp: 14.4 C, battery: 28, rssi: 59, lqi: 255"
|
||||||
|
description ==~ /contactState:.*accelerationState:.*temp:.*battery:.*rssi:.*lqi:.*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean isRssiLqiMessage(String description) {
|
||||||
|
// "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255"
|
||||||
|
description ==~ /lastHopRssi:.*lastHopLqi:.*rssi:.*lqi:.*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean isOrientationMessage(String description) {
|
||||||
|
// "x: 0, y: 33, z: 1017, rssi: 102, lqi: 255"
|
||||||
|
description ==~ /x:.*y:.*z:.*rssi:.*lqi:.*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseName(String description) {
|
||||||
|
if (isSupportedDescription(description)) {
|
||||||
|
return "contact"
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseValue(String description) {
|
||||||
|
if (!isSupportedDescription(description)) {
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
else if (zigbee.translateStatusZoneType19(description)) {
|
||||||
|
return "open"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "closed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseDescriptionText(String linkText, String value, String description) {
|
||||||
|
if (!isSupportedDescription(description)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
value ? "$linkText was ${value == 'open' ? 'opened' : value}" : ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
/**
|
||||||
|
* SmartSense Open/Closed Accelerometer Sensor
|
||||||
|
*
|
||||||
|
* Copyright 2014 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "SmartSense Open/Closed Accelerometer Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Battery"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Contact Sensor"
|
||||||
|
capability "Acceleration Sensor"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
command "enrollResponse"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||||
|
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("contact", "device.contact", width: 2, height: 2) {
|
||||||
|
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||||
|
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||||
|
}
|
||||||
|
standardTile("acceleration", "device.acceleration") {
|
||||||
|
state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0")
|
||||||
|
state("inactive", label:'${name}', icon:"st.motion.acceleration.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", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main (["contact", "acceleration", "temperature"])
|
||||||
|
details(["contact","acceleration", "temperature","battery","refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "description: $description"
|
||||||
|
|
||||||
|
Map map = [:]
|
||||||
|
if (description?.startsWith('catchall:')) {
|
||||||
|
map = parseCatchAllMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('read attr -')) {
|
||||||
|
map = parseReportAttributeMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('temperature: ')) {
|
||||||
|
map = parseCustomMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('zone status')) {
|
||||||
|
map = parseIasMessage(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug "Parse returned $map"
|
||||||
|
def result = map ? createEvent(map) : null
|
||||||
|
|
||||||
|
if (description?.startsWith('enroll request')) {
|
||||||
|
List cmds = enrollResponse()
|
||||||
|
log.debug "enroll response: ${cmds}"
|
||||||
|
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseCatchAllMessage(String description) {
|
||||||
|
Map resultMap = [:]
|
||||||
|
def cluster = zigbee.parse(description)
|
||||||
|
if (shouldProcessMessage(cluster)) {
|
||||||
|
switch(cluster.clusterId) {
|
||||||
|
case 0x0001:
|
||||||
|
resultMap = getBatteryResult(cluster.data.last())
|
||||||
|
break
|
||||||
|
|
||||||
|
case 0xFC02:
|
||||||
|
break
|
||||||
|
|
||||||
|
case 0x0402:
|
||||||
|
log.debug 'TEMP'
|
||||||
|
// temp is last 2 data values. reverse to swap endian
|
||||||
|
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||||
|
def value = getTemperature(temp)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldProcessMessage(cluster) {
|
||||||
|
// 0x0B is default response indicating message got through
|
||||||
|
// 0x07 is bind message
|
||||||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
|
cluster.command == 0x0B ||
|
||||||
|
cluster.command == 0x07 ||
|
||||||
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
|
return !ignoredMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getHumidity(value) {
|
||||||
|
return Math.round(Double.parseDouble(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseReportAttributeMessage(String description) {
|
||||||
|
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
log.debug "Desc Map: $descMap"
|
||||||
|
|
||||||
|
Map resultMap = [:]
|
||||||
|
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||||
|
def value = getTemperature(descMap.value)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "FC02" && descMap.attrId == "0002") {
|
||||||
|
Integer.parseInt(descMap.value,8)
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||||
|
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseCustomMessage(String description) {
|
||||||
|
Map resultMap = [:]
|
||||||
|
if (description?.startsWith('temperature: ')) {
|
||||||
|
def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseIasMessage(String description) {
|
||||||
|
List parsedMsg = description.split(' ')
|
||||||
|
String msgCode = parsedMsg[2]
|
||||||
|
|
||||||
|
Map resultMap = [:]
|
||||||
|
switch(msgCode) {
|
||||||
|
case '0x0020': // Closed/No Motion/Dry
|
||||||
|
resultMap = getContactResult('closed')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0021': // Open/Motion/Wet
|
||||||
|
resultMap = getContactResult('open')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0022': // Tamper Alarm
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0023': // Battery Alarm
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0024': // Supervision Report
|
||||||
|
resultMap = getContactResult('closed')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0025': // Restore Report
|
||||||
|
resultMap = getContactResult('open')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0026': // Trouble/Failure
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0028': // Test Mode
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
def getTemperature(value) {
|
||||||
|
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||||
|
if(getTemperatureScale() == "C"){
|
||||||
|
return celsius
|
||||||
|
} else {
|
||||||
|
return celsiusToFahrenheit(celsius) as Integer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getBatteryResult(rawValue) {
|
||||||
|
log.debug 'Battery'
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
|
||||||
|
def result = [
|
||||||
|
name: 'battery'
|
||||||
|
]
|
||||||
|
|
||||||
|
def volts = rawValue / 10
|
||||||
|
def descriptionText
|
||||||
|
if (volts > 3.5) {
|
||||||
|
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
def minVolts = 2.1
|
||||||
|
def maxVolts = 3.0
|
||||||
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
|
result.value = Math.min(100, (int) pct * 100)
|
||||||
|
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getTemperatureResult(value) {
|
||||||
|
log.debug 'TEMP'
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
if (tempOffset) {
|
||||||
|
def offset = tempOffset as int
|
||||||
|
def v = value as int
|
||||||
|
value = v + offset
|
||||||
|
}
|
||||||
|
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
||||||
|
return [
|
||||||
|
name: 'temperature',
|
||||||
|
value: value,
|
||||||
|
descriptionText: descriptionText
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getContactResult(value) {
|
||||||
|
log.debug 'Contact Status'
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
|
||||||
|
return [
|
||||||
|
name: 'contact',
|
||||||
|
value: value,
|
||||||
|
descriptionText: descriptionText
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAccelerationResult(numValue) {
|
||||||
|
def name = "acceleration"
|
||||||
|
def value = numValue.endsWith("1") ? "active" : "inactive"
|
||||||
|
//def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "$linkText was $value"
|
||||||
|
def isStateChange = isStateChange(device, name, value)
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
//unit: null,
|
||||||
|
//linkText: linkText,
|
||||||
|
descriptionText: descriptionText,
|
||||||
|
//handlerName: value,
|
||||||
|
isStateChange: isStateChange
|
||||||
|
// displayed: displayed(description, isStateChange)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh()
|
||||||
|
{
|
||||||
|
log.debug "Refreshing Temperature and Battery "
|
||||||
|
[
|
||||||
|
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||||
|
//"st rattr 0x${device.deviceNetworkId} 1 0xFC02 2", "delay 200",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
|
||||||
|
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||||
|
log.debug "Confuguring Reporting, IAS CIE, and Bindings."
|
||||||
|
def configCmds = [
|
||||||
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
"zcl global send-me-a-report 0xFC02 2 0x18 300 3600 {01}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC02 {${device.zigbeeId}} {}", "delay 500",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}"
|
||||||
|
|
||||||
|
]
|
||||||
|
return configCmds + refresh() // send refresh cmds as part of config
|
||||||
|
}
|
||||||
|
|
||||||
|
def enrollResponse() {
|
||||||
|
log.debug "Sending enroll response"
|
||||||
|
[
|
||||||
|
|
||||||
|
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1"
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
private hex(value) {
|
||||||
|
new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
int i = 0;
|
||||||
|
int j = array.length - 1;
|
||||||
|
byte tmp;
|
||||||
|
while (j > i) {
|
||||||
|
tmp = array[j];
|
||||||
|
array[j] = array[i];
|
||||||
|
array[i] = tmp;
|
||||||
|
j--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
/**
|
||||||
|
* SmartSense Open/Closed Sensor
|
||||||
|
*
|
||||||
|
* Copyright 2014 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Battery"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Contact Sensor"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
|
||||||
|
command "enrollResponse"
|
||||||
|
|
||||||
|
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300-S"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||||
|
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("contact", "device.contact", width: 2, height: 2) {
|
||||||
|
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||||
|
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTile("temperature", "device.temperature", inactiveLabel: false) {
|
||||||
|
state "temperature", label:'${currentValue}°',
|
||||||
|
backgroundColors:[
|
||||||
|
[value: 31, color: "#153591"],
|
||||||
|
[value: 44, color: "#1e9cbb"],
|
||||||
|
[value: 59, color: "#90d2a7"],
|
||||||
|
[value: 74, color: "#44b621"],
|
||||||
|
[value: 84, color: "#f1d801"],
|
||||||
|
[value: 95, color: "#d04e00"],
|
||||||
|
[value: 96, color: "#bc2323"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main (["contact", "temperature"])
|
||||||
|
details(["contact","temperature","battery","refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "description: $description"
|
||||||
|
|
||||||
|
Map map = [:]
|
||||||
|
if (description?.startsWith('catchall:')) {
|
||||||
|
map = parseCatchAllMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('read attr -')) {
|
||||||
|
map = parseReportAttributeMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('temperature: ')) {
|
||||||
|
map = parseCustomMessage(description)
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('zone status')) {
|
||||||
|
map = parseIasMessage(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug "Parse returned $map"
|
||||||
|
def result = map ? createEvent(map) : null
|
||||||
|
|
||||||
|
if (description?.startsWith('enroll request')) {
|
||||||
|
List cmds = enrollResponse()
|
||||||
|
log.debug "enroll response: ${cmds}"
|
||||||
|
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseCatchAllMessage(String description) {
|
||||||
|
Map resultMap = [:]
|
||||||
|
def cluster = zigbee.parse(description)
|
||||||
|
if (shouldProcessMessage(cluster)) {
|
||||||
|
switch(cluster.clusterId) {
|
||||||
|
case 0x0001:
|
||||||
|
resultMap = getBatteryResult(cluster.data.last())
|
||||||
|
break
|
||||||
|
|
||||||
|
case 0x0402:
|
||||||
|
log.debug 'TEMP'
|
||||||
|
// temp is last 2 data values. reverse to swap endian
|
||||||
|
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||||
|
def value = getTemperature(temp)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldProcessMessage(cluster) {
|
||||||
|
// 0x0B is default response indicating message got through
|
||||||
|
// 0x07 is bind message
|
||||||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
|
cluster.command == 0x0B ||
|
||||||
|
cluster.command == 0x07 ||
|
||||||
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
|
return !ignoredMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getHumidity(value) {
|
||||||
|
return Math.round(Double.parseDouble(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseReportAttributeMessage(String description) {
|
||||||
|
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
log.debug "Desc Map: $descMap"
|
||||||
|
|
||||||
|
Map resultMap = [:]
|
||||||
|
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||||
|
def value = getTemperature(descMap.value)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
|
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||||
|
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseCustomMessage(String description) {
|
||||||
|
Map resultMap = [:]
|
||||||
|
if (description?.startsWith('temperature: ')) {
|
||||||
|
def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseIasMessage(String description) {
|
||||||
|
List parsedMsg = description.split(' ')
|
||||||
|
String msgCode = parsedMsg[2]
|
||||||
|
|
||||||
|
Map resultMap = [:]
|
||||||
|
switch(msgCode) {
|
||||||
|
case '0x0020': // Closed/No Motion/Dry
|
||||||
|
resultMap = getContactResult('closed')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0021': // Open/Motion/Wet
|
||||||
|
resultMap = getContactResult('open')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0022': // Tamper Alarm
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0023': // Battery Alarm
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0024': // Supervision Report
|
||||||
|
resultMap = getContactResult('closed')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0025': // Restore Report
|
||||||
|
resultMap = getContactResult('open')
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0026': // Trouble/Failure
|
||||||
|
break
|
||||||
|
|
||||||
|
case '0x0028': // Test Mode
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
def getTemperature(value) {
|
||||||
|
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||||
|
if(getTemperatureScale() == "C"){
|
||||||
|
return celsius
|
||||||
|
} else {
|
||||||
|
return celsiusToFahrenheit(celsius) as Integer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getBatteryResult(rawValue) {
|
||||||
|
log.debug 'Battery'
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
|
||||||
|
def result = [
|
||||||
|
name: 'battery'
|
||||||
|
]
|
||||||
|
|
||||||
|
def volts = rawValue / 10
|
||||||
|
def descriptionText
|
||||||
|
if (volts > 3.5) {
|
||||||
|
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
def minVolts = 2.1
|
||||||
|
def maxVolts = 3.0
|
||||||
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
|
result.value = Math.min(100, (int) pct * 100)
|
||||||
|
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getTemperatureResult(value) {
|
||||||
|
log.debug 'TEMP'
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
if (tempOffset) {
|
||||||
|
def offset = tempOffset as int
|
||||||
|
def v = value as int
|
||||||
|
value = v + offset
|
||||||
|
}
|
||||||
|
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
||||||
|
return [
|
||||||
|
name: 'temperature',
|
||||||
|
value: value,
|
||||||
|
descriptionText: descriptionText
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getContactResult(value) {
|
||||||
|
log.debug 'Contact Status'
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
|
||||||
|
return [
|
||||||
|
name: 'contact',
|
||||||
|
value: value,
|
||||||
|
descriptionText: descriptionText
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh()
|
||||||
|
{
|
||||||
|
log.debug "Refreshing Temperature and Battery"
|
||||||
|
[
|
||||||
|
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
|
||||||
|
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||||
|
log.debug "Confuguring Reporting, IAS CIE, and Bindings."
|
||||||
|
def configCmds = [
|
||||||
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
|
||||||
|
//"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||||
|
//"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}"
|
||||||
|
]
|
||||||
|
return configCmds + refresh() // send refresh cmds as part of config
|
||||||
|
}
|
||||||
|
|
||||||
|
def enrollResponse() {
|
||||||
|
log.debug "Sending enroll response"
|
||||||
|
[
|
||||||
|
|
||||||
|
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1"
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
private hex(value) {
|
||||||
|
new BigInteger(Math.round(value).toString()).toString(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private String swapEndianHex(String hex) {
|
||||||
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] reverseArray(byte[] array) {
|
||||||
|
int i = 0;
|
||||||
|
int j = array.length - 1;
|
||||||
|
byte tmp;
|
||||||
|
while (j > i) {
|
||||||
|
tmp = array[j];
|
||||||
|
array[j] = array[i];
|
||||||
|
array[i] = tmp;
|
||||||
|
j--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user