UNPKG

@homebridge-plugins/homebridge-aladdin-connect

Version:
262 lines 12.9 kB
"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.AladdinConnect = exports.AladdinDesiredDoorStatus = exports.AladdinDoorStatus = void 0; const cache_manager_1 = require("cache-manager"); const keyv_1 = require("keyv"); const cacheable_1 = require("cacheable"); const https = __importStar(require("https")); const axios_1 = __importDefault(require("axios")); const axios_retry_1 = __importDefault(require("axios-retry")); const pubsub_js_1 = __importDefault(require("pubsub-js")); const async_lock_1 = __importDefault(require("async-lock")); const crypto_1 = require("crypto"); var AladdinDoorStatus; (function (AladdinDoorStatus) { AladdinDoorStatus[AladdinDoorStatus["UNKNOWN"] = 0] = "UNKNOWN"; AladdinDoorStatus[AladdinDoorStatus["OPEN"] = 1] = "OPEN"; AladdinDoorStatus[AladdinDoorStatus["OPENING"] = 2] = "OPENING"; AladdinDoorStatus[AladdinDoorStatus["TIMEOUT_OPENING"] = 3] = "TIMEOUT_OPENING"; AladdinDoorStatus[AladdinDoorStatus["CLOSED"] = 4] = "CLOSED"; AladdinDoorStatus[AladdinDoorStatus["CLOSING"] = 5] = "CLOSING"; AladdinDoorStatus[AladdinDoorStatus["TIMEOUT_CLOSING"] = 6] = "TIMEOUT_CLOSING"; AladdinDoorStatus[AladdinDoorStatus["NOT_CONFIGURED"] = 7] = "NOT_CONFIGURED"; })(AladdinDoorStatus || (exports.AladdinDoorStatus = AladdinDoorStatus = {})); var AladdinDesiredDoorStatus; (function (AladdinDesiredDoorStatus) { AladdinDesiredDoorStatus[AladdinDesiredDoorStatus["CLOSED"] = 0] = "CLOSED"; AladdinDesiredDoorStatus[AladdinDesiredDoorStatus["OPEN"] = 1] = "OPEN"; AladdinDesiredDoorStatus[AladdinDesiredDoorStatus["NONE"] = 99] = "NONE"; })(AladdinDesiredDoorStatus || (exports.AladdinDesiredDoorStatus = AladdinDesiredDoorStatus = {})); var AladdinLink; (function (AladdinLink) { // noinspection JSUnusedGlobalSymbols AladdinLink[AladdinLink["UNKNOWN"] = 0] = "UNKNOWN"; AladdinLink[AladdinLink["NOT_CONFIGURED"] = 1] = "NOT_CONFIGURED"; AladdinLink[AladdinLink["PAIRED"] = 2] = "PAIRED"; AladdinLink[AladdinLink["CONNECTED"] = 3] = "CONNECTED"; })(AladdinLink || (AladdinLink = {})); class AladdinConnect { log; config; static PUB_SUB_DOOR_STATUS_TOPIC = 'door'; static DOOR_STATUS_STATIONARY_CACHE_TTL_S_DEFAULT = 15; static DOOR_STATUS_STATIONARY_CACHE_TTL_S_MIN = 5; static DOOR_STATUS_STATIONARY_CACHE_TTL_S_MAX = 60; static DOOR_STATUS_TRANSITIONING_CACHE_TTL_S_DEFAULT = 5; static DOOR_STATUS_TRANSITIONING_CACHE_TTL_S_MIN = 1; static DOOR_STATUS_TRANSITIONING_CACHE_TTL_S_MAX = 30; static DOOR_STATUS_POLL_INTERVAL_MS_DEFAULT = 15 * 1000; static DOOR_STATUS_POLL_INTERVAL_MS_MIN = 5 * 1000; static DOOR_STATUS_POLL_INTERVAL_MS_MAX = 60 * 1000; static API_HOST = 'api.smartgarage.systems'; static API_TIMEOUT = 5000; static AUTH_HOST = 'cognito-idp.us-east-2.amazonaws.com'; static AUTH_CLIENT_ID = '27iic8c3bvslqngl3hso83t74b'; static AUTH_CLIENT_SECRET = '7bokto0ep96055k42fnrmuth84k7jdcjablestb7j53o8lp63v5'; static DOOR_STATUS_LOCK = 'DOOR_STATUS'; lock = new async_lock_1.default(); cache; session; constructor(log, config) { this.log = log; this.config = config; const store = new cacheable_1.KeyvCacheableMemory({ ttl: undefined, // No default ttl lruSize: 0, // Infinite capacity }); const keyv = new keyv_1.Keyv({ store }); this.cache = (0, cache_manager_1.createCache)({ stores: [keyv] }); this.session = axios_1.default.create({ httpsAgent: new https.Agent({ keepAlive: true }), timeout: AladdinConnect.API_TIMEOUT, }); (0, axios_retry_1.default)(this.session, { retries: 3, retryCondition: (error) => !error.response || error.response.status >= 400, shouldResetTimeout: true, }); } subscribe(door, func) { const isFirstSubscription = pubsub_js_1.default.countSubscriptions(AladdinConnect.PUB_SUB_DOOR_STATUS_TOPIC) === 0; const token = pubsub_js_1.default.subscribe(AladdinConnect.doorStatusTopic(door), async (_, data) => { if (!data) { return; } func(data); }); this.log.debug('[API] Status subscription added for door %s [token=%s]', door.name, token); // When this is the first subscription, start polling to publish updates. if (isFirstSubscription) { const poll = async () => { // Stop polling when there are no active subscriptions. if (pubsub_js_1.default.countSubscriptions(AladdinConnect.PUB_SUB_DOOR_STATUS_TOPIC) === 0) { this.log.debug('[API] There are no door status subscriptions; skipping poll'); return; } // Acquire the status lock before emitting any new events. this.log.debug('[API] Polling status for all doors'); try { (await this.getAllDoors()).map((doorStatus) => { pubsub_js_1.default.publish(AladdinConnect.doorStatusTopic(doorStatus), doorStatus); }); } catch (error) { // getDoorStatus() logs any errors already. } setTimeout(poll, this.pollIntervalMs); }; setTimeout(poll, 0); } return token; } // noinspection JSUnusedGlobalSymbols unsubscribe(token) { pubsub_js_1.default.unsubscribe(token); this.log.debug('[API] Status subscription removed for token %s', token); } async getAllDoors() { return this.lock.acquire(AladdinConnect.DOOR_STATUS_LOCK, async () => this.cache.wrap('getAllDoors', async () => { let response; try { response = await this.session.get(`https://${AladdinConnect.API_HOST}/devices`, { headers: { Authorization: `Bearer ${await this.getAccessToken()}`, }, }); } catch (error) { if (error instanceof Error) { this.log.error('[API] An error occurred getting devices from account; %s', error.message); } throw error; } if (this.config.logApiResponses) { this.log.debug('[API] Configuration response: %s', JSON.stringify(response.data)); } return response.data.devices.flatMap((device) => device.doors.map((door) => { const name = door?.name || 'Garage Door'; const status = door?.status ?? AladdinDoorStatus.UNKNOWN; const linkStatus = door?.link_status ?? AladdinLink.UNKNOWN; const batteryLevel = door?.battery_level ?? 0; const fault = !!door?.fault; return { deviceId: device.id, id: door.id, index: door.door_index, serialNumber: device.serial_number, name, // The devices that do not have batteries report a level of 0. // I could not identify a better way to figure this out as I only have // non-battery devices. hasBatteryLevel: (door.battery_level ?? 0) > 0, ownership: device.ownership, status: status, batteryPercent: linkStatus === AladdinLink.CONNECTED && batteryLevel > 0 ? batteryLevel : null, fault, }; })); }, (doors) => doors.some((door) => [AladdinDoorStatus.CLOSING, AladdinDoorStatus.OPENING].includes(door?.status ?? AladdinDoorStatus.UNKNOWN)) ? this.doorStatusTransitioningCacheTtl : this.doorStatusStationaryCacheTtl)); } async setDoorStatus(door, desiredStatus) { return this.lock.acquire(AladdinConnect.DOOR_STATUS_LOCK, async () => { const command = desiredStatus === AladdinDesiredDoorStatus.OPEN ? 'OPEN_DOOR' : 'CLOSE_DOOR'; let response; try { response = await this.session.post(`https://${AladdinConnect.API_HOST}/command/devices/${door.deviceId}/doors/${door.index}`, { command, }, { headers: { Authorization: `Bearer ${await this.getAccessToken()}`, }, }); } catch (error) { if (error instanceof Error) { this.log.error('[API] An error occurred sending command %s to door %s; %s', command, door.name, error.message); } throw error; } if (this.config.logApiResponses) { this.log.debug('[API] Genie %s response: %s', command, JSON.stringify(response.data)); } await this.cache.del(AladdinConnect.doorStatusCacheKey(door)); }); } async getAccessToken() { return (await this.cache.wrap('getAccessToken', async () => { let response; try { response = await this.session.post(`https://${AladdinConnect.AUTH_HOST}`, { ClientId: AladdinConnect.AUTH_CLIENT_ID, AuthFlow: 'USER_PASSWORD_AUTH', AuthParameters: { USERNAME: this.config.username, PASSWORD: this.config.password, SECRET_HASH: (0, crypto_1.createHmac)('sha256', AladdinConnect.AUTH_CLIENT_SECRET) .update(this.config.username + AladdinConnect.AUTH_CLIENT_ID) .digest('base64'), }, }, { headers: { 'Content-Type': 'application/x-amz-json-1.1', 'X-Amz-Target': 'AWSCognitoIdentityProviderService.InitiateAuth', }, }); } catch (error) { if (error instanceof Error) { this.log.error('[API] An error occurred getting operator oauth token; %s', error.message); } throw error; } return response.data.AuthenticationResult; }, ({ ExpiresIn: expiresIn }) => expiresIn - 30)).AccessToken; } get doorStatusStationaryCacheTtl() { return Math.max(AladdinConnect.DOOR_STATUS_STATIONARY_CACHE_TTL_S_MIN, Math.min(AladdinConnect.DOOR_STATUS_STATIONARY_CACHE_TTL_S_MAX, this.config.doorStatusStationaryCacheTtl ?? AladdinConnect.DOOR_STATUS_STATIONARY_CACHE_TTL_S_DEFAULT)); } get doorStatusTransitioningCacheTtl() { return Math.max(AladdinConnect.DOOR_STATUS_TRANSITIONING_CACHE_TTL_S_MIN, Math.min(AladdinConnect.DOOR_STATUS_TRANSITIONING_CACHE_TTL_S_MAX, this.config.doorStatusTransitioningCacheTtl ?? AladdinConnect.DOOR_STATUS_TRANSITIONING_CACHE_TTL_S_DEFAULT)); } get pollIntervalMs() { return Math.max(AladdinConnect.DOOR_STATUS_POLL_INTERVAL_MS_MIN, Math.min(AladdinConnect.DOOR_STATUS_POLL_INTERVAL_MS_MAX, this.config.doorStatusPollInterval ?? AladdinConnect.DOOR_STATUS_POLL_INTERVAL_MS_DEFAULT)); } static doorStatusTopic(door) { return `${AladdinConnect.PUB_SUB_DOOR_STATUS_TOPIC}.${door.deviceId}.${door.index}`; } static doorStatusCacheKey(door) { return `${door.deviceId}:${door.index}`; } } exports.AladdinConnect = AladdinConnect; //# sourceMappingURL=aladdinConnect.js.map