diff --git a/README.md b/README.md index 6681f1a9..2c16a571 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ Before you can configure, you must go to the [Tuya IoT Platform](https://iot.tuy - `options.endpoint` - **required** : The endpoint URL taken from the [API Reference > Endpoints](https://developer.tuya.com/en/docs/iot/api-request?id=Ka4a8uuo1j4t4#title-1-Endpoints) table. - `options.accessId` - **required** : The Access ID obtained from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) - `options.accessKey` - **required** : The Access Secret obtained from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) +- `options.debug` - **optional**: Includes debugging output in the Homebridge log. (Default: `false`) +- `options.debugLevel` - **optional**: An optional list of strings seperated with comma `,`. `api` represents for HTTP API log, `mqtt` represents for MQTT log, and device ID represents for device log. If blank, all logs are outputed. #### For "Smart Home" Project @@ -88,7 +90,9 @@ Before you can configure, you must go to the [Tuya IoT Platform](https://iot.tuy - `options.password` - **required** : The app account's password. MD5 salted password is also available for increased security. - `options.appSchema` - **required** : The app schema: 'tuyaSmart' for the Tuya Smart App, or 'smartlife' for the Smart Life App. - `options.endpoint` - **optional** : The endpoint URL can be inferred from the [API Reference > Endpoints](https://developer.tuya.com/en/docs/iot/api-request?id=Ka4a8uuo1j4t4#title-1-Endpoints) table based on the country code provided. Only manually set this value if you encounter login issues and need to specify the endpoint for your account location. -- `options.homeWhitelist` - **optional**: An array of integer values for the home IDs you want to whitelist. If provided, only devices with matching Home IDs will be included. You can find the Home ID in the homebridge log. +- `options.homeWhitelist` - **optional**: An array of integer values for the home IDs you want to whitelist. If provided, only devices with matching Home IDs will be included. You can find the Home ID in the Homebridge log. +- `options.debug` - **optional**: Includes debugging output in the Homebridge log. (Default: `false`) +- `options.debugLevel` - **optional**: An optional list of strings seperated with comma `,`. `api` represents for API and MQTT log, device ID represents for specific device log. If blank, all logs are outputed. #### Advanced options @@ -160,15 +164,9 @@ After Homebridge has been successfully launched, the device information list wil **⚠️Please make sure to remove sensitive information such as `ip`, `lon`, `lat`, `local_key`, and `uid` before submitting the file.** -#### 2. Enable Homebridge Debug Mode +#### 2. Enable Debug Mode -For Homebridge Web UI users: -- Go to the `Homebridge Setting` page -- Turn on the `Homebridge Debug Mode -D` switch -- Restart Homebridge. - -For Homebridge Command Line Users: -- Start Homebridge with the `-D` flag: `homebridge -D` +Add debug option in the plugin config, then restart Homebridge. #### 3. Collect Logs diff --git a/config.schema.json b/config.schema.json index 6f82e5c5..35e18571 100644 --- a/config.schema.json +++ b/config.schema.json @@ -268,6 +268,16 @@ } } } + }, + "debug": { + "title": "Enable Debug Logging", + "type": "boolean", + "default": false + }, + "debugLevel": { + "title": "Debug Level", + "description": "An optional list of strings seperated with comma `,`. `api` represents for API and MQTT log, device ID represents for specific device log. If blank, all logs are outputed.", + "type": "string" } } } diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index bd0b0402..03f6659e 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -30,7 +30,14 @@ class BaseAccessory { public deviceManager = this.platform.deviceManager!; public device = this.deviceManager.getDevice(this.accessory.context.deviceID)!; - public log = new PrefixLogger(this.platform.log, this.device.name.length > 0 ? this.device.name : this.device.id); + public log = new PrefixLogger( + this.platform.log, + this.device.name.length > 0 ? this.device.name : this.device.id, + this.platform.options.debug && ((this.platform.options.debugLevel ?? '').length > 0 + ? this.platform.options.debugLevel?.includes(this.device.id) + : true), + ); + public intialized = false; public adaptiveLightingController?; diff --git a/src/config.ts b/src/config.ts index dca1587e..af9cb4e4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -27,6 +27,8 @@ export interface TuyaPlatformCustomConfigOptions { username: string; password: string; deviceOverrides?: Array; + debug?: boolean; + debugLevel?: string; } export interface TuyaPlatformHomeConfigOptions { @@ -40,6 +42,8 @@ export interface TuyaPlatformHomeConfigOptions { appSchema: string; homeWhitelist?: Array; deviceOverrides?: Array; + debug?: boolean; + debugLevel?: string; } export type TuyaPlatformConfigOptions = TuyaPlatformCustomConfigOptions | TuyaPlatformHomeConfigOptions; @@ -54,6 +58,8 @@ export const customOptionsSchema = { accessId: { type: 'string', required: true }, accessKey: { type: 'string', required: true }, deviceOverrides: { 'type': 'array' }, + debug: { type: 'boolean' }, + debugLevel: { 'type': 'string' }, }, }; @@ -68,5 +74,7 @@ export const homeOptionsSchema = { appSchema: { 'type': 'string', required: true }, homeWhitelist: { 'type': 'array' }, deviceOverrides: { 'type': 'array' }, + debug: { type: 'boolean' }, + debugLevel: { 'type': 'string' }, }, }; diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 658337e0..6bc4c4af 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -85,8 +85,9 @@ export default class TuyaOpenAPI { public accessKey: string, public log: Logger = console, public lang = 'en', + public debug = false, ) { - this.log = new PrefixLogger(log, TuyaOpenAPI.name); + this.log = new PrefixLogger(log, TuyaOpenAPI.name, debug); } static getDefaultEndpoint(countryCode: number) { diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index 8eff9be6..e614ed83 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -37,8 +37,9 @@ export default class TuyaOpenMQ { constructor( public api: TuyaOpenAPI, public log: Logger = console, + public debug = false, ) { - this.log = new PrefixLogger(log, TuyaOpenMQ.name); + this.log = new PrefixLogger(log, TuyaOpenMQ.name, debug); } start() { diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index 43168a75..189a714a 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -6,8 +6,9 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { constructor( public api: TuyaOpenAPI, + public debug = false, ) { - super(api); + super(api, debug); this.mq.version = '2.0'; } diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index d1840450..d7209b59 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -32,11 +32,12 @@ export default class TuyaDeviceManager extends EventEmitter { constructor( public api: TuyaOpenAPI, + public debug = false, ) { super(); const log = (this.api.log as PrefixLogger).log; - this.log = new PrefixLogger(log, TuyaDeviceManager.name); + this.log = new PrefixLogger(log, TuyaDeviceManager.name, debug); this.mq = new TuyaOpenMQ(api, log); this.mq.addMessageListener(this.onMQTTMessage.bind(this)); diff --git a/src/platform.ts b/src/platform.ts index 84ad180b..ab04e00a 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -253,9 +253,10 @@ export class TuyaPlatform implements DynamicPlatformPlugin { const DEFAULT_PASS = 'homebridge'; let res; - const { endpoint, accessId, accessKey } = this.options; - const api = new TuyaOpenAPI(endpoint, accessId, accessKey, this.log); - const deviceManager = new TuyaCustomDeviceManager(api); + const { endpoint, accessId, accessKey, debug, debugLevel } = this.options; + const debugMode = debug && ((debugLevel ?? '').length > 0 ? debugLevel?.includes('api') : true); + const api = new TuyaOpenAPI(endpoint, accessId, accessKey, this.log, 'en', debugMode); + const deviceManager = new TuyaCustomDeviceManager(api, debugMode); this.log.info('Get token.'); res = await api.getToken(); @@ -341,13 +342,16 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } let res; - const { accessId, accessKey, countryCode, username, password, appSchema, endpoint } = this.options; + const { accessId, accessKey, countryCode, username, password, appSchema, endpoint, debug, debugLevel } = this.options; + const debugMode = debug && ((debugLevel ?? '').length > 0 ? debugLevel?.includes('api') : true); const api = new TuyaOpenAPI( (endpoint && endpoint.length > 0) ? endpoint : TuyaOpenAPI.getDefaultEndpoint(countryCode), accessId, accessKey, - this.log); - const deviceManager = new TuyaHomeDeviceManager(api); + this.log, + 'en', + debugMode); + const deviceManager = new TuyaHomeDeviceManager(api, debugMode); this.log.info('Log in to Tuya Cloud.'); res = await api.homeLogin(countryCode, username, password, appSchema); diff --git a/src/util/FfmpegStreamingProcess.ts b/src/util/FfmpegStreamingProcess.ts index 71ac17bf..e557adad 100644 --- a/src/util/FfmpegStreamingProcess.ts +++ b/src/util/FfmpegStreamingProcess.ts @@ -36,17 +36,15 @@ export class FfmpegStreamingProcess { readonly stdin: Writable; constructor( - cameraName: string, sessionId: string, videoProcessor: string, ffmpegArgs: string[], log: PrefixLogger, - debug = false, delegate: StreamingDelegate, callback?: StreamRequestCallback, ) { - log.debug('Stream command: ' + videoProcessor + ' ' + ffmpegArgs.join(' '), cameraName, debug); + log.debug(`Stream command: ${videoProcessor} ${ffmpegArgs.map(value => JSON.stringify(value)).join(' ')}`); let started = false; const startTime = Date.now(); @@ -63,11 +61,11 @@ export class FfmpegStreamingProcess { const runtime = (Date.now() - startTime) / 1000; const message = 'Getting the first frames took ' + runtime + ' seconds.'; if (runtime < 5) { - log.debug(message, cameraName, debug); + log.debug(message); } else if (runtime < 22) { - log.warn(message, cameraName); + log.warn(message); } else { - log.error(message, cameraName); + log.error(message); } } } @@ -81,14 +79,12 @@ export class FfmpegStreamingProcess { callback(); callback = undefined; } - if (debug && line.match(/\[(panic|fatal|error)\]/)) { // For now only write anything out when debug is set - log.error(line, cameraName); - } else if (debug) { - log.debug(line, cameraName, true); + if (line.match(/\[(panic|fatal|error)\]/)) { + log.error(line); } }); this.process.on('error', (error: Error) => { - log.error('FFmpeg process creation failed: ' + error.message, cameraName); + log.error('FFmpeg process creation failed: ' + error.message); if (callback) { callback(new Error('FFmpeg process creation failed')); } @@ -102,15 +98,15 @@ export class FfmpegStreamingProcess { const message = 'FFmpeg exited with code: ' + code + ' and signal: ' + signal; if (this.killTimeout && code === 0) { - log.debug(message + ' (Expected)', cameraName, debug); + log.debug(message + ' (Expected)'); } else if (code === null || code === 255) { if (this.process.killed) { - log.debug(message + ' (Forced)', cameraName, debug); + log.debug(message + ' (Forced)'); } else { - log.error(message + ' (Unexpected)', cameraName); + log.error(message + ' (Unexpected)'); } } else { - log.error(message + ' (Error)', cameraName); + log.error(message + ' (Error)'); delegate.stopStream(sessionId); if (!started && callback) { callback(new Error(message)); diff --git a/src/util/Logger.ts b/src/util/Logger.ts index 6cae0847..bad75f86 100644 --- a/src/util/Logger.ts +++ b/src/util/Logger.ts @@ -11,8 +11,17 @@ export class PrefixLogger { constructor( public log: Logger, public prefix: string, + public debugMode = false, ) { + this.debugMode = this.debugMode || process.argv.includes('-D') || process.argv.includes('--debug'); + } + debug(message?: any, ...args: any[]) { + if (this.debugMode) { + this.log.info((this.prefix ? `[${this.prefix}] ` : '') + message, ...args); + } else { + this.log.debug((this.prefix ? `[${this.prefix}] ` : '') + message, ...args); + } } info(message?: any, ...args: any[]) { @@ -27,8 +36,4 @@ export class PrefixLogger { this.log.error((this.prefix ? `[${this.prefix}] ` : '') + message, ...args); } - debug(message?: any, ...args: any[]) { - this.log.debug((this.prefix ? `[${this.prefix}] ` : '') + message, ...args); - } - } diff --git a/src/util/TuyaStreamDelegate.ts b/src/util/TuyaStreamDelegate.ts index 620f8b76..c2f112cb 100644 --- a/src/util/TuyaStreamDelegate.ts +++ b/src/util/TuyaStreamDelegate.ts @@ -182,32 +182,24 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr try { session.socket?.close(); } catch (error) { - this.camera.log.error(`Error occurred closing socket: ${error}`, this.camera.accessory.displayName, 'Homebridge'); + this.camera.log.error(`Error occurred closing socket: ${error}`); } try { session.mainProcess?.stop(); } catch (error) { - this.camera.log.error( - `Error occurred terminating main FFmpeg process: ${error}`, - this.camera.accessory.displayName, - 'Homebridge', - ); + this.camera.log.error(`Error occurred terminating main FFmpeg process: ${error}`); } try { session.returnProcess?.stop(); } catch (error) { - this.camera.log.error( - `Error occurred terminating two-way FFmpeg process: ${error}`, - this.camera.accessory.displayName, - 'Homebridge', - ); + this.camera.log.error(`Error occurred terminating two-way FFmpeg process: ${error}`); } delete this.ongoingSessions[sessionId]; - this.camera.log.info('Stopped video stream.', this.camera.accessory.displayName); + this.camera.log.info('Stopped video stream.'); } } @@ -220,14 +212,11 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr callback: SnapshotRequestCallback, ) { try { - this.camera.log.debug(`Snapshot requested: ${request.width} x ${request.height}`, this.camera.accessory.displayName); + this.camera.log.debug(`Snapshot requested: ${request.width} x ${request.height}`); const snapshot = await this.fetchSnapshot(); - this.camera.log.debug( - 'Sending snapshot', - this.camera.accessory.displayName, - ); + this.camera.log.debug('Sending snapshot'); callback(undefined, snapshot); } catch (error) { @@ -291,27 +280,21 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr ) { switch (request.type) { case this.hap.StreamRequestTypes.START: { - this.camera.log.debug( - `Start stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps`, - this.camera.accessory.displayName, - ); + this.camera.log.debug(`Start stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps`); await this.startStream(request, callback); break; } case this.hap.StreamRequestTypes.RECONFIGURE: { - this.camera.log.debug( - `Reconfigure stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps (Ignored)`, - this.camera.accessory.displayName, - ); + this.camera.log.debug(`Reconfigure stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps (Ignored)`); callback(); break; } case this.hap.StreamRequestTypes.STOP: { - this.camera.log.debug('Stop stream requested', this.camera.accessory.displayName); + this.camera.log.debug('Stop stream requested'); this.stopStream(request.sessionID); callback(); @@ -335,7 +318,7 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr const sessionInfo = this.pendingSessions[request.sessionID]; if (!sessionInfo) { - this.camera.log.error('Error finding session information.', this.camera.accessory.displayName); + this.camera.log.error('Error finding session information.'); callback(new Error('Error finding session information')); } @@ -408,11 +391,7 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr `srtp://${sessionInfo.address}:${sessionInfo.audioPort}?rtcpport=${sessionInfo.audioPort}&pkt_size=188`, ); } else { - this.camera.log.error( - `Unsupported audio codec requested: ${request.audio.codec}`, - this.camera.accessory.displayName, - 'Homebridge', - ); + this.camera.log.error(`Unsupported audio codec requested: ${request.audio.codec}`); } ffmpegArgs.push('-progress', 'pipe:1'); @@ -422,7 +401,7 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr activeSession.socket = createSocket(sessionInfo.addressVersion === 'ipv6' ? 'udp6' : 'udp4'); activeSession.socket.on('error', (err: Error) => { - this.camera.log.error('Socket error: ' + err.message, this.camera.accessory.displayName); + this.camera.log.error('Socket error: ' + err.message); this.stopStream(request.sessionID); }); @@ -431,7 +410,7 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr clearTimeout(activeSession.timeout); } activeSession.timeout = setTimeout(() => { - this.camera.log.info('Device appears to be inactive. Stopping stream.', this.camera.accessory.displayName); + this.camera.log.info('Device appears to be inactive. Stopping stream.'); this.controller.forceStopStreamingSession(request.sessionID); this.stopStream(request.sessionID); }, request.video.rtcp_interval * 5 * 1000); @@ -440,12 +419,10 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr activeSession.socket.bind(sessionInfo.videoIncomingPort); activeSession.mainProcess = new FfmpegStreamingProcess( - this.camera.accessory.displayName, request.sessionID, defaultFfmpegPath, ffmpegArgs, this.camera.log, - true, this, callback, ); @@ -455,10 +432,9 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr } private async fetchSnapshot(): Promise { - this.camera.log.debug('Running Snapshot commands for %s', this.camera.accessory.displayName); - if (!this.camera.device.online) { - throw new Error(`${this.camera.accessory.displayName} is currently offline.`); + this.camera.log.debug('Device is currently offline.'); + throw new Error('Device is currently offline.'); } // TODO: Check if there is a stream already running to fetch snapshot. @@ -478,6 +454,8 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr return new Promise((resolve, reject) => { + this.camera.log.debug(`Running Snapshot command: ${defaultFfmpegPath} ${ffmpegArgs.map(value => JSON.stringify(value)).join(' ')}`); + const ffmpeg = spawn( defaultFfmpegPath, ffmpegArgs.map(x => x.toString()), @@ -493,10 +471,7 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr }); ffmpeg.on('error', (error) => { - this.camera.log.error( - `FFmpeg process creation failed: ${error.message} - Showing "offline" image instead.`, - this.camera.accessory.displayName, - ); + this.camera.log.error(`FFmpeg process creation failed: ${error.message} - Showing "offline" image instead.`); reject('Failed to fetch snapshot.'); }); @@ -509,13 +484,13 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr if (snapshotBuffer.length > 0) { resolve(snapshotBuffer); } else { - this.camera.log.error('Failed to fetch snapshot. Showing "offline" image instead.', this.camera.accessory.displayName); + this.camera.log.error('Failed to fetch snapshot. Showing "offline" image instead.'); if (errors.length > 0) { - this.camera.log.error(errors.join(' - '), this.camera.accessory.displayName, 'Homebridge'); + this.camera.log.error(errors.join(' - ')); } - reject(`Unable to fetch snapshot for: ${this.camera.accessory.displayName}`); + reject('Unable to fetch snapshot.'); } }); });