homebridge-calypshome
Version:
Homebridge plugin for Calyps'Home
171 lines • 6.38 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CalypshomeAPI = exports.getObjectsSchema = void 0;
const events_1 = require("events");
const undici_1 = require("undici");
const zod_1 = require("zod");
const rollingShutter_1 = require("./rollingShutter");
const ws_1 = __importDefault(require("ws")); // can be replaced with native node later
exports.getObjectsSchema = zod_1.z.object({
objects: zod_1.z.array(zod_1.z.object({
room: zod_1.z.object({
id: zod_1.z.number(),
name: zod_1.z.string(),
}),
id: zod_1.z.string(),
status: zod_1.z.array(zod_1.z.object({
value: zod_1.z.string(),
name: zod_1.z.string(),
time: zod_1.z.string(),
})),
categories: zod_1.z.unknown(),
name: zod_1.z.string(),
type: zod_1.z.enum(['Rolling_Shutter', 'Composite', 'EZSP']),
img: zod_1.z.string(),
gw: zod_1.z.string(),
eventId: zod_1.z.string(),
connected: zod_1.z.boolean(),
actions: zod_1.z.array(zod_1.z.enum(['OPEN', 'CLOSE', 'STOP', 'LEVEL', 'TILT', 'SCAN', 'JOIN'])),
})),
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
class CalypshomeAPI extends events_1.EventEmitter {
logger;
url;
wsUrl;
inMemoryDevices = {};
static DEVICE_UPDATE = 'device_update';
constructor(config, logger) {
super();
this.logger = logger;
this.url = config.url;
this.wsUrl = `${config.url.replace('http', 'ws')}/ws`;
}
close() {
this.ws?.close();
}
async devices() {
return this.apiCall(`${this.url}/m?a=getObjects`, {
headers: {
Accept: 'application/json',
},
})
.then((x) => x.body.json())
.then((data) => exports.getObjectsSchema
.parse(data)
.objects.filter((entry) => entry.type === 'Rolling_Shutter')
.map((g) => new rollingShutter_1.RollingShutter(g)))
.then((devices) => {
this.inMemoryDevices = devices.reduce((acc, device) => {
acc[device.id] = device;
return acc;
}, {});
return devices;
});
}
async action(id, action, args) {
return this.apiCall(`${this.url}/m?a=command`, {
body: new URLSearchParams({
id,
action,
args: args ? JSON.stringify(args) : '',
}).toString(),
})
.then((response) => response.statusCode === 200)
.catch((e) => {
this.logger.error('Error in action', { id, action, args, e });
return false;
});
}
apiCall(url, options) {
const ac = new AbortController();
options.method ??= 'POST';
options.signal = ac.signal;
options.dispatcher = new undici_1.Agent({
keepAliveTimeout: 10,
keepAliveMaxTimeout: 10,
});
this.logger.debug(`API call ${url}`, options);
const timer = setTimeout(() => {
ac.abort();
}, 5 * 1000);
return (0, undici_1.request)(url, options)
.then((response) => {
clearTimeout(timer);
this.logger.debug(`API call ${url} response`, {
status: response.statusCode,
});
return response;
})
.catch((e) => {
this.logger.error(`API call ${url}`, e);
throw e;
});
}
device(id) {
return this.inMemoryDevices[id];
}
connectWebSocket() {
this.logger.info('Connecting WebSocket');
// const START_TIMESTAMP = Math.round(new Date().getTime() / 1000);
this.ws = new ws_1.default(this.wsUrl, 'lws-mirror-protocol');
this.ws.on('open', () => {
this.logger.info('WebSocket connected');
this.ws.send('p1 1 _web / login');
});
this.ws.on('close', () => {
this.logger.warn('WebSocket onclose() retrying in 30s');
setTimeout(() => {
this.connectWebSocket();
}, 30000);
});
this.ws.on('error', (event) => {
this.logger.error('WebSocket error', event);
});
this.ws.on('message', (event) => {
this.handleWebSocketMessage(event.toString('utf-8'));
});
}
handleWebSocketMessage(data) {
const [, , src, dest, cmd, rest, b64, value] = data.split(' ');
// decode base64
const message = b64.startsWith('@') ? Buffer.from(b64.substring(1), 'base64').toString() : b64;
const sysmatch = message.match(/^event\/system\/gateway\/dev-\d\/id-self\/(.*)/);
if (message === 'event/ui/web/connect') {
return;
}
if (sysmatch) {
if (['uptime', 'cpu_idle', 'disk_free', 'memory_free', 'load_5', 'system_uptime'].includes(sysmatch[1]) || sysmatch[1].startsWith('gw_')) {
return;
}
this.logger.info('WebSocket system message', data, { match: sysmatch[1], value });
return;
}
const devmatch = message.match(/^event\/io\/ezsp\/dev-\d\/([^/]+)\/(level|angle|status)/);
if (devmatch) {
const [, device, type] = devmatch;
const matchedDevice = Object.values(this.inMemoryDevices).find((d) => d.id.includes(device));
if (matchedDevice && ['level', 'angle', 'status'].includes(type)) {
this.update(matchedDevice, type, value);
return;
}
this.logger.warn('WebSocket dev message', data, { device, type });
return;
}
this.logger.warn('WebSocket unknown message', data, { src, dest, cmd, rest, message, value });
}
update(device, key, value) {
if (key === 'angle') {
device.angle = Number(value);
}
else if (key === 'level') {
device.level = Number(value);
}
this.emit(CalypshomeAPI.DEVICE_UPDATE, device, key);
}
}
exports.CalypshomeAPI = CalypshomeAPI;
//# sourceMappingURL=calypshomeAPI.js.map