From 3d1d3727dc1506ec10140159eca33d8f7e4bcb48 Mon Sep 17 00:00:00 2001 From: Roman Sokolov <12689+vityevato@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:18:59 +0300 Subject: [PATCH] hikvision-doorbell: fixes (#1970) * Let's try to fix the plugin freezing * hikvision-doorbell version up after merging from main --- plugins/hikvision-doorbell/package-lock.json | 148 +++++++++--------- plugins/hikvision-doorbell/package.json | 2 +- .../hikvision-doorbell/src/auth-request.ts | 64 +++++--- .../hikvision-doorbell/src/doorbell-api.ts | 106 +++++++++---- plugins/hikvision-doorbell/src/main.ts | 23 ++- plugins/hikvision-doorbell/src/utils.ts | 35 +++-- 6 files changed, 242 insertions(+), 136 deletions(-) diff --git a/plugins/hikvision-doorbell/package-lock.json b/plugins/hikvision-doorbell/package-lock.json index f48fe36a3..ebf9f90a3 100644 --- a/plugins/hikvision-doorbell/package-lock.json +++ b/plugins/hikvision-doorbell/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vityevato/hikvision-doorbell", - "version": "1.0.1", + "version": "2.0.0d", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@vityevato/hikvision-doorbell", - "version": "1.0.1", + "version": "2.0.0d", "license": "Apache", "dependencies": { "@scrypted/common": "file:../../common", @@ -30,39 +30,41 @@ "license": "ISC", "dependencies": { "@scrypted/sdk": "file:../sdk", + "@scrypted/types": "^0.5.27", "http-auth-utils": "^5.0.1", "typescript": "^5.5.3" }, "devDependencies": { - "@types/node": "^20.11.0", + "@types/node": "^20.19.11", "monaco-editor": "^0.50.0", "ts-node": "^10.9.2" } }, "../../sdk": { "name": "@scrypted/sdk", - "version": "0.3.118", + "version": "0.5.48", "license": "ISC", "dependencies": { - "@babel/preset-typescript": "^7.26.0", - "@rollup/plugin-commonjs": "^28.0.1", + "@babel/preset-typescript": "^7.27.1", + "@rollup/plugin-commonjs": "^28.0.5", "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^15.3.0", - "@rollup/plugin-typescript": "^12.1.1", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-typescript": "^12.1.2", "@rollup/plugin-virtual": "^3.0.2", "adm-zip": "^0.5.16", - "axios": "^1.7.8", - "babel-loader": "^9.2.1", + "axios": "^1.10.0", + "babel-loader": "^10.0.0", "babel-plugin-const-enum": "^1.2.0", "ncp": "^2.0.0", + "openai": "^6.1.0", "raw-loader": "^4.0.2", "rimraf": "^6.0.1", - "rollup": "^4.27.4", + "rollup": "^4.43.0", "tmp": "^0.2.3", - "ts-loader": "^9.5.1", + "ts-loader": "^9.5.2", "tslib": "^2.8.1", - "typescript": "^5.6.3", - "webpack": "^5.96.1", + "typescript": "^5.8.3", + "webpack": "^5.99.9", "webpack-bundle-analyzer": "^4.10.2" }, "bin": { @@ -75,60 +77,62 @@ "scrypted-webpack": "bin/scrypted-webpack.js" }, "devDependencies": { - "@types/node": "^22.10.1", + "@types/node": "^24.0.1", "ts-node": "^10.9.2", - "typedoc": "^0.26.11" + "typedoc": "^0.28.5" } }, "../../server": { "name": "@scrypted/server", - "version": "0.138.1", + "version": "0.142.9", "hasInstallScript": true, "license": "ISC", "dependencies": { "@scrypted/ffmpeg-static": "^6.1.0-build3", - "@scrypted/node-pty": "^1.0.22", - "@scrypted/types": "^0.3.108", + "@scrypted/node-pty": "^1.0.25", + "@scrypted/types": "^0.5.43", "adm-zip": "^0.5.16", - "body-parser": "^1.20.3", + "body-parser": "^2.2.0", "cookie-parser": "^1.4.7", - "dotenv": "^16.4.5", - "engine.io": "^6.6.2", - "express": "^4.21.1", + "dotenv": "^16.5.0", + "engine.io": "^6.6.4", + "express": "^5.1.0", "follow-redirects": "^1.15.9", - "http-auth": "^4.2.0", - "level": "^8.0.1", + "http-auth": "^4.2.1", + "level": "^10.0.0", "lodash": "^4.17.21", + "mime-types": "^3.0.1", "node-dijkstra": "^2.5.0", "node-forge": "^1.3.1", - "node-gyp": "^10.2.0", - "py": "npm:@bjia56/portable-python@^0.1.112", - "semver": "^7.6.3", - "sharp": "^0.33.5", + "node-gyp": "^11.2.0", + "py": "npm:@bjia56/portable-python@^0.1.141", + "semver": "^7.7.2", + "sharp": "^0.34.2", "source-map-support": "^0.5.21", "tar": "^7.4.3", "tslib": "^2.8.1", - "typescript": "^5.5.4", + "typescript": "^5.8.3", "whatwg-mimetype": "^4.0.0", - "ws": "^8.18.0" + "ws": "^8.18.2" }, "bin": { "scrypted-serve": "bin/scrypted-serve" }, "devDependencies": { "@types/adm-zip": "^0.5.7", - "@types/cookie-parser": "^1.4.8", - "@types/express": "^5.0.0", + "@types/cookie-parser": "^1.4.9", + "@types/express": "^5.0.3", "@types/follow-redirects": "^1.14.4", "@types/http-auth": "^4.1.4", - "@types/lodash": "^4.17.13", - "@types/node": "^22.10.1", + "@types/lodash": "^4.17.17", + "@types/mime-types": "^3.0.1", + "@types/node": "^24.0.3", "@types/node-dijkstra": "^2.5.6", "@types/node-forge": "^1.3.11", - "@types/semver": "^7.5.8", + "@types/semver": "^7.7.0", "@types/source-map-support": "^0.5.10", "@types/whatwg-mimetype": "^3.0.2", - "@types/ws": "^8.5.13", + "@types/ws": "^8.18.1", "rimraf": "^6.0.1" } }, @@ -249,7 +253,8 @@ "version": "file:../../common", "requires": { "@scrypted/sdk": "file:../sdk", - "@types/node": "^20.11.0", + "@scrypted/types": "^0.5.27", + "@types/node": "^20.19.11", "http-auth-utils": "^5.0.1", "monaco-editor": "^0.50.0", "ts-node": "^10.9.2", @@ -259,28 +264,29 @@ "@scrypted/sdk": { "version": "file:../../sdk", "requires": { - "@babel/preset-typescript": "^7.26.0", - "@rollup/plugin-commonjs": "^28.0.1", + "@babel/preset-typescript": "^7.27.1", + "@rollup/plugin-commonjs": "^28.0.5", "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^15.3.0", - "@rollup/plugin-typescript": "^12.1.1", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-typescript": "^12.1.2", "@rollup/plugin-virtual": "^3.0.2", - "@types/node": "^22.10.1", + "@types/node": "^24.0.1", "adm-zip": "^0.5.16", - "axios": "^1.7.8", - "babel-loader": "^9.2.1", + "axios": "^1.10.0", + "babel-loader": "^10.0.0", "babel-plugin-const-enum": "^1.2.0", "ncp": "^2.0.0", + "openai": "^6.1.0", "raw-loader": "^4.0.2", "rimraf": "^6.0.1", - "rollup": "^4.27.4", + "rollup": "^4.43.0", "tmp": "^0.2.3", - "ts-loader": "^9.5.1", + "ts-loader": "^9.5.2", "ts-node": "^10.9.2", "tslib": "^2.8.1", - "typedoc": "^0.26.11", - "typescript": "^5.6.3", - "webpack": "^5.96.1", + "typedoc": "^0.28.5", + "typescript": "^5.8.3", + "webpack": "^5.99.9", "webpack-bundle-analyzer": "^4.10.2" } }, @@ -288,44 +294,46 @@ "version": "file:../../server", "requires": { "@scrypted/ffmpeg-static": "^6.1.0-build3", - "@scrypted/node-pty": "^1.0.22", - "@scrypted/types": "^0.3.108", + "@scrypted/node-pty": "^1.0.25", + "@scrypted/types": "^0.5.43", "@types/adm-zip": "^0.5.7", - "@types/cookie-parser": "^1.4.8", - "@types/express": "^5.0.0", + "@types/cookie-parser": "^1.4.9", + "@types/express": "^5.0.3", "@types/follow-redirects": "^1.14.4", "@types/http-auth": "^4.1.4", - "@types/lodash": "^4.17.13", - "@types/node": "^22.10.1", + "@types/lodash": "^4.17.17", + "@types/mime-types": "^3.0.1", + "@types/node": "^24.0.3", "@types/node-dijkstra": "^2.5.6", "@types/node-forge": "^1.3.11", - "@types/semver": "^7.5.8", + "@types/semver": "^7.7.0", "@types/source-map-support": "^0.5.10", "@types/whatwg-mimetype": "^3.0.2", - "@types/ws": "^8.5.13", + "@types/ws": "^8.18.1", "adm-zip": "^0.5.16", - "body-parser": "^1.20.3", + "body-parser": "^2.2.0", "cookie-parser": "^1.4.7", - "dotenv": "^16.4.5", - "engine.io": "^6.6.2", - "express": "^4.21.1", + "dotenv": "^16.5.0", + "engine.io": "^6.6.4", + "express": "^5.1.0", "follow-redirects": "^1.15.9", - "http-auth": "^4.2.0", - "level": "^8.0.1", + "http-auth": "^4.2.1", + "level": "^10.0.0", "lodash": "^4.17.21", + "mime-types": "^3.0.1", "node-dijkstra": "^2.5.0", "node-forge": "^1.3.1", - "node-gyp": "^10.2.0", - "py": "npm:@bjia56/portable-python@^0.1.112", + "node-gyp": "^11.2.0", + "py": "npm:@bjia56/portable-python@^0.1.141", "rimraf": "^6.0.1", - "semver": "^7.6.3", - "sharp": "^0.33.5", + "semver": "^7.7.2", + "sharp": "^0.34.2", "source-map-support": "^0.5.21", "tar": "^7.4.3", "tslib": "^2.8.1", - "typescript": "^5.5.4", + "typescript": "^5.8.3", "whatwg-mimetype": "^4.0.0", - "ws": "^8.18.0" + "ws": "^8.18.2" } }, "@types/ip": { diff --git a/plugins/hikvision-doorbell/package.json b/plugins/hikvision-doorbell/package.json index b1e3192c3..8499871d7 100644 --- a/plugins/hikvision-doorbell/package.json +++ b/plugins/hikvision-doorbell/package.json @@ -1,6 +1,6 @@ { "name": "@vityevato/hikvision-doorbell", - "version": "2.0.2", + "version": "2.0.8", "description": "Hikvision Doorbell Plugin for Scrypted", "author": "Roman Sokolov", "license": "Apache", diff --git a/plugins/hikvision-doorbell/src/auth-request.ts b/plugins/hikvision-doorbell/src/auth-request.ts index 5bf9a351f..ff304be0a 100644 --- a/plugins/hikvision-doorbell/src/auth-request.ts +++ b/plugins/hikvision-doorbell/src/auth-request.ts @@ -42,34 +42,44 @@ export class AuthRequst { const req = Http.request(url, opt) + // Apply timeout if specified (Node.js http.request doesn't use timeout from options) + if (opt.timeout) { + req.setTimeout (opt.timeout, () => { + req.destroy (new Error (`Request timeout after ${opt.timeout}ms`)); + }); + } + req.once('response', async (resp) => { + try { + if (resp.statusCode == 401) { - if (resp.statusCode == 401) { + // Hikvision quirk: even if we already had a sessionAuth, a fresh + // WWW-Authenticate challenge may require rebuilding credentials. + // Limit the number of digest rebuilds to avoid infinite loops. + const attempt = (opt.digestRetry ?? 0); + if (attempt >= 2) { + // Give up after a couple of rebuild attempts and surface the 401 response + resolve(await this.parseResponse (opt.responseType, resp)); + return; + } - // Hikvision quirk: even if we already had a sessionAuth, a fresh - // WWW-Authenticate challenge may require rebuilding credentials. - // Limit the number of digest rebuilds to avoid infinite loops. - const attempt = (opt.digestRetry ?? 0); - if (attempt >= 2) { - // Give up after a couple of rebuild attempts and surface the 401 response - resolve(await this.parseResponse (opt.responseType, resp)); - return; + const newAuth = this.createAuth(resp.headers['www-authenticate'], !!this.auth); + // Clear cached auth to avoid stale nonce reuse + this.auth = undefined; + opt.sessionAuth = newAuth; + opt.digestRetry = attempt + 1; + const result = await this.request(url, opt, body); + resolve(result); } - - const newAuth = this.createAuth(resp.headers['www-authenticate'], !!this.auth); - // Clear cached auth to avoid stale nonce reuse - this.auth = undefined; - opt.sessionAuth = newAuth; - opt.digestRetry = attempt + 1; - const result = await this.request(url, opt, body); - resolve(result); - } - else { - // Cache the negotiated session auth only if it was provided for this request. - if (opt.sessionAuth) { - this.auth = opt.sessionAuth; + else { + // Cache the negotiated session auth only if it was provided for this request. + if (opt.sessionAuth) { + this.auth = opt.sessionAuth; + } + resolve(await this.parseResponse(opt.responseType, resp)); } - resolve(await this.parseResponse(opt.responseType, resp)); + } catch (error) { + reject(error); } }); @@ -169,6 +179,10 @@ export class AuthRequst { readable.once('end', () => { resolve(result); }); + + readable.once('error', (error) => { + reject(error); + }); }); } @@ -184,6 +198,10 @@ export class AuthRequst { readable.once('end', () => { resolve(result); }); + + readable.once('error', (error) => { + reject(error); + }); }); } diff --git a/plugins/hikvision-doorbell/src/doorbell-api.ts b/plugins/hikvision-doorbell/src/doorbell-api.ts index 96db2fc07..175bc1d68 100644 --- a/plugins/hikvision-doorbell/src/doorbell-api.ts +++ b/plugins/hikvision-doorbell/src/doorbell-api.ts @@ -49,6 +49,7 @@ interface AcsEventResponse { const maxEventAgeSeconds = 30; // Ignore events older than this many seconds const callPollingIntervalSec = 1; // Call status polling interval in seconds const alertTickTimeoutSec = 60; // Alert stream tick timeout in seconds +const acsPollingTimeoutSec = 5; // ACS polling request timeout in seconds const EventCodeMap = new Map([ ['5,25', HikvisionDoorbellEvent.DoorOpened], @@ -113,16 +114,19 @@ export class HikvisionDoorbellAPI extends HikvisionCameraAPI password: string, callStatusPolling: boolean, public console: Console, - public storage: Storage + public storage: Storage, + skipCapabilitiesInit: boolean = false ) { - let endpoint = libip.isV4Format(address) ? `${address}:${port}` : `[${address}]:${port}`; + let endpoint = libip.isV4Format (address) ? `${address}:${port}` : `[${address}]:${port}`; super (endpoint, username, password, console); this.endpoint = endpoint; this.auth = new AuthRequst (username, password, console); - // Initialize door capabilities - this.initializeDoorCapabilities(); + // Initialize door capabilities (skip for event-only API instances) + if (!skipCapabilitiesInit) { + this.initializeDoorCapabilities(); + } this.useCallStatusPolling = callStatusPolling; } @@ -136,26 +140,36 @@ export class HikvisionDoorbellAPI extends HikvisionCameraAPI { // Create a promise for this specific request to prevent queue blocking const requestPromise = this.requestQueue.then(async () => { - let url: string = urlOrOptions as string; + let url: string | undefined; let opt: AuthRequestOptions | undefined; - if (typeof urlOrOptions !== 'string') { - url = urlOrOptions.url as string; - if (typeof urlOrOptions.url !== 'string') { - url = (urlOrOptions.url as URL).toString(); + + if (typeof urlOrOptions === 'string') { + url = urlOrOptions; + } else { + if (urlOrOptions.url) { + url = typeof urlOrOptions.url === 'string' + ? urlOrOptions.url + : urlOrOptions.url.toString(); } opt = { method: urlOrOptions.method, responseType: urlOrOptions.responseType || 'buffer', headers: urlOrOptions.headers as OutgoingHttpHeaders, + timeout: urlOrOptions.timeout, }; } + // Validate URL before making request + if (!url || url.includes ('undefined')) { + throw new Error (`Invalid request URL: ${url}`); + } + // Safety fallback and attach debug id if (!opt) { opt = { responseType: 'buffer' } as AuthRequestOptions; } - return await this.auth.request(url, opt, body); + return await this.auth.request (url, opt, body); }); // Update the queue to continue after this request (success or failure) @@ -416,7 +430,8 @@ export class HikvisionDoorbellAPI extends HikvisionCameraAPI // If already loading, wait for the existing promise if (this.loadCapabilitiesPromise) { - return this.loadCapabilitiesPromise; + await this.loadCapabilitiesPromise; + return; } // Start loading and store the promise @@ -654,23 +669,35 @@ export class HikvisionDoorbellAPI extends HikvisionCameraAPI this.console.error ('Failed to set phone number record:', e); } - // Set call button configuration + // Small delay to allow device to process previous request + await new Promise (resolve => setTimeout (resolve, 500)); + + // Set call button configuration with retry logic const keyCfgData = `${buttonNumber}${roomNumber}10`; - try { - const response = await this.request ({ - url: `http://${this.endpoint}/ISAPI/VideoIntercom/keyCfg/${buttonNumber}`, - method: 'PUT', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - responseType: 'text', - }, keyCfgData); - - this.console.debug (`Call button ${buttonNumber} configured for room ${roomNumber}: ${response.body}`); - } - catch (e) { - this.console.error (`Failed to configure call button ${buttonNumber}:`, e); + for (let attempt = 0; attempt < 2; attempt++) { + try { + const response = await this.request ({ + url: `http://${this.endpoint}/ISAPI/VideoIntercom/keyCfg/${buttonNumber}`, + method: 'PUT', + headers: { + 'Content-Type': 'application/xml' + }, + responseType: 'text', + }, keyCfgData); + + this.console.debug (`Call button ${buttonNumber} configured for room ${roomNumber}: ${response.body}`); + break; + } + catch (e) { + if (attempt === 0 && (e.code === 'EPIPE' || e.code === 'ECONNRESET')) { + this.console.warn (`Call button ${buttonNumber} configuration failed (${e.code}), retrying...`); + await new Promise (resolve => setTimeout (resolve, 1000)); + continue; + } + this.console.error (`Failed to configure call button ${buttonNumber}:`, e); + break; + } } @@ -719,8 +746,8 @@ export class HikvisionDoorbellAPI extends HikvisionCameraAPI private isCallPollingActive: boolean = false; // ACS event polling properties - private acsEventPollingInterval?: NodeJS.Timeout; private lastAcsEventTime: Date = new Date(); + private isAcsPollingInProgress: boolean = false; // Timezone properties private deviceTimezone?: string; // GMT offset in format like '+03:00' @@ -901,6 +928,7 @@ export class HikvisionDoorbellAPI extends HikvisionCameraAPI const response = await this.request ({ url: `http://${this.endpoint}/ISAPI/AccessControl/AcsEvent?format=json`, method: 'POST', + timeout: acsPollingTimeoutSec * 1000, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', @@ -961,8 +989,15 @@ export class HikvisionDoorbellAPI extends HikvisionCameraAPI * This method can be called periodically to check for new events * @param lastEventTime - Optional timestamp to filter events newer than this time */ - private async pollAndProcessAcsEvents (lastEventTime?: Date): Promise + private async pollAndProcessAcsEvents (lastEventTime?: Date, isRetry: boolean = false): Promise { + // Prevent multiple concurrent polling requests + if (this.isAcsPollingInProgress) { + this.console.debug ('ACS polling already in progress, skipping'); + return; + } + + this.isAcsPollingInProgress = true; try { const eventResponse = await this.getAcsEvents(); let latestEventTime: Date | undefined; @@ -993,7 +1028,16 @@ export class HikvisionDoorbellAPI extends HikvisionCameraAPI } catch (error) { this.console.error (`Failed to poll and process ACS events: ${error}`); - throw error; + + // Retry once after a short delay if this was the first attempt + if (!isRetry) { + this.console.debug (`Retrying ACS polling after ${acsPollingTimeoutSec} seconds...`); + this.isAcsPollingInProgress = false; + await new Promise (resolve => setTimeout (resolve, acsPollingTimeoutSec * 1000)); + return this.pollAndProcessAcsEvents (lastEventTime, true); + } + } finally { + this.isAcsPollingInProgress = false; } } @@ -1061,7 +1105,8 @@ export class HikvisionDoorbellAPI extends HikvisionCameraAPI url: `http://${this.endpoint}/ISAPI/Event/notification/alertStream`, responseType: 'readable', headers: { - 'Accept': '*/*' + 'Accept': '*/*', + 'Connection': 'keep-alive' } }); @@ -1162,6 +1207,7 @@ export class HikvisionDoorbellAPI extends HikvisionCameraAPI this.console.debug (`AlertStream JSON: ${JSON.stringify (eventData, null, 2)}`); + // Poll ACS events (errors are handled internally) this.pollAndProcessAcsEvents (this.lastAcsEventTime); } } diff --git a/plugins/hikvision-doorbell/src/main.ts b/plugins/hikvision-doorbell/src/main.ts index 37f3fc013..4a3c3599d 100644 --- a/plugins/hikvision-doorbell/src/main.ts +++ b/plugins/hikvision-doorbell/src/main.ts @@ -93,6 +93,14 @@ export class HikvisionCameraDoorbell extends HikvisionCamera implements Camera, const debugEnabled = this.storage.getItem ('debug'); this.debugController.setDebugEnabled (debugEnabled === 'true'); + // Add global unhandledRejection handler to prevent silent failures + process.on ('unhandledRejection', (reason: any, promise: Promise) => { + this.console.error (`Unhandled Promise Rejection: ${reason}`); + if (reason?.stack) { + this.console.error (`Stack trace: ${reason.stack}`); + } + }); + this.updateSip(); } @@ -210,6 +218,8 @@ export class HikvisionCameraDoorbell extends HikvisionCamera implements Camera, this.stopIntercom(); }); } + }).catch(e => { + this.console.error('Failed to stop call during reconnection:', e); }); return; } @@ -1152,6 +1162,9 @@ export class HikvisionCameraDoorbell extends HikvisionCamera implements Camera, this.httpStreamSwitcher.destroy(); this.httpStreamSwitcher = undefined; } + } catch (error) { + this.console.error (`Failed to stop intercom: ${error}`); + // Don't throw - we want to ensure cleanup happens } finally { // Always reset state this.intercomBusy = false; @@ -1168,6 +1181,7 @@ export class HikvisionCameraDoorbell extends HikvisionCamera implements Camera, private createEventApi(): HikvisionDoorbellAPI { + // Event API only listens for events, skip door capabilities initialization return new HikvisionDoorbellAPI ( this.getIPAddress(), this.getHttpPort(), @@ -1175,7 +1189,8 @@ export class HikvisionCameraDoorbell extends HikvisionCamera implements Camera, this.getPassword(), this.isCallPolling(), this.console, - this.storage + this.storage, + true // skipCapabilitiesInit ); } @@ -1218,7 +1233,11 @@ export class HikvisionCameraDoorbell extends HikvisionCamera implements Camera, } catch (e) { this.console.error (`Error installing fake SIP settings: ${e}`); // repeat if unreached - this.installSipSettingsOnDeviceTimeout = setTimeout (() => this.installSipSettingsOnDevice(), UNREACHED_RETRY_SEC * 1000); + this.installSipSettingsOnDeviceTimeout = setTimeout (() => { + this.installSipSettingsOnDevice().catch(err => { + this.console.error('Failed to retry installing SIP settings:', err); + }); + }, UNREACHED_RETRY_SEC * 1000); } } } diff --git a/plugins/hikvision-doorbell/src/utils.ts b/plugins/hikvision-doorbell/src/utils.ts index 472e42fbf..59da90906 100644 --- a/plugins/hikvision-doorbell/src/utils.ts +++ b/plugins/hikvision-doorbell/src/utils.ts @@ -2,22 +2,37 @@ import sdk from '@scrypted/sdk'; import { isLoopback, isV4Format, isV6Format } from 'ip'; import dgram from 'node:dgram'; +const MAX_RETRIES = 10; +const RETRY_DELAY_SEC = 10; + export async function localServiceIpAddress (doorbellIp: string): Promise { - let host = "localhost"; - try { - const typeCheck = isV4Format (doorbellIp) ? isV4Format : isV6Format; - for (const address of await sdk.endpointManager.getLocalAddresses()) { - if (!isLoopback(address) && typeCheck(address)) { - host = address; - break; + const typeCheck = isV4Format (doorbellIp) ? isV4Format : isV6Format; + + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) + { + try + { + const addresses = await sdk.endpointManager.getLocalAddresses(); + + for (const address of addresses || []) + { + if (!isLoopback (address) && typeCheck (address)) + { + return address; + } } } - } - catch (e) { + catch (e) { + } + + // Wait before retry if addresses not available yet + if (attempt < MAX_RETRIES - 1) { + await awaitTimeout (RETRY_DELAY_SEC * 1000); + } } - return host; + throw new Error('Could not find local service IP address'); } export function udpSocketType (ip: string): dgram.SocketType {