@uboness/homebridge-unifi-access
Version:
Homebridge Unifi Access Plugin
194 lines • 8.34 kB
JavaScript
;
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