LogicMachine Forum
TTLock integration with LM - Printable Version

+- LogicMachine Forum (https://forum.logicmachine.net)
+-- Forum: LogicMachine eco-system (https://forum.logicmachine.net/forumdisplay.php?fid=1)
+--- Forum: General (https://forum.logicmachine.net/forumdisplay.php?fid=2)
+--- Thread: TTLock integration with LM (/showthread.php?tid=6460)



TTLock integration with LM - Chandrias - 10.06.2026

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)


RE: TTLock integration with LM - Daniel - 10.06.2026

Share the documentation for this API.


RE: TTLock integration with LM - Chandrias - 10.06.2026

(10.06.2026, 07:23)Daniel Wrote: Share the documentation for this API.

Everything is in their website
https://euopen.ttlock.com/document/doc?urlName=userGuide%2FekeyEn.html


RE: TTLock integration with LM - gtsamis - 10.06.2026

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.


RE: TTLock integration with LM - Chandrias - 10.06.2026

OK. I am not the expert to identify all that code.
Can you help me build a simple Unlock command?


RE: TTLock integration with LM - gtsamis - 11.06.2026

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))



RE: TTLock integration with LM - Chandrias - 11.06.2026

Shouldn't we get the access token first?


RE: TTLock integration with LM - gtsamis - 11.06.2026

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


RE: TTLock integration with LM - Chandrias - 11.06.2026

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.


RE: TTLock integration with LM - gtsamis - 11.06.2026

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.


RE: TTLock integration with LM - Chandrias - 11.06.2026

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""


RE: TTLock integration with LM - gtsamis - 11.06.2026

Probably you have not got the access token

Check the how to get access token: 
https://cnopen.ttlock.com/document/doc?urlName=cloud%2Foauth2%2FgetAccessTokenEn.html


RE: TTLock integration with LM - Chandrias - 12.06.2026

(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?urlName=cloud%2Foauth2%2FgetAccessTokenEn.html

I tried that and I get the error message


RE: TTLock integration with LM - Chandrias - 12.06.2026

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}