UNPKG

homebridge-jci-hitachi-platform

Version:

Homebridge platform plugin providing HomeKit support for Jci Hitachi air conditioners.

305 lines 16.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const jci_hitachi_aws_api_1 = __importDefault(require("./jci-hitachi-aws-api")); const climate_1 = __importDefault(require("./accessories/climate")); const logger_1 = __importDefault(require("./logger")); const settings_1 = require("./settings"); const SUPPORT_DEVICE_TYPE = { CLIMATE: 1, DEHUMIDIFIER: 2, AIR_PURIFIER: 3 }; /** * JciHitachi JciHitachiAWSAPI Platform Plugin for Homebridge * Based on https://github.com/homebridge/homebridge-plugin-template */ class JciHitachiPlatform { /** * This constructor is where you should parse the user config * and discover/register accessories with Homebridge. * * @param logger Homebridge logger * @param config Homebridge platform config * @param api Homebridge API */ constructor(homebridgeLogger, config, api) { this.api = api; this.Service = this.api.hap.Service; this.Characteristic = this.api.hap.Characteristic; // Used to track restored cached accessories this.accessories = []; this.reconnectAttempts = 0; this.jciHitachiAccessoryDict = {}; this.platformConfig = config; // Initialise logging utility this.log = new logger_1.default(homebridgeLogger, this.platformConfig.debugMode); this.jciHitachiAWSAPI = new jci_hitachi_aws_api_1.default(this.platformConfig.email, this.platformConfig.password, this.log); this.jciHitachiAWSAPI.setCallback(this.notifyCallback.bind(this)); /** * 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" /* APIEvent.DID_FINISH_LAUNCHING */, () => { this.log.debug('Finished launching and restored cached accessories.'); this.configurePlugin(); }); // Disconnect from AWS IoT cleanly when Homebridge shuts down or restarts, // instead of leaving the MQTT session to die by keep-alive timeout. this.api.on("shutdown" /* APIEvent.SHUTDOWN */, () => { this.jciHitachiAWSAPI.Logout().catch(() => { }); }); } notifyCallback(thing) { if (this.jciHitachiAWSAPI.isConnected == false) { this.scheduleReconnect(); return; } if (thing === undefined) return; this.log.debug(`NotifyCallback: ${JSON.stringify(thing)}`); const jciHitachiAccessory = this.jciHitachiAccessoryDict[thing.ThingName]; if (jciHitachiAccessory !== undefined) { jciHitachiAccessory.updateStatus(); } else if (this.isSupportedDevice(thing.DeviceType) && !this.isIgnoredDevice(thing.ThingName)) { // Only a supported-but-unregistered device (e.g. an AC added mid-session) // warrants a re-discovery. Unsupported things (dehumidifiers, purifiers) // and user-ignored devices are never registered, so re-discovering on // their pushes would loop forever. this.log.debug(`NotifyCallback: ${thing.ThingName} is not registered.`); this.discoverDevices(); } } async configurePlugin() { await this.loginAndDiscoverDevices(); } async loginAndDiscoverDevices() { var _a, _b; if (!this.platformConfig.email) { this.log.error('Email is not configured - aborting plugin start. ' + 'Please set the field `email` in your config and restart Homebridge.'); return; } if (!this.platformConfig.password) { this.log.error('Password is not configured - aborting plugin start. ' + 'Please set the field `password` in your config and restart Homebridge.'); return; } if (this.jciHitachiAWSAPI === undefined || this.jciHitachiAWSAPI.isLoginFailed == true) { this.log.info('Creating New JciHitachiAWSAPI.'); // Tear down the failed instance before abandoning it. Its mqtt5 client keeps // retrying internally (e.g. AWS_ERROR_HTTP_WEBSOCKET_UPGRADE_FAILURE with stale // credentials never succeeds), so an orphaned instance would spam connect // failures and leak the native client. const previousTokens = (_a = this.jciHitachiAWSAPI) === null || _a === void 0 ? void 0 : _a.aws_tokens; (_b = this.jciHitachiAWSAPI) === null || _b === void 0 ? void 0 : _b.Logout().catch(() => { }); // Create JciHitachiAWSAPI communication module this.jciHitachiAWSAPI = new jci_hitachi_aws_api_1.default(this.platformConfig.email, this.platformConfig.password, this.log); // Hand the tokens over so Login() can keep using the refresh token instead // of falling back to a password login on every reconnect attempt. this.jciHitachiAWSAPI.aws_tokens = previousTokens; this.jciHitachiAWSAPI.setCallback(this.notifyCallback.bind(this)); } this.log.info('Attempting to log into JciHitachiAWSAPI.'); this.jciHitachiAWSAPI.Login() .then(() => { if (this.jciHitachiAWSAPI.isConnected) { this.log.info('Successfully logged in.'); // Cancel any retry that got armed by the disconnection event Logout() fires // mid-login, otherwise it would later kick off a spurious reconnect cycle. this.clearReconnect(); this.discoverDevices(); } else { this.log.error('Login failed. Skipping device discovery.'); this.scheduleReconnect(); } }) .catch((error) => { this.log.error('Login failed. Skipping device discovery.'); this.log.debug(error); this.scheduleReconnect(); }); } /** * Schedules a single reconnect attempt with exponential backoff. * * We never stop retrying: when the cloud is down for maintenance the plugin keeps * trying (with a capped delay) and recovers on its own once the service is back. * The pending-timer guard prevents the connect/disconnect storm (issue #11) that * happened when every accessory and every dropped-connection event scheduled its * own retry. */ scheduleReconnect() { if (this._loginRetryTimeout) { // A retry is already pending - don't stack timers. return; } const delay = Math.min(settings_1.LOGIN_RETRY_DELAY * Math.pow(2, this.reconnectAttempts), settings_1.MAX_LOGIN_RETRY_DELAY); this.reconnectAttempts++; this.log.info(`The JciHitachiAWSAPI server might be experiencing issues at the moment. ` + `The plugin will try to log in again in ${Math.round(delay / 1000)} seconds ` + `(attempt ${this.reconnectAttempts}). If the issue persists, make sure you ` + `configured the correct email and password and run the latest version of the plugin.`); this._loginRetryTimeout = setTimeout(() => { this._loginRetryTimeout = undefined; this.loginAndDiscoverDevices(); }, delay); } /** Cancels a pending reconnect timer and resets the backoff counter. */ clearReconnect() { if (this._loginRetryTimeout) { clearTimeout(this._loginRetryTimeout); this._loginRetryTimeout = undefined; } this.reconnectAttempts = 0; } /** * This function is invoked when Homebridge restores cached accessories from disk at startup. * It should be used to set up event handlers for characteristics and update respective values. */ configureAccessory(accessory) { this.log.info(`Loading accessory '${accessory.displayName}' from cache.`); /** * We don't have to set up the handlers here, * because our device discovery function takes care of that. * * But we need to add the restored accessory to the * accessories cache so we can access it during that process. */ this.accessories.push(accessory); } isSupportedDevice(deviceType) { if (deviceType == SUPPORT_DEVICE_TYPE.CLIMATE) { return true; } this.log.debug(`isUnsupportedDevice: ${deviceType}`); return false; } /** True when the user excluded this device from HomeKit via the settings UI. */ isIgnoredDevice(thingName) { var _a; return ((_a = this.platformConfig.ignoredDevices) !== null && _a !== void 0 ? _a : []).includes(thingName); } /** * Fetches all of the user's devices from JciHitachiAWSAPI and sets up handlers. * * Accessories must only be registered once. Previously created accessories * must not be registered again to prevent "duplicate UUID" errors. */ async discoverDevices() { this.log.info('Discovering devices on JciHitachiAWSAPI.'); try { const aws_thing_dict = this.jciHitachiAWSAPI.getDevices(); // Loop over the discovered (indoor) devices and register each // one if it has not been registered before. for (const thingName in aws_thing_dict === null || aws_thing_dict === void 0 ? void 0 : aws_thing_dict.getAllThings()) { const device = aws_thing_dict === null || aws_thing_dict === void 0 ? void 0 : aws_thing_dict.getDevice(thingName); if (device === undefined) { continue; } // Check if the device is supported if (!this.isSupportedDevice(device.DeviceType)) { this.log.info(`Skipping unsupport device '${device.CustomDeviceName}' with ${device.DeviceType}`); continue; } // Check if the user excluded the device via the settings UI if (this.isIgnoredDevice(device.ThingName)) { this.log.info(`Skipping ignored device '${device.CustomDeviceName}' (${device.ThingName}).`); continue; } // Generate a unique id for the accessory. // This should be generated from something globally unique, // but constant, for example, the device serial number or MAC address const uuid = this.api.hap.uuid.generate(device.ThingName); // Check if an accessory with the same uuid has already been registered and restored from // the cached devices we stored in the `configureAccessory` method above. const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); if (existingAccessory !== undefined) { // The accessory already exists this.log.info(`Restoring accessory '${existingAccessory.displayName}' ` + `(${device.ThingName}) from cache.`); // If you need to update the accessory.context then you should run // `api.updatePlatformAccessories`. eg.: existingAccessory.context.device = device; this.api.updatePlatformAccessories([existingAccessory]); // Create the accessory handler only once. Re-discovery (after a // reconnect) must reuse the existing handler, otherwise every run // spawns another ClimateAccessory whose refresh setInterval is // never cleared, multiplying the polling over time. if (this.jciHitachiAccessoryDict[device.ThingName] === undefined) { const jciHitachiAccessory = this.createJciHitachiAccessory(device.DeviceType, this, existingAccessory); if (jciHitachiAccessory !== undefined) { this.jciHitachiAccessoryDict[device.ThingName] = jciHitachiAccessory; } } } else { this.log.info(`Adding accessory '${device.CustomDeviceName}' (${device.ThingName}).`); // The accessory does not yet exist, so we need to create it const accessory = new this.api.platformAccessory(device.CustomDeviceName, uuid); // Store a copy of the device object in the `accessory.context` property, // which can be used to store any data about the accessory you may need. accessory.context.device = device; // Create the accessory handler for the newly create accessory // this is imported from `platformAccessory.ts` let jciHitachiAccessory = this.createJciHitachiAccessory(device.DeviceType, this, accessory); if (jciHitachiAccessory !== undefined) { this.jciHitachiAccessoryDict[device.ThingName] = jciHitachiAccessory; } // Link the accessory to your platform this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]); // Track it so the next discovery run treats it as existing instead of // registering the same UUID again. this.accessories.push(accessory); } } // At this point, we set up all devices from JciHitachiAWSAPI, but we did not unregister // cached devices that do not exist on the JciHitachiAWSAPI account anymore. // Iterate over a copy because removals splice the array. for (const cachedAccessory of [...this.accessories]) { if (cachedAccessory.context.device) { const thingName = cachedAccessory.context.device.ThingName; if (this.jciHitachiAWSAPI.getDevice(thingName) === undefined || this.isIgnoredDevice(thingName)) { // This cached device does not exist on the JciHitachiAWSAPI account // (anymore), or the user excluded it via the settings UI. this.log.info(`Removing accessory '${cachedAccessory.displayName}' (${thingName}) ` + 'because it does not exist on the JciHitachiAWSAPI account (anymore?) or is ignored.'); this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [cachedAccessory]); // Dispose the handler (stops its polling interval) and forget the accessory. // Without this, the next discovery run would unregister it again, and a // device re-added to the account mid-session would be treated as already // registered and never appear in HomeKit until a restart. if (this.jciHitachiAccessoryDict[thingName] !== undefined) { this.jciHitachiAccessoryDict[thingName].dispose(); delete this.jciHitachiAccessoryDict[thingName]; } const index = this.accessories.indexOf(cachedAccessory); if (index >= 0) { this.accessories.splice(index, 1); } } } } } catch (error) { this.log.error('An error occurred during device discovery. ' + 'Turn on debug mode for more information.'); this.log.debug(error); } } createJciHitachiAccessory(deviceType, platform, accessory) { if (deviceType == SUPPORT_DEVICE_TYPE.CLIMATE) { return new climate_1.default(platform, accessory); } this.log.info(`Skipping unsupported deviceType: '${deviceType}' `); return undefined; } } exports.default = JciHitachiPlatform; //# sourceMappingURL=platform.js.map