@palekseii/homebridge-tuya-platform
Version:
Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.
448 lines • 20.5 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TuyaPlatform = void 0;
const jsonschema_1 = require("jsonschema");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const TuyaDeviceManager_1 = __importDefault(require("./device/TuyaDeviceManager"));
const TuyaCustomDeviceManager_1 = __importDefault(require("./device/TuyaCustomDeviceManager"));
const TuyaHomeDeviceManager_1 = __importDefault(require("./device/TuyaHomeDeviceManager"));
const settings_1 = require("./settings");
const config_1 = require("./config");
const AccessoryFactory_1 = __importDefault(require("./accessory/AccessoryFactory"));
const TuyaOpenAPI_1 = __importStar(require("./core/TuyaOpenAPI"));
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
class TuyaPlatform {
validate() {
let result;
if (!this.options) {
this.log.error('Not configured, exit.');
return false;
}
else if (this.options.projectType === '1') {
result = new jsonschema_1.Validator().validate(this.options, config_1.customOptionsSchema);
}
else if (this.options.projectType === '2') {
result = new jsonschema_1.Validator().validate(this.options, config_1.homeOptionsSchema);
}
else {
this.log.error(`Unsupported projectType: ${this.options['projectType']}, exit.`);
return false;
}
result.errors.forEach(error => this.log.error(error.stack));
if (result.errors.length > 0) {
return false;
}
if (!this.validateDeviceOverrides() || !this.validateSchema()) {
return false;
}
return true;
}
validateDeviceOverrides() {
var _a;
if (!this.options.deviceOverrides) {
return true;
}
const idMap = new Map();
for (const item of this.options.deviceOverrides) {
if (idMap.has(item.id)) {
(_a = idMap.get(item.id)) === null || _a === void 0 ? void 0 : _a.push(item);
}
else {
idMap.set(item.id, [item]);
}
}
for (const items of idMap.values()) {
if (items.length > 1) {
this.log.error('"deviceOverrides" conflict, "id" must be unique: %o.', items);
return false;
}
}
return true;
}
validateSchema() {
var _a;
if (!this.options.deviceOverrides) {
return true;
}
for (const deviceOverride of this.options.deviceOverrides) {
if (!deviceOverride.schema) {
continue;
}
const idMap = new Map();
for (const item of deviceOverride.schema) {
if (idMap.has(item.code)) {
(_a = idMap.get(item.code)) === null || _a === void 0 ? void 0 : _a.push(item);
}
else {
idMap.set(item.code, [item]);
}
}
for (const items of idMap.values()) {
if (items.length > 1) {
this.log.error('"schema" conflict, "code" must be unique: %o.', items);
return false;
}
}
}
return true;
}
constructor(log, config, api) {
this.log = log;
this.config = config;
this.api = api;
this.Service = this.api.hap.Service;
this.Characteristic = this.api.hap.Characteristic;
this.options = this.config.options;
// this is used to track restored cached accessories
this.cachedAccessories = [];
this.accessoryHandlers = [];
if (!this.validate()) {
return;
}
this.log.debug('Finished initializing platform');
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on('didFinishLaunching', async () => {
this.log.debug('Executed didFinishLaunching callback');
// run the method to discover / register your devices as accessories
await this.initDevices();
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory) {
this.log.info('Loading accessory from cache:', accessory.displayName);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.cachedAccessories.push(accessory);
}
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
async initDevices() {
let devices;
if (this.options.projectType === '1') {
devices = await this.initCustomProject();
}
else if (this.options.projectType === '2') {
devices = await this.initHomeProject();
}
else {
this.log.warn(`Unsupported projectType: ${this.config.options.projectType}.`);
}
if (!devices || !this.deviceManager) {
return;
}
// override device category
for (const device of devices) {
const deviceConfig = this.getDeviceConfig(device);
if (!deviceConfig || !deviceConfig.category) {
continue;
}
this.log.warn('Override %o category from %o to %o', device.name, device.category, deviceConfig.category);
device.category = deviceConfig.category;
}
// override device bridged
for (const device of devices) {
const deviceConfig = this.getDeviceConfig(device);
if (!deviceConfig || !deviceConfig.unbridged) {
continue;
}
this.log.warn('Unbridge %o category %o', device.name, device.category);
device.unbridged = deviceConfig.unbridged;
}
await this.deviceManager.updateInfraredRemotes(devices);
this.log.info(`Got ${devices.length} device(s) and scene(s).`);
const file = path_1.default.join(this.api.user.persistPath(), `TuyaDeviceList.${this.deviceManager.api.tokenInfo.uid}.json`);
this.log.info('Device list saved at %s', file);
if (!fs_1.default.existsSync(this.api.user.persistPath())) {
await fs_1.default.promises.mkdir(this.api.user.persistPath());
}
await fs_1.default.promises.writeFile(file, JSON.stringify(devices, null, 2));
// add accessories
for (const device of devices) {
this.addAccessory(device);
}
// remove unused accessories
for (const cachedAccessory of this.cachedAccessories) {
this.log.warn('Removing unused accessory from cache:', cachedAccessory.displayName);
this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [cachedAccessory]);
}
this.cachedAccessories = [];
this.deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_ADD, this.addAccessory.bind(this));
this.deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, this.updateAccessoryInfo.bind(this));
this.deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_STATUS_UPDATE, this.updateAccessoryStatus.bind(this));
this.deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_DELETE, this.removeAccessory.bind(this));
}
getDeviceConfig(device) {
if (!this.options.deviceOverrides) {
return undefined;
}
const deviceConfig = this.options.deviceOverrides.find(config => config.id === device.id || config.id === device.uuid);
const productConfig = this.options.deviceOverrides.find(config => config.id === device.product_id);
const globalConfig = this.options.deviceOverrides.find(config => config.id === 'global');
return deviceConfig || productConfig || globalConfig;
}
getDeviceSchemaConfig(device, code) {
const deviceConfig = this.getDeviceConfig(device);
if (!deviceConfig || !deviceConfig.schema) {
return undefined;
}
// migrate old config
deviceConfig.schema.forEach(item => {
if (item['oldCode']) {
item.newCode = item.code;
item.code = item['oldCode'];
item['oldCode'] = undefined;
}
});
const schemaConfig = deviceConfig.schema.find(item => item.newCode ? item.newCode === code : item.code === code);
if (!schemaConfig) {
return undefined;
}
return schemaConfig;
}
async initCustomProject() {
if (this.options.projectType !== '1') {
return undefined;
}
const DEFAULT_USER = 'homebridge';
const DEFAULT_PASS = 'homebridge';
let res;
const { endpoint, accessId, accessKey, debug, debugLevel } = this.options;
const debugMode = debug && ((debugLevel !== null && debugLevel !== void 0 ? debugLevel : '').length > 0 ? debugLevel === null || debugLevel === void 0 ? void 0 : debugLevel.includes('api') : true);
const api = new TuyaOpenAPI_1.default(endpoint, accessId, accessKey, this.log, 'en', debugMode);
const deviceManager = new TuyaCustomDeviceManager_1.default(api, debugMode);
this.log.info('Get token.');
res = await api.getToken();
if (res.success === false) {
this.log.error(`Get token failed. code=${res.code}, msg=${res.msg}`);
return undefined;
}
this.log.info(`Search default user "${DEFAULT_USER}"`);
res = await api.customGetUserInfo(DEFAULT_USER);
if (res.success === false) {
this.log.error(`Search user failed. code=${res.code}, msg=${res.msg}`);
return undefined;
}
if (!res.result.user_name) {
this.log.info(`Default user "${DEFAULT_USER}" not exist.`);
this.log.info(`Creating default user "${DEFAULT_USER}".`);
res = await api.customCreateUser(DEFAULT_USER, DEFAULT_PASS);
if (res.success === false) {
this.log.error(`Create default user failed. code=${res.code}, msg=${res.msg}`);
return undefined;
}
}
else {
this.log.info(`Default user "${DEFAULT_USER}" exists.`);
}
const uid = res.result.user_id;
this.log.info('Fetching asset list.');
res = await deviceManager.getAssetList();
if (res.success === false) {
this.log.error(`Fetching asset list failed. code=${res.code}, msg=${res.msg}`);
return undefined;
}
const assetIDList = [];
for (const { asset_id, asset_name } of res.result.list) {
this.log.info(`Got asset_id=${asset_id}, asset_name=${asset_name}`);
assetIDList.push(asset_id);
}
if (assetIDList.length === 0) {
this.log.warn('Asset list is empty. exit.');
return undefined;
}
this.log.info('Authorize asset list.');
res = await deviceManager.authorizeAssetList(uid, assetIDList, true);
if (res.success === false) {
this.log.error(`Authorize asset list failed. code=${res.code}, msg=${res.msg}`);
return undefined;
}
this.log.info(`Log in with user "${DEFAULT_USER}".`);
res = await api.customLogin(DEFAULT_USER, DEFAULT_USER);
if (res.success === false) {
this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`);
if (TuyaOpenAPI_1.LOGIN_ERROR_MESSAGES[res.code]) {
this.log.error(TuyaOpenAPI_1.LOGIN_ERROR_MESSAGES[res.code]);
}
return undefined;
}
this.log.info('Start MQTT connection.');
deviceManager.mq.start();
this.log.info('Fetching device list.');
deviceManager.ownerIDs = assetIDList;
const devices = await deviceManager.updateDevices(assetIDList);
this.deviceManager = deviceManager;
return devices;
}
async initHomeProject() {
if (this.options.projectType !== '2') {
return undefined;
}
let res;
const { accessId, accessKey, countryCode, username, password, appSchema, endpoint, debug, debugLevel } = this.options;
const debugMode = debug && ((debugLevel !== null && debugLevel !== void 0 ? debugLevel : '').length > 0 ? debugLevel === null || debugLevel === void 0 ? void 0 : debugLevel.includes('api') : true);
const api = new TuyaOpenAPI_1.default((endpoint && endpoint.length > 0) ? endpoint : TuyaOpenAPI_1.default.getDefaultEndpoint(countryCode), accessId, accessKey, this.log, 'en', debugMode);
const deviceManager = new TuyaHomeDeviceManager_1.default(api, debugMode);
this.log.info('Log in to Tuya Cloud.');
res = await api.homeLogin(countryCode, username, password, appSchema);
if (res.success === false) {
this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`);
if (TuyaOpenAPI_1.LOGIN_ERROR_MESSAGES[res.code]) {
this.log.error(TuyaOpenAPI_1.LOGIN_ERROR_MESSAGES[res.code]);
}
return undefined;
}
this.log.info('Start MQTT connection.');
deviceManager.mq.start();
this.log.info('Fetching home list.');
res = await deviceManager.getHomeList();
if (res.success === false) {
this.log.error(`Fetching home list failed. code=${res.code}, msg=${res.msg}`);
return undefined;
}
const homeIDList = [];
for (const { home_id, name } of res.result) {
this.log.info(`Got home_id=${home_id}, name=${name}`);
if (this.options.homeWhitelist) {
if (this.options.homeWhitelist.includes(home_id)) {
this.log.info(`Found home_id=${home_id} in whitelist; including devices from this home.`);
homeIDList.push(home_id);
}
else {
this.log.info(`Did not find home_id=${home_id} in whitelist; excluding devices from this home.`);
}
}
else {
homeIDList.push(home_id);
}
}
if (homeIDList.length === 0) {
this.log.warn('Home list is empty.');
}
this.log.info('Fetching device list.');
deviceManager.ownerIDs = homeIDList.map(homeID => homeID.toString());
const devices = await deviceManager.updateDevices(homeIDList);
this.log.info('Fetching scene list.');
for (const homeID of homeIDList) {
const scenes = await deviceManager.getSceneList(homeID);
for (const scene of scenes) {
this.log.info(`Got scene_id=${scene.id}, name=${scene.name}`);
}
devices.push(...scenes);
}
this.deviceManager = deviceManager;
return devices;
}
addAccessory(device) {
if (device.category === 'hidden') {
this.log.info('Hide Accessory:', device.name);
return;
}
const uuid = this.api.hap.uuid.generate(device.id);
const existingAccessory = this.cachedAccessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory && !device.unbridged) {
this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
// Update context
if (!existingAccessory.context || !existingAccessory.context.deviceID) {
this.log.info('Update accessory context:', existingAccessory.displayName);
existingAccessory.context.deviceID = device.id;
this.api.updatePlatformAccessories([existingAccessory]);
}
// create the accessory handler for the restored accessory
const handler = AccessoryFactory_1.default.createAccessory(this, existingAccessory, device);
this.accessoryHandlers.push(handler);
const index = this.cachedAccessories.indexOf(existingAccessory);
if (index >= 0) {
this.cachedAccessories.splice(index, 1);
}
}
else {
// the accessory does not yet exist, so we need to create it
this.log.info('Adding new accessory:', device.name);
// create a new accessory
const accessory = new this.api.platformAccessory(device.name, uuid);
accessory.context.deviceID = device.id;
// create the accessory handler for the newly create accessory
const handler = AccessoryFactory_1.default.createAccessory(this, accessory, device);
this.accessoryHandlers.push(handler);
// link the accessory to your platform
if (device.unbridged) {
this.api.publishExternalAccessories(settings_1.PLUGIN_NAME, [accessory]);
}
else {
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
}
}
}
updateAccessoryInfo(device, info) {
const handler = this.getAccessoryHandler(device.id);
if (!handler) {
return;
}
// this.log.debug('onDeviceInfoUpdate devId = %s, status = %o}', device.id, info);
handler.onDeviceInfoUpdate(info);
}
updateAccessoryStatus(device, status) {
const handler = this.getAccessoryHandler(device.id);
if (!handler) {
return;
}
// this.log.debug('onDeviceStatusUpdate devId = %s, status = %o}', device.id, status);
handler.onDeviceStatusUpdate(status);
}
removeAccessory(deviceID) {
const handler = this.getAccessoryHandler(deviceID);
if (!handler) {
return;
}
const index = this.accessoryHandlers.indexOf(handler);
if (index >= 0) {
this.accessoryHandlers.splice(index, 1);
}
this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [handler.accessory]);
this.log.info('Removing existing accessory from cache:', handler.accessory.displayName);
}
getAccessoryHandler(deviceID) {
return this.accessoryHandlers.find(handler => handler.device.id === deviceID);
}
}
exports.TuyaPlatform = TuyaPlatform;
//# sourceMappingURL=platform.js.map