diff --git a/config.schema.json b/config.schema.json index 2bc04d7..a7336ca 100644 --- a/config.schema.json +++ b/config.schema.json @@ -62,6 +62,13 @@ "required": false } } + }, + "verboseLogging": { + "title": "Verbose Logging", + "type": "boolean", + "required": false, + "default": false, + "description": "If enabled, the plugin will log all debug messages. If everything works as expected, there's no need to enable this." } } }, @@ -74,6 +81,9 @@ { "key": "name", "type": "name" + }, + { + "key": "verboseLogging" } ] }, diff --git a/src/air-quality-sensor-accessory.ts b/src/air-quality-sensor-accessory.ts index 8d82396..467c0ef 100644 --- a/src/air-quality-sensor-accessory.ts +++ b/src/air-quality-sensor-accessory.ts @@ -36,6 +36,7 @@ export class AirQualitySensorAccessory { username: this.config.api.username, password: this.config.api.password, logger: this.platform.log, + verboseLogging: this.config.verboseLogging, }); this.mqttApiClient.subscribe(MQTT_STATUS_TOPIC); @@ -49,6 +50,7 @@ export class AirQualitySensorAccessory { username: this.config.api.username, password: this.config.api.password, logger: this.platform.log, + verboseLogging: this.config.verboseLogging, }); // Only start polling if we're using the HTTP API @@ -57,8 +59,8 @@ export class AirQualitySensorAccessory { this.log.debug(`Starting polling for status...`); - this.httpApiClient.polling.getStatus.on('response', response => { - this.handleStatusResponse(response as IthoStatusSanitizedPayload); // TODO: fix type + this.httpApiClient.polling.getStatus.on('response.getStatus', response => { + this.handleStatusResponse(response as IthoStatusSanitizedPayload); }); } @@ -124,6 +126,7 @@ export class AirQualitySensorAccessory { }, // eslint-disable-next-line @typescript-eslint/no-explicit-any debug: (...parameters: any[]) => { + if (!this.config.verboseLogging) return; this.platform.log.debug(loggerPrefix, ...parameters); }, }; @@ -138,7 +141,7 @@ export class AirQualitySensorAccessory { const currentAirQualityName = this.getAirQualityName(currentValue as number); if (currentValue === value) { - // this.log.debug(`AirQuality: Already set to: ${newAirQualityName}. Ignoring.`); + this.log.debug(`AirQuality: Already set to: ${newAirQualityName}. Ignoring.`); return; } @@ -156,7 +159,7 @@ export class AirQualitySensorAccessory { const roundedValue = Math.round(value); if (currentValue === roundedValue) { - // this.log.debug(`CurrentRelativeHumidity: Already set to: ${value}. Ignoring.`); + this.log.debug(`CurrentRelativeHumidity: Already set to: ${value}. Ignoring.`); return; } @@ -179,7 +182,7 @@ export class AirQualitySensorAccessory { const parsedCurrentValue = parseFloat((currentValue as number).toFixed(1)); if (parsedCurrentValue === parsedValue) { - // this.log.debug(`CurrentTemperature: Already set to: ${parsedValue}. Ignoring.`); + this.log.debug(`CurrentTemperature: Already set to: ${parsedValue}. Ignoring.`); return; } @@ -194,7 +197,7 @@ export class AirQualitySensorAccessory { ).value; if (currentValue === value) { - // this.log.debug(`CarbonDioxideLevel: Already set to: ${value}. Ignoring.`); + this.log.debug(`CarbonDioxideLevel: Already set to: ${value}. Ignoring.`); return; } @@ -223,7 +226,7 @@ export class AirQualitySensorAccessory { } handleMqttMessage(topic: string, message: Buffer): void { - // this.log.debug(`Received new status payload: ${message.toString()}`); + this.log.debug(`Received new status payload: ${message.toString()}`); if (topic === MQTT_STATUS_TOPIC) { const messageString = message.toString(); @@ -241,7 +244,7 @@ export class AirQualitySensorAccessory { this.lastStatusPayload = data; this.lastStatusPayloadTimestamp = Date.now(); - // this.log.debug(`Parsed new status payload to: ${JSON.stringify(data)}`); + this.log.debug(`Parsed new status payload to: ${JSON.stringify(data)}`); const airQuality = this.getAirQualityFromStatusPayload(data); const currentRelativeHumidity = data.hum || 0; @@ -274,7 +277,7 @@ export class AirQualitySensorAccessory { const airQualityName = this.getAirQualityName(currentValue as number); - this.log.debug(`AirQuality is ${airQualityName} (${currentValue})`); + this.log.info(`AirQuality is ${airQualityName} (${currentValue})`); return Promise.resolve(currentValue); } @@ -284,7 +287,7 @@ export class AirQualitySensorAccessory { this.platform.Characteristic.StatusActive, ).value; - this.log.debug(`StatusActive is ${currentValue ? 'ACTIVE' : 'INACTIVE'} (${currentValue})`); + this.log.info(`StatusActive is ${currentValue ? 'ACTIVE' : 'INACTIVE'} (${currentValue})`); return currentValue; } diff --git a/src/api/http.ts b/src/api/http.ts index 02a4344..9272c0b 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -1,4 +1,4 @@ -import { IthoStatusPayload } from '@/types'; +import { IthoGetSpeedResponse, IthoSetSpeedResponse, IthoStatusSanitizedPayload } from '@/types'; import { sanitizeStatusPayload } from '@/utils/api'; import EventEmitter from 'events'; import { Logger } from 'homebridge'; @@ -10,13 +10,15 @@ interface HttpApiOptions { ip: string; username?: string; password?: string; + verboseLogging?: boolean; logger: Logger; } export class HttpApi { private readonly url: URL; private readonly eventEmitter: EventEmitter; - private readonly log: Logger; + private readonly logger: Logger; + private readonly verboseLogging: boolean; protected isPolling: Record = {}; constructor(options: HttpApiOptions) { @@ -32,22 +34,38 @@ export class HttpApi { this.eventEmitter = new EventEmitter(); - this.log = options.logger; + this.logger = options.logger; + + this.verboseLogging = options.verboseLogging || false; + } + + protected log(...args: unknown[]): void { + if (!this.logger) return; + if (!this.verboseLogging) return; + + return this.logger.debug('[HTTP API] ->', ...args); } - on(event: 'response', listener: (response: T) => void): void; + on( + event: 'response.getStatus', + listener: (response: T) => void, + ): void; + on( + event: 'response.getSpeed', + listener: (response: T) => void, + ): void; on(event: 'error', listener: (error: Error) => void): void; // eslint-disable-next-line @typescript-eslint/no-explicit-any on(event: string, listener: (...args: any[]) => void) { return this.eventEmitter.on(event, listener); } - async setSpeed(speed: number): Promise { + async setSpeed(speed: number): Promise { // Make a copy of the URL so we don't modify the original const requestUrl = new URL(this.url.toString()); requestUrl.searchParams.set('speed', speed.toString()); - this.log.debug(`[API] -> Setting speed to ${speed} at ${requestUrl}`); + this.log(`Setting speed to ${speed} at ${requestUrl}`); const response = await request(requestUrl, { method: 'GET', @@ -64,12 +82,12 @@ export class HttpApi { return speed as T; } - async getSpeed(): Promise { + async getSpeed(): Promise { // Make a copy of the URL so we don't modify the original const requestUrl = new URL(this.url.toString()); requestUrl.searchParams.set('get', 'currentspeed'); - this.log.debug(`[API] -> Getting speed at ${requestUrl}`); + this.log(`Getting speed at ${requestUrl}`); const response = await request(requestUrl, { method: 'GET', @@ -90,12 +108,12 @@ export class HttpApi { return currentSpeed as T; } - async getStatus(): Promise { + async getStatus(): Promise { // Make a copy of the URL so we don't modify the original const requestUrl = new URL(this.url.toString()); requestUrl.searchParams.set('get', 'ithostatus'); - this.log.debug(`[API] -> Getting status at ${requestUrl}`); + this.log(`Getting status at ${requestUrl}`); const response = await request(requestUrl, { method: 'GET', @@ -140,13 +158,13 @@ export class HttpApi { */ protected stopPolling(method: string): void { if (!this.isPolling[method]) { - this.log.debug(`Polling for "${method}" is not started or already stopped.`); + this.log(`Polling for "${method}" is not started or already stopped.`); return; } this.isPolling[method] = false; - this.log.debug(`Stopping polling for "${method}".`); + this.log(`Stopping polling for "${method}".`); } protected async startPolling(method: string, apiMethod: () => Promise): Promise { @@ -156,19 +174,19 @@ export class HttpApi { try { const response = await apiMethod(); - this.log.debug( + this.log( `Received response while polling "${method}". Emitting "response": ${JSON.stringify( response, )}`, ); - this.eventEmitter.emit('response', response); + this.eventEmitter.emit(`response.${method}`, response); } catch (error) { - this.log.debug(`Received error while polling "${method}": ${JSON.stringify(error)}`); + this.log(`Received error while polling "${method}": ${JSON.stringify(error)}`); - this.eventEmitter.emit('error', error); + this.eventEmitter.emit(`error.${method}`, error); } finally { - this.log.debug(`Waiting for next polling interval for "${method}"...`); + this.log(`Waiting for next polling interval for "${method}"...`); await new Promise(resolve => setTimeout(resolve, DEFAULT_POLLING_INTERVAL)); } } diff --git a/src/api/mqtt.ts b/src/api/mqtt.ts index 00bf17d..3cf7a39 100644 --- a/src/api/mqtt.ts +++ b/src/api/mqtt.ts @@ -6,12 +6,14 @@ interface MqttApiOptions { port: number; username?: string; password?: string; + verboseLogging?: boolean; logger: Logger; } export class MqttApi { private readonly mqttApiClient: mqtt.Client; - private readonly log: Logger; + private readonly logger: Logger; + private readonly verboseLogging: boolean; constructor(options: MqttApiOptions) { this.mqttApiClient = mqtt.connect({ @@ -25,7 +27,16 @@ export class MqttApi { this.mqttApiClient.on('connect', this.handleMqttConnect.bind(this)); this.mqttApiClient.on('error', this.handleMqttError.bind(this)); - this.log = options.logger; + this.logger = options.logger; + + this.verboseLogging = options.verboseLogging || false; + } + + protected log(...args: unknown[]): void { + if (!this.logger) return; + if (!this.verboseLogging) return; + + return this.logger.debug('[MQTT API] ->', ...args); } subscribe(topic: string | string[]): mqtt.Client { @@ -41,10 +52,10 @@ export class MqttApi { } handleMqttConnect(packet: mqtt.IConnackPacket) { - this.log.debug(`MQTT connect: ${JSON.stringify(packet)}`); + this.log(`MQTT connect: ${JSON.stringify(packet)}`); } handleMqttError(error: Error) { - this.log.debug(`MQTT error: ${JSON.stringify(error)}`); + this.log(`MQTT error: ${JSON.stringify(error)}`); } } diff --git a/src/config.schema.test.ts b/src/config.schema.test.ts index 9f41ed0..83733ac 100644 --- a/src/config.schema.test.ts +++ b/src/config.schema.test.ts @@ -7,6 +7,8 @@ import { DEFAULT_BRIDGE_NAME, PLATFORM_NAME } from './settings'; describe('config.schema.json', () => { it('should have the correct name property', async () => { const nameProperty: keyof ConfigSchema = 'name'; + const apiProperty: keyof ConfigSchema = 'api'; + const verboseLoggingProperty: keyof ConfigSchema = 'verboseLogging'; const properties = configSchemaJson.schema.properties; @@ -17,8 +19,15 @@ describe('config.schema.json', () => { expect(properties.name.type).toBe('string'); expect(properties.name.default).toBe(DEFAULT_BRIDGE_NAME); + expect(properties).toHaveProperty(verboseLoggingProperty); + expect(properties.verboseLogging).toHaveProperty('required'); + expect(properties.verboseLogging.required).toBe(false); + expect(properties.verboseLogging.type).toBe('boolean'); + expect(properties.verboseLogging.default).toBe(false); + const apiProperties = properties.api.properties; + expect(properties).toHaveProperty(apiProperty); expect(apiProperties.protocol).toHaveProperty('required'); expect(apiProperties.protocol.required).toBe(true); expect(apiProperties.protocol.type).toBe('string'); diff --git a/src/config.schema.ts b/src/config.schema.ts index 9b1024c..8d5f1c5 100644 --- a/src/config.schema.ts +++ b/src/config.schema.ts @@ -2,20 +2,6 @@ import { z } from 'zod'; import isIP from 'validator/lib/isIP'; import { PlatformConfig } from 'homebridge'; -// const customErrorMap: z.ZodErrorMap = (issue, ctx) => { -// if (issue.code === z.ZodIssueCode.invalid_type) { -// if (issue.expected === 'string') { -// return { message: 'bad type!' }; -// } -// } -// if (issue.code === z.ZodIssueCode.custom) { -// return { message: `less-than-${(issue.params || {}).minimum}` }; -// } -// return { message: ctx.defaultError }; -// }; - -// z.setErrorMap(customErrorMap); - // this schema should match the config.schema.json // using zod to validate the config gives us type-safety over the config in our code export const configSchema = z.object({ @@ -50,6 +36,11 @@ export const configSchema = z.object({ }) .optional(), }), + verboseLogging: z + .boolean({ + invalid_type_error: "'verboseLogging' must be a boolean", + }) + .optional(), }); export type ConfigSchema = z.infer & PlatformConfig; diff --git a/src/fan-accessory.ts b/src/fan-accessory.ts index 5d59ad3..c928c54 100644 --- a/src/fan-accessory.ts +++ b/src/fan-accessory.ts @@ -46,6 +46,7 @@ export class FanAccessory { username: this.config.api.username, password: this.config.api.password, logger: this.platform.log, + verboseLogging: this.config.verboseLogging, }); this.mqttApiClient.subscribe([MQTT_STATE_TOPIC, MQTT_STATUS_TOPIC]); @@ -59,24 +60,23 @@ export class FanAccessory { username: this.config.api.username, password: this.config.api.password, logger: this.platform.log, + verboseLogging: this.config.verboseLogging, }); // Only start polling if we're using the HTTP API if (this.config.api.protocol === 'http') { this.httpApiClient.polling.getSpeed.start(); + this.httpApiClient.polling.getStatus.start(); - this.log.debug(`Starting polling for speed...`); - - // TODO: make this work, currently it's not working because on(response) is giving the same responses for both methods - // this.httpApiClient.polling.getStatus.start(); - - this.httpApiClient.polling.getSpeed.on('response', response => { - this.handleSpeedResponse(response as number); // TODO: fix type - }); + this.httpApiClient.polling.getSpeed.on( + 'response.getSpeed', + this.handleSpeedResponse.bind(this), + ); - // this.httpApiClient.polling.getStatus.on('response', response => { - // console.log('Status response', response); - // }); + this.httpApiClient.polling.getStatus.on( + 'response.getStatus', + this.handleStatusResponse.bind(this), + ); } const informationService = this.accessory.getService( @@ -158,6 +158,7 @@ export class FanAccessory { }, // eslint-disable-next-line @typescript-eslint/no-explicit-any debug: (...parameters: any[]) => { + if (!this.config.verboseLogging) return; this.platform.log.debug(loggerPrefix, ...parameters); }, }; diff --git a/src/types.ts b/src/types.ts index d9c8050..69662b5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -58,6 +58,9 @@ export type IthoStatusPayload = { [K in keyof IthoStatusSanitizedPayload]: number | string; }; +export type IthoGetSpeedResponse = number; // example: 45 +export type IthoSetSpeedResponse = number; // example: 45 + export type IthoStatePayload = string; // example: "45" export enum VirtualRemoteOptions {