homebridge-panasonic-ac-platform
Version:
Homebridge platform plugin providing HomeKit support for Panasonic Comfort Cloud devices.
288 lines • 15.7 kB
JavaScript
;
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 () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__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 });
const cheerio = __importStar(require("cheerio"));
const comfort_cloud_1 = __importDefault(require("./comfort-cloud"));
const indoor_unit_1 = __importDefault(require("./indoor-unit"));
const logger_1 = __importDefault(require("./logger"));
const settings_1 = require("./settings");
/**
* Panasonic AC Platform Plugin for Homebridge
* Based on https://github.com/homebridge/homebridge-plugin-template
*/
class PanasonicPlatform {
/**
* 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.noOfFailedLoginAttempts = 0;
this.platformConfig = config;
// Initialise logging utility
this.log = new logger_1.default(homebridgeLogger, this.platformConfig.logsLevel);
// Create Comfort Cloud communication module
this.comfortCloud = new comfort_cloud_1.default(this.platformConfig, this.log);
/**
* 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();
});
}
async configurePlugin() {
this.log.info(`Plugin app version: ${settings_1.APP_VERSION}.`);
if (this.platformConfig.overwriteVersion) {
this.log.info(`Overwrite Version: ${this.platformConfig.overwriteVersion}.`);
}
await this.getAppStoreVersion();
await this.loginAndDiscoverDevices();
}
async getAppStoreVersion() {
this.log.debug('Attempting to fetch latest Comfort Cloud version from the App Store.');
const $ = await cheerio.fromURL('https://apps.apple.com/app/panasonic-comfort-cloud/id1348640525');
const matches = $('p.whats-new__latest__version').first().text().match(/\d+(.)\d+(.)\d+/);
if (Array.isArray(matches)) {
this.log.info(`App Store app version: ${matches[0]}.`);
}
else {
this.log.error('Could not find App Store app version.');
await this.getPlayStoreVersion();
}
}
async getPlayStoreVersion() {
// Version data is not displayed in clear text on the page, but instead included in some cryptic JS function call.
// The function call is `AF_initDataCallback()`, but there are many of them.
// The function call additionally contains (several) references to the app store page for the app in other languages,
// so it also contains the package name which allows us to further narrow it down.
// Finally, the version number is of the format major.minor.patch, surrounded by quotation marks.
this.log.debug('Attempting to fetch latest Comfort Cloud version from the Play Store.');
const $ = await cheerio.fromURL('https://play.google.com/store/apps/details?id=com.panasonic.ACCsmart');
$('script').each((idx, script) => {
const textContent = $(script).text();
const isCallback = textContent.includes('AF_initDataCallback(') && textContent.includes('com.panasonic.ACCsmart');
if (isCallback) {
const matches = textContent.match(/['"](\d+\.\d+\.\d+)['"]/);
if (Array.isArray(matches) && (1 in matches)) {
this.log.info(`Play Store app version: ${matches[1]}.`);
}
}
});
}
async loginAndDiscoverDevices() {
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;
}
this.log.debug('Attempting to log into Comfort Cloud.');
this.comfortCloud.login()
.then(() => {
this.log.info('Successfully logged in to Comfort Cloud.');
this.noOfFailedLoginAttempts = 0;
this.discoverDevices();
})
.catch((error) => {
this.noOfFailedLoginAttempts++;
this.log.error(`Error: ${error.message}`);
if (error.message === 'Request failed with status code 429') {
this.log.error('Too many incorect login attempts '
+ 'or other suspicious activity on the account.'
+ 'You have to wait until Panasonic will unlock the account '
+ '(it may take up to 24 hours) '
+ 'or change IP of Homebridge (restart router). ');
this.log.error('Next login attempt in 8 hours.');
clearTimeout(this._loginRetryTimeout);
this._loginRetryTimeout = setTimeout(this.loginAndDiscoverDevices.bind(this), 28800 * 1000);
}
else if (error.message === 'Request failed with status code 401') {
this.log.error('Incorect login / password or incorect app version.'
+ 'Enter the correct values in the plugin settings and restart.');
this.log.error('Next login attempt in 8 hours.');
clearTimeout(this._loginRetryTimeout);
this._loginRetryTimeout = setTimeout(this.loginAndDiscoverDevices.bind(this), 28800 * 1000);
}
else {
this.log.error('The Comfort Cloud server might be experiencing issues at the moment. '
+ 'If issue persists check Truobleshooting section in plugin homepage.');
const delayMap = new Map([
[1, 300], // 5 min
[2, 1800], // 30 min
[3, 3600], // 60 min
]);
const nextRetryDelay = delayMap.get(this.noOfFailedLoginAttempts) || 28800;
this.log.error(`Next login attempt in ${nextRetryDelay / 60} minutes.`);
clearTimeout(this._loginRetryTimeout);
this._loginRetryTimeout = setTimeout(this.loginAndDiscoverDevices.bind(this), nextRetryDelay * 1000);
}
});
}
/**
* 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);
}
/**
* Fetches all of the user's devices from Comfort Cloud 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() {
var _a;
this.log.debug('Discovering devices on Comfort Cloud.');
try {
// Fetch devices from Comfort Cloud
let cloudDevices = await this.comfortCloud.getDevices();
this.log.info(`Comfort Cloud total devices: ${Object.keys(cloudDevices).length}.`);
this.log.debug(`Comfort Cloud devices: ${JSON.stringify(cloudDevices, null, 2)}`);
// Get devices from plugin configuration
const configDevices = (((_a = this.platformConfig) === null || _a === void 0 ? void 0 : _a.devices) || []).filter(device => device.name && device.name !== '');
// Check if there is at least one device added to plugin config
if (configDevices.length > 0) {
this.log.info(`Plugin config total devices: ${configDevices.length}.`);
this.log.debug(`Plugin config devices: ${JSON.stringify(configDevices, null, 2)}.`);
// Find devices in config that don't exist in Comfort Cloud
const missingDevices = configDevices
.filter(configDevice => cloudDevices.every(cloudDevice => cloudDevice.deviceName !== configDevice.name
&& cloudDevice.deviceGuid !== configDevice.name))
.map(device => device.name);
if (missingDevices.length > 0) {
this.log.info('Devices added to plugin config but not found '
+ `in Comfort Cloud: ${missingDevices.length}. `
+ `Missing devices: ${missingDevices.join(', ')}.`);
}
// Exclude by individual device config
const devicesToExclude = configDevices
.filter(device => device.excludeDevice === true)
.map(device => device.name);
if (devicesToExclude.length > 0) {
cloudDevices = cloudDevices.filter(cloudDevice => !devicesToExclude.includes(cloudDevice.deviceGuid)
&& !devicesToExclude.includes(cloudDevice.deviceName));
this.log.info(`Devices added to plugin config to exclude: ${devicesToExclude.length}. `
+ `Devices to exclude: ${devicesToExclude.join(', ')}.`);
}
}
else {
this.log.info('Plugin config total devices: 0.');
}
// Loop over the discovered (indoor) devices and register each
// one if it has not been registered before.
for (const device of cloudDevices) {
// 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.deviceGuid);
// 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 device '${existingAccessory.displayName}' `
+ `(${device.deviceGuid})(${uuid}) 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 for the restored accessory
new indoor_unit_1.default(this, existingAccessory);
}
else {
this.log.info(`Adding device '${device.deviceName}' (${device.deviceGuid})(${uuid}).`);
// The accessory does not yet exist, so we need to create it
const accessory = new this.api.platformAccessory(device.deviceName, 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
new indoor_unit_1.default(this, accessory);
// Link the accessory to your platform
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
}
}
// At this point, we set up all devices from Comfort Cloud, but we did not unregister
// cached devices that do not exist on the Comfort Cloud account anymore.
for (const cachedAccessory of this.accessories) {
if (cachedAccessory.context.device) {
const guid = cachedAccessory.context.device.deviceGuid;
const cloudDevice = cloudDevices.find(device => device.deviceGuid === guid);
if (cloudDevice === undefined) {
// This cached devices does not exist on the Comfort Cloud account (anymore).
this.log.info(`Removing device '${cachedAccessory.displayName}' (${guid}) `
+ 'because it does not exist on the Comfort Cloud account or has been excluded in plugin config.');
this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [cachedAccessory]);
}
}
}
}
catch (error) {
this.log.error('An error occurred during device discovery. '
+ 'Turn on debug mode for more information.');
this.log.debug(error);
}
}
}
exports.default = PanasonicPlatform;
//# sourceMappingURL=platform.js.map