@homebridge-plugins/homebridge-aladdin-connect
Version:
Aladdin Connect plugin for Homebridge.
262 lines • 12.9 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 (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