homebridge-interqr 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Or Evron
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/orevron/ha-interqr/main/logo.png" alt="InterQR Logo" width="180" />
3
+ </p>
4
+
5
+ # homebridge-interqr
6
+
7
+ <p align="center">
8
+ <a href="https://github.com/orevron/homebridge-interqr/releases"><img alt="Release" src="https://img.shields.io/github/v/release/orevron/homebridge-interqr?style=flat-square" /></a>
9
+ <a href="https://github.com/homebridge/homebridge"><img alt="Homebridge Compatible" src="https://img.shields.io/badge/homebridge-compatible-4db8ea.svg?style=flat-square" /></a>
10
+ <a href="https://github.com/orevron/homebridge-interqr/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/orevron/homebridge-interqr?style=flat-square" /></a>
11
+ </p>
12
+
13
+ <p align="center">Control your InterQR smart locks directly from Homebridge.</p>
14
+
15
+ ## Description
16
+
17
+ **InterQR** is a QR-code-based building access and smart lock system commonly used in residential and office buildings. The `homebridge-interqr` plugin allows you to integrate your InterQR locks seamlessly into your Apple Home ecosystem.
18
+
19
+ You can easily add your locks using the same phone number and SMS verification code used in the InterQR mobile app. Once authenticated, your authorized locks automatically populate as physical Lock mechanisms within Homebridge and Apple Home.
20
+
21
+ ## Features
22
+
23
+ - **Unlock InterQR Locks:** Open connected building or office doors with a tap from the Home app or via Siri.
24
+ - **Auto-Relock State Tracking:** InterQR systems are typically "unlock-only" access controls (the physical door locks automatically). To ensure consistency, Homebridge will automatically revert the device state back to "locked" 5 seconds after an unlock command is issued.
25
+ - **Seamless Authentication:** Log in securely via the Homebridge UI settings using your mobile number and SMS 2FA code. The plugin securely manages session tokens in the background and attempts automatic silent re-authentication.
26
+ - **Multi-Lock Support:** Automatically discovers and creates accessory entities for all locks assigned to your InterQR account.
27
+
28
+ ## Supported Devices
29
+
30
+ - InterQR Smart Locks (including Palgate-compatible locks managed via InterQR)
31
+
32
+ ## Installation & Setup
33
+
34
+ 1. Install the plugin using `npm`:
35
+ ```sh
36
+ npm install -g homebridge-interqr
37
+ ```
38
+ Or install it directly via the Homebridge Config UI X (search for `homebridge-interqr`).
39
+
40
+ 2. Navigate to the Plugins tab in your Homebridge Config UI X.
41
+
42
+ 3. Open the **Settings** for `homebridge-interqr`.
43
+
44
+ 4. Use the custom configuration interface to:
45
+ - Enter your phone number (with the country code, e.g., `+1234567890`)
46
+ - Click "Send SMS Code"
47
+ - Enter the 4-8 digit SMS verification code received
48
+ - Click "Verify Code"
49
+
50
+ 5. Restart Homebridge. Your connected locks will be discovered and added to Apple Home automatically.
51
+
52
+ ## Usage
53
+
54
+ Once installed and configured, your InterQR lock(s) will appear as standard Lock accessories in Homebridge and Apple Home, displaying a Locked/Unlocked status.
55
+
56
+ ### Behaviours
57
+
58
+ - **Unlocking:** Toggling the lock to "Unlocked" will trigger the physical unlock command via the InterQR cloud API.
59
+ - **Auto Re-Locking:** As the physical InterQR locks are unlock-only, the Apple Home accessory status will briefly show "Unlocked" and wait 5 seconds before reverting to "Locked" to reflect the actual state of the entrance door.
60
+ - **Actioning "Lock":** Manually toggling the lock to "Locked" does not send a command but correctly establishes the consistent state in HomeKit.
61
+
62
+ ## Error Handling & Re-authentication
63
+
64
+ If a session token naturally expires, the plugin will log an error and attempt to silently refresh the token via the background API. If your token was significantly revoked, you may see an `Authentication failed` log. In this case, simply revisit the Homebridge UI Plugin Settings, walk through the SMS login workflow again, and restart the service.
65
+
66
+ ## Security
67
+
68
+ This plugin:
69
+ - Employs secure SMS-based 2FA.
70
+ - Communicates directly with the official InterQR Cloud HTTPS API.
71
+ - Stores zero personally identifiable credential data in your public repositories.
72
+ - Only maintains minimal session payloads inside the standard config UI context JSON.
73
+
74
+ ## Disclaimer
75
+
76
+ This plugin is not officially affiliated with or endorsed by InterQR. It was developed independently. Use at your own risk.
@@ -0,0 +1,37 @@
1
+ {
2
+ "pluginAlias": "InterQR",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "customUi": true,
6
+ "headerDisplay": "Manage your InterQR locks.",
7
+ "schema": {
8
+ "type": "object",
9
+ "properties": {
10
+ "phoneNumber": {
11
+ "title": "Phone Number",
12
+ "type": "string",
13
+ "description": "The phone number used to login to InterQR (with country code, e.g., +1234567890)",
14
+ "required": true
15
+ },
16
+ "token": {
17
+ "title": "Authentication Token",
18
+ "type": "string",
19
+ "description": "Auth token (automatically generated via the login UI)",
20
+ "required": true
21
+ },
22
+ "deviceUuid": {
23
+ "title": "Device UUID",
24
+ "type": "string",
25
+ "description": "Device UUID (automatically generated via the login UI)",
26
+ "required": true
27
+ },
28
+ "updateInterval": {
29
+ "title": "Update Interval (minutes)",
30
+ "type": "integer",
31
+ "default": 5,
32
+ "description": "How often to poll the InterQR API for lock changes.",
33
+ "minimum": 1
34
+ }
35
+ }
36
+ }
37
+ }
package/dist/api.js ADDED
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.InterQRApiClient = void 0;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const DEFAULT_BASE_URL = "https://www.interqr.com/api";
9
+ const ENDPOINT_INIT = "/init";
10
+ const ENDPOINT_LOGIN = "/login";
11
+ const ENDPOINT_LOGOUT = "/logout";
12
+ const ENDPOINT_TWOFA_START = "/twofa/start";
13
+ const ENDPOINT_TWOFA_VERIFY = "/twofa/verify";
14
+ const ENDPOINT_USER_DETAILS = "/resource/user/details";
15
+ const ENDPOINT_UNLOCK = "/locks/{uuid}/unlock";
16
+ const ENDPOINT_UNLOCK_LONG = "/locks/{uuid}/unlock-long";
17
+ const APP_VERSION = "3.5.8";
18
+ const DEVICE_MANUFACTURER = "Athom";
19
+ const DEVICE_MODEL = "Integration";
20
+ const DEVICE_PLATFORM = "Homebridge";
21
+ class InterQRApiClient {
22
+ baseUrl;
23
+ token = null;
24
+ deviceUuid = null;
25
+ constructor(baseUrl = DEFAULT_BASE_URL) {
26
+ this.baseUrl = baseUrl.replace(/\/$/, '');
27
+ }
28
+ setToken(token) {
29
+ this.token = token;
30
+ }
31
+ setDeviceUuid(uuid) {
32
+ this.deviceUuid = uuid;
33
+ }
34
+ async _request(method, endpoint, options = {}) {
35
+ const url = `${this.baseUrl}${endpoint}`;
36
+ const headers = {
37
+ 'Content-Type': 'application/json',
38
+ };
39
+ if (options.authenticated && this.token) {
40
+ headers['Authorization'] = `Bearer ${this.token}`;
41
+ }
42
+ const fetchOptions = {
43
+ method,
44
+ headers,
45
+ };
46
+ if (options.jsonData) {
47
+ fetchOptions.body = JSON.stringify(options.jsonData);
48
+ }
49
+ const response = await fetch(url, fetchOptions);
50
+ const contentType = response.headers.get('Content-Type') || '';
51
+ if (!contentType.includes('application/json')) {
52
+ throw new Error(`Unexpected response type from server: ${contentType}`);
53
+ }
54
+ const data = await response.json();
55
+ if (response.status === 401) {
56
+ throw new Error('Authentication failed');
57
+ }
58
+ if (response.status >= 400) {
59
+ throw new Error(data.message || `API error (HTTP ${response.status})`);
60
+ }
61
+ return data;
62
+ }
63
+ async initDevice(deviceUuid = null) {
64
+ if (!deviceUuid) {
65
+ deviceUuid = crypto_1.default.randomUUID();
66
+ }
67
+ this.deviceUuid = deviceUuid;
68
+ const payload = {
69
+ device_uuid: this.deviceUuid,
70
+ manufacturer: DEVICE_MANUFACTURER,
71
+ model: DEVICE_MODEL,
72
+ platform: DEVICE_PLATFORM,
73
+ os_version: "1.0",
74
+ app_version: APP_VERSION,
75
+ };
76
+ const result = await this._request('POST', ENDPOINT_INIT, { jsonData: payload });
77
+ if (result && result.data && result.data.device_uuid) {
78
+ this.deviceUuid = result.data.device_uuid;
79
+ }
80
+ return result;
81
+ }
82
+ async start2FA(phoneNumber, deviceUuid) {
83
+ const payload = {
84
+ number: phoneNumber,
85
+ device_uuid: deviceUuid,
86
+ };
87
+ return await this._request('POST', ENDPOINT_TWOFA_START, { jsonData: payload });
88
+ }
89
+ async verify2FA(phoneNumber, code, deviceUuid, secondAuthToken = null) {
90
+ const payload = {
91
+ number: phoneNumber,
92
+ code: code,
93
+ device_uuid: deviceUuid,
94
+ };
95
+ if (secondAuthToken) {
96
+ payload.second_auth_token = secondAuthToken;
97
+ }
98
+ const result = await this._request('POST', ENDPOINT_TWOFA_VERIFY, { jsonData: payload });
99
+ if (result && result.data && result.data.token) {
100
+ this.token = result.data.token;
101
+ }
102
+ else {
103
+ throw new Error('No token in verify response');
104
+ }
105
+ return result;
106
+ }
107
+ async login(deviceUuid = null) {
108
+ const uuidToUse = deviceUuid || this.deviceUuid;
109
+ if (!uuidToUse) {
110
+ throw new Error('No device_uuid available for login');
111
+ }
112
+ const payload = { device_uuid: uuidToUse };
113
+ const result = await this._request('POST', ENDPOINT_LOGIN, { jsonData: payload });
114
+ if (result && result.data && result.data.token) {
115
+ this.token = result.data.token;
116
+ }
117
+ return result;
118
+ }
119
+ async logout() {
120
+ if (!this.token)
121
+ return;
122
+ try {
123
+ await this._request('POST', ENDPOINT_LOGOUT, { authenticated: true });
124
+ }
125
+ catch (err) {
126
+ // Best-effort logout
127
+ }
128
+ finally {
129
+ this.token = null;
130
+ }
131
+ }
132
+ async getUserDetails() {
133
+ return await this._request('GET', ENDPOINT_USER_DETAILS, { authenticated: true });
134
+ }
135
+ async unlock(lockUuid) {
136
+ const endpoint = ENDPOINT_UNLOCK.replace('{uuid}', lockUuid);
137
+ return await this._request('POST', endpoint, { authenticated: true });
138
+ }
139
+ async unlockLong(lockUuid) {
140
+ const endpoint = ENDPOINT_UNLOCK_LONG.replace('{uuid}', lockUuid);
141
+ return await this._request('POST', endpoint, { authenticated: true });
142
+ }
143
+ }
144
+ exports.InterQRApiClient = InterQRApiClient;
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ const settings_1 = require("./settings");
3
+ const platform_1 = require("./platform");
4
+ module.exports = (api) => {
5
+ api.registerPlatform(settings_1.PLATFORM_NAME, platform_1.InterQRPlatform);
6
+ };
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InterQRPlatform = void 0;
4
+ const settings_1 = require("./settings");
5
+ const platformAccessory_1 = require("./platformAccessory");
6
+ const api_1 = require("./api");
7
+ class InterQRPlatform {
8
+ log;
9
+ config;
10
+ api;
11
+ Service;
12
+ Characteristic;
13
+ accessories = new Map();
14
+ apiClient;
15
+ updateIntervalMs;
16
+ refreshTimer;
17
+ constructor(log, config, api) {
18
+ this.log = log;
19
+ this.config = config;
20
+ this.api = api;
21
+ this.Service = this.api.hap.Service;
22
+ this.Characteristic = this.api.hap.Characteristic;
23
+ this.log.debug('Finished initializing platform:', this.config.name);
24
+ this.apiClient = new api_1.InterQRApiClient();
25
+ this.updateIntervalMs = (this.config.updateInterval || 5) * 60 * 1000;
26
+ this.api.on('didFinishLaunching', () => {
27
+ this.log.debug('Executed didFinishLaunching callback');
28
+ if (!this.config.token || !this.config.deviceUuid) {
29
+ this.log.error('Authentication token or Device UUID missing. Please configure the plugin via the Homebridge UI.');
30
+ return;
31
+ }
32
+ this.apiClient.setToken(this.config.token);
33
+ this.apiClient.setDeviceUuid(this.config.deviceUuid);
34
+ this.discoverDevices();
35
+ });
36
+ }
37
+ configureAccessory(accessory) {
38
+ this.log.info('Loading accessory from cache:', accessory.displayName);
39
+ this.accessories.set(accessory.UUID, accessory);
40
+ }
41
+ async discoverDevices() {
42
+ try {
43
+ const details = await this.apiClient.getUserDetails();
44
+ if (!details || !details.data || !details.data.locks) {
45
+ this.log.warn('No locks found for this account.');
46
+ return;
47
+ }
48
+ const locks = details.data.locks;
49
+ const discoveredUUIDs = [];
50
+ for (const lock of locks) {
51
+ const uuid = this.api.hap.uuid.generate(lock.uuid);
52
+ discoveredUUIDs.push(uuid);
53
+ let accessory = this.accessories.get(uuid);
54
+ if (accessory) {
55
+ this.log.info('Restoring existing accessory from cache:', accessory.displayName);
56
+ accessory.context.device = lock;
57
+ this.api.updatePlatformAccessories([accessory]);
58
+ new platformAccessory_1.InterQRAccessory(this, accessory);
59
+ }
60
+ else {
61
+ this.log.info('Adding new accessory:', lock.description || lock.name);
62
+ accessory = new this.api.platformAccessory(lock.description || lock.name, uuid);
63
+ accessory.context.device = lock;
64
+ this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
65
+ this.accessories.set(uuid, accessory);
66
+ new platformAccessory_1.InterQRAccessory(this, accessory);
67
+ }
68
+ }
69
+ // Remove stale accessories
70
+ for (const [uuid, accessory] of this.accessories.entries()) {
71
+ if (!discoveredUUIDs.includes(uuid)) {
72
+ this.log.info('Removing existing accessory from cache:', accessory.displayName);
73
+ this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
74
+ this.accessories.delete(uuid);
75
+ }
76
+ }
77
+ this.scheduleNextUpdate();
78
+ }
79
+ catch (error) {
80
+ if (error.message.includes('Authentication failed')) {
81
+ this.log.warn('Authentication failed. Attempting silent token refresh via login...');
82
+ const success = await this.refreshAuthToken();
83
+ if (success) {
84
+ this.log.info('Token refreshed successfully. Retrying discovery...');
85
+ return this.discoverDevices();
86
+ }
87
+ else {
88
+ this.log.error('Failed to refresh token. Please re-authenticate via Homebridge Settings or check configuration.');
89
+ }
90
+ }
91
+ else {
92
+ this.log.error('Error discovering devices:', error.message);
93
+ }
94
+ // Try again later even if failed
95
+ this.scheduleNextUpdate();
96
+ }
97
+ }
98
+ async refreshAuthToken() {
99
+ try {
100
+ if (!this.config.deviceUuid)
101
+ return false;
102
+ const result = await this.apiClient.login(this.config.deviceUuid);
103
+ if (result && result.data && result.data.token) {
104
+ // Technically, this token is stored in memory. We'd ideally save it back to config.
105
+ // Since homebridge doesn't officially support programmatic config updates easily,
106
+ // we'll at least keep it working until restart. The user should update config if they restart.
107
+ this.config.token = result.data.token;
108
+ this.log.warn('Token refreshed in memory. Please note that if you restart Homebridge, you might need to re-login via UI if the old token is permanently revoked.');
109
+ return true;
110
+ }
111
+ return false;
112
+ }
113
+ catch (error) {
114
+ this.log.error('Silent refresh failed:', error.message);
115
+ return false;
116
+ }
117
+ }
118
+ scheduleNextUpdate() {
119
+ if (this.refreshTimer) {
120
+ clearTimeout(this.refreshTimer);
121
+ }
122
+ this.refreshTimer = setTimeout(() => {
123
+ this.discoverDevices();
124
+ }, this.updateIntervalMs);
125
+ }
126
+ }
127
+ exports.InterQRPlatform = InterQRPlatform;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InterQRAccessory = void 0;
4
+ class InterQRAccessory {
5
+ platform;
6
+ accessory;
7
+ service;
8
+ autoRelockTimer;
9
+ constructor(platform, accessory) {
10
+ this.platform = platform;
11
+ this.accessory = accessory;
12
+ const device = accessory.context.device;
13
+ this.accessory.getService(this.platform.Service.AccessoryInformation)
14
+ .setCharacteristic(this.platform.Characteristic.Manufacturer, 'InterQR')
15
+ .setCharacteristic(this.platform.Characteristic.Model, 'Smart Lock')
16
+ .setCharacteristic(this.platform.Characteristic.SerialNumber, device.uuid);
17
+ this.service = this.accessory.getService(this.platform.Service.LockMechanism) || this.accessory.addService(this.platform.Service.LockMechanism);
18
+ this.service.setCharacteristic(this.platform.Characteristic.Name, device.description || device.name);
19
+ // Initial state is Locked
20
+ this.service.updateCharacteristic(this.platform.Characteristic.LockCurrentState, this.platform.Characteristic.LockCurrentState.SECURED);
21
+ this.service.updateCharacteristic(this.platform.Characteristic.LockTargetState, this.platform.Characteristic.LockTargetState.SECURED);
22
+ this.service.getCharacteristic(this.platform.Characteristic.LockTargetState)
23
+ .onSet(this.setLockTargetState.bind(this));
24
+ }
25
+ async setLockTargetState(value) {
26
+ if (value === this.platform.Characteristic.LockTargetState.SECURED) {
27
+ this.platform.log.debug(`[${this.accessory.displayName}] Ignoring lock command since InterQR is unlock-only.`);
28
+ this.service.updateCharacteristic(this.platform.Characteristic.LockCurrentState, this.platform.Characteristic.LockCurrentState.SECURED);
29
+ return;
30
+ }
31
+ // Unsecured requested
32
+ try {
33
+ this.platform.log.info(`[${this.accessory.displayName}] Sending unlock command...`);
34
+ await this.platform.apiClient.unlock(this.accessory.context.device.uuid);
35
+ this.platform.log.info(`[${this.accessory.displayName}] Unlocked successfully.`);
36
+ this.service.updateCharacteristic(this.platform.Characteristic.LockCurrentState, this.platform.Characteristic.LockCurrentState.UNSECURED);
37
+ if (this.autoRelockTimer) {
38
+ clearTimeout(this.autoRelockTimer);
39
+ }
40
+ this.autoRelockTimer = setTimeout(() => {
41
+ this.platform.log.debug(`[${this.accessory.displayName}] Auto-relocking homebridge state.`);
42
+ this.service.updateCharacteristic(this.platform.Characteristic.LockTargetState, this.platform.Characteristic.LockTargetState.SECURED);
43
+ this.service.updateCharacteristic(this.platform.Characteristic.LockCurrentState, this.platform.Characteristic.LockCurrentState.SECURED);
44
+ }, 5000);
45
+ }
46
+ catch (error) {
47
+ this.platform.log.error(`[${this.accessory.displayName}] Failed to unlock: ${error.message}`);
48
+ // Revert to locked state
49
+ this.service.updateCharacteristic(this.platform.Characteristic.LockTargetState, this.platform.Characteristic.LockTargetState.SECURED);
50
+ this.service.updateCharacteristic(this.platform.Characteristic.LockCurrentState, this.platform.Characteristic.LockCurrentState.SECURED);
51
+ }
52
+ }
53
+ }
54
+ exports.InterQRAccessory = InterQRAccessory;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PLUGIN_NAME = exports.PLATFORM_NAME = void 0;
4
+ /**
5
+ * This is the name of the platform that users will use to register the plugin in the Homebridge config.json
6
+ */
7
+ exports.PLATFORM_NAME = 'InterQR';
8
+ /**
9
+ * This must match the name of your plugin as defined the package.json
10
+ */
11
+ exports.PLUGIN_NAME = 'homebridge-interqr';
@@ -0,0 +1,117 @@
1
+ <div class="card text-center" id="intro-card">
2
+ <div class="card-body">
3
+ <h5 class="card-title">InterQR Login</h5>
4
+ <p class="card-text">Connect to your InterQR account to control your locks from Homebridge.</p>
5
+ <button id="btn-start" class="btn btn-primary">Login with Phone Number</button>
6
+ </div>
7
+ </div>
8
+
9
+ <div class="card text-center d-none" id="phone-card">
10
+ <div class="card-body">
11
+ <h5 class="card-title">Enter Phone Number</h5>
12
+ <p class="card-text">Enter your phone number in international format (e.g. +1234567890).</p>
13
+ <div class="form-group mb-3">
14
+ <input type="text" class="form-control" id="phone-input" placeholder="+1234567890">
15
+ </div>
16
+ <button id="btn-request-sms" class="btn btn-primary">Send SMS Code</button>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="card text-center d-none" id="code-card">
21
+ <div class="card-body">
22
+ <h5 class="card-title">Enter SMS Code</h5>
23
+ <p class="card-text">Enter the verification code sent to your phone.</p>
24
+ <div class="form-group mb-3">
25
+ <input type="text" class="form-control" id="code-input" placeholder="1234">
26
+ </div>
27
+ <button id="btn-verify-sms" class="btn btn-primary">Verify Code</button>
28
+ </div>
29
+ </div>
30
+
31
+ <div class="card text-center d-none" id="success-card">
32
+ <div class="card-body">
33
+ <h5 class="card-title text-success">Login Successful!</h5>
34
+ <p class="card-text">Your configuration has been updated. You can now close this window and restart Homebridge.</p>
35
+ </div>
36
+ </div>
37
+
38
+ <script>
39
+ (async () => {
40
+ // 1. Get plugin config to check if already logged in
41
+ const pluginConfig = await homebridge.getPluginConfig();
42
+ const isConfigured = pluginConfig && pluginConfig.length > 0 && pluginConfig[0].token;
43
+
44
+ if (isConfigured) {
45
+ document.getElementById('intro-card').innerHTML = `
46
+ <div class="card-body text-success">
47
+ <h5 class="card-title">Already Logged In</h5>
48
+ <p class="card-text">You are already connected to InterQR. Do you want to re-authenticate?</p>
49
+ <button id="btn-start" class="btn btn-warning">Re-Authenticate</button>
50
+ </div>
51
+ `;
52
+ }
53
+
54
+ // 2. Start flow
55
+ document.getElementById('btn-start')?.addEventListener('click', () => {
56
+ document.getElementById('intro-card').classList.add('d-none');
57
+ document.getElementById('phone-card').classList.remove('d-none');
58
+ });
59
+
60
+ let currentPhone = '';
61
+
62
+ // 3. Request SMS
63
+ document.getElementById('btn-request-sms').addEventListener('click', async () => {
64
+ currentPhone = document.getElementById('phone-input').value.trim();
65
+ if (!currentPhone) {
66
+ homebridge.toast.error('Please enter a phone number', 'Error');
67
+ return;
68
+ }
69
+
70
+ homebridge.showSpinner();
71
+ try {
72
+ await homebridge.request('/request-sms', { phone: currentPhone });
73
+ homebridge.hideSpinner();
74
+
75
+ document.getElementById('phone-card').classList.add('d-none');
76
+ document.getElementById('code-card').classList.remove('d-none');
77
+ homebridge.toast.success('SMS code sent to your phone', 'Success');
78
+ } catch (err) {
79
+ homebridge.hideSpinner();
80
+ homebridge.toast.error(err.message || 'Failed to request SMS', 'Error');
81
+ }
82
+ });
83
+
84
+ // 4. Verify SMS
85
+ document.getElementById('btn-verify-sms').addEventListener('click', async () => {
86
+ const code = document.getElementById('code-input').value.trim();
87
+ if (!code) {
88
+ homebridge.toast.error('Please enter the SMS code', 'Error');
89
+ return;
90
+ }
91
+
92
+ homebridge.showSpinner();
93
+ try {
94
+ const response = await homebridge.request('/verify-sms', { phone: currentPhone, code: code });
95
+
96
+ // Update plugin config
97
+ const config = pluginConfig && pluginConfig.length > 0 ? pluginConfig[0] : { name: "InterQR" };
98
+ config.phoneNumber = currentPhone;
99
+ config.token = response.token;
100
+ config.deviceUuid = response.deviceUuid;
101
+
102
+ await homebridge.updatePluginConfig([config]);
103
+ await homebridge.savePluginConfig();
104
+
105
+ homebridge.hideSpinner();
106
+
107
+ document.getElementById('code-card').classList.add('d-none');
108
+ document.getElementById('success-card').classList.remove('d-none');
109
+ homebridge.toast.success('Login Successful! Please restart Homebridge.', 'Success');
110
+ } catch (err) {
111
+ homebridge.hideSpinner();
112
+ homebridge.toast.error(err.message || 'Failed to verify SMS', 'Error');
113
+ }
114
+ });
115
+
116
+ })();
117
+ </script>
@@ -0,0 +1,50 @@
1
+ const { HomebridgePluginUiServer } = require('@homebridge/plugin-ui-utils');
2
+
3
+ class PluginUiServer extends HomebridgePluginUiServer {
4
+ constructor() {
5
+ super();
6
+ let api;
7
+ try {
8
+ api = require('../dist/api').InterQRApiClient;
9
+ } catch (e) {
10
+ this.pushEvent('print-log', 'Failed to load api client. Please ensure you built the plugin.');
11
+ }
12
+
13
+ this.apiClient = new api();
14
+ this.currentDeviceUuid = null;
15
+
16
+ this.onRequest('/request-sms', this.handleRequestSms.bind(this));
17
+ this.onRequest('/verify-sms', this.handleVerifySms.bind(this));
18
+
19
+ this.ready();
20
+ }
21
+
22
+ async handleRequestSms(payload) {
23
+ try {
24
+ const initRes = await this.apiClient.initDevice();
25
+ this.currentDeviceUuid = initRes.data.device_uuid;
26
+
27
+ await this.apiClient.start2FA(payload.phone, this.currentDeviceUuid);
28
+ return { success: true };
29
+ } catch (e) {
30
+ throw new Error(e.message || 'Failed to request SMS code');
31
+ }
32
+ }
33
+
34
+ async handleVerifySms(payload) {
35
+ try {
36
+ const res = await this.apiClient.verify2FA(payload.phone, payload.code, this.currentDeviceUuid);
37
+ return {
38
+ success: true,
39
+ token: res.data.token,
40
+ deviceUuid: this.currentDeviceUuid
41
+ };
42
+ } catch (e) {
43
+ throw new Error(e.message || 'Failed to verify SMS code');
44
+ }
45
+ }
46
+ }
47
+
48
+ (() => {
49
+ return new PluginUiServer();
50
+ })();
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "homebridge-interqr",
3
+ "displayName": "Homebridge InterQR",
4
+ "version": "1.0.0",
5
+ "description": "Homebridge plugin for InterQR smart locks",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git://github.com/orevron/homebridge-interqr.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/orevron/homebridge-interqr/issues"
13
+ },
14
+ "engines": {
15
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0",
16
+ "homebridge": "^1.6.0"
17
+ },
18
+ "main": "dist/index.js",
19
+ "scripts": {
20
+ "lint": "eslint src/**.ts --max-warnings=0",
21
+ "watch": "npm run build && npm run link && nodemon",
22
+ "build": "rimraf dist && tsc",
23
+ "prepublishOnly": "npm run lint && npm run build"
24
+ },
25
+ "keywords": [
26
+ "homebridge-plugin",
27
+ "interqr",
28
+ "smart-lock",
29
+ "lock"
30
+ ],
31
+ "files": [
32
+ "dist",
33
+ "homebridge-ui",
34
+ "config.schema.json"
35
+ ],
36
+ "dependencies": {
37
+ "axios": "^1.6.0"
38
+ },
39
+ "devDependencies": {
40
+ "@homebridge/plugin-ui-utils": "^1.0.1",
41
+ "@types/node": "^20.0.0",
42
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
43
+ "@typescript-eslint/parser": "^7.0.0",
44
+ "eslint": "^8.0.0",
45
+ "homebridge": "^1.8.0",
46
+ "nodemon": "^3.0.0",
47
+ "rimraf": "^5.0.0",
48
+ "typescript": "^5.4.0"
49
+ }
50
+ }