HASS Addon TTLock offline integration

Hi automation enthusiasts !

During the past couple of month I have worked on developing an addon for the ‘open’ TTLock BLE biometric locks that can be controlled without a gateway by directly connecting it to your HA instance (provided your host has a Bluetooth adapter). I think I am now ready to share my work with the community so here it goes:

There are a couple of caveats for now for which I’m sure that a solution will be presented in time:

  • The signal of the lock is pretty weak so it needs to be fairly close to your Bluetooth device especially if using a Raspberry PI that is known for it’s weak wireless capabilities. It would be possible to manufacture a proxy gateway using some ESP32 in the future.
  • There are no notifications for locking and unlocking yet. It will require analyzing the TTLock gateway communication to see how to activate this feature (see below why).
  • Not all the features present in the TTLock app have yet been implemented in the UI but it will be possible in time

The hardest part of the development was the reverse engineering of TTLock BLE protocol and creating my own SDK (I choose typescript for this as it was more convenient). This was done from their Android SDK sources, which includes code for multiple (older) lock types and protocols and honestly is a mess to follow. Also sometimes the lock does not react to input as it should and also not everything from their app is implemented in their Android SDK. My guess is because they don’t actually use this SDK in their official app.

UPDATE 27.03.2021: I figured out how the gateway finds out about those events and implemented the same mechanism in the latest addon release.

The code for activation of the lock/unlock event notifications is not present in their Android SDK as it would not make sense (the phone is not always in range of the lock and it would just drain battery). This is why a gateway is used for this purpose so at the moment it is not possible to receive such notifications. But, since the official gateway uses an ESP8266 and an external BLE module, it is possible to sniff the communication and figure out what commands the gateway sends and replicate them.

Now, for the interesting part of this development, I did all of this without actually having the lock myself (I ordered one but did not pay much attention and I got a Tuya WiFi version :face_vomiting:) so a big shoutout to @Valentino_Stillhardt who actually started all this and provided me with remote access to his lock, a PI and his HA installation. Cheers !

As this is my first addon and very much in the ALPHA stage, there will be bugs so please be gentle :smiley: I would preffer the bug reports to go directly into the github issues for better tracking.

Enjoy !

17 Likes

Great.
I installed the addon on my dev hassio on pi 4. What would be the options on the configuration?
Also, am I supposed to pair a lock first before I can see a thing on the UI?

LOGS:

[email protected] start /app
node ./index.js
Options: {“mqttHost”:“core-mosquitto”,“mqttPort”:“1883”,“mqttSSL”:“false”,“mqttUser”:“addons”,“mqttPass”:“cc815GtsUi73bcIcDN3CthZT-az2GbOg”}
[Error: ENOENT: no such file or directory, access ‘/data/lockData.json’] {
errno: -2,
code: ‘ENOENT’,
syscall: ‘access’,
path: ‘/data/lockData.json’
}
{“mqttUrl”:“mqtt://core-mosquitto:1883”,“mqttUser”:“addons”,“mqttPass”:“cc815GtsUi73bcIcDN3CthZT-az2GbOg”}
Setting up Home Assistant MQTT connection
MQTT connected
Server started
BLE Scan started
Restarting scan 1
BLE Scan stopped
BLE Scan started
Restarting scan 2
BLE Scan stopped
BLE Scan started

There is no configuration needed. Seems to be working fine (don’t worry about the missing lockData file as it will be saved when you pair a lock).
You just need to put the lock into pairing mode (reset to defaults either via the app or via the button on the back for 2s).
But be warned, the lock cannot work with both the app and this addon, only one pairing is possible at the moment.

Thanks. Any way to install this on stand alone? My pi 4 couldn’t detect the lock.
Testing with a mobile with ttlock installed, I had to stand near the lock in order for TTLock app to see it.

What do you mean ? This addon does not require a gateway or the TTLock cloud, it uses the bluetooth from your HA host to connect directly to the lock. But for the initial pairing the lock has to be awake (keyboard lit) for it to be discovered and paired. When the keyboard lights turn off (I think 10s or so) so does the bluetooth in the lock (to save power I guess). So you have to keep it awake during the discovery and paring. You can restart the scan process of the addon using the ‘refresh’ icon located on the top right. When the scan is finished and the lock is in pairing mode it should show up in the UI.

How far is the lock from your PI ?

1 Like

The ttlock app on my mobile was only for testing, it wasn’t also able to see the lock when I stand where the pi 4 is but it could see it when I’m very near. This means there’s a problem on the pi or it’s very far (only 5 steps away).
hcitool scan doesn’t show anything too.

That could be an issue (the limited BLE range of the lock, in addition to the already limited range of the PI in general). I am planing to develop a BLE proxy gateway using an ESP32 since this kind of support is already implemented on the bluetooth library that I use, but it will probably take a long time as my experience with Arduino is very limited. Maybe someone can help in this regard.

An unpaired lock will not show up in scans unless it’s awake. After the lock is paired (via app, or addon etc.) it will always show in scans.

Will keep on troubleshooting. I did a “sudo hcitool -i hci0 lescan” and it was able to see the lock before I paired it again. I’m running supervised by the way and i run the command on the raspbian.
I’ll try to do a hardware reset instead of deleting the lock from the app.

No luck. Does this support n20 device, similar to below image?
image

I have no ideea honestly, but if it supports the V3 protocol it should.
You could first make sure that the lock is visible to the PI via the hcitool and then run a scan from the addon and check the logs, there should be some info there about what was found (maybe even some error if we’re lucky).

I am able to control it using the v3 api before I removed it from ttlock app and yes, it’s discoverable from the pi using hcitool lescan, it shows the same mac address of the lock indicated on the ttlock. I can only see the same logs on the addon.

You are reffering to the V3 api of the TTLock cloud, I’m talking about the BLE V3 protocol :slight_smile:

Did you restart the scan from the addon’s UI ? Do this while the lock is awake.

Tried on freshly installed hassio running on raspberry pi 3b located near the lock but it couldn’t see it even after refreshing.
I went inside the container and do a bluetoothctl scan on and was able to find the lock’s mac address. Tried pairing and connecting it but the web UI still doesn’t show anything. The logs was able to see it though:

BLE Scan started
BLE Scan started
BLE Scan started
BLE Scan started
BLE Scan started
BLE Scan started
BLE Scan started
BLE Scan started
BLE Scan started
BLE Scan started
BLE Scan started
BLE Scan started
Disconnected from lock F6:C5:FB:AB:D9:F9
BLE Scan started
BLE Scan started
BLE Scan started
BLE Scan started
BLE Scan started
BLE Scan started

I made an update to log discovered locks regardless of them being paired or not, could you please update the addon (Supervisor -> Add-on Store -> Reload and then it should allow you to update).

Here’s the new entry (discovered lock) on the logs after the latest version: Couldn’t still see anything on the UI.
Some details on the lock - uses fingerprint and bluetooth. The gateway is required to unlock/lock it remotely or configure automatic lock/schedule.

{
	"id": "xxxxXXXXxxxx",
	"name": "N20_f9d9ab",
	"manufacturer": "unknown",
	"model": "unknown",
	"hardware": "unknown",
	"firmware": "unknown",
	"address": "F6:C5:FB:AB:D9:F9",
	"rssi": -68,
	"protocolType": 5,
	"protocolVersion": 3,
	"scene": 2,
	"groupId": 0,
	"orgId": 0,
	"lockType": 5,
	"isTouch": true,
	"isSettingMode": true,
	"isUnlock": true,
	"txPowerLevel": 0,
	"batteryCapacity": 100,
	"date": 0,
	"isWristband": false,
	"isRoomLock": true,
	"isSafeLock": false,
	"isBicycleLock": false,
	"isLockcar": false,
	"isGlassLock": false,
	"isPadLock": false,
	"isCyLinder": false,
	"isRemoteControlDevice": false,
	"isDfuMode": false,
	"isNoLockService": false,
	"remoteUnlockSwitch": 0,
	"disconnectStatus": 0,
	"parkStatus": 0,
	"autoLockTime": -1,
	"privateData": {}
}

Hi @accelle, sorry for the delay, took a while to test this and I think I finally fixed the discovery part for new unpaired locks. Update the addon and you should be able to see the lock in HA and pair it. It should go like this:

  • go to addon’s UI and start scan (top right refresh icon)
  • wake up the lock by touching the keyboard
  • the lock should appear
  • wait for the scan to finish (top right icon stops spinning)
  • wake the lock again and click pair

Fingers crossed :smiley:

PS: It is possible that pairing won’t work the first time, if that happens you can retry wakeup+pair a couple of times.

I was able to pair after restarting the addon a few times. Here’s some of the log when I was almost able to pair it:

========= feature list
Sending command: 7f5a0503010001000101aa001c0d0a
Waiting for response
Received response: 7f5a050302000a002254aa10ab63db99b03f196079f431384295b006f20d0a
Waited for a response for 13 = 65 ms
Sleeping a bit
Sending command: 7f5a0503010001000101aa001c0d0a
Waiting for response
Received response: 7f5a050302000a002254aa10ab63db99b03f196079f431384295b006f20d0a
Waited for a response for 15 = 75 ms
Sleeping a bit
Sending command: 7f5a0503010001000101aa001c0d0a
Waiting for response
Received response: 7f5a050302000a002254aa10ab63db99b03f196079f431384295b006f20d0a
Waited for a response for 12 = 60 ms
Error while initialising lock Error: Malformed response, bad CRC
at TTBluetoothDevice.sendCommand (/app/node_modules/ttlock-sdk-js/dist/device/TTBluetoothDevice.js:141:35)
at async TTLock.searchDeviceFeatureCommand (/app/node_modules/ttlock-sdk-js/dist/device/TTLockApi.js:155:34)
at async TTLock.initLock (/app/node_modules/ttlock-sdk-js/dist/device/TTLock.js:100:33)
at async Manager.initLock (/app/src/manager.js:166:19)
at async WebSocket. (/app/api/index.js:33:30)
Disconnected from lock F6:C5:FB:AB:D9:F9

The commands fail most of the time, and it is only able to execute it after addon restart. Note that I was trying to wake up the lock before I hit unlock/add fingerprint.

I couldn’t remove auto unlock though. I used the slider and set the number to 0 manually but it doesn’t set.

Here are some of the logs:

Unlocking:

Error unlocking the lock Error: No response to checkUserTime
at TTLock.checkUserTime (/app/node_modules/ttlock-sdk-js/dist/device/TTLockApi.js:587:19)
at async TTLock.unlock (/app/node_modules/ttlock-sdk-js/dist/device/TTLock.js:249:32)
at async Manager.unlockLock (/app/src/manager.js:191:21)
at async WebSocket. (/app/api/index.js:59:30)
========= check user time
Error unlocking the lock Error: No response to checkUserTime
at TTLock.checkUserTime (/app/node_modules/ttlock-sdk-js/dist/device/TTLockApi.js:587:19)
at async TTLock.unlock (/app/node_modules/ttlock-sdk-js/dist/device/TTLock.js:249:32)
at async Manager.unlockLock (/app/src/manager.js:191:21)
at async WebSocket. (/app/api/index.js:59:30)

Fingerprint add/set autolock to 0:

========= check admin
macro_adminLogin: Error: No response to checkAdmin
at TTLock.checkAdminCommand (/app/node_modules/ttlock-sdk-js/dist/device/TTLockApi.js:514:19)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
at async TTLock.macro_adminLogin (/app/node_modules/ttlock-sdk-js/dist/device/TTLockApi.js:1233:32)
at async TTLock.getPassCodes (/app/node_modules/ttlock-sdk-js/dist/device/TTLock.js:602:17)
at async Manager.getCredentials (/app/src/manager.js:223:27)
at async WebSocket. (/app/api/index.js:77:35)
========= check admin
macro_adminLogin: Error: No response to checkAdmin
at TTLock.checkAdminCommand (/app/node_modules/ttlock-sdk-js/dist/device/TTLockApi.js:514:19)
========= check admin
at processTicksAndRejections (internal/process/task_queues.js:97:5)
at async TTLock.macro_adminLogin (/app/node_modules/ttlock-sdk-js/dist/device/TTLockApi.js:1233:32)
at async TTLock.getICCards (/app/node_modules/ttlock-sdk-js/dist/device/TTLock.js:737:17)
at async Manager.getCredentials (/app/src/manager.js:224:23)
at async WebSocket. (/app/api/index.js:77:35)
macro_adminLogin: Error: No response to checkAdmin
at TTLock.checkAdminCommand (/app/node_modules/ttlock-sdk-js/dist/device/TTLockApi.js:514:19)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
at async TTLock.macro_adminLogin (/app/node_modules/ttlock-sdk-js/dist/device/TTLockApi.js:1233:32)
at async TTLock.getFingerprints (/app/node_modules/ttlock-sdk-js/dist/device/TTLock.js:872:17)
at async Manager.getCredentials (/app/src/manager.js:225:25)
at async WebSocket. (/app/api/index.js:77:35)

I’m going to try a little bit troubleshooting later. My setup btw is freshly installed hassos on rpi4.
THere’s no keypad on the lock, only fingerprint.
Thanks for the awesome codes!

Yeah, the auto lock time is not implemented yet, so that’s fine.

Could you look in the logs and send me the lines related to “========= feature list” (they should be present right after the addon stars and discovers the paired lock). Since I only tested with a lock that has fingerprint, keypad and cards I did not implement any restrictions in case those are missing.

Here you go:

========= feature list
Sending command: 7f5a0503010001000101aa001c0d0a
Waiting for response
Received response: 7f5a050302000a002254aa10dde5c0863562baacda7df8ac0c53374ab80d0a
Waited for a response for 11 = 55 ms
Command: 640044d1f4
<Buffer 64 00 44 d1 f4>
========= feature list Set { 2, 4, 5, 6, 7, 8, 12, 14, 15, 18, 22 }
========= check admin
Sending command: 7f5a0503010001000141aa10d4f205e7fffc8c949a94cd028623c154790d0a
Waiting for response
Received response: 7f5a050302000a002254aa10bd197178469aedd1fcdb0c75697ccec3970d0a
Waited for a response for 17 = 85 ms
Command: 00002163
========= check admin: 8547
========= check random
Sending command: 7f5a0503010001000130aa106170ff49016905c1a2cb95160054acc5880d0a
Waiting for response
Received response: 7f5a050302000a002254aa10e16c0a554435520dea445e5257b8aac3590d0a
Waited for a response for 16 = 80 ms
Command: 64
========= check random
========= autoLockTime
Sending command: 7f5a0503010001000136aa104588d6bd5c1ad10d957217ecdef0fc5e730d0a
Waiting for response
Received response: 7f5a050302000a002254aa10c26e771597a2b613bce27f6de6da17f07b0d0a
Waited for a response for 10 = 50 ms
Command: 6401000500050384
========= autoLockTime: 5
========= check lock status
Sending command: 7f5a0503010001000114aa107232f6035034936c522400af0e497ddbfa0d0a
Waiting for responses
Received response: 7f5a050302000a002254aa10f77b82a7f741a88dc0e74f245e07e58d0f0d0a
Waited for a response for 16 = 80 ms
Command: 640100

Here’s after it successfully discovers the enrolled fingerprint:

Subscribing to descriptor notifications
Connected to paired lock F6:C5:FB:AB:D9:F9
========= check admin
Sending command: 7f5a0503010001000141aa10d4f205e7fffc8c949a94cd028623c154790d0a
Waiting for response
Received response: 7f5a050302000a002254aa1013d75fd854c2e90ca7286c5f400ee116650d0a
Waited for a response for 18 = 90 ms
Command: 00004d0a
========= check admin: 19722
========= check random
Sending command: 7f5a0503010001000130aa1085de4ed1256ea14b96b82a5bf9f62f0d320d0a
Waiting for response
Received response: 7f5a050302000a002254aa10e16c0a554435520dea445e5257b8aac3590d0a
Waited for a response for 10 = 50 ms
Command: 64
========= check random
========= get passCodes 0
Sending command: 7f5a0503010001000107aa109007f27a3ee5491595a5741f16401cf28d0d0a
Waiting for response
Received response: 7f5a050302000a002254aa108a66f8812cba9407cd551019f41962aa360d0a
Waited for a response for 16 = 80 ms
Command: 0000
========= get passCodes { sequence: -1, data: }
========= check admin
Sending command: 7f5a0503010001000141aa10d4f205e7fffc8c949a94cd028623c154790d0a
Waiting for response
Received response: 7f5a050302000a002254aa10d2a901e8831b36094db23822fa084030540d0a
Waited for a response for 12 = 60 ms
Command: 00005d1c
========= check admin: 23836
========= check random
Sending command: 7f5a0503010001000130aa1034b65d4802ebc7ca02397b8bad2621d5350d0a
Waiting for response
Received response: 7f5a050302000a002254aa10e16c0a554435520dea445e5257b8aac3590d0a
Waited for a response for 13 = 65 ms
Command: 64
========= check random
========= get IC Cards 0
Sending command: 7f5a0503010001000105aa1044634522efb618e8534e323cd937dc20230d0a
Waiting for response
Received response: 7f5a050302000a002254aa10bc25e50ace6645801a9a40cd89500151950d0a
Waited for a response for 7 = 35 ms
Command: 1b
========= check admin
Sending command: 7f5a0503010001000141aa10d4f205e7fffc8c949a94cd028623c154790d0a
Waiting for response
Received response: 7f5a050302000a002254aa10a5bed5999172f7a8ce73c9fd8c55b7fc4e0d0a
Waited for a response for 16 = 80 ms
Command: 000050ca
========= check admin: 20682
========= check random
Sending command: 7f5a0503010001000130aa10f4d2ae434766acf11ce64ffae677be5f3d0d0a
Waiting for response
Received response: 7f5a050302000a002254aa10e16c0a554435520dea445e5257b8aac3590d0a
Waited for a response for 10 = 50 ms
Command: 64
========= check random
========= get Fingerprints 0
Sending command: 7f5a0503010001000106aa10d4e4a568d531024756c93619ff0192fa720d0a
Waiting for response
Received response: 7f5a050302000a002254aa207da607828b30ac8f692350c59ec745e3deef525927723c908ff3e55a7b6314a7060d0a
Waited for a response for 21 = 105 ms
Command: 6406000127916ec600000001010000630c01173b
========= get Fingerprints {
sequence: 1,
data: [
{
fpNumber: ‘43505582211072’,
startDate: ‘200001010000’,
endDate: ‘209912012359’
}
]
}

Thanks, only needed:
========= feature list Set { 2, 4, 5, 6, 7, 8, 12, 14, 15, 18, 22 }

The lock I test on has more:
========= feature list Set { 0, 1, 2, 4, 5, 6, 7, 8, 12, 14, 15, 18, 22 }

0 - PIN code
1 - Card
2- Fingerprint

So I need to implement some restrictions in case some of those are missing. It will take a few days, I will notify you when it’s done. I might go ahead and implement the auto-lock setting also while I’m at it.

BTW, the CRC errors are also present in my testing. I’m not exactly sure why those happen as resending the packet gets the exact same bad CRC response. I think it’s just a bug in the lock’s firmware and it just craps out sometime.
But the funny thing is that there are a few responses from the lock that always have a bad CRC and they are treated with exceptions in the TTLock Android SDK :smiley: :smiley: :smiley: