If you do not have hot lines at the wall plates, and want to replace bulbs with GE Link bulbs or equivalent. you can pull out the existing on/off switch, wire the lights to stay on, and use this combo SmartApp and Device App. Will require wiring a PEQ door open/close sensor to a 3-way switch and placing that inside the wall box, but will only cost about $25.
/**
* PEQ modded multi-way on off toggle switch for multiple lights
* July 24, 2015
*
* This SmartApp uses a modified PEQ door open/close sensor wired to SPDT (3-way) switch to toggle on and off
* a switch that controls a GE Link Light or similar light or switch. The intent is to have a simple and inexpensive battery
* operated replacement for a standard wall switch to operate multiple, 3-way or x-way zigbee or zwave light or
* switch.
*
* The PEQ sensor has been on sale intermittently at Best Buy for under $20, and the 3 way SPDT wall switch costs a few
* dollars at any hardware store, so you can put the whole thing together for about $25.00.
*
* The PEQ door open/close sensor can be easily modded and wired to the 3-way wall switch by replacing
* (deslodering the relay and resoldering three wires) the magnetic reed relay with 3 wires soldered
* to the 1 reed common pad and 2 reed switch pads on the PCB. I will be posting a detailed HOW-TO for
* modification of the PEQ Sensor on the Smartthings community board shortly.
*
* A future version will be published to toggle on/off multiple switches simultaneously,
* and to set the dimmer level.
*
* Copyright (c) 2015 Joel Goldwein (goldwein at gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
definition(
name: "Multi-way On/Off Toggle Switch Using a Modded PEQ Door Open/Close Sensor for Multiple Lights",
namespace: "JWGthings",
author: "Joel Goldwein (goldwein at gmail.com)",
description: "PEQ multi-way On/Off Switch - This SmartApp uses a modified PEQ door open/close sensor wired to SPDT (3-way) switch to toggle on and off MULTIPLE GE Link Light, similar light or switch. The intent is to have a simple and inexpensive battery operated replacement for a standard wall switch to operate a single, 3-way or multi-way zigbee or zwave light or switch.",
category: "My Apps",
iconUrl: "http://cdn.device-icons.smartthings.com/Home/home30-icn@2x.png",
iconX2Url: "http://cdn.device-icons.smartthings.com/Home/home30-icn@2x.png",
iconX3Url: "http://cdn.device-icons.smartthings.com/Home/home30-icn@2x.png"
)
// //https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png
preferences {
section("Select switch or switches") {
input "contact1", "capability.contactSensor", title: "Which Switch or Switches?", required: true, multiple: true, submitOnChange: true
}
// What light should this app be configured for?
section("Turn on/off which Link Lights...") {
input "switches", "capability.switch", title: "Which Lights", required: true, multiple: true
}
}
def installed() {
// log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
// log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
subscribe(contact1, "contact", contactHandler)
}
def contactHandler(evt) {
def cname = "${contact1.displayName}"
def lname = "${switches.displayName}"
log.debug "Triggered. Sensor was $evt.value and $lname was $switches.currentSwitch"
// The contactSensor capability can be either "open" or "closed"
// The actual sensor state is not relevant. What is only important is the state change,
// Code is triggered any time there is a state change in the door sensor
// Eventually, I'm going to try doing this with a SPST switch instead of a SPDT switch
//
// If the switch is UP (closed)
if("closed" == evt.value) {
// Turn it ON
log.debug "JWG: UP"
switches.on()
// and if the switch is DOWN (open)
} else if("open" == evt.value) {
// Turn it ON
log.debug "Status: Down"
switches.off()
} else {
log.debug "Status: Uncertain"
switches.off()
}
// Send a info message
log.debug "PEQ switch $cname is now $evt.value and $lname is now $switches.currentSwitch"
}
Combined with this device:
/**
* Modified SmartSense (PEQ) Open/Closed Sensor to behave like an on/off toggle switch
*
* Copyright 2014 SmartThings and 2015 - Joel Goldwein
* 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: "Modded SmartSense Open/Closed Sensor/Switch", 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:"DOWN", icon:"st.Home.home30", backgroundColor:"#C7B4FA")
state("closed", label:"UP", icon:"st.Home.home30", backgroundColor:"#2E5FF2")
}
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) {
// log.debug "description A: $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) {
// log.debug "description B: $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]
// log.debug "msgCode: $msgCode"
Map resultMap = [:]
switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry/On/Up
resultMap = getContactResult('closed')
break
case '0x0021': // Open/Motion/Wet/off/down
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 'Switch Status'
def linkText = getLinkText(device)
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
// log.debug "linkText: $descriptionText"
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
}