This forum uses cookies
This forum makes use of cookies to store your login information if you are registered, and your last visit if you are not. Cookies are small text documents stored on your computer; the cookies set by this forum can only be used on this website and pose no security risk. Cookies on this forum also track the specific topics you have read and when you last read them. Please confirm that you accept these cookies being set.

TTLock integration with LM
#1
I have a TTLock, that supports API's. 
If i understand correct, this enables integration with other systems.
Does anybody know how to integrate to my LM.
(It is the first time I will do that, so please provide as many details as possible)
Reply
#2
Share the documentation for this API.
------------------------------
Ctrl+F5
Reply
#3
(10.06.2026, 07:23)Daniel Wrote: Share the documentation for this API.

Everything is in their website
https://euopen.ttlock.com/document/doc?u...keyEn.html
Reply
#4
Hi,

I've been running TTLock locks integrated with LogicMachine for a while. Here's a working setup with two scripts and some notes on remote unlock.

How it works

Each room has a block of LM group addresses. The trigger object (x/x/0) carries two tags — PassCode and Room_X — and its comment holds the LockId:

Offset | Purpose
+0 | Trigger / Generate passcode — tag: PassCode, comment: LockId:XXXXXXXX
+1 | Passcode value
+2 | Guest name (auto-generated)
+3 | Valid from — date
+4 | Valid from — time
+5 | Valid to — date
+6 | Valid to — time
+8 | Last PassCode ID (returned by API)
+9 | Last status message

The accessToken and refreshToken are stored as named LM objects ("TTLock Access Token", "TTLock Refresh Token") so all scripts read/write from one place.

Script 1 — Add Passcode (event script, tag: PassCode)

Triggered when x/x/0 is set to true. Reads the lockId from the object comment, builds the passcode name from room + dates, validates that arrival is in the future and departure is after arrival, then calls /v3/keyboardPwd/add for both the room lock and the main entrance. Includes automatic token refresh on errcode 10004.

When triggered with false, it just generates a random 5-digit code into x/x/1.

Code:
local eventAction = event.getvalue() local baseAddress = event.dstraw local lastMessageObj = grp.find(baseAddress + 9) local msgtxt = "" local https = require("ssl.https") local ltn12 = require("ltn12") local json = require("json") local API_BASE = "https://euapi.ttlock.com" local clientId = "YOUR_CLIENT_ID" local clientSecret = "YOUR_CLIENT_SECRET" local ACCESS_TOKEN_OBJ_NAME  = "TTLock Access Token" local REFRESH_TOKEN_OBJ_NAME = "TTLock Refresh Token" local MainDoorLockId = YOUR_MAIN_DOOR_LOCK_ID local function normCode(code) return tonumber(code) or -1 end local function urlencode(str)   if str == nil then return "" end   str = tostring(str)   str = str:gsub("\n", "\r\n")   str = str:gsub("([^%w _%%%-%.~])", function(c)     return string.format("%%%02X", string.byte(c))   end)   str = str:gsub(" ", "%%20")   return str end local function safeLog(label, text)   local t = tostring(text or "")   if clientSecret and clientSecret ~= "" then     t = t:gsub(clientSecret, "***")   end   log(label, t) end local function readTokens()   return tostring(grp.getvalue(ACCESS_TOKEN_OBJ_NAME) or ""),          tostring(grp.getvalue(REFRESH_TOKEN_OBJ_NAME) or "") end local function writeTokens(newAT, newRT)   if newAT and newAT ~= "" then grp.update(ACCESS_TOKEN_OBJ_NAME, newAT) end   if newRT and newRT ~= "" then grp.update(REFRESH_TOKEN_OBJ_NAME, newRT) end end local function toUTCTimestamp(dateTable, timeTable)   return os.time({     year = dateTable.year, month = dateTable.month, day = dateTable.day,     hour = timeTable.hour, min = timeTable.minute, sec = timeTable.second   }) * 1000 end local function httpPostForm(url, formBody)   local response_body = {}   local _, code = https.request{     url = url, method = "POST",     headers = {       ["Content-Type"] = "application/x-www-form-urlencoded",       ["Content-Length"] = tostring(#formBody)     },     source = ltn12.source.string(formBody),     sink = ltn12.sink.table(response_body)   }   return code, table.concat(response_body or {}) end local accessToken, refreshTokenValue = readTokens() local function refreshToken()   accessToken, refreshTokenValue = readTokens()   if not refreshTokenValue or refreshTokenValue == "" then     log("TTLock: Refresh token missing.")     return false   end   local body =     "clientId=" .. urlencode(clientId) ..     "&clientSecret=" .. urlencode(clientSecret) ..     "&grant_type=refresh_token" ..     "&refresh_token=" .. urlencode(refreshTokenValue)   local code, resp = httpPostForm(API_BASE .. "/oauth2/token", body)   if normCode(code) == 200 then     local ok, data = pcall(json.decode, resp)     if ok and data and data.access_token then       accessToken = data.access_token       if data.refresh_token and data.refresh_token ~= "" then         refreshTokenValue = data.refresh_token       end       writeTokens(accessToken, refreshTokenValue)       log("TTLock: Token refreshed OK")       return true     end   end   log("TTLock: Token refresh failed: " .. tostring(code) .. " " .. (resp or ""))   return false end local function doPostWithRetry(formBody)   local addUrl = API_BASE .. "/v3/keyboardPwd/add"   local code, resp = httpPostForm(addUrl, formBody)   if normCode(code) ~= 200 then return nil, { httpcode = normCode(code), raw = resp } end   local ok, data = pcall(json.decode, resp)   if not ok or not data then return nil, { errcode = -1, errmsg = "Invalid JSON", raw = resp } end   if data.errcode == 10004 then     log("TTLock: Token expired (10004). Refreshing...")     if refreshToken() then       accessToken, refreshTokenValue = readTokens()       local newBody = formBody:gsub("accessToken=[^&]+", "accessToken=" .. urlencode(accessToken))       local code2, resp2 = httpPostForm(addUrl, newBody)       if normCode(code2) ~= 200 then return nil, { httpcode = normCode(code2), raw = resp2 } end       local ok2, data2 = pcall(json.decode, resp2)       if not ok2 or not data2 then return nil, { errcode = -1, errmsg = "Invalid JSON after refresh" } end       return data2, nil     else       return nil, { errcode = 10004, errmsg = "Token refresh failed" }     end   end   return data, nil end if eventAction then   accessToken, refreshTokenValue = readTokens()   if not accessToken or accessToken == "" then     refreshToken()     accessToken, refreshTokenValue = readTokens()   end   local lockObj = grp.find(baseAddress)   local lockId = lockObj and lockObj.comment and tonumber(lockObj.comment:match("LockId:(%d+)"))   if not lockId then     if lastMessageObj then lastMessageObj:update("Error: LockId not found in object comment") end     return   end   local roomName = (lockObj.tagcache or ""):match("Room_([%w%d][%w%d]?)") or "NA"   local keyboardPwd    = grp.getvalue(baseAddress + 1)   local startDateTable = grp.getvalue(baseAddress + 3)   local startTimeTable = grp.getvalue(baseAddress + 4)   local endDateTable   = grp.getvalue(baseAddress + 5)   local endTimeTable   = grp.getvalue(baseAddress + 6)   local lastPassCodeIdObj = grp.find(baseAddress + 8)   local keyboardPwdName = string.format("%s_%02d%02d%02d%02d",     roomName, startDateTable.day, startDateTable.month, endDateTable.day, endDateTable.month)   local nameObj = grp.find(baseAddress + 2)   if nameObj then nameObj:update(keyboardPwdName) end   if lastMessageObj then lastMessageObj:update("Requesting PassCode creation. Please wait...") end   local startTs = toUTCTimestamp(startDateTable, startTimeTable)   local endTs   = toUTCTimestamp(endDateTable,   endTimeTable)   local nowTs   = os.time() * 1000   if startTs < nowTs then     msgtxt = "Error: Arrival date/time is in the past."     if lastMessageObj then lastMessageObj:update(msgtxt) end     return   end   if endTs <= startTs then     msgtxt = "Error: Departure must be after arrival."     if lastMessageObj then lastMessageObj:update(msgtxt) end     return   end   local function buildBody(lid)     return "clientId=" .. urlencode(clientId) ..            "&accessToken=" .. urlencode(accessToken) ..            "&lockId=" .. urlencode(tostring(lid)) ..            "&keyboardPwd=" .. urlencode(tostring(keyboardPwd)) ..            "&keyboardPwdName=" .. urlencode(keyboardPwdName) ..            "&startDate=" .. urlencode(tostring(startTs)) ..            "&endDate=" .. urlencode(tostring(endTs)) ..            "&date=" .. urlencode(tostring(nowTs)) ..            "&addType=2"   end   accessToken, refreshTokenValue = readTokens()   local Rdata, Rerr = doPostWithRetry(buildBody(lockId))   if Rerr then     msgtxt = string.format("Room %s error: %s", roomName, Rerr.errmsg or Rerr.raw or "")   elseif Rdata.keyboardPwdId then     if lastPassCodeIdObj then lastPassCodeIdObj:update(Rdata.keyboardPwdId) end     msgtxt = string.format("Passcode %s added to Room %s (ID: %s)",       tostring(keyboardPwd), roomName, tostring(Rdata.keyboardPwdId))   else     msgtxt = string.format("Room %s error: %s - %s", roomName,       tostring(Rdata.errcode or "N/A"), tostring(Rdata.errmsg or "N/A"))   end   os.sleep(1)   accessToken, refreshTokenValue = readTokens()   local Mdata, Merr = doPostWithRetry(buildBody(MainDoorLockId))   if Merr then     msgtxt = msgtxt .. "\nMain Door error: " .. (Merr.errmsg or Merr.raw or "")   elseif Mdata.keyboardPwdId then     msgtxt = msgtxt .. string.format("\nPasscode %s added to Main Door (ID: %s)",       tostring(keyboardPwd), tostring(Mdata.keyboardPwdId))   else     msgtxt = msgtxt .. string.format("\nMain Door error: %s - %s",       tostring(Mdata.errcode or "N/A"), tostring(Mdata.errmsg or "N/A"))   end   alert(msgtxt) else   math.randomseed(os.time() + os.clock() * 1000)   math.random()   grp.update(baseAddress + 1, string.format("%05d", math.random(10000, 99999)))   msgtxt = "New code generated" end if lastMessageObj then lastMessageObj:update(msgtxt) end

Script 2 — Remove Expired Passcodes (scheduled, runs daily)

Loops through all objects tagged PassCode, fetches the password list for each lock via /v3/lock/listKeyboardPwd, and deletes anything that expired more than 24 hours ago. Set DRY_RUN = true first to verify what would be deleted before enabling real deletion.

Code:
local http = require("socket.http") local ltn12 = require("ltn12") local json = require("json") local DRY_RUN = false local accessToken = grp.getvalue("TTLock Access Token") local clientId = "YOUR_CLIENT_ID" local ExtraLockIds = {     [YOUR_MAIN_DOOR_LOCK_ID] = "MainDoor" } local lockIds = {} local lockNameById = {} for _, lock in ipairs(grp.tag("PassCode")) do     local lockId = tonumber(string.match(lock.comment or "", "LockId:(%d+)"))     local name = string.match(lock.tagcache or "", "Room_%S+") or "Unknown"     if lockId then         table.insert(lockIds, lockId)         lockNameById[lockId] = name     end end for id, name in pairs(ExtraLockIds) do     table.insert(lockIds, id)     lockNameById[id] = name end local function getKeyboardPwdsForLock(lockId)     local timestamp = tostring(os.time() * 1000):gsub("%.0*$", "")     local url = string.format(         "https://euapi.ttlock.com/v3/lock/listKeyboardPwd?" ..         "clientId=%s&accessToken=%s&lockId=%d&pageNo=1&pageSize=200&orderBy=1&date=%s",         clientId, accessToken, lockId, timestamp     )     local response = {}     local _, status = http.request{ url = url, method = "GET", sink = ltn12.sink.table(response) }     local body = table.concat(response)     if status == 200 then         local result = json.decode(body)         return result.list or {}     end     log("API error: " .. body)     return {} end local function deleteKeyboardPwd(lockId, keyboardPwdId)     local timestamp = tostring(os.time() * 1000):gsub("%.0*$", "")     local postData = string.format(         "clientId=%s&accessToken=%s&lockId=%d&keyboardPwdId=%d&deleteType=2&date=%s",         clientId, accessToken, lockId, keyboardPwdId, timestamp     )     local response = {}     local _, status = http.request{         url = "https://euapi.ttlock.com/v3/keyboardPwd/delete",         method = "POST",         headers = { ["Content-Type"] = "application/x-www-form-urlencoded", ["Content-Length"] = tostring(#postData) },         source = ltn12.source.string(postData),         sink = ltn12.sink.table(response)     }     local body = table.concat(response)     return status == 200 and body:find('"errcode"%s*:%s*0') ~= nil end local currentTimeMillis = os.time() * 1000 local expireThreshold = 86400000 for _, lockId in ipairs(lockIds) do     log(string.format("Processing LockId: %d (%s)", lockId, lockNameById[lockId] or "Unknown"))     for _, pwd in ipairs(getKeyboardPwdsForLock(lockId)) do         local endDate = pwd.endDate or 0         if endDate ~= 0 and endDate < (currentTimeMillis - expireThreshold) then             local readableDate = os.date("%Y-%m-%d %H:%M:%S", endDate / 1000)             if DRY_RUN then                 log(string.format("DRY-RUN: %s | ID=%s | Name=%s | Expired: %s",                     lockNameById[lockId], pwd.keyboardPwdId, pwd.keyboardPwdName, readableDate))             else                 local ok = deleteKeyboardPwd(lockId, pwd.keyboardPwdId)                 log(ok and ("Deleted: " .. pwd.keyboardPwdName) or ("Failed: " .. pwd.keyboardPwdName))             end         end     end end

Unlock Door — how to extend this

The TTLock API has a /v3/lock/unlock endpoint. Here's a minimal example you can attach as an event script to a boolean GA:

Code:
local https = require("ssl.https") local ltn12 = require("ltn12") local clientId    = "YOUR_CLIENT_ID" local accessToken = grp.getvalue("TTLock Access Token") -- Read lockId from object comment (same pattern as Script 1) local lockObj = grp.find(event.dst) local lockId  = lockObj and lockObj.comment and tonumber(lockObj.comment:match("LockId:(%d+)")) if not lockId then     log("TTLock Unlock: LockId not found in object comment")     return end local timestamp = tostring(os.time() * 1000) local body = "clientId=" .. clientId ..              "&accessToken=" .. accessToken ..              "&lockId=" .. tostring(lockId) ..              "&date=" .. timestamp local response = {} local _, code = https.request{     url = "https://euapi.ttlock.com/v3/lock/unlock",     method = "POST",     headers = {         ["Content-Type"] = "application/x-www-form-urlencoded",         ["Content-Length"] = tostring(#body)     },     source = ltn12.source.string(body),     sink = ltn12.sink.table(response) } log("Unlock response (" .. tostring(code) .. "): " .. table.concat(response))

Tag the GA with UnlockDoor and put LockId:XXXXXXXX in the object comment — same convention as the passcode objects, so the same pattern scales to multiple doors without duplicating code.


   


Hope this helps as a starting point.
Reply
#5
OK. I am not the expert to identify all that code.
Can you help me build a simple Unlock command?
Reply
#6
Modify as needed


Code:
local https = require("ssl.https") local ltn12 = require("ltn12") local clientId    = "YOUR_CLIENT_ID" local accessToken ="YOUR_TTLOCK_ACCESS_TOKEN" local lockId = "YOUR_LOCKID" local timestamp = tostring(os.time() * 1000) local body = "clientId=" .. clientId ..             "&accessToken=" .. accessToken ..             "&lockId=" .. tostring(lockId) ..             "&date=" .. timestamp local response = {} local _, code = https.request{     url = "https://euapi.ttlock.com/v3/lock/unlock",     method = "POST",     headers = {         ["Content-Type"] = "application/x-www-form-urlencoded",         ["Content-Length"] = tostring(#body)     },     source = ltn12.source.string(body),     sink = ltn12.sink.table(response) } log("Unlock response (" .. tostring(code) .. "): " .. table.concat(response))
Reply
#7
Shouldn't we get the access token first?
Reply
#8
It's all here: TTLock Open Platform

1.1 Register a developer account
1.2 Create an application in the developer account and apply for it. If the application is approved, you will see the client_id and client_secret in the Application.

2.1 Add locks in TTlock App
2.2 Get TTlock Account‘s Access Token
2.3 After obtaining the accessToken, you can use it to call other APIs
Reply
#9
I already have an account to TTLock Open Platform and have added the lock to my application.
I still haven't manage to retrieve the Access Token. I am missing this information.
Reply
#10
Please check section 2.2 “Get TTLock Account’s Access Token” in the TTLock Open Platform documentation.

You need to retrieve the accessToken first using your client_id, client_secret, and TTLock account credentials. After that, use the returned accessToken in the unlock API request.
Reply
#11
If I understand correct, we need to get it using a Cloud API and then Unlock using a second API.
I tried to replicate your API call, which is for Unlock command, changing the parameters, but I have no success.
I get the error ""errmsg":"invalid_request, Bad request content type. Expecting: application/x-www-form-urlencoded""
Reply
#12
Probably you have not got the access token

Check the how to get access token: 
https://cnopen.ttlock.com/document/doc?u...kenEn.html
Reply
#13
(11.06.2026, 21:31)gtsamis Wrote: Probably you have not got the access token

Check the how to get access token: 
https://cnopen.ttlock.com/document/doc?u...kenEn.html

I tried that and I get the error message
Reply
#14
OK. I managed to read the access token. How do i retrieve it from the response?
I get this message: {"access_token":"My_Access_Token","refresh_token":"My_Refresh_Token","uid":My_UID,"openid":My_OpenID,"scope":"user,key,room","token_type":"Bearer","expires_in":7774672}
Reply


Forum Jump: