PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 All 36 releases
vikbooking / admin / helpers / src / dooraccess / provider / ttlock.php

ttlock.php in VikBooking Hotel Booking Engine & PMS trunk, at admin/helpers/src/dooraccess/provider/ttlock.php

1,489 lines 58.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2025 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 /**
15 * Door Access integration provider for TTLock.
16 *
17 * @since 1.18.4 (J) - 1.8.4 (WP)
18 *
19 * @link https://euopen.ttlock.com/document/doc?urlName=userGuide%2FekeyEn.html
20 */
21 final class VBODooraccessProviderTtlock extends VBODooraccessIntegrationAware
22 {
23 /**
24 * @var ?string
25 */
26 private ?string $oauthToken = null;
27
28 /**
29 * @var array
30 */
31 private array $httpHeaders = [];
32
33 /**
34 * @inheritDoc
35 */
36 public function getAlias()
37 {
38 return basename(__FILE__, '.php');
39 }
40
41 /**
42 * @inheritDoc
43 */
44 public function getName()
45 {
46 return 'TTLock - Smart Locks';
47 }
48
49 /**
50 * @inheritDoc
51 */
52 public function getShortName()
53 {
54 return 'TTLock';
55 }
56
57 /**
58 * @inheritDoc
59 */
60 public function getIcon()
61 {
62 return VBO_ADMIN_URI . 'resources/ttlock-vikbooking-integration-logo.png';
63 }
64
65 /**
66 * @inheritDoc
67 */
68 public function getParams()
69 {
70 return [
71 'ai' => [
72 'type' => 'checkbox',
73 'label' => JText::translate('VBO_AI_SUPPORT'),
74 'help' => JText::translate('VBO_DAC_AI_SUPPORT_HELP'),
75 'default' => 1,
76 ],
77 'firstaccess_notif' => [
78 'type' => 'checkbox',
79 'label' => JText::translate('VBO_NOTIFY_FIRST_ACCESS'),
80 'help' => JText::translate('VBO_NOTIFY_FIRST_ACCESS_HELP'),
81 'default' => 0,
82 ],
83 'passquant' => [
84 'type' => 'select',
85 'label' => JText::translate('VBO_PASSCODES'),
86 'help' => JText::translate('VBO_PASSCODES_QUANT_HELP'),
87 'options' => [
88 1 => JText::translate('VBO_ONE_PER_DEVICE'),
89 2 => JText::translate('VBO_ONE_PER_BOOKING'),
90 ],
91 'default' => 1,
92 ],
93 'client_id' => [
94 'type' => 'text',
95 'label' => 'Client ID',
96 ],
97 'client_secret' => [
98 'type' => 'password',
99 'label' => 'Client Secret',
100 ],
101 'username' => [
102 'type' => 'text',
103 'label' => 'Username',
104 ],
105 'password' => [
106 'type' => 'password',
107 'label' => 'Password',
108 ],
109 ];
110 }
111
112 /**
113 * @inheritDoc
114 */
115 public function canUnlockDevices()
116 {
117 // this method is called when the integration has loaded its profile record
118 // we return true only if the apposite AI setting is enabled
119
120 $settings = $this->getSettings();
121
122 return !empty($settings['ai']);
123 }
124
125 /**
126 * @inheritDoc
127 */
128 public function canWatchFirstAccess()
129 {
130 // this method is called when the integration has loaded its profile record
131 // we return true only if the apposite setting is enabled
132
133 $settings = $this->getSettings();
134
135 return !empty($settings['firstaccess_notif']);
136 }
137
138 /**
139 * Device capability implementation to unlock a device.
140 *
141 * @param VBODooraccessIntegrationDevice $device The device executing the capability.
142 * @param ?array $options Optional settings populated from capability parameters.
143 *
144 * @return VBODooraccessDeviceCapabilityResult
145 *
146 * @link https://euopen.ttlock.com/document/doc?urlName=cloud%2Fgateway%2FunlockEn.html
147 */
148 public function unlockDevice(VBODooraccessIntegrationDevice $device, ?array $options = null)
149 {
150 // start transporter
151 $transporter = $this->createHTTPTransporter();
152
153 // obtain integration settings after initializing the transporter
154 $settings = $this->getSettings();
155
156 // build request data
157 $data = [
158 'clientId' => $settings['client_id'],
159 'accessToken' => $settings['_oauth']['access_token'],
160 'lockId' => $device->getID(),
161 'date' => time() . '000',
162 ];
163
164 // make the API request
165 $response = $transporter->post('https://euapi.ttlock.com/v3/lock/unlock', $data, [], 60);
166
167 // obtain the response data
168 $responseData = (array) json_decode((string) $response->body, true);
169
170 if ($response->code != 200 || !empty($responseData['errcode'])) {
171 // an error occurred
172 throw new Exception($responseData['errmsg'] ?? $response->body ?: 'Error unlocking the device.', ($response->code != 200 ? $response->code : 500));
173 }
174
175 return (new VBODooraccessDeviceCapabilityResult)->setText(sprintf('The device "%s" was unlocked!', $device->getName()));
176 }
177
178 /**
179 * Device capability implementation to lock a device.
180 *
181 * @param VBODooraccessIntegrationDevice $device The device executing the capability.
182 * @param ?array $options Optional settings populated from capability parameters.
183 *
184 * @return VBODooraccessDeviceCapabilityResult
185 *
186 * @link https://euopen.ttlock.com/document/doc?urlName=cloud%2Fgateway%2FunlockEn.html
187 */
188 public function lockDevice(VBODooraccessIntegrationDevice $device, ?array $options = null)
189 {
190 // start transporter
191 $transporter = $this->createHTTPTransporter();
192
193 // obtain integration settings after initializing the transporter
194 $settings = $this->getSettings();
195
196 // build request data
197 $data = [
198 'clientId' => $settings['client_id'],
199 'accessToken' => $settings['_oauth']['access_token'],
200 'lockId' => $device->getID(),
201 'date' => time() . '000',
202 ];
203
204 // make the API request
205 $response = $transporter->post('https://euapi.ttlock.com/v3/lock/lock', $data, [], 60);
206
207 // obtain the response data
208 $responseData = (array) json_decode((string) $response->body, true);
209
210 if ($response->code != 200 || !empty($responseData['errcode'])) {
211 // an error occurred
212 throw new Exception($responseData['errmsg'] ?? $response->body ?: 'Error locking the device.', ($response->code != 200 ? $response->code : 500));
213 }
214
215 return (new VBODooraccessDeviceCapabilityResult)->setText(sprintf('The device "%s" was locked!', $device->getName()));
216 }
217
218 /**
219 * Device capability implementation to list a device passcodes.
220 *
221 * @param VBODooraccessIntegrationDevice $device The device executing the capability.
222 * @param ?array $options Optional settings populated from capability parameters.
223 *
224 * @return VBODooraccessDeviceCapabilityResult
225 *
226 * @throws Exception
227 *
228 * @link https://euopen.ttlock.com/document/doc?urlName=cloud%2Fpasscode%2FlistEn.html
229 */
230 public function listPasscodes(VBODooraccessIntegrationDevice $device, ?array $options = null)
231 {
232 $passcodes = [];
233
234 // start transporter
235 $transporter = $this->createHTTPTransporter();
236
237 // obtain integration settings after initializing the transporter
238 $settings = $this->getSettings();
239
240 // request page settings
241 $pageNo = 1;
242 $pageSize = 100;
243 $reqCount = 0;
244 $reqMax = 5;
245
246 // start a loop to support pagination
247 while (true) {
248 if ($reqCount >= $reqMax) {
249 // too many requests
250 break;
251 }
252
253 // build query string data
254 $data = [
255 'clientId' => $settings['client_id'],
256 'accessToken' => $settings['_oauth']['access_token'],
257 'lockId' => $device->getID(),
258 'searchStr' => $options['search'] ?? null,
259 'pageNo' => $pageNo,
260 'pageSize' => $pageSize,
261 'orderBy' => 1,
262 'date' => time() . '000',
263 ];
264
265 // make a request to obtain all created passcodes of a lock
266 $response = $transporter->get('https://euapi.ttlock.com/v3/lock/listKeyboardPwd?' . http_build_query($data), [], 20);
267
268 // obtain the response data
269 $responseData = (array) json_decode((string) $response->body, true);
270
271 if ($response->code != 200 || !empty($responseData['errcode'])) {
272 // an error occurred
273 throw new Exception($responseData['errmsg'] ?? $response->body ?: 'Error fetching device passcodes.', ($response->code != 200 ? $response->code : 500));
274 }
275
276 // increase request counter
277 $reqCount++;
278
279 if (!empty($responseData['list'])) {
280 $passcodes = array_merge($passcodes, $responseData['list']);
281 }
282
283 if (($responseData['pages'] ?? 0) > $pageNo) {
284 // go to the next loop
285 $pageNo++;
286
287 continue;
288 }
289
290 // all passcodes were read
291 break;
292 }
293
294 if (!$passcodes) {
295 throw new Exception('No passcodes found for the device.', 404);
296 }
297
298 // list of passcode IDs and values obtained
299 $passcodesAssoc = [];
300
301 // build HTML output
302 $output = '';
303
304 // lang defs
305 $lang_passcode = JText::translate('VBO_PASSCODE');
306 $lang_startdate = JText::translate('VBNEWPKGDFROM');
307 $lang_enddate = JText::translate('VBNEWPKGDTO');
308 $lang_createdon = JText::translate('VBOINVCREATIONDATE');
309 $lang_createdby = JText::translate('VBCSVCREATEDBY');
310 $lang_custom = JText::translate('VBO_CUSTOM');
311
312 // table head
313 $output .= <<<HTML
314 <div class="vbo-dac-table-wrap">
315 <table class="vbo-dac-table">
316 <thead>
317 <tr>
318 <td>Passcode ID</td>
319 <td>Passcode Name</td>
320 <td>{$lang_passcode}</td>
321 <td>Passcode Type</td>
322 <td>{$lang_startdate}</td>
323 <td>{$lang_enddate}</td>
324 <td>{$lang_createdon}</td>
325 <td>{$lang_createdby}</td>
326 <td>{$lang_custom}</td>
327 </tr>
328 </thead>
329 <tbody>
330 HTML;
331
332 // scan all passcodes obtained
333 foreach ($passcodes as $passcode) {
334 // set passcode properties
335 $passcodeId = $passcode['keyboardPwdId'] ?? '';
336 $passcodeValue = $passcode['keyboardPwd'] ?? '';
337 $passcodeName = $passcode['keyboardPwdName'] ?? '';
338 $passcodeType = $this->getPasscodeTypes((int) ($passcode['keyboardPwdType'] ?? 0), true);
339 $startDate = !empty($passcode['startDate']) ? date('Y-m-d H:i:s', ($passcode['startDate'] ?: 1000) / 1000) : '---';
340 $endDate = !empty($passcode['endDate']) ? date('Y-m-d H:i:s', ($passcode['endDate'] ?: 1000) / 1000) : '---';
341 $sendDate = !empty($passcode['sendDate']) ? date('Y-m-d H:i:s', ($passcode['sendDate'] ?: 1000) / 1000) : '---';
342 $senderUsername = $passcode['senderUsername'] ?? '';
343 $isCustom = ($passcode['isCustom'] ?? 0) == 1 ? JText::translate('VBYES') : JText::translate('VBNO');
344
345 // bind passcode id values
346 $passcodesAssoc[$passcodeId] = [
347 'name' => $passcodeName,
348 'value' => $passcodeValue,
349 ];
350
351 // build passcode HTML code
352 $output .= <<<HTML
353 <tr>
354 <td><span class="vbo-dac-table-passcode-id">{$passcodeId}</span></td>
355 <td><span class="vbo-dac-table-passcode-name">{$passcodeName}</span></td>
356 <td><span class="vbo-dac-table-passcode-code">{$passcodeValue}</span></td>
357 <td>{$passcodeType}</td>
358 <td>{$startDate}</td>
359 <td>{$endDate}</td>
360 <td>{$sendDate}</td>
361 <td>{$senderUsername}</td>
362 <td>{$isCustom}</td>
363 </tr>
364 HTML;
365 }
366
367 // close table
368 $output .= <<<HTML
369 </tbody>
370 </table>
371 </div>
372 HTML;
373
374 // return the capability result object by setting the output value
375 return (new VBODooraccessDeviceCapabilityResult($passcodesAssoc))
376 ->setOutput($output);
377 }
378
379 /**
380 * Device capability implementation to create a random passcode for a device (generated by TTLock).
381 * Notice that the random passcode generation of TTLock is based on the password type and the validity dates.
382 * This means that deleting a passcode for a device that was valid on certain dates, will trigger an error
383 * if another random passcode is generated for the same device and validity dates. It is, therefore, unsafe
384 * to rely on this passcode generation method when passcodes are generated at the time of booking, because
385 * modifications or cancellations may occur and a new re-generation of passcode may be requested for new bookings.
386 *
387 * @param VBODooraccessIntegrationDevice $device The device executing the capability.
388 * @param ?array $options Optional settings populated from capability parameters.
389 *
390 * @return VBODooraccessDeviceCapabilityResult
391 *
392 * @link https://euopen.ttlock.com/document/doc?urlName=cloud%2Fpasscode%2FgetEn.html
393 */
394 public function createRandomPasscode(VBODooraccessIntegrationDevice $device, ?array $options = null)
395 {
396 // start transporter
397 $transporter = $this->createHTTPTransporter();
398
399 // obtain integration settings after initializing the transporter
400 $settings = $this->getSettings();
401
402 // build request data
403 $data = [
404 'clientId' => $settings['client_id'],
405 'accessToken' => $settings['_oauth']['access_token'],
406 'lockId' => $device->getID(),
407 'keyboardPwdType' => (int) ($options['pwdtype'] ?? 3),
408 'keyboardPwdName' => $options['pwdname'] ?? null,
409 'startDate' => strtotime($options['startdate'] ?? date('Y-m-d H:i:s')) . '000',
410 'endDate' => strtotime($options['enddate'] ?? date('Y-m-d H:i:s')) . '000',
411 'date' => time() . '000',
412 ];
413
414 // make the API request
415 $response = $transporter->post('https://euapi.ttlock.com/v3/keyboardPwd/get', $data, [], 60);
416
417 // obtain the response data
418 $responseData = (array) json_decode((string) $response->body, true);
419
420 if ($response->code != 200 || !empty($responseData['errcode'])) {
421 // an error occurred
422 throw new Exception($responseData['errmsg'] ?? $response->body ?: 'Error getting a random passcode for the device.', ($response->code != 200 ? $response->code : 500));
423 }
424
425 // build result properties to bind
426 $resultProps = [
427 'keyboardPwd' => (string) ($responseData['keyboardPwd'] ?? ''),
428 'keyboardPwdId' => (string) ($responseData['keyboardPwdId'] ?? ''),
429 'listingId' => (int) ($options['listing_id'] ?? 0),
430 ];
431
432 // get the listing name, if available
433 $listingName = '';
434 if (!empty($resultProps['listingId'])) {
435 $listingData = VikBooking::getRoomInfo($resultProps['listingId'], ['name'], true);
436 $listingName = sprintf('%s: ', $listingData['name'] ?? '');
437 }
438
439 // wrap and return the device capability result object
440 return (new VBODooraccessDeviceCapabilityResult($resultProps))
441 ->setPasscode($resultProps['keyboardPwd'])
442 ->setText($listingName . JText::sprintf('VBO_PASSCODE_GEN_OK_DEVICE', $resultProps['keyboardPwd'], $device->getName()));
443 }
444
445 /**
446 * Device capability implementation to create a custom passcode for a device.
447 *
448 * @param VBODooraccessIntegrationDevice $device The device executing the capability.
449 * @param ?array $options Optional settings populated from capability parameters.
450 *
451 * @return VBODooraccessDeviceCapabilityResult
452 *
453 * @link https://euopen.ttlock.com/document/doc?urlName=cloud%2Fpasscode%2FaddEn.html
454 *
455 * @since 1.18.6 (J) - 1.8.6 (WP) API errors will throw a Door Access Exception with retry-data.
456 */
457 public function createCustomPasscode(VBODooraccessIntegrationDevice $device, ?array $options = null)
458 {
459 // start transporter
460 $transporter = $this->createHTTPTransporter();
461
462 // obtain integration settings after initializing the transporter
463 $settings = $this->getSettings();
464
465 if (empty($options['pwdvalue'])) {
466 // passcode value cannot be empty
467 $options['pwdvalue'] = $this->generateRandomPasscode();
468 }
469
470 // build request data
471 $data = [
472 'clientId' => $settings['client_id'],
473 'accessToken' => $settings['_oauth']['access_token'],
474 'lockId' => $device->getID(),
475 'keyboardPwd' => (int) $options['pwdvalue'],
476 'keyboardPwdName' => $options['pwdname'] ?? null,
477 'keyboardPwdType' => (int) ($options['pwdtype'] ?? 3),
478 'startDate' => strtotime($options['startdate'] ?? date('Y-m-d H:i:s')) . '000',
479 'endDate' => strtotime($options['enddate'] ?? date('Y-m-d H:i:s')) . '000',
480 'addType' => 2,
481 'date' => time() . '000',
482 ];
483
484 // make the API request
485 $response = $transporter->post('https://euapi.ttlock.com/v3/keyboardPwd/add', $data, [], 60);
486
487 // obtain the response data
488 $responseData = (array) json_decode((string) $response->body, true);
489
490 if ($response->code != 200 || !empty($responseData['errcode'])) {
491 // an error occurred, build DAC Exception with retry-data
492 $dacError = (new VBODooraccessException($responseData['errmsg'] ?? $response->body ?: 'Error adding a custom passcode to the device.', ($response->code != 200 ? $response->code : 500)))
493 ->setDevice($device)
494 ->setRetryCallback('createCustomPasscode')
495 ->setRetryData($options);
496
497 // throw error
498 throw $dacError;
499 }
500
501 // build result properties to bind
502 $resultProps = [
503 'keyboardPwd' => (string) $options['pwdvalue'],
504 'keyboardPwdId' => (string) ($responseData['keyboardPwdId'] ?? ''),
505 'listingId' => (int) ($options['listing_id'] ?? 0),
506 ];
507
508 // get the listing name, if available
509 $listingName = '';
510 if (!empty($resultProps['listingId'])) {
511 $listingData = VikBooking::getRoomInfo($resultProps['listingId'], ['name'], true);
512 $listingName = sprintf('%s: ', $listingData['name'] ?? '');
513 }
514
515 // wrap and return the device capability result object
516 return (new VBODooraccessDeviceCapabilityResult($resultProps))
517 ->setPasscode($resultProps['keyboardPwd'])
518 ->setText($listingName . JText::sprintf('VBO_PASSCODE_GEN_OK_DEVICE', $resultProps['keyboardPwd'], $device->getName()));
519 }
520
521 /**
522 * Device capability implementation to delete a passcode from a device.
523 *
524 * @param VBODooraccessIntegrationDevice $device The device executing the capability.
525 * @param ?array $options Optional settings populated from capability parameters.
526 *
527 * @return VBODooraccessDeviceCapabilityResult
528 *
529 * @link https://euopen.ttlock.com/document/doc?urlName=cloud%2Fpasscode%2FdeleteEn.html
530 */
531 public function deletePasscode(VBODooraccessIntegrationDevice $device, ?array $options = null)
532 {
533 // start transporter
534 $transporter = $this->createHTTPTransporter();
535
536 // obtain integration settings after initializing the transporter
537 $settings = $this->getSettings();
538
539 // build request data
540 $data = [
541 'clientId' => $settings['client_id'],
542 'accessToken' => $settings['_oauth']['access_token'],
543 'lockId' => $device->getID(),
544 'keyboardPwdId' => $options['pwdid'] ?? null,
545 'deleteType' => 2,
546 'date' => time() . '000',
547 ];
548
549 // make the API request
550 $response = $transporter->post('https://euapi.ttlock.com/v3/keyboardPwd/delete', $data, [], 60);
551
552 // obtain the response data
553 $responseData = (array) json_decode((string) $response->body, true);
554
555 if ($response->code != 200 || !empty($responseData['errcode'])) {
556 // an error occurred
557 throw new Exception($responseData['errmsg'] ?? $response->body ?: 'Error deleting passcode from device.', ($response->code != 200 ? $response->code : 500));
558 }
559
560 return (new VBODooraccessDeviceCapabilityResult)
561 ->setText(JText::sprintf('VBO_PASSCODE_DEL_OK_DEVICE', $device->getName()));
562 }
563
564 /**
565 * Device capability implementation to show the list of activity logs (unlock records) of a device.
566 *
567 * @param VBODooraccessIntegrationDevice $device The device executing the capability.
568 * @param ?array $options Optional settings populated from capability parameters.
569 *
570 * @return VBODooraccessDeviceCapabilityResult
571 *
572 * @throws Exception
573 *
574 * @link https://euopen.ttlock.com/document/doc?urlName=cloud%2FlockRecord%2FlistEn.html
575 *
576 * @since 1.18.6 (J) - 1.8.6 (WP)
577 */
578 public function showActivityLogs(VBODooraccessIntegrationDevice $device, ?array $options = null)
579 {
580 $activities = [];
581
582 // start transporter
583 $transporter = $this->createHTTPTransporter();
584
585 // obtain integration settings after initializing the transporter
586 $settings = $this->getSettings();
587
588 // request page settings
589 $pageNo = 1;
590 $pageSize = 150;
591 $reqCount = 0;
592 $reqMax = 5;
593
594 // start a loop to support pagination
595 while (true) {
596 if ($reqCount >= $reqMax) {
597 // too many requests
598 break;
599 }
600
601 // build query string data
602 $data = [
603 'clientId' => $settings['client_id'],
604 'accessToken' => $settings['_oauth']['access_token'],
605 'lockId' => $device->getID(),
606 'startDate' => (!empty($options['startdate']) ? strtotime($options['startdate']) . '000' : null),
607 'endDate' => (!empty($options['enddate']) ? strtotime($options['enddate']) . '000' : null),
608 'pageNo' => $pageNo,
609 'pageSize' => $pageSize,
610 'recordType' => (!empty($options['recordtype']) ? (int) $options['recordtype'] : null),
611 'searchStr' => $options['search'] ?? null,
612 'date' => time() . '000',
613 ];
614
615 // make a request to obtain the unlock records of a lock
616 $response = $transporter->get('https://euapi.ttlock.com/v3/lockRecord/list?' . http_build_query($data), [], 20);
617
618 // obtain the response data
619 $responseData = (array) json_decode((string) $response->body, true);
620
621 if ($response->code != 200 || !empty($responseData['errcode'])) {
622 // an error occurred
623 throw new Exception($responseData['errmsg'] ?? $response->body ?: 'Error fetching device unlock records.', ($response->code != 200 ? $response->code : 500));
624 }
625
626 // increase request counter
627 $reqCount++;
628
629 if (!empty($responseData['list'])) {
630 $activities = array_merge($activities, $responseData['list']);
631 }
632
633 if (($responseData['pages'] ?? 0) > $pageNo) {
634 // go to the next loop
635 $pageNo++;
636
637 continue;
638 }
639
640 // all records were read
641 break;
642 }
643
644 if (!$activities) {
645 throw new Exception('No unlock records found for the device.', 404);
646 }
647
648 // build HTML output
649 $output = '';
650
651 // lang defs
652 $lang_passcode = JText::translate('VBO_PASSCODE');
653 $lang_type = JText::translate('VBPSHOWSEASONSTHREE');
654 $lang_status = JText::translate('VBSTATUS');
655 $lang_createdon = JText::translate('VBOINVCREATIONDATE');
656 $lang_createdby = JText::translate('VBCSVCREATEDBY');
657
658 // table head
659 $output .= <<<HTML
660 <div class="vbo-dac-table-wrap">
661 <table class="vbo-dac-table">
662 <thead>
663 <tr>
664 <td>ID</td>
665 <td>{$lang_passcode}</td>
666 <td>{$lang_type}</td>
667 <td>{$lang_status}</td>
668 <td>{$lang_createdon}</td>
669 <td>{$lang_createdby}</td>
670 </tr>
671 </thead>
672 <tbody>
673 HTML;
674
675 // scan all activites obtained
676 foreach ($activities as $activity) {
677 // set activity properties
678 $activityId = $activity['recordId'] ?? '';
679 $passcodeValue = $activity['keyboardPwd'] ?? '---';
680 $activityType = $this->getActivityRecordTypes((int) ($activity['recordType'] ?? $activity['recordTypeFromLock'] ?? 0));
681 $isSuccess = ($activity['success'] ?? 0) == 1 ? JText::translate('VBYES') : JText::translate('VBNO');
682 $createdOn = !empty($activity['lockDate']) ? date('Y-m-d H:i:s', ($activity['lockDate'] / 1000)) : '';
683 $createdOn = empty($createdOn) && !empty($activity['serverDate']) ? date('Y-m-d H:i:s', ($activity['serverDate'] / 1000)) : $createdOn;
684 $createdOn = $createdOn ?: '---';
685 $createdBy = $activity['username'] ?? '---';
686
687 // build passcode HTML code
688 $output .= <<<HTML
689 <tr>
690 <td><span class="vbo-dac-table-passcode-id">{$activityId}</span></td>
691 <td><span class="vbo-dac-table-passcode-code">{$passcodeValue}</span></td>
692 <td>{$activityType}</td>
693 <td>{$isSuccess}</td>
694 <td>{$createdOn}</td>
695 <td>{$createdBy}</td>
696 </tr>
697 HTML;
698 }
699
700 // close table
701 $output .= <<<HTML
702 </tbody>
703 </table>
704 </div>
705 HTML;
706
707 // return the capability result object by setting the output value
708 return (new VBODooraccessDeviceCapabilityResult($activities))
709 ->setOutput($output);
710 }
711
712 /**
713 * @inheritDoc
714 */
715 public function createBookingDoorAccess(VBODooraccessIntegrationDevice $device, int $listingId, VBOBookingRegistry $registry)
716 {
717 // access the integration settings
718 $settings = $this->getSettings();
719
720 // build booking-listing signature
721 $signature = sprintf('%d-%d', $registry->getID(), $listingId);
722
723 // access booking registry DAC data for passcodes generated
724 $passcodesBuffer = $registry->getDACProperty($this->getAlias(), 'passcodes', []);
725
726 // determine the passcode value to use, either a new one or a previous one for the same booking
727 if (($settings['passquant'] ?? 0) == 2 && ($passcodesBuffer[$signature] ?? null)) {
728 // use the previously generated passcode for this booking and listing also on this device
729 $passcodeValue = $passcodesBuffer[$signature];
730 } else {
731 // generate custom, yet random, passcode value of 8 digits for this device
732 $passcodeValue = $this->generateRandomPasscode();
733 }
734
735 // prepare the options for creating a custom passcode (randomly generated by us)
736 $options = [
737 // use a password name that can be used later to find it under this booking and listing ID
738 'pwdname' => sprintf('bid:%d-%d', $registry->getID(), $listingId),
739 // set the passcode validity start date and time
740 'startdate' => date('Y-m-d H:i:00', $registry->getProperty('checkin', 0)),
741 // set the passcode validity end date and time
742 'enddate' => date('Y-m-d H:i:00', $registry->getProperty('checkout', 0)),
743 // custom passcode value to create on the device
744 'pwdvalue' => $passcodeValue,
745 // inject the listing ID for completion of data
746 'listing_id' => $listingId,
747 ];
748
749 // create custom passcode on the current device
750 $result = $this->createCustomPasscode($device, $options);
751
752 // update booking registry DAC data for passcodes generated
753 $passcodesBuffer[$signature] = $result->getPasscode();
754 $registry->setDACProperty($this->getAlias(), 'passcodes', $passcodesBuffer);
755
756 return $result;
757 }
758
759 /**
760 * @inheritDoc
761 */
762 public function modifyBookingDoorAccess(VBODooraccessIntegrationDevice $device, int $listingId, VBOBookingRegistry $registry)
763 {
764 // searching, deleting and re-creating passcodes is always safer in case of
765 // booking modification for possibly different listing IDs involved
766
767 // find the passcode data that were previously created for this booking
768 $previousDevicePasscodes = VikBooking::getBookingHistoryInstance($registry->getID())
769 ->getEventsWithData(['ND', 'MD'], function($data) use ($device) {
770 $data = (array) $data;
771 // ensure the passcode was generated for this provider, profile and device
772 return ($data['provider'] ?? '') == $this->getProfileProvider() &&
773 ($data['profile'] ?? '') == $this->getProfileID() &&
774 ($data['device'] ?? '') == $device->getID() &&
775 (!empty($data['passcode']) || !empty($data['props']));
776 });
777
778 if (!$previousDevicePasscodes) {
779 // no passcodes were previously created for this booking
780 // process the modification as a new door access creation (with TTLock random passcode)
781 return $this->createBookingDoorAccess($device, $listingId, $registry);
782 }
783
784 // scan all previously created passcodes in DESC order on this device and delete them
785 $previousPasscodeIds = [];
786 foreach (array_reverse($previousDevicePasscodes) as $previousData) {
787 // ensure we only have array values
788 $previousData = (array) json_decode(json_encode($previousData), true);
789
790 // get the previous passcode
791 $previousPasscode = ($previousData['passcode'] ?? '') ?: ($previousData['props']['keyboardPwdId'] ?? '');
792
793 if (empty($previousPasscode) || in_array($previousPasscode, $previousPasscodeIds)) {
794 // no passcode ID to delete, or already deleted
795 continue;
796 }
797
798 // push processed passcode ID
799 $previousPasscodeIds[] = $previousPasscode;
800
801 try {
802 // delete previous passcode for this booking
803 $this->deletePasscode($device, [
804 'pwdid' => $previousPasscode,
805 ]);
806 } catch (Exception $e) {
807 // do nothing on error
808 }
809 }
810
811 // process the modification as a new door access creation, always with custom passcodes
812 return $this->createBookingDoorAccess($device, $listingId, $registry);
813 }
814
815 /**
816 * @inheritDoc
817 */
818 public function cancelBookingDoorAccess(VBODooraccessIntegrationDevice $device, int $listingId, VBOBookingRegistry $registry)
819 {
820 try {
821 // find the previously created passcode for this booking and listing
822 $findResult = $this->listPasscodes($device, [
823 'search' => sprintf('bid:%d-%d', $registry->getID(), $listingId),
824 ]);
825
826 if (!$findResult->getProperties()) {
827 // passcode not found
828 throw new Exception('Previous passcode not found.', 404);
829 }
830 } catch (Exception $e) {
831 // nothing to cancel, but prevent unwanted errors not related to the real cancellation
832 return null;
833 }
834
835 // iterate the list of passcodes found, even if only one is expected
836 foreach ($findResult->getProperties() as $pwdId => $pwdData) {
837 // delete the first passcode found
838 return $this->deletePasscode($device, [
839 'pwdid' => $pwdId,
840 ]);
841 }
842 }
843
844 /**
845 * @inheritDoc
846 */
847 public function handleUnlockDevice(VBODooraccessIntegrationDevice $device)
848 {
849 // unlock the requested device
850 return $this->unlockDevice($device);
851 }
852
853 /**
854 * @inheritDoc
855 */
856 public function getPasscodeFromHistoryResult(array $resultProperties)
857 {
858 // creating a passcode should bind its value within the device capability result object
859 return $resultProperties['keyboardPwd'] ?? null;
860 }
861
862 /**
863 * @inheritDoc
864 *
865 * @since 1.18.6 (J) - 1.8.6 (WP)
866 */
867 public function detectFirstAccess(VBODooraccessIntegrationDevice $device, int $listingId, VBOBookingRegistry $registry)
868 {
869 // the expected passcode name to match in the activities "username"
870 $matchPwdName = sprintf('bid:%d-%d', $registry->getID(), $listingId);
871 try {
872 // access the activity logs for the booking stay dates in the current device
873 $findResult = $this->showActivityLogs($device, [
874 'startdate' => date('Y-m-d 00:00:00', $registry->getProperty('checkin', 0)),
875 'enddate' => date('Y-m-d 23:59:59', $registry->getProperty('checkout', 0)),
876 ]);
877
878 if (!$findResult->getProperties()) {
879 // no activities found
880 throw new Exception('No activities found.', 404);
881 }
882 } catch (Exception $e) {
883 // nothing useful was detected, but prevent unwanted errors to be thrown
884 return null;
885 }
886
887 // iterate all activities found
888 foreach ($findResult->getProperties() as $activity) {
889 if (($activity['username'] ?? '') == $matchPwdName) {
890 // booking access found within the device activity logs for matching passcode name
891 $createdOn = !empty($activity['lockDate']) ? date('Y-m-d H:i:s', ($activity['lockDate'] / 1000)) : '';
892 $createdOn = empty($createdOn) && !empty($activity['serverDate']) ? date('Y-m-d H:i:s', ($activity['serverDate'] / 1000)) : $createdOn;
893
894 // return the capability result with the matching activity
895 return (new VBODooraccessDeviceCapabilityResult($activity))
896 ->setPasscode($activity['keyboardPwd'] ?? '')
897 ->setText(sprintf('%s (%s)', ($activity['keyboardPwd'] ?? ''), $createdOn));
898 }
899 }
900
901 // if nothing is found, scan booking registry DAC passcodes data as fallback
902 foreach ($registry->getDACProperty($this->getAlias(), 'passcodes_data', []) as $passcodeData) {
903 // get the passcode value
904 $passcodeValue = is_array($passcodeData) ? $this->getPasscodeFromHistoryResult($passcodeData) : $passcodeData;
905
906 if (!is_string($passcodeValue) || !$passcodeValue) {
907 // no passcode to look for
908 continue;
909 }
910
911 foreach ($findResult->getProperties() as $activity) {
912 if (($activity['keyboardPwd'] ?? '') == $passcodeValue) {
913 // booking access found within the device activity logs for matching passcode value
914 $createdOn = !empty($activity['lockDate']) ? date('Y-m-d H:i:s', ($activity['lockDate'] / 1000)) : '';
915 $createdOn = empty($createdOn) && !empty($activity['serverDate']) ? date('Y-m-d H:i:s', ($activity['serverDate'] / 1000)) : $createdOn;
916
917 // return the capability result with the matched property
918 return (new VBODooraccessDeviceCapabilityResult($activity))
919 ->setPasscode($activity['keyboardPwd'] ?? '')
920 ->setText(sprintf('%s (%s)', ($activity['keyboardPwd'] ?? ''), $createdOn));
921 }
922 }
923 }
924
925 return null;
926 }
927
928 /**
929 * @inheritDoc
930 *
931 * @link https://euopen.ttlock.com/document/doc?urlName=cloud%2Flock%2FlistEn.html
932 */
933 protected function fetchRemoteDevices()
934 {
935 $devices = [];
936
937 // start transporter
938 $transporter = $this->createHTTPTransporter();
939
940 // obtain settings after initializing the transporter
941 $settings = $this->getSettings();
942
943 // request page settings
944 $pageNo = 1;
945 $pageSize = 20;
946 $reqCount = 0;
947 $reqMax = 20;
948
949 // start a loop to support pagination
950 while (true) {
951 if ($reqCount >= $reqMax) {
952 // too many requests
953 break;
954 }
955
956 // build query string data
957 $data = [
958 'clientId' => $settings['client_id'],
959 'accessToken' => $settings['_oauth']['access_token'],
960 'pageNo' => $pageNo,
961 'pageSize' => $pageSize,
962 'date' => time() . '000',
963 ];
964
965 // make a request to obtain the lock list of an account
966 $response = $transporter->get('https://euapi.ttlock.com/v3/lock/list?' . http_build_query($data), [], 20);
967
968 // obtain the response data
969 $responseData = (array) json_decode((string) $response->body, true);
970
971 if ($response->code != 200 || !empty($responseData['errcode'])) {
972 // an error occurred
973 throw new Exception($responseData['errmsg'] ?? $response->body ?: 'Error fetching the remove devices.', ($response->code != 200 ? $response->code : 500));
974 }
975
976 // increase request counter
977 $reqCount++;
978
979 if (!empty($responseData['list'])) {
980 $devices = array_merge($devices, $responseData['list']);
981 }
982
983 if (($responseData['pages'] ?? 0) > $pageNo) {
984 // go to the next loop
985 $pageNo++;
986
987 continue;
988 }
989
990 // all devices were read
991 break;
992 }
993
994 if (!$devices) {
995 throw new Exception('No devices found under the current account.', 500);
996 }
997
998 return $devices;
999 }
1000
1001 /**
1002 * @inheritDoc
1003 */
1004 protected function decorateDeviceProperties(VBODooraccessIntegrationDevice $decorator, array $device)
1005 {
1006 // set device ID
1007 $decorator->setID($device['lockId'] ?? '');
1008
1009 // set device name
1010 $decorator->setName($device['lockAlias'] ?? $device['lockName'] ?? '');
1011
1012 // set device description
1013 $decorator->setDescription($device['groupName'] ?? '');
1014
1015 // set device icon
1016 $decorator->setIcon('<i class="' . VikBookingIcons::i('fingerprint') . '"></i>');
1017
1018 // set device model
1019 $decorator->setModel($device['lockName'] ?? '');
1020
1021 if ($device['electricQuantity'] ?? null) {
1022 // set device battery level
1023 $decorator->setBatteryLevel((float) $device['electricQuantity']);
1024 }
1025
1026 // set device capabilities
1027 $decorator->setCapabilities([
1028 // unlock device
1029 $this->createDeviceCapability([
1030 'id' => 'unlock_device',
1031 'title' => JText::translate('VBDASHUNLOCK'),
1032 'description' => JText::translate('VBO_UNLOCK_DEVICE_HELP'),
1033 'icon' => '<i class="' . VikBookingIcons::i('unlock') . '"></i>',
1034 'callback' => 'unlockDevice',
1035 ]),
1036 // lock device
1037 $this->createDeviceCapability([
1038 'id' => 'lock_device',
1039 'title' => JText::translate('VBO_LOCK'),
1040 'description' => JText::translate('VBO_LOCK_DEVICE_HELP'),
1041 'icon' => '<i class="' . VikBookingIcons::i('lock') . '"></i>',
1042 'callback' => 'lockDevice',
1043 ]),
1044 // read passcodes
1045 $this->createDeviceCapability([
1046 'id' => 'list_passcodes',
1047 'title' => JText::translate('VBO_LIST_PASSCODES'),
1048 'description' => JText::translate('VBO_LIST_PASSCODES_HELP'),
1049 'icon' => '<i class="' . VikBookingIcons::i('key') . '"></i>',
1050 'callback' => 'listPasscodes',
1051 'params' => [
1052 'search' => [
1053 'type' => 'text',
1054 'label' => JText::translate('VBO_SEARCH_PASSCODE'),
1055 'help' => JText::translate('VBO_OPT_SEARCH_KEYWORD'),
1056 ],
1057 ],
1058 ]),
1059 // create (random) passcode
1060 $this->createDeviceCapability([
1061 'id' => 'create_passcode',
1062 'title' => JText::translate('VBO_CREATE_PASSCODE'),
1063 'description' => JText::translate('VBO_CREATE_PASSCODE_HELP'),
1064 'icon' => '<i class="' . VikBookingIcons::i('plus') . '"></i>',
1065 'callback' => 'createRandomPasscode',
1066 'params' => [
1067 'pwdname' => [
1068 'type' => 'text',
1069 'label' => JText::translate('VBO_PASSCODE_NAME'),
1070 'help' => JText::translate('VBO_OPT_PASSCODE_NAME'),
1071 ],
1072 'pwdtype' => [
1073 'type' => 'select',
1074 'label' => JText::translate('VBPSHOWSEASONSTHREE'),
1075 'options' => array_combine(array_keys($this->getPasscodeTypes()), array_column($this->getPasscodeTypes(), 'name')),
1076 'default' => 3,
1077 ],
1078 'startdate' => [
1079 'type' => 'datetime',
1080 'label' => JText::translate('VBNEWPKGDFROM'),
1081 'help' => JText::translate('VBO_PASSCODE_VALID_START'),
1082 ],
1083 'enddate' => [
1084 'type' => 'datetime',
1085 'label' => JText::translate('VBNEWPKGDTO'),
1086 'help' => JText::translate('VBO_PASSCODE_VALID_END'),
1087 ],
1088 ],
1089 ]),
1090 // create (custom) passcode
1091 $this->createDeviceCapability([
1092 'id' => 'create_custom_passcode',
1093 'title' => JText::translate('VBO_CREATE_PASSCODECUST'),
1094 'description' => JText::translate('VBO_CREATE_PASSCODECUST_HELP'),
1095 'icon' => '<i class="' . VikBookingIcons::i('user-plus') . '"></i>',
1096 'callback' => 'createCustomPasscode',
1097 'params' => [
1098 'pwdvalue' => [
1099 'type' => 'text',
1100 'label' => JText::translate('VBO_PASSCODE'),
1101 'help' => JText::translate('VBO_PASSCODE_EMPTY_HELP') . ' 4-9 digits, first digit should not be 0.',
1102 'attributes' => [
1103 'pattern' => '[1-9][0-9]{3,8}',
1104 ],
1105 ],
1106 'pwdname' => [
1107 'type' => 'text',
1108 'label' => JText::translate('VBO_PASSCODE_NAME'),
1109 'help' => JText::translate('VBO_OPT_PASSCODE_NAME'),
1110 ],
1111 'pwdtype' => [
1112 'type' => 'select',
1113 'label' => JText::translate('VBPSHOWSEASONSTHREE'),
1114 'options' => array_combine(array_keys($this->getPasscodeTypes()), array_column($this->getPasscodeTypes(), 'name')),
1115 'default' => 3,
1116 ],
1117 'startdate' => [
1118 'type' => 'datetime',
1119 'label' => JText::translate('VBNEWPKGDFROM'),
1120 'help' => JText::translate('VBO_PASSCODE_VALID_START'),
1121 ],
1122 'enddate' => [
1123 'type' => 'datetime',
1124 'label' => JText::translate('VBNEWPKGDTO'),
1125 'help' => JText::translate('VBO_PASSCODE_VALID_END'),
1126 ],
1127 ],
1128 ]),
1129 // delete passcode
1130 $this->createDeviceCapability([
1131 'id' => 'delete_passcode',
1132 'title' => JText::translate('VBO_DELETE_PASSCODE'),
1133 'description' => JText::translate('VBO_DELETE_PASSCODE_HELP'),
1134 'icon' => '<i class="' . VikBookingIcons::i('trash') . '"></i>',
1135 'callback' => 'deletePasscode',
1136 'params' => [
1137 'pwdid' => [
1138 'type' => 'text',
1139 'label' => 'Passcode ID',
1140 ],
1141 ],
1142 ]),
1143 // show activity logs
1144 $this->createDeviceCapability([
1145 'id' => 'activity_logs',
1146 'title' => JText::translate('VBO_ACTIVITY_LOGS'),
1147 'description' => JText::translate('VBO_ACTIVITY_LOGS_HELP'),
1148 'icon' => '<i class="' . VikBookingIcons::i('search') . '"></i>',
1149 'callback' => 'showActivityLogs',
1150 'params' => [
1151 'startdate' => [
1152 'type' => 'datetime',
1153 'label' => JText::translate('VBOREPORTSDATEFROM'),
1154 ],
1155 'enddate' => [
1156 'type' => 'datetime',
1157 'label' => JText::translate('VBOREPORTSDATETO'),
1158 ],
1159 'recordtype' => [
1160 'type' => 'select',
1161 'label' => JText::translate('VBPSHOWSEASONSTHREE'),
1162 'options' => ([JText::translate('VBANYTHING')] + $this->getActivityRecordTypes()),
1163 ],
1164 'search' => [
1165 'type' => 'text',
1166 'label' => JText::translate('VBO_SEARCH_PASSCODE'),
1167 'help' => JText::translate('VBO_OPT_SEARCH_KEYWORD'),
1168 ],
1169 ],
1170 ]),
1171 ]);
1172
1173 // set device payload by unsetting the unwanted properties
1174 unset($device['lockData']);
1175 $decorator->setPayload($device);
1176 }
1177
1178 /**
1179 * Generates a random serial code made of only digits with a given length.
1180 * The sequence obtained will never start with 0 to allow integer casting.
1181 *
1182 * @param int $length The passcode length.
1183 *
1184 * @return string
1185 */
1186 private function generateRandomPasscode(int $length = 8)
1187 {
1188 return rand(1, 9) . VikBooking::getCPinInstance()->generateSerialCode($length - 1, ['0123456789']);
1189 }
1190
1191 /**
1192 * Maps the supported passcode type identifiers with name and description.
1193 *
1194 * @param ?int $type Optional passcode type identifier to fetch.
1195 * @param bool $name True to get only the passcode name.
1196 *
1197 * @return array|string Full list, passcode type array or passcode string name.
1198 */
1199 private function getPasscodeTypes(?int $type = null, bool $name = false)
1200 {
1201 $list = [
1202 1 => [
1203 'name' => 'One-time',
1204 'descr' => 'Valid only once within 6 hours from the Start Time.',
1205 ],
1206 2 => [
1207 'name' => 'Permanent',
1208 'descr' => 'Code must be used at least once within 24 Hours after the Start Time, or it will be invalidated.',
1209 ],
1210 3 => [
1211 'name' => 'Period',
1212 'descr' => 'Code must be used at least once within 24 Hours after the Start Time, or it will be invalidated.',
1213 ],
1214 4 => [
1215 'name' => 'Delete',
1216 'descr' => 'The code will delete all other codes.',
1217 ],
1218 5 => [
1219 'name' => 'Weekend Cyclic',
1220 'descr' => 'The code is valid during the time period at the weekend.',
1221 ],
1222 6 => [
1223 'name' => 'Daily Cyclic',
1224 'descr' => 'The code is valid during the time period everyday.',
1225 ],
1226 7 => [
1227 'name' => 'Workday Cyclic',
1228 'descr' => 'The code is valid during the time period on workdays.',
1229 ],
1230 8 => [
1231 'name' => 'Monday Cyclic',
1232 'descr' => 'The code is valid during the time period on Mondays.',
1233 ],
1234 9 => [
1235 'name' => 'Tuesday Cyclic',
1236 'descr' => 'The code is valid during the time period on Tuesdays.',
1237 ],
1238 10 => [
1239 'name' => 'Wednesday Cyclic',
1240 'descr' => 'The code is valid during the time period on Wednesdays.',
1241 ],
1242 11 => [
1243 'name' => 'Thursday Cyclic',
1244 'descr' => 'The code is valid during the time period on Thursdays.',
1245 ],
1246 12 => [
1247 'name' => 'Friday Cyclic',
1248 'descr' => 'The code is valid during the time period on Fridays.',
1249 ],
1250 13 => [
1251 'name' => 'Saturday Cyclic',
1252 'descr' => 'The code is valid during the time period on Saturdays.',
1253 ],
1254 14 => [
1255 'name' => 'Sunday Cyclic',
1256 'descr' => 'The code is valid during the time period on Sundays.',
1257 ],
1258 ];
1259
1260 if (is_null($type)) {
1261 return $list;
1262 }
1263
1264 if (!$name) {
1265 return $list[$type] ?? [];
1266 }
1267
1268 return $list[$type]['name'] ?? '';
1269 }
1270
1271 /**
1272 * Maps the known record types for the unlock records (activity logs).
1273 *
1274 * @param ?int $type Optional passcode type identifier to fetch.
1275 *
1276 * @return array|string Full list, or record type (event) name.
1277 *
1278 * @since 1.18.6 (J) - 1.8.6 (WP)
1279 */
1280 private function getActivityRecordTypes(?int $type = null)
1281 {
1282 $list = [
1283 4 => 'unlock by passcode',
1284 1 => 'unlock by app',
1285 7 => 'unlock by IC card',
1286 8 => 'unlock by fingerprint',
1287 9 => 'unlock by wrist strap',
1288 10 => 'unlock by Mechanical key',
1289 12 => 'unlock by gateway',
1290 46 => 'unlock by unlock key',
1291 49 => 'unlock by hotel card',
1292 50 => 'Unlocked due to the high temperature',
1293 57 => 'Unlock with QR code success',
1294 58 => 'Unlock with QR code failed, it\'s expired',
1295 51 => 'Try to unlock with a deleted card',
1296 5 => 'Rise the lock (for parking lock)',
1297 6 => 'Lower the lock (for parking lock)',
1298 11 => 'lock by app',
1299 29 => 'apply some force on the Lock',
1300 30 => 'Door sensor closed',
1301 31 => 'Door sensor open',
1302 32 => 'open from inside',
1303 33 => 'lock by fingerprint',
1304 34 => 'lock by passcode',
1305 35 => 'lock by IC card',
1306 36 => 'lock by Mechanical key',
1307 37 => 'Use APP button to control the lock (rise, fall, stop, lock), mostly used for roller shutter door',
1308 42 => 'received new local mail',
1309 43 => 'received new other cities mail',
1310 44 => 'Tamper alert',
1311 45 => 'Auto Lock',
1312 47 => 'lock by lock key',
1313 48 => 'System locked ( Caused by, for example: Using INVALID Passcode/Fingerprint/Card several times)',
1314 52 => 'Dead lock with APP',
1315 53 => 'Dead lock with passcode',
1316 54 => 'The car left (for parking lock)',
1317 55 => 'Use remote control lock or unlock lock',
1318 59 => 'Double locked',
1319 60 => 'Cancel double lock',
1320 61 => 'Lock with QR code success',
1321 62 => 'Lock with QR code failed, the lock is double locked',
1322 63 => 'Auto unlock at passage mode',
1323 64 => 'Door unclosed alarm',
1324 65 => 'Failed to unlock',
1325 66 => 'Failed to lock',
1326 67 => 'Face unlock success',
1327 68 => 'Face unlock failed (door locked from inside)',
1328 69 => 'Lock with face',
1329 71 => 'Face unlock failed (expired or ineffective)',
1330 75 => 'Unlocked by App granting',
1331 76 => 'Unlocked by remote granting',
1332 77 => 'Dual authentication Bluetooth unlock verification success, waiting for second user',
1333 78 => 'Dual authentication password unlock verification success, waiting for second user',
1334 79 => 'Dual authentication fingerprint unlock verification success, waiting for second user',
1335 80 => 'Dual authentication IC card unlock verification success, waiting for second user',
1336 81 => 'Dual authentication face card unlock verification success, waiting for second user',
1337 82 => 'Dual authentication wireless key unlock verification success, waiting for second user',
1338 83 => 'Dual authentication palm vein unlock verification success, waiting for second user',
1339 84 => 'Palm vein unlock success',
1340 85 => 'Palm vein unlock success',
1341 86 => 'Lock with palm vein',
1342 88 => 'Palm vein unlock failed (expired or ineffective)',
1343 92 => 'Administrator password to unlock ',
1344 ];
1345
1346 if (is_null($type)) {
1347 return $list;
1348 }
1349
1350 return $list[$type] ?? '';
1351 }
1352
1353 /**
1354 * Creates the HTTP Transporter to establish API connections with TTLock.
1355 * An integration profile record is supposed to be set before making an HTTP request.
1356 *
1357 * @param ?array $options Optional transporter options.
1358 *
1359 * @return object The prepared HTTP transporter object with bearer token.
1360 *
1361 * @throws Exception
1362 */
1363 private function createHTTPTransporter(?array $options = null)
1364 {
1365 // access current profile settings
1366 $settings = $this->getSettings();
1367
1368 if (empty($settings['client_id']) || empty($settings['client_secret']) || empty($settings['username']) || empty($settings['password'])) {
1369 throw new Exception('Missing integration profile credentials (settings).', 500);
1370 }
1371
1372 // obtain a valid OAuth token
1373 if ($options['renew_token'] ?? null) {
1374 // force the token renewal
1375 $this->oauthToken = $this->renewOauthToken();
1376 } else {
1377 // get the possibly valid token
1378 $this->oauthToken = $this->getOauthToken();
1379 }
1380
1381 // set HTTP headers
1382 $this->httpHeaders = [
1383 'Authorization' => "Bearer {$this->oauthToken}",
1384 'ContentType' => 'application/x-www-form-urlencoded',
1385 ];
1386
1387 return new JHttp;
1388 }
1389
1390 /**
1391 * Obtains an active OAuth (Bearer) token to establish API connections with TTLock.
1392 *
1393 * @return string An active OAuth (Bearer) token.
1394 *
1395 * @throws Exception
1396 */
1397 private function getOauthToken()
1398 {
1399 // access current profile settings
1400 $settings = $this->getSettings();
1401
1402 if (empty($settings['_oauth']['access_token']) || empty($settings['_oauth']['expiry_ts'])) {
1403 // the token should be obtained from scratch
1404 return $this->renewOauthToken();
1405 }
1406
1407 if ($settings['_oauth']['expiry_ts'] < time()) {
1408 // the token should be renewed
1409 return $this->renewOauthToken($refresh = true);
1410 }
1411
1412 // return the supposingly active token
1413 return (string) $settings['_oauth']['access_token'];
1414 }
1415
1416 /**
1417 * Makes an API request with TTLock to get and save the OAuth token for any HTTP request.
1418 * It can either get a new access token, or it can refresh an existing access token.
1419 *
1420 * @param bool $refresh True to refresh an existing token, false to obtain a new one.
1421 *
1422 * @return string An active OAuth (Bearer) token.
1423 *
1424 * @throws Exception
1425 *
1426 * @link https://euopen.ttlock.com/document/doc?urlName=cloud%2Foauth2%2FgetAccessTokenEn.html
1427 * @link https://euopen.ttlock.com/document/doc?urlName=cloud%2Foauth2%2FrefreshAccessTokenEn.html
1428 */
1429 private function renewOauthToken(bool $refresh = false)
1430 {
1431 // access current profile settings
1432 $settings = $this->getSettings();
1433
1434 if (empty($settings['client_id']) || empty($settings['client_secret']) || empty($settings['username']) || empty($settings['password'])) {
1435 throw new Exception('Missing integration profile credentials (settings).', 500);
1436 }
1437
1438 // build request data
1439 if ($refresh === true && !empty($settings['_oauth']['refresh_token'])) {
1440 // refresh an existing token
1441 $data = [
1442 'client_id' => $settings['client_id'],
1443 'client_secret' => $settings['client_secret'],
1444 'grant_type' => 'refresh_token',
1445 'refresh_token' => $settings['_oauth']['refresh_token'],
1446 ];
1447 } else {
1448 // get a new token
1449 $data = [
1450 'client_id' => $settings['client_id'],
1451 'client_secret' => $settings['client_secret'],
1452 'username' => $settings['username'],
1453 'password' => md5($settings['password']),
1454 ];
1455 }
1456
1457 // exchange the settings to obtain the OAuth token details
1458 $response = (new JHttp)->post('https://euapi.ttlock.com/oauth2/token', http_build_query($data), ['ContentType' => 'application/x-www-form-urlencoded'], 10);
1459
1460 // obtain the response data
1461 $responseData = (array) json_decode((string) $response->body, true);
1462
1463 if ($response->code != 200) {
1464 // an error occurred
1465 throw new Exception($response->body ?: 'OAuth token error.', $response->code);
1466 }
1467
1468 if (empty($responseData['access_token'])) {
1469 // invalid response
1470 throw new Exception($responseData['errmsg'] ?? $response->body ?: 'Generic response error.', 500);
1471 }
1472
1473 // calculate and set the token expiration timestamp
1474 $responseData['expiry_ts'] = strtotime(sprintf('+%d seconds', (int) ($responseData['expires_in'] ?? 0)));
1475
1476 // inject OAuth details within the current integration settings
1477 $settings['_oauth'] = $responseData;
1478
1479 // update integration record settings
1480 $this->setProfileRecordProp('settings', $settings);
1481
1482 // store integration record settings
1483 VBODooraccessFactory::getInstance()->saveIntegrationRecord($this, ['settings' => $this->getSettings()]);
1484
1485 // return the current access token
1486 return $responseData['access_token'];
1487 }
1488 }
1489