Compare commits

..

1 Commits

Author SHA1 Message Date
Terry Kaul
8c6295d182 MSA-2074: PE653 poolcontroller 2017-07-03 07:19:03 -07:00
6 changed files with 476 additions and 306 deletions

View File

@@ -0,0 +1,464 @@
/**
* Intermatic PE653 Pool Control System
*
* Copyright 2014 bigpunk6
*
* 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.
*
* Don't use Cooper Lee's code (vTile_ms, ms_w_vts) he was working on a different implementation than me.
*
* Install my device type then use the Multi-Channel Controll App by SmartThings from the Marketplace under the More section.
*
*/
metadata {
definition (name: "Intermatic PE653 Pool Control System", author: "bigpunk6", namespace: "bigpunk6") {
capability "Actuator"
capability "Switch"
capability "Polling"
capability "Configuration"
capability "Refresh"
capability "Temperature Measurement"
capability "Sensor"
capability "Zw Multichannel"
capability "Thermostat"
attribute "operationMode", "string"
attribute "firemanTimeout", "string"
attribute "temperatureOffsets", "string"
attribute "poolspaConfig", "string"
attribute "poolSetpoint", "string"
attribute "spaSetpoint", "string"
attribute "setPoolSetpoint", "string"
attribute "pool", "string"
attribute "setSpaSetpoint", "string"
attribute "spa", "string"
command "quickSetPool"
command "quickSetSpa"
fingerprint deviceId: "0x1001", inClusters: "0x91,0x73,0x72,0x86,0x81,0x60,0x70,0x85,0x25,0x27,0x43,0x31", outClusters: "0x82"
}
preferences {
input "operationMode1", "enum", title: "Boster/Cleaner Pump",
options:[1:"No",
2:"Uses Circuit-1",
3:"Variable Speed pump Speed-1",
4:"Variable Speed pump Speed-2",
5:"Variable Speed pump Speed-3",
6:"Variable Speed pump Speed-4"]
input "operationMode2", "enum", title: "Pump Type",
options:[0:"1 Speed Pump without Booster/Cleaner",
1:"1 Speed Pump with Booster/Cleaner",
2:"2 Speed Pump without Booster/Cleaner",
3:"2 Speed Pump with Booster/Cleaner"]
input "poolSpa1", "enum", title: "Pool or Spa", options:[0:"Pool",1:"Spa",2:"Both"]
input "fireman", "enum", title: "Fireman Timeout",
options:["255":"No heater installed",
"0":"No cool down period",
"1":"1 minute",
"2":"2 minute",
"3":"3 minute",
"4":"4 minute",
"5":"5 minute",
"6":"6 minute",
"7":"7 minute",
"8":"8 minute",
"9":"9 minute",
"10":"10 minute",
"11":"11 minute",
"12":"12 minute",
"13":"13 minute",
"14":"14 minute",
"15":"15 minute"]
input "tempOffsetwater", "number", title: "Water temperature offset", defaultValue: 0, required: true
input "tempOffsetair", "number",
title: "Air temperature offset - Sets the Offset of the air temerature for the add-on Thermometer in degrees Fahrenheit -20F to +20F", defaultValue: 0, required: true
}
simulator {
status "on": "command: 2003, payload: FF"
status "off": "command: 2003, payload: 00"
reply "8E010101,delay 800,6007": "command: 6008, payload: 4004"
reply "8505": "command: 8506, payload: 02"
reply "59034002": "command: 5904, payload: 8102003101000000"
reply "6007": "command: 6008, payload: 0002"
reply "600901": "command: 600A, payload: 10002532"
reply "600902": "command: 600A, payload: 210031"
}
// tile definitions
tiles(scale: 2) {
multiAttributeTile(name:"temperature", type: "thermostat", width: 6, height: 4){
tileAttribute ("device.temperature", key: "PRIMARY_CONTROL") {
attributeState "temperature", label:'${currentValue}°',
backgroundColors:[
[value: 32, color: "#153591"],
[value: 54, color: "#1e9cbb"],
[value: 64, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 90, color: "#f1d801"],
[value: 98, color: "#d04e00"],
[value: 110, color: "#bc2323"]
]
}
tileAttribute ("device.poolSetpoint", key: "VALUE_CONTROL") {
attributeState "poolSetpoint", action:"quickSetPool"
}
tileAttribute ("device.spaSetpoint", key: "SECONDARY_CONTROL") {
attributeState "spaSetpoint", label:'Spa set to ${currentValue}°F'
}
}
controlTile("poolSliderControl", "device.poolSetpoint", "slider", height: 2, width: 4, inactiveLabel: false, range:"(40..104)") {
state "setPoolSetpoint", action:"quickSetPool", backgroundColor:"#d04e00"
}
valueTile("poolSetpoint", "device.poolSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "pool", label:'${currentValue}° pool', backgroundColor:"#ffffff"
}
controlTile("spaSliderControl", "device.spaSetpoint", "slider", height: 2, width: 4, inactiveLabel: false, range:"(40..104)") {
state "setSpaSetpoint", action:"quickSetSpa", backgroundColor: "#1e9cbb"
}
valueTile("spaSetpoint", "device.spaSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "spa", label:'${currentValue}° spa', backgroundColor:"#ffffff"
}
/*valueTile("temperature", "device.temperature") {
state("temperature", label:'${currentValue}°', unit:"F",
backgroundColors:[
[value: 32, color: "#153591"],
[value: 54, color: "#1e9cbb"],
[value: 64, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 90, color: "#f1d801"],
[value: 98, color: "#d04e00"],
[value: 110, color: "#bc2323"]
]
)
}*/
standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
standardTile("configure", "device.configure", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
}
main "temperature"
details(["temperature", "poolSliderControl", "poolSetpoint", "spaSliderControl", "spaSetpoint", "configure", "refresh"])
}
}
def parse(String description) {
def result = null
if (description.startsWith("Err")) {
log.warn "Error in Parse"
result = createEvent(descriptionText:description, isStateChange:true)
} else {
def cmd = zwave.parse(description, [0x20: 1, 0x25:1, 0x27:1, 0x31:1, 0x43:1, 0x60:3, 0x70:2, 0x81:1, 0x85:1, 0x86: 1, 0x73:1])
if (cmd) {
result = zwaveEvent(cmd)
}
}
log.debug("'$description' parsed to $result")
return result
}
//Thermostat
def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) {
log.debug "ThermostatModeReport $cmd"
def map = [:]
switch (cmd.mode) {
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_HEAT:
map.value = "pool"
break
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_FURNACE:
map.value = "spa"
break
}
map.name = "thermostatMode"
map
}
def quickSetPool(degrees) {
log.debug "quickSetPool $degrees"
setPoolSetpoint(degrees, 1000)
}
def setPoolSetpoint(degrees, delay = 30000) {
setPoolSetpoint(degrees.toDouble(), delay)
}
def setPoolSetpoint(Double degrees, Integer delay = 30000) {
log.trace "setPoolSetpoint($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 quickSetSpa(degrees) {
setSpaSetpoint(degrees, 1000)
}
def setSpaSetpoint(degrees, delay = 30000) {
setSpaSetpoint(degrees.toDouble(), delay)
}
def setSpaSetpoint(Double degrees, Integer delay = 30000) {
log.trace "setSpaSetpoint($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: 7, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(),
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 7).format()
], delay)
}
//Reports
def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) {
log.debug "configuration: $cmd"
def map = [:]
map.value = cmd.configurationValue
map.displayed = false
switch (cmd.parameterNumber) {
case 1:
map.name = "operationMode"
break;
case 2:
map.name = "firemanTimeout"
break;
case 3:
map.name = "temperatureOffsets"
break;
case 19:
map.name = "poolspaConfig"
break;
default:
return [:]
}
createEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv1.SensorMultilevelReport cmd) {
log.debug "Sensor: $cmd"
def map = [:]
map.value = cmd.scaledSensorValue.toString()
map.unit = cmd.scale == 1 ? "F" : "C"
map.name = "temperature"
createEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv1.ThermostatSetpointReport cmd) {
def map = [:]
map.value = cmd.scaledValue.toString()
map.unit = cmd.scale == 1 ? "F" : "C"
map.displayed = false
switch (cmd.setpointType) {
case 1:
map.name = "poolSetpoint"
break;
case 7:
map.name = "spaSetpoint"
break;
default:
return [:]
}
// So we can respond with same format
state.size = cmd.size
state.scale = cmd.scale
state.precision = cmd.precision
createEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
log.debug "$cmd"
if (cmd.value == 0) {
createEvent(name: "switch", value: "off")
} else if (cmd.value == 255) {
createEvent(name: "switch", value: "on")
}
}
def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiInstanceReport cmd) {
log.debug "MultiInstanceReport $cmd"
}
private List loadEndpointInfo() {
if (state.endpointInfo) {
state.endpointInfo
} else if (device.currentValue("epInfo")) {
fromJson(device.currentValue("epInfo"))
} else {
[]
}
}
def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelEndPointReport cmd) {
log.debug "$cmd"
updateDataValue("endpoints", cmd.endPoints.toString())
if (!state.endpointInfo) {
state.endpointInfo = loadEndpointInfo()
}
if (state.endpointInfo.size() > cmd.endPoints) {
cmd.endpointInfo
}
state.endpointInfo = [null] * cmd.endPoints
//response(zwave.associationV2.associationGroupingsGet())
[ createEvent(name: "epInfo", value: util.toJson(state.endpointInfo), displayed: false, descriptionText:""),
response(zwave.multiChannelV3.multiChannelCapabilityGet(endPoint: 1)) ]
}
def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCapabilityReport cmd) {
log.debug "$cmd"
def result = []
def cmds = []
if(!state.endpointInfo) state.endpointInfo = []
state.endpointInfo[cmd.endPoint - 1] = cmd.format()[6..-1]
if (cmd.endPoint < getDataValue("endpoints").toInteger()) {
cmds = zwave.multiChannelV3.multiChannelCapabilityGet(endPoint: cmd.endPoint + 1).format()
} else {
log.debug "endpointInfo: ${state.endpointInfo.inspect()}"
}
result << createEvent(name: "epInfo", value: util.toJson(state.endpointInfo), displayed: false, descriptionText:"")
if(cmds) result << response(cmds)
result
}
def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationGroupingsReport cmd) {
log.debug "$cmd"
state.groups = cmd.supportedGroupings
if (cmd.supportedGroupings > 1) {
[response(zwave.associationGrpInfoV1.associationGroupInfoGet(groupingIdentifier:2, listMode:1))]
}
}
def zwaveEvent(physicalgraph.zwave.commands.associationgrpinfov1.AssociationGroupInfoReport cmd) {
log.debug "$cmd"
def cmds = []
for (def i = 2; i <= state.groups; i++) {
cmds << response(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:i, nodeId:zwaveHubNodeId))
}
cmds
}
def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
log.debug "$cmd"
def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1])
if (encapsulatedCommand) {
if (state.enabledEndpoints.find { it == cmd.sourceEndPoint }) {
def formatCmd = ([cmd.commandClass, cmd.command] + cmd.parameter).collect{ String.format("%02X", it) }.join()
createEvent(name: "epEvent", value: "$cmd.sourceEndPoint:$formatCmd", isStateChange: true, displayed: false, descriptionText: "(fwd to ep $cmd.sourceEndPoint)")
} else {
zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer)
}
}
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
log.warn "Captured zwave command $cmd"
createEvent(descriptionText: "$device.displayName: $cmd", isStateChange: true)
}
//Commands
def epCmd(Integer ep, String cmds) {
log.debug "epCmd: $ep $cmds"
if (cmds.contains('2001FF')){
log.debug "contained 2001FF"
delayBetween([
encap(zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF), ep),
encap(zwave.switchBinaryV1.switchBinaryGet(), ep)
], 2300)
} else if (cmds.contains('200100')) {
log.debug "contained 2001FF"
delayBetween([
encap(zwave.switchBinaryV1.switchBinarySet(switchValue: 0), ep),
encap(zwave.switchBinaryV1.switchBinaryGet(), ep)
], 2300)
} else if (cmds.contains('2002')) {
encap(zwave.switchBinaryV1.switchBinaryGet(), ep)
} else {
log.warn "No CMD found"
}
}
def enableEpEvents(enabledEndpoints) {
state.enabledEndpoints = enabledEndpoints.split(",").findAll()*.toInteger()
null
}
private command(physicalgraph.zwave.Command cmd) {
log.debug "command: $cmd"
cmd.format()
}
private encap(cmd, endpoint) {
if (endpoint) {
command(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint, sourceEndPoint: endpoint).encapsulate(cmd))
} else {
command(cmd)
}
}
def poll() {
zwave.sensorMultilevelV1.sensorMultilevelGet().format()
}
def refresh() {
delayBetween([
zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:1, destinationEndPoint:1, commandClass:37, command:2).format(),
zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:2, destinationEndPoint:2, commandClass:37, command:2).format(),
zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:3, destinationEndPoint:3, commandClass:37, command:2).format(),
zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:4, destinationEndPoint:4, commandClass:37, command:2).format(),
zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint:5, destinationEndPoint:5, commandClass:37, command:2).format(),
zwave.sensorMultilevelV1.sensorMultilevelGet().format(),
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(),
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 7).format(),
zwave.configurationV2.configurationGet(parameterNumber: 1).format(),
zwave.configurationV2.configurationGet(parameterNumber: 2).format(),
zwave.configurationV2.configurationGet(parameterNumber: 3).format(),
zwave.configurationV2.configurationGet(parameterNumber: 19).format()
], 3000)
}
def configure() {
def cmds = []
cmds << zwave.configurationV2.configurationSet(configurationValue: [operationMode1.toInteger(), operationMode2.toInteger()], parameterNumber: 1, size: 2).format()
cmds << zwave.configurationV2.configurationSet(configurationValue: [tempOffsetwater.toInteger(), tempOffsetair.toInteger(), 0, 0], parameterNumber: 3, size: 4).format()
cmds << zwave.configurationV2.configurationSet(configurationValue: [poolSpa1.toInteger()], parameterNumber: 19, size: 1).format()
cmds << zwave.configurationV2.configurationSet(configurationValue: [fireman.toInteger()], parameterNumber: 2, size: 1).format()
delayBetween(cmds, 2500)
}

View File

@@ -158,7 +158,7 @@ def generateEvent(Map results) {
if(results) {
def linkText = getLinkText(device)
def supportedThermostatModes = ["off"]
def supportedThermostatModes = []
def thermostatMode = null
results.each { name, value ->

View File

@@ -28,8 +28,9 @@ Works with:
## Device Health
ZigBee Button is marked offline only in the case when Hub is offline.
SmartThings platform will ping the device after `checkInterval` seconds of inactivity in last attempt to reach the device before marking it `OFFLINE`
* __722min__ checkInterval
## Troubleshooting

View File

@@ -13,8 +13,6 @@
* for the specific language governing permissions and limitations under the License.
*
*/
import groovy.json.JsonOutput
import physicalgraph.zigbee.zcl.DataType
metadata {
@@ -185,6 +183,13 @@ private Map parseNonIasButtonMessage(Map descMap){
}
}
/**
* PING is used by Device-Watch in attempt to reach the Device
* */
def ping() {
refresh()
}
def refresh() {
log.debug "Refreshing Battery"
@@ -193,6 +198,8 @@ def refresh() {
}
def configure() {
// Device-Watch allows 2 check-in misses from device (plus 2 mins lag time)
sendEvent(name: "checkInterval", value: 2 * 6 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def cmds = []
if (device.getDataValue("model") == "3450-L") {
@@ -252,8 +259,6 @@ def updated() {
}
def initialize() {
// Arrival sensors only goes OFFLINE when Hub is off
sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zigbee", scheme:"untracked"]), displayed: false)
if ((device.getDataValue("manufacturer") == "OSRAM") && (device.getDataValue("model") == "LIGHTIFY Dimming Switch")) {
sendEvent(name: "numberOfButtons", value: 2)
}

View File

@@ -58,7 +58,6 @@ metadata {
def installed() {
// Device-Watch simply pings if no device events received for 32min(checkInterval)
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
response(refresh())
}
def updated() {

View File

@@ -1,299 +0,0 @@
/**
* Visonic Door/Window Sensor
*
* Copyright 2017 Tomas Axerot
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
metadata {
definition (name: "Visonic Door/Window Sensor", namespace: "tomasaxerot", author: "Tomas Axerot") {
capability "Battery"
capability "Configuration"
capability "Contact Sensor"
capability "Refresh"
capability "Temperature Measurement"
capability "Health Check"
capability "Sensor"
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Visonic", model: "MCT-340 SMA"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Visonic", model: "MCT-340 E"
}
simulator {
}
preferences {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
tiles(scale: 2) {
multiAttributeTile(name: "contact", type: "generic", width: 6, height: 4) {
tileAttribute("device.contact", key: "PRIMARY_CONTROL") {
attributeState "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#e86d13"
attributeState "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#00A0DC"
}
}
valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) {
state "temperature", label: '${currentValue}°',
backgroundColors: [
[value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
}
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
state "battery", label: '${currentValue}% battery', unit: ""
}
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", action: "refresh.refresh", icon: "st.secondary.refresh"
}
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
}
main(["contact", "temperature"])
details(["contact", "temperature", "battery", "refresh", "configure"])
}
}
def parse(String description) {
log.debug "description: $description"
Map map = [:]
if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description)
}
else if (description?.startsWith('read attr -')) {
map = parseReportAttributeMessage(description)
}
else if (description?.startsWith('temperature: ')) {
map = parseCustomMessage(description)
}
else if (description?.startsWith('zone status')) {
map = parseIasMessage(description)
}
log.debug "Parse returned $map"
def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) {
List cmds = enrollResponse()
log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
}
return result
}
private Map parseCatchAllMessage(String description) {
Map resultMap = [:]
def cluster = zigbee.parse(description)
if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) {
case 0x0001:
resultMap = getBatteryResult(cluster.data.last())
break
case 0x0402:
log.debug 'TEMP'
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp)
resultMap = getTemperatureResult(value)
break
}
}
return resultMap
}
private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage
}
private Map parseReportAttributeMessage(String description) {
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
log.debug "Desc Map: $descMap"
Map resultMap = [:]
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = getTemperature(descMap.value)
resultMap = getTemperatureResult(value)
}
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
}
return resultMap
}
private Map parseCustomMessage(String description) {
Map resultMap = [:]
if (description?.startsWith('temperature: ')) {
def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
resultMap = getTemperatureResult(value)
}
return resultMap
}
private Map parseIasMessage(String description) {
ZoneStatus zs = zigbee.parseZoneStatus(description)
return zs.isAlarm1Set() ? getContactResult('open') : getContactResult('closed')
}
def getTemperature(value) {
def celsius = Integer.parseInt(value, 16).shortValue() / 100
if(getTemperatureScale() == "C"){
return celsius
} else {
return celsiusToFahrenheit(celsius) as Integer
}
}
private Map getBatteryResult(rawValue) {
log.debug 'Battery'
def linkText = getLinkText(device)
def result = [:]
if (!(rawValue == 0 || rawValue == 255)) {
def volts = rawValue / 10
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%"
result.name = 'battery'
}
return result
}
private Map getTemperatureResult(value) {
log.debug 'TEMP'
def linkText = getLinkText(device)
if (tempOffset) {
def offset = tempOffset as int
def v = value as int
value = v + offset
}
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
return [
name: 'temperature',
value: value,
descriptionText: descriptionText,
unit: temperatureScale
]
}
private Map getContactResult(value) {
log.debug 'Contact Status'
def linkText = getLinkText(device)
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
return [
name: 'contact',
value: value,
descriptionText: descriptionText
]
}
/**
* PING is used by Device-Watch in attempt to reach the Device
* */
def ping() {
return zigbee.readAttribute(0x0402, 0x0000) // Read the Temperature Cluster
}
def refresh()
{
log.debug "Refreshing Temperature and Battery"
def refreshCmds = [
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
]
return refreshCmds + enrollResponse()
}
def configure() {
// Device-Watch allows 2 check-in misses from device
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def enrollCmds = [
"delay 1000",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
//"raw 0x500 {01 23 00 00 00}", "delay 200",
//"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
]
return enrollCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // send refresh cmds as part of config
}
def enrollResponse() {
log.debug "Sending enroll response"
[
"raw 0x500 {01 23 00 00 00}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1"
]
}
private hex(value) {
new BigInteger(Math.round(value).toString()).toString(16)
}
private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
}
private byte[] reverseArray(byte[] array) {
int i = 0;
int j = array.length - 1;
byte tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j--;
i++;
}
return array
}