UNPKG

@uboness/homebridge-unifi-access

Version:
194 lines 8.34 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UnifiAccessClient = void 0; const node_events_1 = __importDefault(require("node:events")); const undici_1 = require("undici"); const common_1 = require("./common"); // we'll use this agent to connect to unifi access such that we'll ignore their self-signed certs. const agent = new undici_1.Agent({ connect: { rejectUnauthorized: false } }); class UnifiAccessClient { constructor(config, logger) { this.emitter = new node_events_1.default(); this._state = 'init'; this.timeouts = [1, 1, 2, 3, 5, 8, 13, 21, 34]; this.config = config; this.logger = logger.getLogger('client'); this.emitter.setMaxListeners(1000); this.emitter.on('connect', () => { this.logger.info('connected'); }); } get connected() { return this._state === 'connected'; } async start() { this._state = 'starting'; await this.connect(0, 5); } async close() { var _a; this._state = 'closing'; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.close(); this._state = 'closed'; this.emitter.emit('close'); } timeout(attempt) { return this.timeouts.length > (attempt) ? this.timeouts[attempt] : this.timeouts[this.timeouts.length - 1]; } async connect(attempt, maxAttempts) { if (attempt + 1 === maxAttempts) { return Promise.reject('Failed to connect'); } return new Promise((resolve, reject) => { const url = this.wsUrl(); if (attempt > 0) { this.logger.warn(`reconnecting to [${url}] (attempt: ${attempt + 1})...`); } else { this.logger.debug(`connecting to [${url}]`); } this.socket = new undici_1.WebSocket(url, { dispatcher: agent, headers: { Authorization: `Bearer ${this.config.token}` } }); this.socket.addEventListener('message', msg => { var _a, _b, _c, _d, _e, _f; const { event, data } = JSON.parse(msg.data); switch (event) { case 'access.data.v2.location.update': if (data.location_type === 'door') { this.emitter.emit('message', { type: 'door-update', id: data.id, name: data.name, locked: ((_a = data.state) === null || _a === void 0 ? void 0 : _a.lock) !== undefined ? data.state.lock === 'locked' : undefined, available: (((_b = data.state) === null || _b === void 0 ? void 0 : _b.is_unavailable) === undefined ? true : !data.state.is_unavailable) && ((_d = (_c = data.state) === null || _c === void 0 ? void 0 : _c.enable) !== null && _d !== void 0 ? _d : true) }); } return; case 'access.logs.add': const door = (_f = (_e = data._source) === null || _e === void 0 ? void 0 : _e.target) === null || _f === void 0 ? void 0 : _f.find(location => location.type === 'door'); if (door) { this.emitter.emit('message', { type: 'door-access', door: { id: door.id, name: door.display_name }, actor: { id: data._source.actor.id, type: data._source.actor.type, name: data._source.actor.display_name, auth: data._source.authentication.credential_provider } }); } } }); this.socket.addEventListener('error', (event) => { var _a; this.logger.error(`Webhook socket init error`, event); (_a = this.socket) === null || _a === void 0 ? void 0 : _a.close(); // we might not need the code below as we're calling 'close' and that will handle the rejection or reconnection // if (attempt + 1 === maxAttempts) { // reject('Failed to connect'); // return; // } }); this.socket.addEventListener('open', () => { this.logger.info(`connected`); this._state = 'connected'; this.emitter.emit('connect'); resolve(); }); this.socket.addEventListener('close', () => { this.logger.warn(`disconnected`); this.emitter.emit('disconnect'); this.socket = undefined; if (this._state !== 'closing') { const newAttempt = this._state == 'connected' ? 0 : attempt + 1; const newMaxAttempts = this._state === 'starting' ? maxAttempts : Infinity; this._state = 'disconnected'; if (newAttempt + 1 === newMaxAttempts) { reject('Failed to connect'); return; } const timeout = this.timeout(newAttempt); this.logger.info(`Reconnecting in ${timeout} seconds...`); setTimeout(() => { this.connect(newAttempt, newMaxAttempts); }, timeout * 1000); } }); }); } on(event, handler) { this.emitter.on(event, handler); return { detach: () => this.emitter.off(event, handler) }; } async listDevices() { return await this.listDoors(); } async listDoors() { const resp = await this.rest('get', '/doors'); return resp.map(door => ({ type: 'door', id: door.id, model: 'door', name: door.name, locked: door.door_lock_relay_status === 'lock', position: (0, common_1.isNil)(door.door_position_status) || door.door_position_status === 'none' ? undefined : door.door_position_status })); } async unlockDoor(id) { await this.rest('put', `/doors/${id}/unlock`); } async identifyDevice(type, id) { this.logger.warn(`At the moment, Unifi Access API doesn't expose the identify functionality`); } async rest(method, endpoint, reqBody) { const url = this.restUrl(endpoint); const resp = await (0, undici_1.fetch)(url, { dispatcher: agent, method, headers: { 'Authorization': `Bearer ${this.config.token}`, 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: reqBody ? JSON.stringify(reqBody) : undefined }); if (!resp.ok) { throw new Error(`Failed to fetch [${endpoint}]. ${resp.status}: ${resp.statusText}`); } const respBody = (await resp.json()); if (respBody.code !== 'SUCCESS') { throw new UnifiError(`Failed to fetch [${endpoint}]. ${respBody.message} [code: ${respBody.code}]`, respBody.code); } return respBody.data; } restUrl(endpoint) { return `https://${this.config.host}:${this.config.port}/api/v1/developer${endpoint}`; } wsUrl() { return `wss://${this.config.host}:${this.config.port}/api/v1/developer/devices/notifications`; } } exports.UnifiAccessClient = UnifiAccessClient; class UnifiError extends Error { constructor(message, code, cause) { super(message); this.code = code; this.cause = cause; } } //# sourceMappingURL=UnifiAccessClient.js.map