diff --git a/drivers/everything-presence-one/device.ts b/drivers/everything-presence-one/device.ts index 35e3c6c..b70fbf1 100644 --- a/drivers/everything-presence-one/device.ts +++ b/drivers/everything-presence-one/device.ts @@ -1,9 +1,15 @@ +import Debug from 'debug'; + import { z } from 'zod'; // @ts-expect-error Client is not typed import { Client, Connection } from '@2colors/esphome-native-api'; import Homey from 'homey'; +const debug = Debug('epo'); + +Debug.enable(Homey.env.DEBUG_LOGGING); + const CONNECT_TIMEOUT = 15000; enum DRIVER_SETTINGS { @@ -201,8 +207,12 @@ interface DiscoveryResult { // }; class EverythingPresenceOneDevice extends Homey.Device { + private debugEntity = debug.extend('entity'); + private debugClient = debug.extend('client'); + private debugDiscovery = debug.extend('discovery'); private client?: Client; private entities: Map = new Map(); + private connectingPromise: Promise | null = null; /** * onInit is called when the device is initialized. @@ -220,7 +230,7 @@ class EverythingPresenceOneDevice extends Homey.Device { host: `${this.getStoreValue('host')}.local`, port: this.getStoreValue('port') }; - this.log('Client connect:', addressProps); + this.debugClient('connecting:', addressProps); this.client = new Client({ ...addressProps, clearSession: false, @@ -243,35 +253,38 @@ class EverythingPresenceOneDevice extends Homey.Device { // Listen for client errors this.client.on('error', (error: unknown) => { - this.log('Client error:', error); + this.debugClient('error:', error); this.setUnavailable(this.homey.__('error.unavailable')).catch((err) => - this.error('Could not set unavailable', err) + this.log('Could not set unavailable', err) ); }); - return new Promise((resolve, reject) => { + this.connectingPromise = new Promise((resolve, reject) => { const connectTimeout = this.homey.setTimeout( () => reject(new Error(this.homey.__('error.connect_timeout'))), CONNECT_TIMEOUT ); this.client.connect(); this.client.on('initialized', () => { - this.log('Client initialized', addressProps); + this.debugClient('connected', addressProps); this.homey.clearTimeout(connectTimeout); // Fetch all entities this.client.connection.listEntitiesService(); // Mark device as available in case it was unavailable - this.setAvailable().catch((err) => this.error('Could not set available', err)); + this.setAvailable().catch((err) => this.log('Could not set available', err)); + this.connectingPromise = null; return resolve(this.client); }); }); + + return this.connectingPromise; } disconnect() { - this.log('Client disconnect'); + this.debugClient('disconnect'); this.client?.disconnect(); this.client?.removeAllListeners(); this.entities.forEach((entity) => { @@ -289,6 +302,10 @@ class EverythingPresenceOneDevice extends Homey.Device { } async reconnect() { + // If already connecting just return + if (this.connectingPromise) return; + + this.debugClient('reconnect'); this.disconnect(); return this.connect(); } @@ -301,7 +318,7 @@ class EverythingPresenceOneDevice extends Homey.Device { // Parse entity data const parseEntityResult = entitySchema.safeParse(entity); if (!parseEntityResult.success) { - this.error('Invalid entity object received, error:', parseEntityResult.error, entity); + this.debugEntity('Invalid entity object received, error:', parseEntityResult.error, entity); return; } @@ -310,7 +327,10 @@ class EverythingPresenceOneDevice extends Homey.Device { data: parseEntityResult.data, original: entity }); - this.log(`Register entity: ${parseEntityResult.data.config.objectId}:`, parseEntityResult.data); + this.debugEntity( + `Register entity: ${parseEntityResult.data.config.objectId}:`, + parseEntityResult.data + ); // Validate entity.connection if ( @@ -349,7 +369,7 @@ class EverythingPresenceOneDevice extends Homey.Device { onEntityState(entityId: string, state: unknown) { const parseResult = entityStateSchema.safeParse(state); if (!parseResult.success) { - this.error( + this.debugEntity( `Got invalid entity state for entityId ${entityId}, error:`, parseResult.error, state @@ -363,7 +383,7 @@ class EverythingPresenceOneDevice extends Homey.Device { const entity = this.entities.get(entityId)?.data; if (!entity) throw new Error(`Missing entity ${entityId}`); - this.log(`state`, { + this.debugEntity(`state`, { config: entity.config, name: entity.name, type: entity.type, @@ -376,52 +396,52 @@ class EverythingPresenceOneDevice extends Homey.Device { case 'temperature': // Throw when state is not a number z.number().parse(parsedState.state); - this.log(`Capability: measure_temperature: state event`, parsedState.state); + this.debugEntity(`Capability: measure_temperature: state event`, parsedState.state); this.setCapabilityValue('measure_temperature', parsedState.state).catch((err) => - this.error('Failed to set measure_temperature capability value', err) + this.debugEntity('Failed to set measure_temperature capability value', err) ); break; case 'humidity': // Throw when state is not a number z.number().parse(parsedState.state); - this.log(`Capability: measure_humidity: state event`, parsedState.state); + this.debugEntity(`Capability: measure_humidity: state event`, parsedState.state); this.setCapabilityValue('measure_humidity', parsedState.state).catch((err) => - this.error('Failed to set measure_humidity capability value', err) + this.debugEntity('Failed to set measure_humidity capability value', err) ); break; case 'illuminance': // Throw when state is not a number z.number().parse(parsedState.state); - this.log(`Capability: measure_luminance: state event`, parsedState.state); + this.debugEntity(`Capability: measure_luminance: state event`, parsedState.state); this.setCapabilityValue('measure_luminance', parsedState.state).catch((err) => - this.error('Failed to set measure_luminance capability value', err) + this.debugEntity('Failed to set measure_luminance capability value', err) ); break; case 'motion': // Throw when state is not a boolean z.boolean().parse(parsedState.state); - this.log(`Capability: alarm_motion.pir: state event`, parsedState.state); + this.debugEntity(`Capability: alarm_motion.pir: state event`, parsedState.state); this.setCapabilityValue('alarm_motion.pir', parsedState.state).catch((err) => - this.error('Failed to set alarm_motion.pir capability value', err) + this.debugEntity('Failed to set alarm_motion.pir capability value', err) ); break; case 'occupancy': // Throw when state is not a boolean z.boolean().parse(parsedState.state); if (entity.config.uniqueId.includes('binary_sensor_mmwave')) { - this.log(`Capability: alarm_motion.mmwave: state event`, parsedState.state); + this.debugEntity(`Capability: alarm_motion.mmwave: state event`, parsedState.state); this.setCapabilityValue('alarm_motion.mmwave', parsedState.state).catch((err) => - this.error('Failed to set alarm_motion.mmwave capability value', err) + this.debugEntity('Failed to set alarm_motion.mmwave capability value', err) ); } else if (entity.config.uniqueId.includes('binary_sensor_occupancy')) { - this.log(`Capability: alarm_motion: state event`, parsedState.state); + this.debugEntity(`Capability: alarm_motion: state event`, parsedState.state); this.setCapabilityValue('alarm_motion', parsedState.state).catch((err) => - this.error('Failed to set alarm_motion capability value', err) + this.debugEntity('Failed to set alarm_motion capability value', err) ); } break; default: - this.error('Unknown device class:', entity.config.deviceClass); + this.debugEntity('Unknown device class:', entity.config.deviceClass); } // Read and update settings @@ -432,7 +452,7 @@ class EverythingPresenceOneDevice extends Homey.Device { case DRIVER_SETTINGS.MMWAVE_DISTANCE: // Throw when state is not a number z.number().parse(parsedState.state); - this.log(`Setting: ${entity.config.objectId}: state event`, parsedState.state); + this.debugEntity(`Setting: ${entity.config.objectId}: state event`, parsedState.state); this.setSettings({ [entity.config.objectId]: parsedState.state }); @@ -441,14 +461,14 @@ class EverythingPresenceOneDevice extends Homey.Device { case DRIVER_SETTINGS.ESP_32_STATUS_LED: // Throw when state is not a boolean z.boolean().parse(parsedState.state); - this.log(`Setting: ${entity.config.objectId}: state event`, parsedState.state); + this.debugEntity(`Setting: ${entity.config.objectId}: state event`, parsedState.state); this.setSettings({ [entity.config.objectId]: parsedState.state }); break; default: - this.log('Unknown setting:', entity.config.objectId); + this.debugEntity('Unknown setting:', entity.config.objectId); } } @@ -551,10 +571,7 @@ class EverythingPresenceOneDevice extends Homey.Device { * @returns */ onDiscoveryResult(discoveryResult: DiscoveryResult) { - this.log( - `onDiscoveryResult match: ${discoveryResult.id === this.getData().id}`, - discoveryResult - ); + this.debugDiscovery(`result match: ${discoveryResult.id === this.getData().id}`); return discoveryResult.id === this.getData().id; } @@ -563,7 +580,7 @@ class EverythingPresenceOneDevice extends Homey.Device { * @param discoveryResult */ async onDiscoveryAvailable(discoveryResult: DiscoveryResult) { - this.log('onDiscoveryAvailable', discoveryResult); + this.debugDiscovery('available', discoveryResult); // Update address props and reconnect if needed, when this throws, // the device will become unavailable. @@ -572,7 +589,7 @@ class EverythingPresenceOneDevice extends Homey.Device { } onDiscoveryAddressChanged(discoveryResult: DiscoveryResult) { - this.log('onDiscoveryAddressChanged', discoveryResult); + this.debugDiscovery('address changed', discoveryResult); // Update address props and reconnect if needed this.handleAddressChanged(discoveryResult) @@ -582,12 +599,12 @@ class EverythingPresenceOneDevice extends Homey.Device { } }) .catch((err) => { - this.error('onDiscoveryAddressChanged -> failed to reconnect', err); + this.debugDiscovery('address changed -> failed to reconnect', err); }); } async onDiscoveryLastSeenChanged(discoveryResult: DiscoveryResult) { - this.log('onDiscoveryLastSeenChanged', discoveryResult); + this.debugDiscovery('last seen changed', discoveryResult); } } diff --git a/package-lock.json b/package-lock.json index 41880e3..e27632f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,13 @@ "version": "1.0.0", "dependencies": { "@2colors/esphome-native-api": "^1.2.3", + "debug": "^4.3.4", "source-map-support": "^0.5.21", "zod": "^3.21.4" }, "devDependencies": { "@tsconfig/node12": "^1.0.11", + "@types/debug": "^4.1.8", "@types/homey": "npm:homey-apps-sdk-v3-types@^0.3.4", "@types/node": "^20.0.0", "eslint": "^7.32.0", @@ -313,6 +315,15 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/homey": { "name": "homey-apps-sdk-v3-types", "version": "0.3.4", @@ -341,6 +352,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, "node_modules/@types/node": { "version": "20.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.0.0.tgz", @@ -1404,7 +1421,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -4511,8 +4527,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multicast-dns": { "version": "7.2.5", @@ -6641,6 +6656,15 @@ "@types/node": "*" } }, + "@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, "@types/homey": { "version": "npm:homey-apps-sdk-v3-types@0.3.4", "resolved": "https://registry.npmjs.org/homey-apps-sdk-v3-types/-/homey-apps-sdk-v3-types-0.3.4.tgz", @@ -6670,6 +6694,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, "@types/node": { "version": "20.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.0.0.tgz", @@ -7428,7 +7458,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "requires": { "ms": "2.1.2" } @@ -9781,8 +9810,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "multicast-dns": { "version": "7.2.5", diff --git a/package.json b/package.json index 0291573..d8c25a6 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "devDependencies": { "@tsconfig/node12": "^1.0.11", + "@types/debug": "^4.1.8", "@types/homey": "npm:homey-apps-sdk-v3-types@^0.3.4", "@types/node": "^20.0.0", "eslint": "^7.32.0", @@ -22,6 +23,7 @@ }, "dependencies": { "@2colors/esphome-native-api": "^1.2.3", + "debug": "^4.3.4", "source-map-support": "^0.5.21", "zod": "^3.21.4" }