Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
ad8a665
Update switchbotSystem
dds82 Mar 22, 2024
08e2cf4
Update switchbotSystem
dds82 Mar 22, 2024
df0bdb4
Update switchbotBlindTilt
dds82 Mar 22, 2024
2a2ebe9
Update switchbotBot
dds82 Mar 22, 2024
693026a
Update switchbotContactSensor
dds82 Mar 22, 2024
48c8ad4
Update switchbotCurtain
dds82 Mar 22, 2024
61bc84c
Update switchbotEventsApp
dds82 Mar 22, 2024
cb948cd
Update switchbotHumidifier
dds82 Mar 22, 2024
9720009
Update switchbotIRDevice
dds82 Mar 22, 2024
1c2ff4f
Update switchbotMeter
dds82 Mar 22, 2024
1dfc8d4
Update switchbotMotionSensor
dds82 Mar 22, 2024
f1569d6
Update switchbotPlugMini
dds82 Mar 22, 2024
85c9e06
Update switchbotSmartLock
dds82 Mar 22, 2024
8023f71
Update switchbotStripLight
dds82 Mar 22, 2024
b81b1be
Create switchbotKeypadTouch
dds82 Mar 22, 2024
e5b4d83
Update switchbotSystem
dds82 Mar 22, 2024
47923a0
Update switchbotKeypadTouch
dds82 Mar 22, 2024
231d0e1
added rereadDevices
dds82 Mar 25, 2024
273cddf
added decryptPasscode
dds82 Mar 25, 2024
d417d64
fixed variables
dds82 Mar 25, 2024
e4447f2
add package path
dds82 Mar 25, 2024
e517508
implemented key codes
dds82 Mar 25, 2024
db09fd3
added requestor to rereadDevices
dds82 Mar 25, 2024
fd6fba1
pass in this device as the requestor to rereadDevices
dds82 Mar 25, 2024
aed1e90
use DNI
dds82 Mar 25, 2024
a6351b8
use DNI
dds82 Mar 25, 2024
bbe1639
implement LockCodes capability
dds82 Mar 25, 2024
8461b5f
added bounds check
dds82 Mar 25, 2024
7b8fee6
added map of codes by name
dds82 Mar 25, 2024
52abb71
Hub mini 2 and smart lock pro
dds82 Apr 4, 2024
0bda7b8
smart lock pro
dds82 Apr 4, 2024
126389d
smart lock pro
dds82 Apr 4, 2024
e2e4d2b
Smart Lock Pro doesn't send a device type in its updates
dds82 Apr 4, 2024
f5ad9c8
Added battery level
dds82 Apr 4, 2024
7a10f65
added missing import
dds82 Apr 4, 2024
eda7725
fixed typo
dds82 Apr 4, 2024
3676e4d
fixed lock code decryption
dds82 Apr 5, 2024
b7ea431
debug logging
dds82 Apr 5, 2024
fd28fd9
fixed atomicstate usage
dds82 Apr 5, 2024
f587cf7
logging
dds82 Apr 5, 2024
cd94af1
logging
dds82 Apr 5, 2024
14fcfde
logging
dds82 Apr 5, 2024
4965a02
more user friendly support for timed keys
dds82 Apr 5, 2024
a2dea54
changed label on createKey
dds82 Apr 5, 2024
e3ac8ea
lock code encryption
dds82 Apr 5, 2024
570caf5
fixed lock code manager json
dds82 Apr 5, 2024
c4a298b
lock code encryption
dds82 Apr 5, 2024
7ede505
Made auto delete expired codes optional
dds82 May 5, 2024
4edc9b6
Save code status in state
dds82 May 5, 2024
f81e8a9
Added debug logging
dds82 May 5, 2024
220cd34
don't allow creating timed codes effective in the past
dds82 May 5, 2024
d57b949
exception handling
dds82 May 5, 2024
5c001c6
added lock pro device type
dds82 May 7, 2024
69db597
Added battery capability
dds82 May 10, 2024
11550c1
fixes for create key
dds82 May 10, 2024
a752a3c
Update switchbotBlindTilt
dds82 May 12, 2024
c6834c8
Update switchbotBot
dds82 May 12, 2024
9451214
Update switchbotContactSensor
dds82 May 12, 2024
ef18a1d
Update switchbotCurtain
dds82 May 12, 2024
59c30ce
Update switchbotHumidifier
dds82 May 12, 2024
5aa32be
Update switchbotIRDevice
dds82 May 12, 2024
8914c4c
Update switchbotMeter
dds82 May 12, 2024
a95d3eb
Update switchbotMotionSensor
dds82 May 12, 2024
0c4ef6e
Update switchbotPlugMini
dds82 May 12, 2024
73bb7bf
Update switchbotSmartLock
dds82 May 12, 2024
e950cb0
Update switchbotStripLight
dds82 May 12, 2024
1818bc4
Update switchbotKeypadTouch
dds82 May 12, 2024
13282f7
Update switchbotSystem
dds82 May 12, 2024
58d3826
Update switchbotSystem
dds82 May 12, 2024
856ca0e
Update switchbotSystem
dds82 May 13, 2024
a0df0f9
Update switchbotKeypadTouch
dds82 May 13, 2024
d4d0e55
Update switchbotKeypadTouch
dds82 May 13, 2024
d522104
got rid of pending commands count
dds82 May 17, 2024
39827bb
Update switchbotSystem
dds82 Aug 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion switchbotContactSensor
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,3 @@ def parse(body)
sendEvent(name: "motion", value: ([true, "DETECTED"].contains(body.moveDetected)) ? "active" : "inactive")
}
}

17 changes: 13 additions & 4 deletions switchbotEventsApp
Original file line number Diff line number Diff line change
Expand Up @@ -157,15 +157,22 @@ def observeEvent()

def processEvent(eventMap)
{
if(!eventMap) { return }

logDebug("incoming event: ${eventMap}")

if(!eventMap) { return }

if("changeReport" != eventMap?.eventType) { return }

// check whether we know about this type of device
def devTypeInfo = deviceTypesMap.getAt(eventMap.context.deviceType)
if(!devTypeInfo) { return }
if(!devTypeInfo) {
if (eventMap.context.lockState) {
// This is a Smart Lock Pro which doesn't include device type in the message
devTypeInfo = deviceTypesMap.getAt("WoLock")
logDebug("Received event with no device type, mapping to Smart Lock")
}
else return
}

// build the event that we'll pass to the driver
def newEvent = [:]
Expand Down Expand Up @@ -196,14 +203,16 @@ import groovy.transform.Field
[
WoContact: [name: "Contact Sensor", attrs: contactSensorAttrsMap],
WoLock: [name: "Smart Lock"],
WoLockPro: [name: "Smart Lock"]
WoMeter: [name: "Meter"],
WoMeterPlus: [name: "Meter"],
WoHub2: [name: "Meter"],
WoIOSensor: [name: "Meter"],
WoPlugJP: [name: "Plug Mini", attrs: commonSwitchAttrsMap],
WoPlugUS: [name: "Plug Mini", attrs: commonSwitchAttrsMap],
WoPresence: [name: "Motion Sensor", attrs: motionSensorAttrsMap],
WoStrip: [name: "Strip Light", attrs: commonSwitchAttrsMap],
WoStrip: [name: "Strip Light", attrs: commonSwitchAttrsMap],
WoKeypadTouch: [name: "Keypad Touch"],
]

static @Field allDevsAttrs =
Expand Down
310 changes: 310 additions & 0 deletions switchbotKeypadTouch
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
/*

Copyright 2024 - dsegall

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 history:

0.9.22 dsegall Added Keypad Touch

*/

import groovy.json.JsonBuilder
import groovy.transform.Field

import java.util.Calendar
import javax.crypto.*
import javax.crypto.spec.*

import hubitat.helper.HexUtils

@Field static final String PERMANENT = "permanent"
@Field static final String TIMED = "timeLimit"
@Field static final String DISPOSABLE = "disposable"
@Field static final String EMERGENCY = "urgent"

@Field static final String SUCCESS = "success"
@Field static final String FAILED = "failed"
@Field static final String TIMEOUT = "timeout"

@Field static final String PASSCODE_NORMAL = "normal"
@Field static final String PASSCODE_EXPIRED = "expired"

@Field static final MILLISECONDS_IN_DAY = 86400000
@Field static final MILLISECONDS_IN_HOUR = 3600000

metadata
{
definition(name: "SwitchBot Keypad Touch", namespace: "tomw", author: "dsegall", importUrl: "")
{
capability "Refresh"
capability "LockCodes"
capability "Initialize"

command "createKey", [[name: "Name*", type: "STRING"], [name: "Password*", type: "STRING"], [name: "Type", type: "ENUM", constraints: [PERMANENT, TIMED, DISPOSABLE, EMERGENCY]], [name: "Start Time", type: "STRING"], [name: "End Time", type: "STRING"]]
command "deleteKeyByName", [[name: "Name*", type: "STRING"]]
}

preferences
{
section
{
input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: false
input name: "lockCodeEncryption", type: "bool", title: "Enable lock code encryption", defaultValue: false
input name: "deleteExpiredCodes", type: "bool", title: "Automatically delete expired codes", defaultValue: true
}
}
}

def logDebug(msg)
{
if (logEnable)
{
log.debug(msg)
}
}

def initialize()
{
sendEvent(name: "codeLength", value: 12)
sendEvent(name: "maxCodes", value: 100)
refresh()
}

def updated() {
updateCodesAttr()
}

def refresh()
{
readDeviceDetails()
}

def parse(body)
{
if (logEnable)
logDebug("parse: ${body}")

if (body.result == "success") {
if (body.eventName == "createKey")
sendEvent(name: "codeChanged", value: "added")
else if (body.eventName == "deleteKey")
sendEvent(name: "codeChanged", value: "deleted")

atomicState.lastSuccessfulModificationTime = new Date()
logDebug("Scheduling refresh task...")
runIn(30000, "refresh")
}
else sendEvent(name: "codeChanged", value: "failed")
}

def writeDeviceCommand(command, parameter = "default", commandType = "command")
{
def id = getParent()?.getId(device.getDeviceNetworkId())
if(id)
{
getParent()?.writeDeviceCommand(id, command, parameter, commandType)
}
}

def createKey(String name, String code, String type=PERMANENT, String startTimeS=null, String endTimeS=null) {
if (!atomicState.lockCodesByName.containsKey(name)) {
logDebug("createKey ${name}")
long startTime = 0
long endTime = 0
Date now = new Date()

if (type == TIMED && !startTimeS) {
startTime = now.getTime() + 5000
}

if (startTimeS) {
Calendar cal = timeToday(startTimeS)
Date date = cal.getTime()
startTime = date.getTime()
if (startTime < now.getTime())
startTime = now.getTime() + 5000
}

if (type == TIMED && !endTimeS) {
endTimeS = "23:59"
}

if (endTimeS) {
Calendar cal = timeToday(endTimeS)
Date date = cal.getTime()
endTime = date.getTime()
if (now.after(date)) {
if (startTimeS) startTime += MILLISECONDS_IN_DAY
endTime += MILLISECONDS_IN_DAY
}

if (startTimeS && endTime - startTime < MILLISECONDS_IN_HOUR)
endTime = startTime + MILLISECONDS_IN_HOUR + 60000
}

Map params = ["name": name, "type": type, "password": code, "startTime": startTime, "endTime": endTime]
try {
writeDeviceCommand("createKey", params)
}
catch (Exception e) {
logDebug("createKey failed: ${e.getMessage()}")
}
}
}

Calendar timeToday(String time) {
Calendar today = Calendar.getInstance()
today.set(Calendar.SECOND, 0)
today.set(Calendar.MILLISECOND, 0)
String[] parts = time.split(":")
today.set(Calendar.HOUR_OF_DAY, parts[0] as int)
today.set(Calendar.MINUTE, parts[1] as int)
return today
}

def deleteKey(String id) {
logDebug("deleteKey id=${id}")
try {
writeDeviceCommand("deleteKey", ["id": id])
}
catch (Exception e) {
logDebug("deleteKey failed: ${e.getMessage()}")
}
}

def deleteKeyByName(String name) {
Map lockCode = atomicState.lockCodesByName[name]
logDebug("deleteKeyByName found key ${lockCode}")
if (lockCode) {
deleteAndRemoveLockCode(lockCode)
}
}

def deleteAndRemoveLockCode(Map lockCode) {
if (lockCode) {
def newCodes = []
def newCodesByName = [:]

newCodes.addAll(atomicState.lockCodes)
newCodesByName.putAll(atomicState.lockCodesByName)
newCodes.remove(lockCode)
newCodesByName.remove(lockCode.name)
atomicState.lockCodes = newCodes
atomicState.lockCodesByName = newCodesByName
deleteKey(lockCode.id.toString())
}
}

def readDeviceDetails()
{
if(!parent) { return }

devices = parent.readDevices()?.deviceList
details = devices?.find { it.deviceId == parent.getId(device.getDeviceNetworkId()) }

logDebug("Details=" + details)

atomicState.lockCodes = []
atomicState.lockCodesByName = [:]
def expiredCodes = []

def newCodes = []
def newCodesByName = [:]

logDebug("Details=" + details)
for (it in details?.keyList) {
logDebug("Code=" + it)
Map lockCode = ["id": it.id, "name": it.name, "code": it.password, "iv": it.iv, "status": it.status]
newCodes << lockCode
newCodesByName[it.name] = lockCode
if (it.status == PASSCODE_EXPIRED)
expiredCodes << it.id
}

atomicState.lockCodes = newCodes
atomicState.lockCodesByName = newCodesByName

updateCodesAttr()

logDebug("Found ${expiredCodes.size()} expired codes")
if (!expiredCodes.isEmpty()) {
cleanExpiredCodes(expiredCodes)
}
}

def updateCodesAttr() {
sendEvent(name: "lockCodes", value: getCodes())
}

def cleanExpiredCodes(expiredCodes) {
boolean clean = deleteExpiredCodes == null ? true : deleteExpiredCodes
logDebug("cleanExpiredCodes=${clean} size=${expiredCodes.size}")
if (clean) {
for (code in expiredCodes) {
String codeStr = code.toString()
logDebug("Deleting expired code ${codeStr}")
deleteKey(codeStr)
}
}
}

def deleteCode(position) {
if (position >= 0 && position < atomicState.lockCodes.size() && atomicState.lockCodes[position])
deleteAndRemoveLockCode(atomicState.lockCodes[position])
}

def setCodeLength() {
// Not supported
}

def setCode(codeposition, pincode, name) {
deleteCode(codeposition)
createKey(name, pincode)
}

def getCodes() {
boolean encryptionEnabled=lockCodeEncryption == null ? false : lockCodeEncryption
def codes = [:]
int i = 0
for (it in atomicState.lockCodes) {
codes[i++] = ["name": it.name, "code": decryptPasscode(it.iv, it.code)]
}

String str = new JsonBuilder(codes).toString()
return encryptionEnabled ? encrypt(str) : str
}

def decryptPasscode(String iv, String encrypted) {
if (!parent) return
parent.cacheSecretKey()
String secretKey = parent.getDataValue("secretKey")
try {
byte[] ivBytes = HexUtils.hexStringToByteArray(iv)
def ivSpec = new IvParameterSpec(ivBytes)
def skeySpec = new SecretKeySpec(HexUtils.hexStringToByteArray(secretKey), "AES")

def cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, skeySpec, ivSpec)
byte[] original = cipher.doFinal(encrypted.decodeBase64())

return new String(original)
} catch (Exception ex) {
logDebug(ex)
}

return null
}
8 changes: 7 additions & 1 deletion switchbotMeter
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ limitations under the License.

Change history:

0.9.14 - dsegall - Added Battery capability
0.9.13 - tomw - Added Contact Sensor and Motion Sensor support.
0.9.12 - tomw - Added Smart Lock and webhook events support.
0.9.0 - tomw - Initial release.
Expand All @@ -32,6 +33,7 @@ metadata
capability "RelativeHumidityMeasurement"
capability "Refresh"
capability "TemperatureMeasurement"
capability "Battery"
}
}

Expand Down Expand Up @@ -70,5 +72,9 @@ def parse(body)
def tempVal = degC ? body.temperature.toDouble() : celsiusToFahrenheit(body.temperature.toDouble())
sendEvent(name: "temperature", value: tempVal?.toDouble())
}
if(null != body.battery)
{
def battery = body.battery.toInteger()
sendEvent(name: "battery", value: battery)
}
}

1 change: 0 additions & 1 deletion switchbotMotionSensor
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,3 @@ def parse(body)
sendEvent(name: "motion", value: ([true, "DETECTED"].contains(body.moveDetected)) ? "active" : "inactive")
}
}

Loading