homebridge-jci-hitachi-platform
Version:
Homebridge platform plugin providing HomeKit support for Jci Hitachi air conditioners.
305 lines • 16.6 kB
JavaScript
"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