-
-
Save xwcg/667e228b1542b62693cabdb376400e35 to your computer and use it in GitHub Desktop.
Aqara Roller Shade Driver E1 - Groovy Driver for Hubitat
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright 2023 xwcg | |
* | |
* Version: v3 | |
* | |
* This program is free software: you can redistribute it and/or modify | |
* it under the terms of the GNU General Public License as published by | |
* the Free Software Foundation, either version 3 of the License, or | |
* (at your option) any later version. | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU General Public License for more details. | |
* | |
* You should have received a copy of the GNU General Public License | |
* along with this program. If not, see <https://www.gnu.org/licenses/>. | |
* | |
*/ | |
/* | |
Inspired by a driver from shin4299 which can be found here: | |
github.com/shin4299/XiaomiSJ/blob/master/devicetypes/shinjjang/xiaomi-curtain-b1.src/xiaomi-curtain-b1.groovy | |
Shamelessly stolen from | |
https://raw.githubusercontent.com/markus-li/Hubitat/development/drivers/expanded/zigbee-aqara-smart-curtain-motor-expanded.groovy | |
and made to work with the Roller Shade E1 | |
*/ | |
import groovy.json.JsonSlurper | |
import groovy.json.JsonOutput | |
import java.security.MessageDigest | |
import hubitat.helper.HexUtils | |
metadata { | |
definition (name: "Zigbee - Aqara Roller Shade Driver E1", namespace: "de.xwcg", author: "xwcg", filename: "zigbee-aqara-roller-shade-driver-e1", importUrl: "https://raw.githubusercontent.com/markus-li/Hubitat/development/drivers/expanded/zigbee-aqara-smart-curtain-motor-expanded.groovy") { | |
capability "Sensor" | |
capability "PresenceSensor" | |
capability "Initialize" | |
capability "Refresh" | |
capability "WindowShade" | |
// capability "Actuator" | |
// capability "Switch" | |
// capability "Light" | |
// capability "SwitchLevel" | |
// capability "ChangeLevel" | |
// BEGIN:getDefaultMetadataAttributes() | |
attribute "driver", "string" | |
// END: getDefaultMetadataAttributes() | |
// BEGIN:getMetadataAttributesForLastCheckin() | |
attribute "lastCheckin", "Date" | |
attribute "lastCheckinEpoch", "number" | |
attribute "notPresentCounter", "number" | |
attribute "restoredCounter", "number" | |
// command "resetRestoredCounter" | |
// command "forceRecoveryMode", [[name:"Minutes*", type: "NUMBER", description: "Maximum minutes to run in Recovery Mode"]] | |
// command "stop" | |
// command "manualOpenEnable" | |
// command "manualOpenDisable" | |
// command "curtainOriginalDirection" | |
// command "curtainReverseDirection" | |
// command "trackDiscoveryMode" | |
// command "toggle" | |
fingerprint profileId: "0104", endpointId: "01", inClusters: "0000,0004,0003,0005,000A,0102,000D,0013,0006,0001,0406", outClusters: "0019,000A,000D,0102,0013,0006,0001,0406", manufacturer: "LUMI", model: "lumi.curtain" | |
} | |
preferences | |
{ | |
input(name: "debugLogging", type: "bool", title:"Enable debug logging", description: "", defaultValue: false, submitOnChange: true, displayDuringSetup: false, required: false) | |
// input(name: "infoLogging", type: "bool", title: "Enable info logging", description: "", defaultValue: true, submitOnChange: true, displayDuringSetup: false, required: false) | |
// input(name: "lastCheckinEnable", type: "bool", title: "Enable Last Checkin Date", description: "Records Date events if enabled", defaultValue: true) | |
// input(name: "lastCheckinEpochEnable", type: "bool", title: "Enable Last Checkin Epoch", description: "Records Epoch events if enabled", defaultValue: false) | |
// input(name: "presenceEnable", type: "bool", title: "Enable Presence", description: "Enables Presence to indicate if the device has sent data within the last 3 hours (REQUIRES at least one of the Checkin options to be enabled)", defaultValue: true) | |
// input(name: "presenceWarningEnable", type: "bool", title: "Enable Presence Warning", description: "Enables Presence Warnings in the Logs (default: true)", defaultValue: true) | |
// input(name: "recoveryMode", type: "enum", title: "Recovery Mode", description: "Select Recovery mode type (default: Slow)<br/>NOTE: The \"Insane\" and \"Suicidal\" modes may destabilize your mesh if run on more than a few devices at once!", options: ["Disabled", "Slow", "Normal", "Insane", "Suicidal"], defaultValue: "Slow") | |
} | |
} | |
// BEGIN:getDeviceInfoFunction() | |
String getDeviceInfoByName(infoName) { | |
Map deviceInfo = ['name': 'Zigbee - Aqara Roller Shade Driver E1', 'namespace': 'de.xwcg', 'author': 'xwcg', 'filename': 'zigbee-aqara-roller-shade-driver-e1', 'importUrl': 'https://raw.githubusercontent.com/markus-li/Hubitat/development/drivers/expanded/zigbee-aqara-smart-curtain-motor-expanded.groovy'] | |
return(deviceInfo[infoName]) | |
} | |
// END: getDeviceInfoFunction() | |
/* These functions are unique to each driver */ | |
ArrayList<String> refresh() { | |
getDriverVersion() | |
configurePresence() | |
startCheckEventInterval() | |
setLogsOffTask(noLogWarning=true) | |
ArrayList<String> cmd = [] | |
cmd += getPosition() | |
cmd += zigbee.readAttribute(CLUSTER_BASIC, 0xFF01, [mfgCode: "0x115F"]) | |
// cmd += zigbee.readAttribute(0x000D, 0x0055) | |
String model = setCleanModelName(newModelToSet=null, acceptedModels=[ | |
"lumi.curtain" | |
]) | |
sendZigbeeCommands(cmd) | |
refreshEvents() | |
} | |
void initialize() { | |
logging("initialize()", 100) | |
unschedule() | |
makeSchedule() | |
refresh() | |
} | |
void installed() { | |
logging("installed()", 100) | |
sendEvent(name:"windowShade", value: 'unknown') | |
sendEvent(name:"switch", value: 'off') | |
sendEvent(name:"level", value: 0) | |
makeSchedule() | |
refresh() | |
} | |
void updated() { | |
logging("updated()", 100) | |
refresh() | |
} | |
void makeSchedule() { | |
// logging("makeSchedule()", 100) | |
// if(getDeviceDataByName('model') != "lumi.curtain") { | |
// Random rnd = new Random() | |
// schedule("${rnd.nextInt(59)} ${rnd.nextInt(59)} 5/12 * * ? *", 'getBattery') | |
// } else { | |
// unschedule('getBattery') | |
// } | |
} | |
ArrayList<String> parse(String description) { | |
// BEGIN:getGenericZigbeeParseHeader(loglevel=0) | |
// logging("PARSE START---------------------", 1) | |
logging("Parsing: '${description}'", 1) | |
ArrayList<String> cmd = [] | |
Map msgMap = null | |
if(description.indexOf('encoding: 4C') >= 0) { | |
msgMap = zigbee.parseDescriptionAsMap(description.replace('encoding: 4C', 'encoding: F2')) | |
msgMap = unpackStructInMap(msgMap) | |
} else if(description.indexOf('attrId: FF01, encoding: 42') >= 0) { | |
msgMap = zigbee.parseDescriptionAsMap(description.replace('encoding: 42', 'encoding: F2')) | |
msgMap["encoding"] = "41" | |
msgMap["value"] = parseXiaomiStruct(msgMap["value"], isFCC0=false, hasLength=true) | |
} else { | |
if(description.indexOf('encoding: 42') >= 0) { | |
List values = description.split("value: ")[1].split("(?<=\\G..)") | |
String fullValue = values.join() | |
Integer zeroIndex = values.indexOf("01") | |
if(zeroIndex > -1) { | |
//logging("zeroIndex: $zeroIndex, fullValue: $fullValue, string: ${values.take(zeroIndex).join()}", 1) | |
msgMap = zigbee.parseDescriptionAsMap(description.replace(fullValue, values.take(zeroIndex).join())) | |
values = values.drop(zeroIndex + 3) | |
msgMap["additionalAttrs"] = [ | |
["encoding": "41", | |
"value": parseXiaomiStruct(values.join(), isFCC0=false, hasLength=true)] | |
] | |
} else { | |
msgMap = zigbee.parseDescriptionAsMap(description) | |
} | |
} else { | |
msgMap = zigbee.parseDescriptionAsMap(description) | |
} | |
if(msgMap.containsKey("encoding") && msgMap.containsKey("value") && msgMap["encoding"] != "41" && msgMap["encoding"] != "42") { | |
msgMap["valueParsed"] = zigbee_generic_decodeZigbeeData(msgMap["value"], msgMap["encoding"]) | |
} | |
if(msgMap == [:] && description.indexOf("zone") == 0) { | |
msgMap["type"] = "zone" | |
java.util.regex.Matcher zoneMatcher = description =~ /.*zone.*status.*0x(?<status>([0-9a-fA-F][0-9a-fA-F])+).*extended.*status.*0x(?<statusExtended>([0-9a-fA-F][0-9a-fA-F])+).*/ | |
if(zoneMatcher.matches()) { | |
msgMap["parsed"] = true | |
msgMap["status"] = zoneMatcher.group("status") | |
msgMap["statusInt"] = Integer.parseInt(msgMap["status"], 16) | |
msgMap["statusExtended"] = zoneMatcher.group("statusExtended") | |
msgMap["statusExtendedInt"] = Integer.parseInt(msgMap["statusExtended"], 16) | |
} else { | |
msgMap["parsed"] = false | |
} | |
} | |
} | |
// logging("msgMap: ${msgMap}", 1) | |
// END: getGenericZigbeeParseHeader(loglevel=0) | |
/* | |
description:catchall: 0104 000D 01 01 0040 00 84C9 00 00 0000 01 01 050086 | | |
msgMap: | |
[ | |
raw:catchall: 0104 000D 01 01 0040 00 84C9 00 00 0000 01 01 050086, | |
profileId:0104, | |
clusterId:000D, | |
clusterInt:13, | |
sourceEndpoint:01, | |
destinationEndpoint:01, | |
options:0040, | |
messageType:00, | |
dni:84C9, | |
isClusterSpecific:false, | |
isManufacturerSpecific:false, | |
manufacturerId:0000, | |
command:01, | |
direction:01, | |
data:[05, 00, 86] | |
] | |
*/ | |
if(msgMap["clusterInt"] == 0) | |
{ | |
logging("Generic device attribute received", 1) | |
} | |
else if(msgMap["clusterInt"] == 5) // 0x0005 | |
{ | |
logging("Presence event", 1) | |
requestPositionUpdate() // Update position on heartbeat | |
setAsPresent() | |
} | |
else if(msgMap["clusterInt"] == 13) // 0x000D | |
{ | |
if(msgMap["attrId"] == "0055") // Position | |
{ | |
//Parsing: 'read attr - raw: 84C901000D105500390000803F, dni: 84C9, endpoint: 01, cluster: 000D, size: 10, attrId: 0055, encoding: 39, command: 01, value: 0000803F' | |
// [raw:84C901000D105500390000803F, dni:84C9, endpoint:01, cluster:000D, size:10, attrId:0055, encoding:39, command:01, value:3F800000, clusterInt:13, attrInt:85, valueParsed:1.0] | |
logging("Position received", 1) | |
Long longValue = Long.parseLong(msgMap["value"], 16) | |
BigDecimal floatValue = Float.intBitsToFloat(longValue.intValue()); | |
curtainPosition = floatValue.intValue() | |
positionEvent(curtainPosition) | |
logging("Position = ${curtainPosition}", 1) | |
} | |
else | |
{ | |
logging("Analogue Output received", 1) | |
logging("unknown msgMap: ${msgMap}", 1) | |
} | |
} | |
else if(msgMap["profileId"] == "0104") | |
{ | |
logging("Generic response received", 1) | |
} | |
else | |
{ | |
log.warn "Unhandled Event - description:${description} | msgMap:${msgMap}" | |
} | |
// BEGIN:getGenericZigbeeParseFooter(loglevel=0) | |
// logging("PARSE END-----------------------", 1) | |
msgMap = null | |
return cmd | |
// END: getGenericZigbeeParseFooter(loglevel=0) | |
} | |
void positionEvent(Integer curtainPosition) { | |
String windowShadeStatus = "" | |
if(curtainPosition <= 2) curtainPosition = 1 | |
if(curtainPosition >= 98) curtainPosition = 100 | |
if(curtainPosition == 100) { | |
logging("Fully Open", 1) | |
windowShadeStatus = "open" | |
} else if(curtainPosition > 1) { | |
logging(curtainPosition + '% Partially Open', 1) | |
windowShadeStatus = "partially open" | |
} else { | |
logging("Closed", 1) | |
windowShadeStatus = "closed" | |
} | |
logging("device.currentValue('position') = ${device.currentValue('position')}, curtainPosition = $curtainPosition", 1) | |
if(device.currentValue('position') == null || | |
curtainPosition < device.currentValue('position') - 1 || | |
curtainPosition > device.currentValue('position') + 1) { | |
logging("CHANGING device.currentValue('position') = ${device.currentValue('position')}, curtainPosition = $curtainPosition", 1) | |
sendEvent(name:"windowShade", value: windowShadeStatus) | |
sendEvent(name:"position", value: curtainPosition) | |
} | |
} | |
/** | |
* --------- WRITE ATTRIBUTE METHODS --------- | |
*/ | |
ArrayList<String> open() | |
{ | |
ArrayList<String> cmd = [] | |
// cmd += zigbeeCommand(CLUSTER_ON_OFF, COMMAND_OPEN) | |
cmd += zigbeeWriteAttribute(CLUSTER_WINDOW_POSITION, POSITION_ATTR_VALUE, ENCODING_SIZE, Float.floatToIntBits(100)) | |
sendZigbeeCommands(cmd) | |
} | |
ArrayList<String> close() | |
{ | |
ArrayList<String> cmd = [] | |
// cmd += zigbeeCommand(CLUSTER_ON_OFF, COMMAND_CLOSE) | |
cmd += zigbeeWriteAttribute(CLUSTER_WINDOW_POSITION, POSITION_ATTR_VALUE, ENCODING_SIZE, Float.floatToIntBits(0)) | |
sendZigbeeCommands(cmd) | |
} | |
void setPosition(position) { | |
if(position == null) {position = 0} | |
if(position <= 2) position = 0 | |
if(position >= 98) position = 100 | |
ArrayList<String> cmd = [] | |
position = position as Integer | |
cmd += zigbeeWriteAttribute(CLUSTER_WINDOW_POSITION, POSITION_ATTR_VALUE, ENCODING_SIZE, Float.floatToIntBits(position)) | |
sendZigbeeCommands(cmd) | |
} | |
ArrayList<String> startPositionChange(String direction) { | |
logging("startPositionChange(direction=$direction)", 1) | |
if(direction == "open") | |
{ | |
return open() | |
} | |
else | |
{ | |
return close() | |
} | |
} | |
void requestPositionUpdate() | |
{ | |
ArrayList<String> cmd = [] | |
cmd += zigbeeReadAttribute(CLUSTER_WINDOW_POSITION, POSITION_ATTR_VALUE) | |
sendZigbeeCommands(cmd) | |
} | |
ArrayList<String> setLevel(level) { | |
logging("setLevel(level: ${level})", 1) | |
return setPosition(level) | |
} | |
ArrayList<String> setLevel(level, duration) { | |
logging("setLevel(level: ${level})", 1) | |
return setPosition(level) | |
} | |
/** | |
* --------- READ ATTRIBUTE METHODS --------- | |
*/ | |
ArrayList<String> getPosition() { | |
logging("getPosition()", 1) | |
ArrayList<String> cmd = [] | |
cmd += zigbeeReadAttribute(CLUSTER_WINDOW_POSITION, POSITION_ATTR_VALUE) | |
logging("cmd: $cmd", 1) | |
return cmd | |
} | |
/** | |
* ----------------------------------------------------------------------------- | |
* Everything below here are LIBRARY includes and should NOT be edited manually! | |
* ----------------------------------------------------------------------------- | |
* --- Nothing to edit here, move along! --------------------------------------- | |
* ----------------------------------------------------------------------------- | |
*/ | |
// BEGIN:getDefaultFunctions() | |
private String getDriverVersion() { | |
comment = "Works with model RSD-M01" | |
if(comment != "") state.comment = comment | |
String version = "v3" | |
logging("getDriverVersion() = ${version}", 100) | |
sendEvent(name: "driver", value: version) | |
updateDataValue('driver', version) | |
return version | |
} | |
// END: getDefaultFunctions() | |
// BEGIN:getLoggingFunction() | |
private boolean logging(message, level) { | |
boolean didLogging = false | |
Integer logLevelLocal = 0 | |
if (infoLogging == null || infoLogging == true) { | |
logLevelLocal = 100 | |
} | |
if (debugLogging == true) { | |
logLevelLocal = 1 | |
} | |
if (logLevelLocal != 0){ | |
switch (logLevelLocal) { | |
case 1: | |
if (level >= 1 && level < 99) { | |
log.debug "$message" | |
didLogging = true | |
} else if (level == 100) { | |
log.info "$message" | |
didLogging = true | |
} | |
break | |
case 100: | |
if (level == 100 ) { | |
log.info "$message" | |
didLogging = true | |
} | |
break | |
} | |
} | |
return didLogging | |
} | |
// END: getLoggingFunction() | |
// BEGIN:getHelperFunctions('zigbee-generic') | |
private getCLUSTER_BASIC() { 0x0000 } | |
private getCLUSTER_POWER() { 0x0001 } | |
private getCLUSTER_WINDOW_COVERING() { 0x0102 } | |
private getCLUSTER_WINDOW_POSITION() { 0x000d } | |
private getCLUSTER_PERCENTAGE() { 0x0005 } | |
private getCLUSTER_ON_OFF() { 0x0006 } | |
private getBASIC_ATTR_POWER_SOURCE() { 0x0007 } | |
private getPOWER_ATTR_BATTERY_PERCENTAGE_REMAINING() { 0x0021 } | |
private getPOSITION_ATTR_VALUE() { 0x0055 } | |
private getCOMMAND_OPEN() { 0x00 } | |
private getCOMMAND_CLOSE() { 0x01 } | |
private getCOMMAND_PAUSE() { 0x02 } | |
private getENCODING_SIZE() { 0x39 } | |
void updateNeededSettings() { | |
} | |
void refreshEvents() { | |
} | |
ArrayList<String> zigbeeCommand(Integer cluster, Integer command, Map additionalParams, int delay = 201, String... payload) { | |
ArrayList<String> cmd = zigbee.command(cluster, command, additionalParams, delay, payload) | |
cmd[0] = cmd[0].replace('0xnull', '0x01') | |
return cmd | |
} | |
ArrayList<String> zigbeeCommand(Integer cluster, Integer command, int delay = 202, String... payload) { | |
ArrayList<String> cmd = zigbee.command(cluster, command, [:], delay, payload) | |
cmd[0] = cmd[0].replace('0xnull', '0x01') | |
return cmd | |
} | |
ArrayList<String> zigbeeCommand(Integer endpoint, Integer cluster, Integer command, int delay = 203, String... payload) { | |
zigbeeCommand(endpoint, cluster, command, [:], delay, payload) | |
} | |
ArrayList<String> zigbeeCommand(Integer endpoint, Integer cluster, Integer command, Map additionalParams, int delay = 204, String... payload) { | |
String mfgCode = "" | |
if(additionalParams.containsKey("mfgCode")) { | |
mfgCode = " {${HexUtils.integerToHexString(HexUtils.hexStringToInt(additionalParams.get("mfgCode")), 2)}}" | |
} | |
String finalPayload = payload != null && payload != [] ? payload[0] : "" | |
String cmdArgs = "0x${device.deviceNetworkId} 0x${HexUtils.integerToHexString(endpoint, 1)} 0x${HexUtils.integerToHexString(cluster, 2)} " + | |
"0x${HexUtils.integerToHexString(command, 1)} " + | |
"{$finalPayload}" + | |
"$mfgCode" | |
ArrayList<String> cmd = ["he cmd $cmdArgs", "delay $delay"] | |
return cmd | |
} | |
ArrayList<String> zigbeeWriteAttribute(Integer cluster, Integer attributeId, Integer dataType, Integer value, Map additionalParams = [:], int delay = 199) { | |
ArrayList<String> cmd = zigbee.writeAttribute(cluster, attributeId, dataType, value, additionalParams, delay) | |
cmd[0] = cmd[0].replace('0xnull', '0x01') | |
return cmd | |
} | |
ArrayList<String> zigbeeWriteAttribute(Integer endpoint, Integer cluster, Integer attributeId, Integer dataType, Integer value, Map additionalParams = [:], int delay = 198) { | |
logging("zigbeeWriteAttribute()", 1) | |
String mfgCode = "" | |
if(additionalParams.containsKey("mfgCode")) { | |
mfgCode = " {${HexUtils.integerToHexString(HexUtils.hexStringToInt(additionalParams.get("mfgCode")), 2)}}" | |
} | |
String wattrArgs = "0x${device.deviceNetworkId} $endpoint 0x${HexUtils.integerToHexString(cluster, 2)} " + | |
"0x${HexUtils.integerToHexString(attributeId, 2)} " + | |
"0x${HexUtils.integerToHexString(dataType, 1)} " + | |
"{${HexUtils.integerToHexString(value, 1)}}" + | |
"$mfgCode" | |
ArrayList<String> cmd = ["he wattr $wattrArgs", "delay $delay"] | |
logging("zigbeeWriteAttribute cmd=$cmd", 1) | |
return cmd | |
} | |
ArrayList<String> zigbeeReadAttribute(Integer cluster, Integer attributeId, Map additionalParams = [:], int delay = 205) { | |
ArrayList<String> cmd = zigbee.readAttribute(cluster, attributeId, additionalParams, delay) | |
cmd[0] = cmd[0].replace('0xnull', '0x01') | |
return cmd | |
} | |
ArrayList<String> zigbeeReadAttribute(Integer endpoint, Integer cluster, Integer attributeId, int delay = 206) { | |
ArrayList<String> cmd = ["he rattr 0x${device.deviceNetworkId} ${endpoint} 0x${HexUtils.integerToHexString(cluster, 2)} 0x${HexUtils.integerToHexString(attributeId, 2)} {}", "delay $delay"] | |
return cmd | |
} | |
ArrayList<String> zigbeeWriteLongAttribute(Integer cluster, Integer attributeId, Integer dataType, Long value, Map additionalParams = [:], int delay = 207) { | |
return zigbeeWriteLongAttribute(1, cluster, attributeId, dataType, value, additionalParams, delay) | |
} | |
ArrayList<String> zigbeeWriteLongAttribute(Integer endpoint, Integer cluster, Integer attributeId, Integer dataType, Long value, Map additionalParams = [:], int delay = 208) { | |
logging("zigbeeWriteLongAttribute()", 1) | |
String mfgCode = "" | |
if(additionalParams.containsKey("mfgCode")) { | |
mfgCode = " {${HexUtils.integerToHexString(HexUtils.hexStringToInt(additionalParams.get("mfgCode")), 2)}}" | |
} | |
String wattrArgs = "0x${device.deviceNetworkId} $endpoint 0x${HexUtils.integerToHexString(cluster, 2)} " + | |
"0x${HexUtils.integerToHexString(attributeId, 2)} " + | |
"0x${HexUtils.integerToHexString(dataType, 1)} " + | |
"{${Long.toHexString(value)}}" + | |
"$mfgCode" | |
ArrayList<String> cmd = ["he wattr $wattrArgs", "delay $delay"] | |
logging("zigbeeWriteLongAttribute cmd=$cmd", 1) | |
return cmd | |
} | |
void sendZigbeeCommand(String cmd) { | |
logging("sendZigbeeCommand(cmd=$cmd)", 1) | |
sendZigbeeCommands([cmd]) | |
} | |
void sendZigbeeCommands(ArrayList<String> cmd) { | |
logging("sendZigbeeCommands(cmd=$cmd)", 1) | |
hubitat.device.HubMultiAction allActions = new hubitat.device.HubMultiAction() | |
cmd.each { | |
allActions.add(new hubitat.device.HubAction(it, hubitat.device.Protocol.ZIGBEE)) | |
} | |
sendHubCommand(allActions) | |
} | |
String setCleanModelName(String newModelToSet=null, List<String> acceptedModels=null) { | |
String model = newModelToSet != null ? newModelToSet : getDeviceDataByName('model') | |
model = model == null ? "null" : model | |
String newModel = model.replaceAll("[^A-Za-z0-9.\\-_ ]", "") | |
boolean found = false | |
if(acceptedModels != null) { | |
acceptedModels.each { | |
if(found == false && newModel.startsWith(it) == true) { | |
newModel = it | |
found = true | |
} | |
} | |
} | |
logging("dirty model = $model, clean model=$newModel", 1) | |
updateDataValue('model', newModel) | |
return newModel | |
} | |
Map unpackStructInMap(Map msgMap, String originalEncoding="4C") { | |
msgMap['encoding'] = originalEncoding | |
List<String> values = msgMap['value'].split("(?<=\\G..)") | |
logging("unpackStructInMap() values=$values", 1) | |
Integer numElements = Integer.parseInt(values.take(2).reverse().join(), 16) | |
values = values.drop(2) | |
List r = [] | |
Integer cType = null | |
List ret = null | |
while(values != []) { | |
cType = Integer.parseInt(values.take(1)[0], 16) | |
values = values.drop(1) | |
ret = zigbee_generic_convertStructValueToList(values, cType) | |
r += ret[0] | |
values = ret[1] | |
} | |
if(r.size() != numElements) throw new Exception("The STRUCT specifies $numElements elements, found ${r.size()}!") | |
msgMap['value'] = r | |
return msgMap | |
} | |
Map parseXiaomiStruct(String xiaomiStruct, boolean isFCC0=false, boolean hasLength=false) { | |
Map tags = [ | |
'01': 'battery', | |
'03': 'deviceTemperature', | |
'04': 'unknown1', | |
'05': 'RSSI_dB', | |
'06': 'LQI', | |
'07': 'unknown2', | |
'08': 'unknown3', | |
'09': 'unknown4', | |
'0A': 'routerid', | |
'0B': 'unknown5', | |
'0C': 'unknown6', | |
'6429': 'temperature', | |
'6410': 'openClose', | |
'6420': 'curtainPosition', | |
'6521': 'humidity', | |
'6510': 'switch2', | |
'66': 'pressure', | |
'6E': 'unknown10', | |
'6F': 'unknown11', | |
'95': 'consumption', | |
'96': 'voltage', | |
'98': 'power', | |
'9721': 'gestureCounter1', | |
'9739': 'consumption', | |
'9821': 'gestureCounter2', | |
'9839': 'power', | |
'99': 'gestureCounter3', | |
'9A21': 'gestureCounter4', | |
'9A20': 'unknown7', | |
'9A25': 'accelerometerXYZ', | |
'9B': 'unknown9', | |
] | |
if(isFCC0 == true) { | |
tags['05'] = 'numBoots' | |
tags['6410'] = 'onOff' | |
tags['95'] = 'current' | |
} | |
List<String> values = xiaomiStruct.split("(?<=\\G..)") | |
if(hasLength == true) values = values.drop(1) | |
Map r = [:] | |
r["raw"] = [:] | |
String cTag = null | |
String cTypeStr = null | |
Integer cType = null | |
String cKey = null | |
List ret = null | |
while(values != []) { | |
cTag = values.take(1)[0] | |
values = values.drop(1) | |
cTypeStr = values.take(1)[0] | |
cType = Integer.parseInt(cTypeStr, 16) | |
values = values.drop(1) | |
if(tags.containsKey(cTag+cTypeStr)) { | |
cKey = tags[cTag+cTypeStr] | |
} else if(tags.containsKey(cTag)) { | |
cKey = tags[cTag] | |
} else { | |
cKey = "unknown${cTag}${cTypeStr}" | |
log.warn("PLEASE REPORT TO DEV - The Xiaomi Struct used an unrecognized tag: 0x$cTag (type: 0x$cTypeStr) (struct: $xiaomiStruct)") | |
} | |
ret = zigbee_generic_convertStructValue(r, values, cType, cKey, cTag) | |
r = ret[0] | |
values = ret[1] | |
} | |
return r | |
} | |
Map parseAttributeStruct(List data, boolean hasLength=false) { | |
Map tags = [ | |
'0000': 'ZCLVersion', | |
'0001': 'applicationVersion', | |
'0002': 'stackVersion', | |
'0003': 'HWVersion', | |
'0004': 'manufacturerName', | |
'0005': 'dateCode', | |
'0006': 'modelIdentifier', | |
'0007': 'powerSource', | |
'0010': 'locationDescription', | |
'0011': 'physicalEnvironment', | |
'0012': 'deviceEnabled', | |
'0013': 'alarmMask', | |
'0014': 'disableLocalConfig', | |
'4000': 'SWBuildID', | |
] | |
List<String> values = data | |
if(hasLength == true) values = values.drop(1) | |
Map r = [:] | |
r["raw"] = [:] | |
String cTag = null | |
String cTypeStr = null | |
Integer cType = null | |
String cKey = null | |
List ret = null | |
while(values != []) { | |
cTag = values.take(2).reverse().join() | |
values = values.drop(2) | |
values = values.drop(1) | |
cTypeStr = values.take(1)[0] | |
cType = Integer.parseInt(cTypeStr, 16) | |
values = values.drop(1) | |
if(tags.containsKey(cTag+cTypeStr)) { | |
cKey = tags[cTag+cTypeStr] | |
} else if(tags.containsKey(cTag)) { | |
cKey = tags[cTag] | |
} else { | |
throw new Exception("The Xiaomi Struct used an unrecognized tag: 0x$cTag (type: 0x$cTypeStr)") | |
} | |
ret = zigbee_generic_convertStructValue(r, values, cType, cKey, cTag) | |
r = ret[0] | |
values = ret[1] | |
} | |
return r | |
} | |
def zigbee_generic_decodeZigbeeData(String value, String cTypeStr, boolean reverseBytes=true) { | |
List values = value.split("(?<=\\G..)") | |
values = reverseBytes == true ? values.reverse() : values | |
Integer cType = Integer.parseInt(cTypeStr, 16) | |
Map rMap = [:] | |
rMap['raw'] = [:] | |
List ret = zigbee_generic_convertStructValue(rMap, values, cType, "NA", "NA") | |
return ret[0]["NA"] | |
} | |
List zigbee_generic_convertStructValueToList(List values, Integer cType) { | |
Map rMap = [:] | |
rMap['raw'] = [:] | |
List ret = zigbee_generic_convertStructValue(rMap, values, cType, "NA", "NA") | |
return [ret[0]["NA"], ret[1]] | |
} | |
List zigbee_generic_convertStructValue(Map r, List values, Integer cType, String cKey, String cTag) { | |
String cTypeStr = cType != null ? integerToHexString(cType, 1) : null | |
switch(cType) { | |
case 0x10: | |
r["raw"][cKey] = values.take(1)[0] | |
r[cKey] = Integer.parseInt(r["raw"][cKey], 16) != 0 | |
values = values.drop(1) | |
break | |
case 0x18: | |
case 0x20: | |
r["raw"][cKey] = values.take(1)[0] | |
r[cKey] = Integer.parseInt(r["raw"][cKey], 16) | |
values = values.drop(1) | |
break | |
case 0x19: | |
case 0x21: | |
r["raw"][cKey] = values.take(2).reverse().join() | |
r[cKey] = Integer.parseInt(r["raw"][cKey], 16) | |
values = values.drop(2) | |
break | |
case 0x1A: | |
case 0x22: | |
r["raw"][cKey] = values.take(3).reverse().join() | |
r[cKey] = Integer.parseInt(r["raw"][cKey], 16) | |
values = values.drop(3) | |
break | |
case 0x1B: | |
case 0x23: | |
r["raw"][cKey] = values.take(4).reverse().join() | |
r[cKey] = Long.parseLong(r["raw"][cKey], 16) | |
values = values.drop(4) | |
break | |
case 0x1C: | |
case 0x24: | |
r["raw"][cKey] = values.take(5).reverse().join() | |
r[cKey] = Long.parseLong(r["raw"][cKey], 16) | |
values = values.drop(5) | |
break | |
case 0x1D: | |
case 0x25: | |
r["raw"][cKey] = values.take(6).reverse().join() | |
r[cKey] = Long.parseLong(r["raw"][cKey], 16) | |
values = values.drop(6) | |
break | |
case 0x1E: | |
case 0x26: | |
r["raw"][cKey] = values.take(7).reverse().join() | |
r[cKey] = Long.parseLong(r["raw"][cKey], 16) | |
values = values.drop(7) | |
break | |
case 0x1F: | |
case 0x27: | |
r["raw"][cKey] = values.take(8).reverse().join() | |
r[cKey] = new BigInteger(r["raw"][cKey], 16) | |
values = values.drop(8) | |
break | |
case 0x28: | |
r["raw"][cKey] = values.take(1).reverse().join() | |
r[cKey] = convertToSignedInt8(Integer.parseInt(r["raw"][cKey], 16)) | |
values = values.drop(1) | |
break | |
case 0x29: | |
r["raw"][cKey] = values.take(2).reverse().join() | |
r[cKey] = (Integer) (short) Integer.parseInt(r["raw"][cKey], 16) | |
values = values.drop(2) | |
break | |
case 0x2B: | |
r["raw"][cKey] = values.take(4).reverse().join() | |
r[cKey] = (Integer) Long.parseLong(r["raw"][cKey], 16) | |
values = values.drop(4) | |
break | |
case 0x30: | |
r["raw"][cKey] = values.take(1)[0] | |
r[cKey] = Integer.parseInt(r["raw"][cKey], 16) | |
values = values.drop(1) | |
break | |
case 0x31: | |
r["raw"][cKey] = values.take(2).reverse().join() | |
r[cKey] = Integer.parseInt(r["raw"][cKey], 16) | |
values = values.drop(2) | |
break | |
case 0x39: | |
r["raw"][cKey] = values.take(4).reverse().join() | |
r[cKey] = parseSingleHexToFloat(r["raw"][cKey]) | |
values = values.drop(4) | |
break | |
case 0x42: | |
Integer strLength = Integer.parseInt(values.take(1)[0], 16) | |
values = values.drop(1) | |
r["raw"][cKey] = values.take(strLength) | |
r[cKey] = r["raw"][cKey].collect { | |
(char)(int) Integer.parseInt(it, 16) | |
}.join() | |
values = values.drop(strLength) | |
break | |
default: | |
throw new Exception("The Struct used an unrecognized type: $cTypeStr ($cType) for tag 0x$cTag with key $cKey (values: $values, map: $r)") | |
} | |
return [r, values] | |
} | |
ArrayList<String> zigbeeWriteHexStringAttribute(Integer cluster, Integer attributeId, Integer dataType, String value, Map additionalParams = [:], int delay = 209) { | |
logging("zigbeeWriteBigIntegerAttribute()", 1) | |
String mfgCode = "" | |
if(additionalParams.containsKey("mfgCode")) { | |
mfgCode = " {${integerToHexString(HexUtils.hexStringToInt(additionalParams.get("mfgCode")), 2, reverse=true)}}" | |
} | |
String wattrArgs = "0x${device.deviceNetworkId} 0x01 0x${HexUtils.integerToHexString(cluster, 2)} " + | |
"0x${HexUtils.integerToHexString(attributeId, 2)} " + | |
"0x${HexUtils.integerToHexString(dataType, 1)} " + | |
"{${value.split("(?<=\\G..)").reverse().join()}}" + | |
"$mfgCode" | |
ArrayList<String> cmd = ["he wattr $wattrArgs", "delay $delay"] | |
logging("zigbeeWriteBigIntegerAttribute cmd=$cmd", 1) | |
return cmd | |
} | |
ArrayList<String> zigbeeReadAttributeList(Integer cluster, List<Integer> attributeIds, Map additionalParams = [:], int delay = 211) { | |
logging("zigbeeReadAttributeList()", 1) | |
String mfgCode = "0000" | |
if(additionalParams.containsKey("mfgCode")) { | |
mfgCode = "${integerToHexString(HexUtils.hexStringToInt(additionalParams.get("mfgCode")), 2, reverse=true)}" | |
log.error "Manufacturer code support is NOT implemented!" | |
} | |
List<String> attributeIdsString = [] | |
attributeIds.each { attributeIdsString.add(integerToHexString(it, 2, reverse=true)) } | |
logging("attributeIds=$attributeIds, attributeIdsString=$attributeIdsString", 100) | |
String rattrArgs = "0x${device.deviceNetworkId} 1 0x01 0x${integerToHexString(cluster, 2)} " + | |
"{000000${attributeIdsString.join()}}" | |
ArrayList<String> cmd = ["he raw $rattrArgs", "delay $delay"] | |
logging("zigbeeWriteLongAttribute cmd=$cmd", 1) | |
return cmd | |
} | |
Float parseSingleHexToFloat(String singleHex) { | |
return Float.intBitsToFloat(Long.valueOf(singleHex, 16).intValue()) | |
} | |
Integer convertToSignedInt8(Integer signedByte) { | |
Integer sign = signedByte & (1 << 7) | |
return (signedByte & 0x7f) * (sign != 0 ? -1 : 1) | |
} | |
Integer parseIntReverseHex(String hexString) { | |
return Integer.parseInt(hexString.split("(?<=\\G..)").reverse().join(), 16) | |
} | |
Long parseLongReverseHex(String hexString) { | |
return Long.parseLong(hexString.split("(?<=\\G..)").reverse().join(), 16) | |
} | |
String integerToHexString(BigDecimal value, Integer minBytes, boolean reverse=false) { | |
return integerToHexString(value.intValue(), minBytes, reverse=reverse) | |
} | |
String integerToHexString(Integer value, Integer minBytes, boolean reverse=false) { | |
if(reverse == true) { | |
return HexUtils.integerToHexString(value, minBytes).split("(?<=\\G..)").reverse().join() | |
} else { | |
return HexUtils.integerToHexString(value, minBytes) | |
} | |
} | |
String bigIntegerToHexString(BigInteger value, Integer minBytes, boolean reverse=false) { | |
if(reverse == true) { | |
return value.toString(16).reverse().join() | |
} else { | |
return String.format("%0${minBytes*2}x", value) | |
} | |
} | |
BigInteger hexStringToBigInteger(String hexString, boolean reverse=false) { | |
if(reverse == true) { | |
return new BigInteger(hexString.split("(?<=\\G..)").reverse().join(), 16) | |
} else { | |
return new BigInteger(hexString, 16) | |
} | |
} | |
void updateManufacturer(String manfacturer) { | |
if(getDataValue("manufacturer") == null) { | |
updateDataValue("manufacturer", manfacturer) | |
} | |
} | |
void updateApplicationId(String application) { | |
if(getDataValue("application") == null) { | |
updateDataValue("application", application) | |
} | |
} | |
Integer retrieveEndpointId() { | |
String endpointIdRaw = getDataValue("endpointId") | |
BigDecimal endpointId = endpointIdRaw == null ? 1 : hexStringToBigInteger(endpointIdRaw) | |
return endpointId.intValue() | |
} | |
Map parseSimpleDescriptorData(List<String> data) { | |
Map<String,String> d = [:] | |
if(data[1] == "00") { | |
d["nwkAddrOfInterest"] = data[2..3].reverse().join() | |
Integer ll = Integer.parseInt(data[4], 16) | |
d["endpointId"] = data[5] | |
d["profileId"] = data[6..7].reverse().join() | |
d["applicationDevice"] = data[8..9].reverse().join() | |
d["applicationVersion"] = data[10] | |
Integer icn = Integer.parseInt(data[11], 16) | |
Integer pos = 12 | |
Integer cPos = null | |
d["inClusters"] = "" | |
if(icn > 0) { | |
(1..icn).each() {b-> | |
cPos = pos+((b-1)*2) | |
d["inClusters"] += data[cPos..cPos+1].reverse().join() | |
if(b < icn) { | |
d["inClusters"] += "," | |
} | |
} | |
} | |
pos += icn*2 | |
Integer ocn = Integer.parseInt(data[pos], 16) | |
pos += 1 | |
d["outClusters"] = "" | |
if(ocn > 0) { | |
(1..ocn).each() {b-> | |
cPos = pos+((b-1)*2) | |
d["outClusters"] += data[cPos..cPos+1].reverse().join() | |
if(b < ocn) { | |
d["outClusters"] += "," | |
} | |
} | |
} | |
logging("d=$d, ll=$ll, icn=$icn, ocn=$ocn", 1) | |
} else { | |
log.warn("Incorrect Simple Descriptor Data received: $data") | |
} | |
return d | |
} | |
void updateDataFromSimpleDescriptorData(List<String> data) { | |
Map<String,String> sdi = parseSimpleDescriptorData(data) | |
if(sdi != [:]) { | |
updateDataValue("endpointId", sdi['endpointId']) | |
updateDataValue("profileId", sdi['profileId']) | |
updateDataValue("inClusters", sdi['inClusters']) | |
updateDataValue("outClusters", sdi['outClusters']) | |
getInfo(true, sdi) | |
} else { | |
log.warn("No VALID Simple Descriptor Data received!") | |
} | |
sdi = null | |
} | |
void getInfo(boolean ignoreMissing=false, Map<String,String> sdi = [:]) { | |
log.debug("Getting info for Zigbee device...") | |
String endpointId = device.getEndpointId() | |
endpointId = endpointId == null ? getDataValue("endpointId") : endpointId | |
String profileId = getDataValue("profileId") | |
String inClusters = getDataValue("inClusters") | |
String outClusters = getDataValue("outClusters") | |
String model = getDataValue("model") | |
String manufacturer = getDataValue("manufacturer") | |
String application = getDataValue("application") | |
if(sdi != [:]) { | |
endpointId = endpointId == null ? sdi['endpointId'] : endpointId | |
profileId = profileId == null ? sdi['profileId'] : profileId | |
inClusters = inClusters == null ? sdi['inClusters'] : inClusters | |
outClusters = outClusters == null ? sdi['outClusters'] : outClusters | |
sdi = null | |
} | |
String extraFingerPrint = "" | |
boolean missing = false | |
String requestingFromDevice = ", requesting it from the device. If it is a sleepy device you may have to wake it up and run this command again. Run this command again to get the new fingerprint." | |
if(ignoreMissing==true) { | |
requestingFromDevice = ". Try again." | |
} | |
if(manufacturer == null) { | |
missing = true | |
log.warn("Manufacturer name is missing for the fingerprint$requestingFromDevice") | |
if(ignoreMissing==false) sendZigbeeCommands(zigbee.readAttribute(CLUSTER_BASIC, 0x0004)) | |
} | |
log.trace("Manufacturer: $manufacturer") | |
if(model == null) { | |
missing = true | |
log.warn("Model name is missing for the fingerprint$requestingFromDevice") | |
if(ignoreMissing==false) sendZigbeeCommands(zigbee.readAttribute(CLUSTER_BASIC, 0x0005)) | |
} | |
log.trace("Model: $model") | |
if(application == null) { | |
log.info("NOT IMPORTANT: Application ID is missing for the fingerprint$requestingFromDevice") | |
if(ignoreMissing==false) sendZigbeeCommands(zigbee.readAttribute(CLUSTER_BASIC, 0x0001)) | |
} else { | |
extraFingerPrint += ", application:\"$application\"" | |
} | |
log.trace("Application: $application") | |
if(profileId == null || endpointId == null || inClusters == null || outClusters == null) { | |
missing = true | |
String endpointIdTemp = endpointId == null ? "01" : endpointId | |
log.warn("One or multiple pieces of data needed for the fingerprint is missing$requestingFromDevice") | |
if(ignoreMissing==false) sendZigbeeCommands(["he raw ${device.deviceNetworkId} 0 0 0x0004 {00 ${zigbee.swapOctets(device.deviceNetworkId)} $endpointIdTemp} {0x0000}"]) | |
} | |
profileId = profileId == null ? "0104" : profileId | |
if(missing == true) { | |
log.info("INCOMPLETE - DO NOT SUBMIT THIS - TRY AGAIN: fingerprint model:\"$model\", manufacturer:\"$manufacturer\", profileId:\"$profileId\", endpointId:\"$endpointId\", inClusters:\"$inClusters\", outClusters:\"$outClusters\"" + extraFingerPrint) | |
} else { | |
log.info("COPY AND PASTE THIS ROW TO THE DEVELOPER: fingerprint model:\"$model\", manufacturer:\"$manufacturer\", profileId:\"$profileId\", endpointId:\"$endpointId\", inClusters:\"$inClusters\", outClusters:\"$outClusters\"" + extraFingerPrint) | |
} | |
} | |
// END: getHelperFunctions('zigbee-generic') | |
// BEGIN:getHelperFunctions('all-default') | |
boolean isDriver() { | |
try { | |
getDeviceDataByName('_unimportant') | |
logging("This IS a driver!", 1) | |
return true | |
} catch (MissingMethodException e) { | |
logging("This is NOT a driver!", 1) | |
return false | |
} | |
} | |
void deviceCommand(String cmd) { | |
def jsonSlurper = new JsonSlurper() | |
def cmds = jsonSlurper.parseText(cmd) | |
r = this."${cmds['cmd']}"(*cmds['args']) | |
updateDataValue('appReturn', JsonOutput.toJson(r)) | |
} | |
void setLogsOffTask(boolean noLogWarning=false) { | |
if (debugLogging == true) { | |
if(noLogWarning==false) { | |
if(runReset != "DEBUG") { | |
log.warn "Debug logging will be disabled in 30 minutes..." | |
} else { | |
log.warn "Debug logging will NOT BE AUTOMATICALLY DISABLED!" | |
} | |
} | |
runIn(1800, "logsOff") | |
} | |
} | |
void logsOff() { | |
if(runReset != "DEBUG") { | |
log.warn "Debug logging disabled... " | |
if(isDriver()) { | |
device.clearSetting("logLevel") | |
device.removeSetting("logLevel") | |
device.updateSetting("logLevel", "0") | |
state?.settings?.remove("logLevel") | |
device.clearSetting("debugLogging") | |
device.removeSetting("debugLogging") | |
device.updateSetting("debugLogging", "false") | |
state?.settings?.remove("debugLogging") | |
} else { | |
app.removeSetting("logLevel") | |
app.updateSetting("logLevel", "0") | |
app.removeSetting("debugLogging") | |
app.updateSetting("debugLogging", "false") | |
} | |
} else { | |
log.warn "OVERRIDE: Disabling Debug logging will not execute with 'DEBUG' set..." | |
if (logLevel != "0" && logLevel != "100") runIn(1800, "logsOff") | |
} | |
} | |
boolean isDeveloperHub() { | |
return generateMD5(location.hub.zigbeeId as String) == "125fceabd0413141e34bb859cd15e067_disabled" | |
} | |
def getEnvironmentObject() { | |
if(isDriver()) { | |
return device | |
} else { | |
return app | |
} | |
} | |
private def getFilteredDeviceDriverName() { | |
def deviceDriverName = getDeviceInfoByName('name') | |
if(deviceDriverName.toLowerCase().endsWith(' (parent)')) { | |
deviceDriverName = deviceDriverName.substring(0, deviceDriverName.length()-9) | |
} | |
return deviceDriverName | |
} | |
private def getFilteredDeviceDisplayName() { | |
def deviceDisplayName = device.displayName.replace(' (parent)', '').replace(' (Parent)', '') | |
return deviceDisplayName | |
} | |
BigDecimal round2(BigDecimal number, Integer scale) { | |
Integer pow = 10; | |
for (Integer i = 1; i < scale; i++) | |
pow *= 10; | |
BigDecimal tmp = number * pow; | |
return ( (Float) ( (Integer) ((tmp - (Integer) tmp) >= 0.5f ? tmp + 1 : tmp) ) ) / pow; | |
} | |
String generateMD5(String s) { | |
if(s != null) { | |
return MessageDigest.getInstance("MD5").digest(s.bytes).encodeHex().toString() | |
} else { | |
return "null" | |
} | |
} | |
Integer extractInt(String input) { | |
return input.replaceAll("[^0-9]", "").toInteger() | |
} | |
String hexToASCII(String hexValue) { | |
StringBuilder output = new StringBuilder("") | |
for (int i = 0; i < hexValue.length(); i += 2) { | |
String str = hexValue.substring(i, i + 2) | |
output.append((char) Integer.parseInt(str, 16) + 30) | |
logging("${Integer.parseInt(str, 16)}", 10) | |
} | |
return output.toString() | |
} | |
// END: getHelperFunctions('all-default') | |
// BEGIN:getHelperFunctions('driver-default') | |
String getDEGREE() { return String.valueOf((char)(176)) } | |
void refresh(String cmd) { | |
deviceCommand(cmd) | |
} | |
def installedDefault() { | |
logging("installedDefault()", 100) | |
try { | |
tasmota_installedPreConfigure() | |
} catch (MissingMethodException e) { | |
} | |
try { | |
installedAdditional() | |
} catch (MissingMethodException e) { | |
} | |
} | |
def configureDefault() { | |
logging("configureDefault()", 100) | |
try { | |
return configureAdditional() | |
} catch (MissingMethodException e) { | |
} | |
try { | |
getDriverVersion() | |
} catch (MissingMethodException e) { | |
} | |
} | |
void configureDelayed() { | |
runIn(10, "configure") | |
runIn(30, "refresh") | |
} | |
void configurePresence() { | |
prepareCounters() | |
if(presenceEnable == null || presenceEnable == true) { | |
Random rnd = new Random() | |
schedule("${rnd.nextInt(59)} ${rnd.nextInt(59)} 1/3 * * ? *", 'checkPresence') | |
checkPresence(false) | |
} else { | |
sendEvent(name: "presence", value: "not present", descriptionText: "Presence Checking Disabled" ) | |
unschedule('checkPresence') | |
} | |
} | |
void stopSchedules() { | |
unschedule() | |
log.info("Stopped ALL Device Schedules!") | |
} | |
void prepareCounters() { | |
if(device.currentValue('restoredCounter') == null) sendEvent(name: "restoredCounter", value: 0, descriptionText: "Initialized to 0" ) | |
if(device.currentValue('notPresentCounter') == null) sendEvent(name: "notPresentCounter", value: 0, descriptionText: "Initialized to 0" ) | |
if(device.currentValue('presence') == null) sendEvent(name: "presence", value: "unknown", descriptionText: "Initialized as Unknown" ) | |
} | |
boolean isValidDate(String dateFormat, String dateString) { | |
try { | |
Date.parse(dateFormat, dateString) | |
} catch (e) { | |
return false | |
} | |
return true | |
} | |
Integer retrieveMinimumMinutesToRepeat(Integer minimumMinutesToRepeat=55) { | |
Integer mmr = null | |
if(state.forcedMinutes == null || state.forcedMinutes == 0) { | |
mmr = minimumMinutesToRepeat | |
} else { | |
mmr = state.forcedMinutes - 1 < 1 ? 1 : state.forcedMinutes.intValue() - 1 | |
} | |
return mmr | |
} | |
boolean sendlastCheckinEvent(Integer minimumMinutesToRepeat=55) { | |
boolean r = false | |
Integer mmr = retrieveMinimumMinutesToRepeat(minimumMinutesToRepeat=minimumMinutesToRepeat) | |
if (lastCheckinEnable == true || lastCheckinEnable == null) { | |
String lastCheckinVal = device.currentValue('lastCheckin') | |
if(lastCheckinVal == null || isValidDate('yyyy-MM-dd HH:mm:ss', lastCheckinVal) == false || now() >= Date.parse('yyyy-MM-dd HH:mm:ss', lastCheckinVal).getTime() + (mmr * 60 * 1000)) { | |
r = true | |
sendEvent(name: "lastCheckin", value: new Date().format('yyyy-MM-dd HH:mm:ss')) | |
logging("Updated lastCheckin", 1) | |
} else { | |
} | |
} | |
if (lastCheckinEpochEnable == true) { | |
if(device.currentValue('lastCheckinEpoch') == null || now() >= device.currentValue('lastCheckinEpoch').toLong() + (mmr * 60 * 1000)) { | |
r = true | |
sendEvent(name: "lastCheckinEpoch", value: now()) | |
logging("Updated lastCheckinEpoch", 1) | |
} else { | |
} | |
} | |
if(r == true) setAsPresent() | |
return r | |
} | |
Long secondsSinceLastCheckinEvent() { | |
Long r = null | |
if (lastCheckinEnable == true || lastCheckinEnable == null) { | |
String lastCheckinVal = device.currentValue('lastCheckin') | |
if(lastCheckinVal == null || isValidDate('yyyy-MM-dd HH:mm:ss', lastCheckinVal) == false) { | |
logging("No VALID lastCheckin event available! This should be resolved by itself within 1 or 2 hours and is perfectly NORMAL as long as the same device don't get this multiple times per day...", 100) | |
r = -1 | |
} else { | |
r = (now() - Date.parse('yyyy-MM-dd HH:mm:ss', lastCheckinVal).getTime()) / 1000 | |
} | |
} | |
if (lastCheckinEpochEnable == true) { | |
if(device.currentValue('lastCheckinEpoch') == null) { | |
logging("No VALID lastCheckin event available! This should be resolved by itself within 1 or 2 hours and is perfectly NORMAL as long as the same device don't get this multiple times per day...", 100) | |
r = r == null ? -1 : r | |
} else { | |
r = (now() - device.currentValue('lastCheckinEpoch').toLong()) / 1000 | |
} | |
} | |
return r | |
} | |
boolean hasCorrectCheckinEvents(Integer maximumMinutesBetweenEvents=90, boolean displayWarnings=true) { | |
Long secondsSinceLastCheckin = secondsSinceLastCheckinEvent() | |
if(secondsSinceLastCheckin != null && secondsSinceLastCheckin > maximumMinutesBetweenEvents * 60) { | |
if(displayWarnings == true && (presenceWarningEnable == null || presenceWarningEnable == true)) log.warn("One or several EXPECTED checkin events have been missed! Something MIGHT be wrong with the mesh for this device. Minutes since last checkin: ${Math.round(secondsSinceLastCheckin / 60)} (maximum expected $maximumMinutesBetweenEvents)") | |
return false | |
} | |
return true | |
} | |
boolean checkPresence(boolean displayWarnings=true) { | |
boolean isPresent = false | |
Long lastCheckinTime = null | |
String lastCheckinVal = device.currentValue('lastCheckin') | |
if ((lastCheckinEnable == true || lastCheckinEnable == null) && isValidDate('yyyy-MM-dd HH:mm:ss', lastCheckinVal) == true) { | |
lastCheckinTime = Date.parse('yyyy-MM-dd HH:mm:ss', lastCheckinVal).getTime() | |
} else if (lastCheckinEpochEnable == true && device.currentValue('lastCheckinEpoch') != null) { | |
lastCheckinTime = device.currentValue('lastCheckinEpoch').toLong() | |
} | |
if(lastCheckinTime != null && lastCheckinTime >= now() - (3 * 60 * 60 * 1000)) { | |
setAsPresent() | |
isPresent = true | |
} else { | |
sendEvent(name: "presence", value: "not present") | |
if(displayWarnings == true) { | |
Integer numNotPresent = device.currentValue('notPresentCounter') | |
numNotPresent = numNotPresent == null ? 1 : numNotPresent + 1 | |
sendEvent(name: "notPresentCounter", value: numNotPresent ) | |
if(presenceWarningEnable == null || presenceWarningEnable == true) { | |
log.warn("No event seen from the device for over 3 hours! Something is not right... (consecutive events: $numNotPresent)") | |
} | |
} | |
} | |
return isPresent | |
} | |
void setAsPresent() { | |
if(device.currentValue('presence') == "not present") { | |
Integer numRestored = device.currentValue('restoredCounter') | |
numRestored = numRestored == null ? 1 : numRestored + 1 | |
sendEvent(name: "restoredCounter", value: numRestored ) | |
sendEvent(name: "notPresentCounter", value: 0 ) | |
} | |
sendEvent(name: "presence", value: "present") | |
} | |
void resetNotPresentCounter() { | |
logging("resetNotPresentCounter()", 100) | |
sendEvent(name: "notPresentCounter", value: 0, descriptionText: "Reset notPresentCounter to 0" ) | |
} | |
void resetRestoredCounter() { | |
logging("resetRestoredCounter()", 100) | |
sendEvent(name: "restoredCounter", value: 0, descriptionText: "Reset restoredCounter to 0" ) | |
} | |
// END: getHelperFunctions('driver-default') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment