fix: enhance device status handling by integrating device cache for improved performance

This commit is contained in:
faris Aljohari
2025-06-25 08:03:23 -06:00
parent 324661e1ee
commit c0a069b460
4 changed files with 128 additions and 84 deletions

View File

@ -69,32 +69,23 @@ export class DeviceStatusFirebaseService {
} }
async addBatchDeviceStatusToOurDb( async addBatchDeviceStatusToOurDb(
batch: { deviceTuyaUuid: string; status: any; log: any }[], batch: { deviceTuyaUuid: string; status: any; log: any }[],
deviceCache: Map<string, any>,
): Promise<void> { ): Promise<void> {
const allLogs = []; const allLogs = [];
const deviceMap = new Map<string, any>();
console.log( console.log(
`🧠 Starting device lookups for batch of ${batch.length} items...`, `🧠 Preparing logs from batch of ${batch.length} items using cached devices only...`,
); );
// Step 1: Parallel device fetching
await Promise.all(
batch.map(async (item) => {
if (!deviceMap.has(item.deviceTuyaUuid)) {
const device = await this.getDeviceByDeviceTuyaUuid(
item.deviceTuyaUuid,
);
device?.uuid && deviceMap.set(item.deviceTuyaUuid, device);
}
}),
);
console.log(`🔍 Found ${deviceMap.size} devices from batch`);
// Step 2: Prepare logs and updates
for (const item of batch) { for (const item of batch) {
const device = deviceMap.get(item.deviceTuyaUuid); const device = deviceCache.get(item.deviceTuyaUuid);
if (!device?.uuid) continue;
if (!device?.uuid) {
console.log(
`⛔ Ignored unknown device in batch: ${item.deviceTuyaUuid}`,
);
continue;
}
const logs = item.log.properties.map((property) => const logs = item.log.properties.map((property) =>
this.deviceStatusLogRepository.create({ this.deviceStatusLogRepository.create({
@ -112,59 +103,53 @@ export class DeviceStatusFirebaseService {
} }
console.log(`📝 Total logs to insert: ${allLogs.length}`); console.log(`📝 Total logs to insert: ${allLogs.length}`);
// Step 3: Insert logs in chunks with ON CONFLICT DO NOTHING const chunkSize = 300;
const insertLogsPromise = (async () => { let insertedCount = 0;
const chunkSize = 300;
let insertedCount = 0;
for (let i = 0; i < allLogs.length; i += chunkSize) { for (let i = 0; i < allLogs.length; i += chunkSize) {
const chunk = allLogs.slice(i, i + chunkSize); const chunk = allLogs.slice(i, i + chunkSize);
try { try {
const result = await this.deviceStatusLogRepository const result = await this.deviceStatusLogRepository
.createQueryBuilder() .createQueryBuilder()
.insert() .insert()
.into('device-status-log') // or use DeviceStatusLogEntity .into('device-status-log') // or use DeviceStatusLogEntity
.values(chunk) .values(chunk)
.orIgnore() // skip duplicates .orIgnore() // skip duplicates
.execute(); .execute();
insertedCount += result.identifiers.length; insertedCount += result.identifiers.length;
console.log( console.log(
`✅ Inserted ${result.identifiers.length} / ${chunk.length} logs (chunk)`, `✅ Inserted ${result.identifiers.length} / ${chunk.length} logs (chunk)`,
); );
} catch (error) { } catch (error) {
console.error('❌ Insert error (skipped chunk):', error.message); console.error('❌ Insert error (skipped chunk):', error.message);
}
} }
}
console.log( console.log(`✅ Total logs inserted: ${insertedCount} / ${allLogs.length}`);
`✅ Total logs inserted: ${insertedCount} / ${allLogs.length}`,
);
})();
// Step 5: Wait for both insert and post-processing to finish
await Promise.all([insertLogsPromise]);
} }
async addDeviceStatusToFirebase( async addDeviceStatusToFirebase(
addDeviceStatusDto: AddDeviceStatusDto, addDeviceStatusDto: AddDeviceStatusDto,
deviceCache: Map<string, any>,
): Promise<AddDeviceStatusDto | null> { ): Promise<AddDeviceStatusDto | null> {
try { try {
const device = await this.getDeviceByDeviceTuyaUuid( const device = deviceCache.get(addDeviceStatusDto.deviceTuyaUuid);
addDeviceStatusDto.deviceTuyaUuid, if (!device?.uuid) {
); console.log(
`⛔ Skipping Firebase update for unknown device: ${addDeviceStatusDto.deviceTuyaUuid}`,
if (device?.uuid) { );
return await this.createDeviceStatusFirebase({ return null;
deviceUuid: device.uuid,
...addDeviceStatusDto,
productType: device.productDevice.prodType,
});
} }
// Return null if device not found or no UUID
return null; // Ensure product info and uuid are attached
addDeviceStatusDto.deviceUuid = device.uuid;
addDeviceStatusDto.productUuid = device.productDevice?.uuid;
addDeviceStatusDto.productType = device.productDevice?.prodType;
return await this.createDeviceStatusFirebase(addDeviceStatusDto);
} catch (error) { } catch (error) {
// Handle the error silently, perhaps log it internally or ignore it console.error('❌ Error in addDeviceStatusToFirebase:', error);
return null; return null;
} }
} }

View File

@ -8,7 +8,10 @@ import { TuyaWebSocketService } from './services/tuya.web.socket.service';
import { OneSignalService } from './services/onesignal.service'; import { OneSignalService } from './services/onesignal.service';
import { DeviceMessagesService } from './services/device.messages.service'; import { DeviceMessagesService } from './services/device.messages.service';
import { DeviceRepositoryModule } from '../modules/device/device.repository.module'; import { DeviceRepositoryModule } from '../modules/device/device.repository.module';
import { DeviceNotificationRepository } from '../modules/device/repositories'; import {
DeviceNotificationRepository,
DeviceRepository,
} from '../modules/device/repositories';
import { DeviceStatusFirebaseModule } from '../firebase/devices-status/devices-status.module'; import { DeviceStatusFirebaseModule } from '../firebase/devices-status/devices-status.module';
import { CommunityPermissionService } from './services/community.permission.service'; import { CommunityPermissionService } from './services/community.permission.service';
import { CommunityRepository } from '../modules/community/repositories'; import { CommunityRepository } from '../modules/community/repositories';
@ -27,6 +30,7 @@ import { SosHandlerService } from './services/sos.handler.service';
DeviceNotificationRepository, DeviceNotificationRepository,
CommunityRepository, CommunityRepository,
SosHandlerService, SosHandlerService,
DeviceRepository,
], ],
exports: [ exports: [
HelperHashService, HelperHashService,

View File

@ -16,35 +16,53 @@ export class SosHandlerService {
); );
} }
async handleSosEventFirebase(devId: string, logData: any): Promise<void> { async handleSosEventFirebase(
devId: string,
logData: any,
deviceCache: Map<string, any>,
): Promise<void> {
try { try {
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ await this.deviceStatusFirebaseService.addDeviceStatusToFirebase(
deviceTuyaUuid: devId,
status: [{ code: 'sos', value: true }],
log: logData,
});
await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb([
{ {
deviceTuyaUuid: devId, deviceTuyaUuid: devId,
status: [{ code: 'sos', value: true }], status: [{ code: 'sos', value: true }],
log: logData, log: logData,
}, },
]); deviceCache,
);
await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb(
[
{
deviceTuyaUuid: devId,
status: [{ code: 'sos', value: true }],
log: logData,
},
],
deviceCache,
);
setTimeout(async () => { setTimeout(async () => {
try { try {
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ await this.deviceStatusFirebaseService.addDeviceStatusToFirebase(
deviceTuyaUuid: devId,
status: [{ code: 'sos', value: false }],
log: logData,
});
await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb([
{ {
deviceTuyaUuid: devId, deviceTuyaUuid: devId,
status: [{ code: 'sos', value: false }], status: [{ code: 'sos', value: false }],
log: logData, log: logData,
}, },
]); deviceCache,
);
await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb(
[
{
deviceTuyaUuid: devId,
status: [{ code: 'sos', value: false }],
log: logData,
},
],
deviceCache,
);
} catch (err) { } catch (err) {
this.logger.error('Failed to send SOS false value', err); this.logger.error('Failed to send SOS false value', err);
} }

View File

@ -3,6 +3,7 @@ import TuyaWebsocket from '../../config/tuya-web-socket-config';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { SosHandlerService } from './sos.handler.service'; import { SosHandlerService } from './sos.handler.service';
import { DeviceRepository } from '@app/common/modules/device/repositories';
@Injectable() @Injectable()
export class TuyaWebSocketService { export class TuyaWebSocketService {
@ -16,11 +17,13 @@ export class TuyaWebSocketService {
}[] = []; }[] = [];
private isProcessing = false; private isProcessing = false;
private deviceCache: Map<string, any> = new Map();
constructor( constructor(
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService, private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService,
private readonly sosHandlerService: SosHandlerService, private readonly sosHandlerService: SosHandlerService,
private readonly deviceRepository: DeviceRepository,
) { ) {
this.isDevEnv = this.isDevEnv =
this.configService.get<string>('NODE_ENV') === 'development'; this.configService.get<string>('NODE_ENV') === 'development';
@ -33,6 +36,11 @@ export class TuyaWebSocketService {
maxRetryTimes: 100, maxRetryTimes: 100,
}); });
this.loadAllActiveDevices();
// Reload device cache every 1 hour
setInterval(() => this.loadAllActiveDevices(), 60 * 60 * 1000);
if (this.configService.get<string>('tuya-config.TRUN_ON_TUYA_SOCKET')) { if (this.configService.get<string>('tuya-config.TRUN_ON_TUYA_SOCKET')) {
this.setupEventHandlers(); this.setupEventHandlers();
this.client.start(); this.client.start();
@ -42,6 +50,22 @@ export class TuyaWebSocketService {
setInterval(() => this.processQueue(), 15000); setInterval(() => this.processQueue(), 15000);
} }
private async loadAllActiveDevices(): Promise<void> {
const devices = await this.deviceRepository.find({
where: { isActive: true },
relations: ['productDevice'],
});
this.deviceCache.clear();
devices.forEach((device) => {
this.deviceCache.set(device.deviceTuyaUuid, device);
});
console.log(
`🔄 Device cache reloaded: ${this.deviceCache.size} active devices at ${new Date().toISOString()}`,
);
}
private setupEventHandlers() { private setupEventHandlers() {
// Event handlers // Event handlers
this.client.open(() => { this.client.open(() => {
@ -51,18 +75,30 @@ export class TuyaWebSocketService {
this.client.message(async (ws: WebSocket, message: any) => { this.client.message(async (ws: WebSocket, message: any) => {
try { try {
const { devId, status, logData } = this.extractMessageData(message); const { devId, status, logData } = this.extractMessageData(message);
if (!Array.isArray(logData?.properties)) { if (!Array.isArray(logData?.properties)) return;
const device = this.deviceCache.get(devId);
if (!device) {
// console.log(`⛔ Ignored unknown device: ${devId}`);
return; return;
} }
if (this.sosHandlerService.isSosTriggered(status)) { if (this.sosHandlerService.isSosTriggered(status)) {
await this.sosHandlerService.handleSosEventFirebase(devId, logData); await this.sosHandlerService.handleSosEventFirebase(
devId,
logData,
this.deviceCache,
);
} else { } else {
// Firebase real-time update // Firebase real-time update
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({ await this.deviceStatusFirebaseService.addDeviceStatusToFirebase(
deviceTuyaUuid: devId, {
status: status, deviceTuyaUuid: devId,
log: logData, status,
}); log: logData,
},
this.deviceCache,
);
} }
// Push to internal queue // Push to internal queue
@ -111,11 +147,12 @@ export class TuyaWebSocketService {
try { try {
await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb( await this.deviceStatusFirebaseService.addBatchDeviceStatusToOurDb(
batch?.map((item) => ({ batch.map((item) => ({
deviceTuyaUuid: item.devId, deviceTuyaUuid: item.devId,
status: item.status, status: item.status,
log: item.logData, log: item.logData,
})), })),
this.deviceCache,
); );
} catch (error) { } catch (error) {
console.error('❌ Error processing batch:', error); console.error('❌ Error processing batch:', error);