node-switchbot
Version:
The node-switchbot is a Node.js module which allows you to control your Switchbot Devices through Bluetooth (BLE) with automatic OpenAPI fallback.
184 lines • 6.3 kB
JavaScript
/* Copyright(C) 2024-2026, donavanbecker (https://github.com/donavanbecker). All rights reserved.
*
* utils/connection-tracker.ts: SwitchBot v4.0.0 - Connection Success Tracking
*/
import { Logger } from './index.js';
/**
* Tracks connection success/failure statistics per device per connection type
*/
export class ConnectionTracker {
deviceId;
stats = new Map();
logger;
constructor(deviceId, logLevel) {
this.deviceId = deviceId;
this.logger = new Logger(`ConnectionTracker:${deviceId}`, logLevel);
// Initialize stats for all connection types
const connectionTypes = ['ble', 'api'];
for (const type of connectionTypes) {
this.stats.set(type, {
connectionType: type,
successCount: 0,
failureCount: 0,
totalAttempts: 0,
successRate: 1, // Start optimistic
averageLatencyMs: 0,
lastAttemptTime: undefined,
lastSuccessTime: undefined,
lastFailureTime: undefined,
});
}
}
/**
* Record a successful attempt
*/
recordSuccess(connectionType, latencyMs = 0) {
const stat = this.stats.get(connectionType);
if (!stat) {
return;
}
stat.successCount++;
stat.totalAttempts++;
stat.lastAttemptTime = new Date();
stat.lastSuccessTime = new Date();
// Update average latency (exponential moving average)
const alpha = 0.3;
stat.averageLatencyMs = stat.averageLatencyMs * (1 - alpha) + latencyMs * alpha;
// Update success rate
stat.successRate = stat.totalAttempts === 0 ? 1 : stat.successCount / stat.totalAttempts;
this.logger.debug(`${connectionType}: success recorded`, {
successRate: stat.successRate.toFixed(2),
latency: latencyMs,
});
}
/**
* Record a failed attempt
*/
recordFailure(connectionType) {
const stat = this.stats.get(connectionType);
if (!stat) {
return;
}
stat.failureCount++;
stat.totalAttempts++;
stat.lastAttemptTime = new Date();
stat.lastFailureTime = new Date();
// Update success rate
stat.successRate = stat.totalAttempts === 0 ? 1 : stat.successCount / stat.totalAttempts;
this.logger.debug(`${connectionType}: failure recorded`, {
successRate: stat.successRate.toFixed(2),
});
}
/**
* Get statistics for a connection type
*/
getStats(connectionType) {
return this.stats.get(connectionType);
}
/**
* Get all statistics
*/
getAllStats() {
return [...this.stats.values()];
}
/**
* Get the best connection type (highest success rate)
*/
getBestConnection(availableTypes = ['ble', 'api']) {
let bestType;
let bestRate = -1;
for (const type of availableTypes) {
const stat = this.stats.get(type);
if (stat && stat.successRate > bestRate) {
bestRate = stat.successRate;
bestType = type;
}
}
if (bestType) {
const stat = this.stats.get(bestType);
if (stat.totalAttempts > 0) {
this.logger.debug(`Best connection: ${bestType} (success rate: ${stat.successRate.toFixed(2)})`);
}
}
return bestType;
}
/**
* Get the most recent successful connection type
*/
getMostRecentSuccessful(availableTypes = ['ble', 'api']) {
let mostRecentType;
let mostRecentTime;
for (const type of availableTypes) {
const stat = this.stats.get(type);
if (stat?.lastSuccessTime) {
if (!mostRecentTime || stat.lastSuccessTime > mostRecentTime) {
mostRecentTime = stat.lastSuccessTime;
mostRecentType = type;
}
}
}
return mostRecentType;
}
/**
* Check if a connection type is considered reliable
* (e.g., success rate > 75% with at least 5 attempts)
*/
isReliable(connectionType, minAttempts = 5, minRate = 0.75) {
const stat = this.stats.get(connectionType);
if (!stat) {
return false;
}
return stat.totalAttempts >= minAttempts && stat.successRate >= minRate;
}
/**
* Get connection recommendation with reasoning
*/
getRecommendation(availableTypes = ['ble', 'api']) {
const stats = availableTypes.map(type => this.stats.get(type)).filter(Boolean);
// If no attempt history, recommend first available
if (stats.every(s => s.totalAttempts === 0)) {
return {
recommended: availableTypes[0],
reason: 'No history, using first available connection',
stats,
};
}
// Find most reliable based on success rate
const bestType = this.getBestConnection(availableTypes);
if (!bestType) {
return {
recommended: undefined,
reason: 'No available connections',
stats,
};
}
const bestStat = this.stats.get(bestType);
let reason = `${bestType} has best success rate (${bestStat.successRate.toFixed(2)})`;
// Add context about latency if applicable
if (bestStat.averageLatencyMs > 0) {
reason += ` with avg latency ${bestStat.averageLatencyMs.toFixed(0)}ms`;
}
return {
recommended: bestType,
reason,
stats,
};
}
/**
* Reset all statistics
*/
reset() {
this.logger.debug('Resetting all statistics');
for (const stat of this.stats.values()) {
stat.successCount = 0;
stat.failureCount = 0;
stat.totalAttempts = 0;
stat.successRate = 1;
stat.averageLatencyMs = 0;
stat.lastAttemptTime = undefined;
stat.lastSuccessTime = undefined;
stat.lastFailureTime = undefined;
}
}
}
//# sourceMappingURL=connection-tracker.js.map