node-red-contrib-tplink-tapo-connect-api
Version:
This unofficial node-RED node allows connection to TP-Link Tapo devices. This project has been enhanced with AI support to enable new features. Starting with v0.50, we have added support for the KLAP protocol. To prioritize the operation of this node, we
351 lines • 12.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.P110Plug = void 0;
const types_1 = require("../../types");
const unified_protocol_1 = require("../../core/unified-protocol");
/**
* P110 Smart Plug - Plug with energy monitoring capabilities
*/
class P110Plug extends types_1.BaseTapoDevice {
constructor(ip, credentials) {
super(ip, credentials);
this.featureCache = new Map();
this.requestQueue = Promise.resolve();
this.unifiedProtocol = new unified_protocol_1.UnifiedTapoProtocol(ip, credentials);
}
async checkDeviceConnectivity() {
try {
const response = await fetch(`http://${this.ip}`, {
method: 'HEAD',
signal: AbortSignal.timeout(5000)
});
return response.status !== 0;
}
catch (error) {
console.log('Device connectivity check failed:', error);
return false;
}
}
async connect() {
console.log('P110Plug.connect() called - using unified protocol');
// Check basic device connectivity first
console.log('Checking device connectivity...');
const isReachable = await this.checkDeviceConnectivity();
if (!isReachable) {
throw new Error(`Device at ${this.ip} is not reachable. Check IP address and network connectivity.`);
}
console.log('Device is reachable, proceeding with unified protocol connection...');
try {
await this.unifiedProtocol.connect();
console.log(`Connected successfully using ${this.unifiedProtocol.getActiveProtocol()} protocol`);
}
catch (error) {
console.error('Unified protocol connection failed:', error);
throw error;
}
}
async disconnect() {
try {
await this.unifiedProtocol.disconnect();
}
catch (error) {
console.warn('Warning during disconnect:', error);
}
}
/**
* Check if device is currently authenticated
*/
isAuthenticated() {
return this.unifiedProtocol.isConnected();
}
async getDeviceInfo() {
var _a;
const request = {
method: 'get_device_info'
};
const response = await this.sendRequest(request);
const rawData = response.result;
// Transform raw data to match interface expectations
const deviceInfo = {
...rawData,
// Ensure required fields are properly set
on_time: rawData.on_time || 0,
overheated: rawData.overheated || false,
// Computed properties for backward compatibility
deviceId: rawData.device_id,
deviceOn: rawData.device_on,
onTime: rawData.on_time || 0,
fwVer: rawData.fw_ver,
hwVer: rawData.hw_ver,
// Base interface properties
deviceType: 'SMART.TAPOPLUG',
type: 'SMART.TAPOPLUG',
region: ((_a = rawData.lang) === null || _a === void 0 ? void 0 : _a.split('_')[1]) || 'US',
specs: '',
rssi: 0,
signalLevel: 0
};
// Cache device model for feature detection
this.deviceModel = deviceInfo.model;
return deviceInfo;
}
/**
* Check if the device supports energy monitoring features
* P110 supports energy monitoring
*/
async hasEnergyMonitoring() {
const cacheKey = 'energy_monitoring';
if (this.featureCache.has(cacheKey)) {
return this.featureCache.get(cacheKey);
}
try {
// Get device info to determine model if not cached
if (!this.deviceModel) {
const deviceInfo = await this.getDeviceInfo();
this.deviceModel = deviceInfo.model;
}
// Check if device model is known to support energy monitoring
if (types_1.energyMonitoringModels.includes(this.deviceModel)) {
this.featureCache.set(cacheKey, true);
return true;
}
// For unknown models, try to make a request to determine support
if (!types_1.energyMonitoringModels.includes(this.deviceModel)) {
try {
const request = {
method: 'get_energy_usage'
};
await this.sendRequest(request);
this.featureCache.set(cacheKey, true);
return true;
}
catch (error) {
this.featureCache.set(cacheKey, false);
return false;
}
}
// If we reach here, model was not in any list - shouldn't happen but handle gracefully
this.featureCache.set(cacheKey, false);
return false;
}
catch (error) {
// If we can't determine support, assume it's not supported
this.featureCache.set(cacheKey, false);
return false;
}
}
/**
* Check if the device supports a specific feature
*/
async supportsFeature(feature) {
switch (feature) {
case 'energy_monitoring':
return this.hasEnergyMonitoring();
case 'schedule':
return true; // Most Tapo devices support scheduling
case 'countdown':
return true; // Most Tapo devices support countdown
default:
return false;
}
}
async turnOn() {
const request = {
method: 'set_device_info',
params: {
device_on: true
}
};
await this.sendRequest(request);
}
async turnOff() {
const request = {
method: 'set_device_info',
params: {
device_on: false
}
};
await this.sendRequest(request);
}
async toggle() {
const deviceInfo = await this.getDeviceInfo();
if (deviceInfo.device_on) {
await this.turnOff();
}
else {
await this.turnOn();
}
}
// Convenience aliases following Python API pattern
async on() {
await this.turnOn();
}
async off() {
await this.turnOff();
}
async isOn() {
const deviceInfo = await this.getDeviceInfo();
return deviceInfo.device_on;
}
/**
* Get current power consumption in watts
*/
async getCurrentPower(options = {}) {
var _a;
const { throwOnUnsupported = true } = options;
const hasEnergyMonitoring = await this.hasEnergyMonitoring();
if (!hasEnergyMonitoring) {
if (throwOnUnsupported) {
throw new types_1.FeatureNotSupportedError('energy_monitoring', this.deviceModel || 'unknown', 'This device does not support current power monitoring');
}
return 0;
}
try {
const request = {
method: 'get_current_power'
};
const response = await this.sendRequest(request);
return ((_a = response.result) === null || _a === void 0 ? void 0 : _a.current_power) || 0;
}
catch (error) {
if (throwOnUnsupported) {
throw new types_1.DeviceCapabilityError('current_power', 'API request failed', `Failed to get current power: ${error instanceof Error ? error.message : error}`);
}
return 0;
}
}
/**
* Get energy usage data
*/
async getEnergyUsage() {
const request = {
method: 'get_energy_usage'
};
const response = await this.sendRequest(request);
return response.result;
}
/**
* Get energy data with detailed statistics
*/
async getEnergyData() {
const request = {
method: 'get_energy_data'
};
const response = await this.sendRequest(request);
return response.result;
}
async getUsageInfo(options = {}) {
const { throwOnUnsupported = true } = options;
const hasEnergyMonitoring = await this.hasEnergyMonitoring();
if (!hasEnergyMonitoring) {
if (throwOnUnsupported) {
throw new types_1.FeatureNotSupportedError('energy_monitoring', this.deviceModel || 'unknown', 'This device does not support energy monitoring features');
}
else {
// Return empty/default usage info when not throwing
return {
todayRuntime: 0,
monthRuntime: 0,
todayEnergy: 0,
monthEnergy: 0,
currentPower: 0
};
}
}
try {
const [energyUsage, currentPower] = await Promise.all([
this.getEnergyUsage(),
this.getCurrentPower({ throwOnUnsupported: false })
]);
return {
todayRuntime: (energyUsage === null || energyUsage === void 0 ? void 0 : energyUsage.today_runtime) || 0,
monthRuntime: (energyUsage === null || energyUsage === void 0 ? void 0 : energyUsage.month_runtime) || 0,
todayEnergy: (energyUsage === null || energyUsage === void 0 ? void 0 : energyUsage.today_energy) || 0,
monthEnergy: (energyUsage === null || energyUsage === void 0 ? void 0 : energyUsage.month_energy) || 0,
currentPower: currentPower || 0
};
}
catch (error) {
if (error instanceof types_1.FeatureNotSupportedError) {
throw error;
}
if (!throwOnUnsupported) {
return {
todayRuntime: 0,
monthRuntime: 0,
todayEnergy: 0,
monthEnergy: 0,
currentPower: 0
};
}
// If the API call fails, it might indicate lack of support
throw new types_1.DeviceCapabilityError('energy_monitoring', 'API request failed', `Failed to get energy usage information: ${error instanceof Error ? error.message : error}`);
}
}
/**
* Get usage info using Result pattern for better error handling
*/
async getUsageInfoResult() {
try {
const data = await this.getUsageInfo({ throwOnUnsupported: true });
return { success: true, data };
}
catch (error) {
if (error instanceof types_1.FeatureNotSupportedError || error instanceof types_1.DeviceCapabilityError) {
return { success: false, error };
}
// Re-throw unexpected errors
throw error;
}
}
async getTodayEnergy(options = {}) {
const usageInfo = await this.getUsageInfo(options);
return usageInfo.todayEnergy;
}
async getMonthEnergy(options = {}) {
const usageInfo = await this.getUsageInfo(options);
return usageInfo.monthEnergy;
}
async getTodayRuntime(options = {}) {
const usageInfo = await this.getUsageInfo(options);
return usageInfo.todayRuntime;
}
async getMonthRuntime(options = {}) {
const usageInfo = await this.getUsageInfo(options);
return usageInfo.monthRuntime;
}
async isOverheated() {
const deviceInfo = await this.getDeviceInfo();
return deviceInfo.overheated || false;
}
async getOnTime() {
const deviceInfo = await this.getDeviceInfo();
return deviceInfo.on_time || 0;
}
async setDeviceInfo(params) {
const request = {
method: 'set_device_info',
params
};
await this.sendRequest(request);
}
async sendRequest(request) {
// Use sequential request queue to prevent KLAP session conflicts (like Python tapo)
return this.requestQueue = this.requestQueue.then(async () => {
try {
// Use unified protocol with improved session management
const result = await this.unifiedProtocol.executeRequest(request);
return {
error_code: 0,
result
};
}
catch (error) {
console.error(`Request ${request.method} failed:`, error);
throw error;
}
});
}
}
exports.P110Plug = P110Plug;
//# sourceMappingURL=p110-plug.js.map