homebridge-config-ui-x
Version:
A web based management, configuration and control platform for Homebridge.
528 lines • 28 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ServerService = void 0;
const node_buffer_1 = require("node:buffer");
const node_child_process_1 = require("node:child_process");
const node_path_1 = require("node:path");
const node_process_1 = __importDefault(require("node:process"));
const node_stream_1 = require("node:stream");
const node_util_1 = require("node:util");
const hap_types_1 = require("@homebridge/hap-client/dist/hap-types");
const common_1 = require("@nestjs/common");
const fs_extra_1 = require("fs-extra");
const node_cache_1 = __importDefault(require("node-cache"));
const systeminformation_1 = require("systeminformation");
const tcp_port_used_1 = require("tcp-port-used");
const config_service_1 = require("../../core/config/config.service");
const homebridge_ipc_service_1 = require("../../core/homebridge-ipc/homebridge-ipc.service");
const logger_service_1 = require("../../core/logger/logger.service");
const accessories_service_1 = require("../accessories/accessories.service");
const config_editor_service_1 = require("../config-editor/config-editor.service");
const pump = (0, node_util_1.promisify)(node_stream_1.pipeline);
let ServerService = class ServerService {
constructor(configService, configEditorService, accessoriesService, homebridgeIpcService, logger) {
this.configService = configService;
this.configEditorService = configEditorService;
this.accessoriesService = accessoriesService;
this.homebridgeIpcService = homebridgeIpcService;
this.logger = logger;
this.serverServiceCache = new node_cache_1.default({ stdTTL: 300 });
this.setupCode = null;
this.paired = false;
this.accessoryId = this.configService.homebridgeConfig.bridge.username.split(':').join('');
this.accessoryInfoPath = (0, node_path_1.join)(this.configService.storagePath, 'persist', `AccessoryInfo.${this.accessoryId}.json`);
}
async deleteSingleDeviceAccessories(id, cachedAccessoriesDir) {
const cachedAccessories = (0, node_path_1.join)(cachedAccessoriesDir, `cachedAccessories.${id}`);
const cachedAccessoriesBackup = (0, node_path_1.join)(cachedAccessoriesDir, `.cachedAccessories.${id}.bak`);
if (await (0, fs_extra_1.pathExists)(cachedAccessories)) {
await (0, fs_extra_1.unlink)(cachedAccessories);
this.logger.warn(`Bridge ${id} accessory removal: removed ${cachedAccessories}.`);
}
if (await (0, fs_extra_1.pathExists)(cachedAccessoriesBackup)) {
await (0, fs_extra_1.unlink)(cachedAccessoriesBackup);
this.logger.warn(`Bridge ${id} accessory removal: removed ${cachedAccessoriesBackup}.`);
}
}
async deleteSingleDevicePairing(id, resetPairingInfo) {
const persistPath = (0, node_path_1.join)(this.configService.storagePath, 'persist');
const accessoryInfo = (0, node_path_1.join)(persistPath, `AccessoryInfo.${id}.json`);
const identifierCache = (0, node_path_1.join)(persistPath, `IdentifierCache.${id}.json`);
if (resetPairingInfo) {
try {
const configFile = await this.configEditorService.getConfigFile();
const username = id.match(/.{1,2}/g).join(':');
const pluginBlocks = configFile.accessories
.concat(configFile.platforms)
.concat([{ _bridge: configFile.bridge }])
.filter((block) => block._bridge?.username?.toUpperCase() === username.toUpperCase());
const pluginBlock = pluginBlocks.find((block) => block._bridge?.port);
const otherBlocks = pluginBlocks.filter((block) => !block._bridge?.port);
if (pluginBlock) {
pluginBlock._bridge.username = this.configEditorService.generateUsername();
pluginBlock._bridge.pin = this.configEditorService.generatePin();
otherBlocks.forEach((block) => {
block._bridge.username = pluginBlock._bridge.username;
});
this.logger.warn(`Bridge ${id} reset: new username: ${pluginBlock._bridge.username} and new pin: ${pluginBlock._bridge.pin}.`);
await this.configEditorService.updateConfigFile(configFile);
}
else {
this.logger.error(`Failed to reset username and pin for child bridge ${id} as the plugin block could not be found.`);
}
}
catch (e) {
this.logger.error(`Failed to reset username and pin for child bridge ${id} as ${e.message}.`);
}
}
if (await (0, fs_extra_1.pathExists)(accessoryInfo)) {
await (0, fs_extra_1.unlink)(accessoryInfo);
this.logger.warn(`Bridge ${id} reset: removed ${accessoryInfo}.`);
}
if (await (0, fs_extra_1.pathExists)(identifierCache)) {
await (0, fs_extra_1.unlink)(identifierCache);
this.logger.warn(`Bridge ${id} reset: removed ${identifierCache}.`);
}
await this.deleteDeviceAccessories(id);
}
async restartServer() {
this.logger.log('Homebridge restart request received.');
if (this.configService.serviceMode && !(await this.configService.uiRestartRequired() || await this.nodeVersionChanged())) {
this.logger.log('UI/Bridge settings have not changed - only restarting Homebridge process.');
this.homebridgeIpcService.restartHomebridge();
this.accessoriesService.resetInstancePool();
return { ok: true, command: 'SIGTERM', restartingUI: false };
}
setTimeout(() => {
if (this.configService.ui.restart) {
this.logger.log(`Executing restart command ${this.configService.ui.restart}.`);
(0, node_child_process_1.exec)(this.configService.ui.restart, (err) => {
if (err) {
this.logger.log('Restart command exited with an error, failed to restart Homebridge.');
}
});
}
else {
this.logger.log('Sending SIGTERM to process...');
node_process_1.default.kill(node_process_1.default.pid, 'SIGTERM');
}
}, 500);
return { ok: true, command: this.configService.ui.restart, restartingUI: true };
}
async resetHomebridgeAccessory() {
this.configService.hbServiceUiRestartRequired = true;
const configFile = await this.configEditorService.getConfigFile();
configFile.bridge.pin = this.configEditorService.generatePin();
configFile.bridge.username = this.configEditorService.generateUsername();
this.logger.warn(`Homebridge bridge reset: new username ${configFile.bridge.username} and new pin ${configFile.bridge.pin}.`);
await this.configEditorService.updateConfigFile(configFile);
await (0, fs_extra_1.remove)((0, node_path_1.resolve)(this.configService.storagePath, 'accessories'));
await (0, fs_extra_1.remove)((0, node_path_1.resolve)(this.configService.storagePath, 'persist'));
this.logger.log('Homebridge bridge reset: accessories and persist directories were removed.');
}
async getDevicePairings() {
const persistPath = (0, node_path_1.join)(this.configService.storagePath, 'persist');
const devices = (await (0, fs_extra_1.readdir)(persistPath))
.filter(x => x.match(/AccessoryInfo\.([A-F,a-f0-9]+)\.json/));
const configFile = await this.configEditorService.getConfigFile();
return Promise.all(devices.map(async (x) => {
return await this.getDevicePairingById(x.split('.')[1], configFile);
}));
}
async getDevicePairingById(deviceId, configFile = null) {
const persistPath = (0, node_path_1.join)(this.configService.storagePath, 'persist');
let device;
try {
device = await (0, fs_extra_1.readJson)((0, node_path_1.join)(persistPath, `AccessoryInfo.${deviceId}.json`));
}
catch (e) {
throw new common_1.NotFoundException();
}
if (!configFile) {
configFile = await this.configEditorService.getConfigFile();
}
const username = deviceId.match(/.{1,2}/g).join(':');
const isMain = this.configService.homebridgeConfig.bridge.username.toUpperCase() === username.toUpperCase();
const pluginBlock = configFile.accessories
.concat(configFile.platforms)
.concat([{ _bridge: configFile.bridge }])
.find((block) => block._bridge?.username?.toUpperCase() === username.toUpperCase());
try {
device._category = Object.entries(hap_types_1.Categories).find(([, value]) => value === device.category)[0].toLowerCase();
}
catch (e) {
device._category = 'Other';
}
device.name = pluginBlock?._bridge.name || pluginBlock?.name || device.displayName;
device._id = deviceId;
device._username = username;
device._main = isMain;
device._isPaired = device.pairedClients && Object.keys(device.pairedClients).length > 0;
device._setupCode = this.generateSetupCode(device);
device._couldBeStale = !device._main && device._category === 'bridge' && !pluginBlock;
delete device.signSk;
delete device.signPk;
delete device.configHash;
delete device.pairedClients;
delete device.pairedClientsPermission;
return device;
}
async deleteDevicePairing(id, resetPairingInfo) {
if (!this.configService.serviceMode) {
this.logger.error('The reset paired bridge command is only available in service mode.');
throw new common_1.BadRequestException('This command is only available in service mode.');
}
this.logger.warn(`Shutting down Homebridge before resetting paired bridge ${id}...`);
await this.homebridgeIpcService.restartAndWaitForClose();
await this.deleteSingleDevicePairing(id, resetPairingInfo);
return { ok: true };
}
async deleteDevicesPairing(bridges) {
if (!this.configService.serviceMode) {
this.logger.error('The reset multiple paired bridges command is only available in service mode.');
throw new common_1.BadRequestException('This command is only available in service mode.');
}
this.logger.warn(`Shutting down Homebridge before resetting paired bridges ${bridges.map(x => x.id).join(', ')}...`);
await this.homebridgeIpcService.restartAndWaitForClose();
for (const { id, resetPairingInfo } of bridges) {
try {
await this.deleteSingleDevicePairing(id, resetPairingInfo);
}
catch (e) {
this.logger.error(`Failed to reset paired bridge ${id} as ${e.message}.`);
}
}
return { ok: true };
}
async deleteDeviceAccessories(id) {
if (!this.configService.serviceMode) {
this.logger.error('The remove bridge\'s accessories command is only available in service mode.');
throw new common_1.BadRequestException('This command is only available in service mode.');
}
this.logger.warn(`Shutting down Homebridge before removing accessories for paired bridge ${id}...`);
await this.homebridgeIpcService.restartAndWaitForClose();
const cachedAccessoriesDir = (0, node_path_1.join)(this.configService.storagePath, 'accessories');
await this.deleteSingleDeviceAccessories(id, cachedAccessoriesDir);
}
async deleteDevicesAccessories(bridges) {
if (!this.configService.serviceMode) {
this.logger.error('The remove bridges\' accessories command is only available in service mode.');
throw new common_1.BadRequestException('This command is only available in service mode.');
}
this.logger.warn(`Shutting down Homebridge before removing accessories for paired bridges ${bridges.map(x => x.id).join(', ')}...`);
await this.homebridgeIpcService.restartAndWaitForClose();
const cachedAccessoriesDir = (0, node_path_1.join)(this.configService.storagePath, 'accessories');
for (const { id } of bridges) {
try {
await this.deleteSingleDeviceAccessories(id, cachedAccessoriesDir);
}
catch (e) {
this.logger.error(`Failed to remove accessories for bridge ${id} as ${e.message}.`);
}
}
}
async getCachedAccessories() {
const cachedAccessoriesDir = (0, node_path_1.join)(this.configService.storagePath, 'accessories');
const cachedAccessoryFiles = (await (0, fs_extra_1.readdir)(cachedAccessoriesDir))
.filter(x => x.match(/^cachedAccessories\.([A-F,0-9]+)$/) || x === 'cachedAccessories');
const cachedAccessories = [];
await Promise.all(cachedAccessoryFiles.map(async (x) => {
const accessories = await (0, fs_extra_1.readJson)((0, node_path_1.join)(cachedAccessoriesDir, x));
for (const accessory of accessories) {
accessory.$cacheFile = x;
cachedAccessories.push(accessory);
}
}));
return cachedAccessories;
}
async deleteCachedAccessory(uuid, cacheFile) {
if (!this.configService.serviceMode) {
this.logger.error('The remove cached accessory command is only available in service mode.');
throw new common_1.BadRequestException('This command is only available in service mode.');
}
cacheFile = cacheFile || 'cachedAccessories';
const cachedAccessoriesPath = (0, node_path_1.resolve)(this.configService.storagePath, 'accessories', cacheFile);
this.logger.warn(`Shutting down Homebridge before removing cached accessory ${uuid}...`);
await this.homebridgeIpcService.restartAndWaitForClose();
const cachedAccessories = await (0, fs_extra_1.readJson)(cachedAccessoriesPath);
const accessoryIndex = cachedAccessories.findIndex(x => x.UUID === uuid);
if (accessoryIndex > -1) {
cachedAccessories.splice(accessoryIndex, 1);
await (0, fs_extra_1.writeJson)(cachedAccessoriesPath, cachedAccessories);
this.logger.warn(`Removed cached accessory with UUID ${uuid} from file ${cacheFile}.`);
}
else {
this.logger.error(`Cannot find cached accessory with UUID ${uuid} from file ${cacheFile}.`);
throw new common_1.NotFoundException();
}
return { ok: true };
}
async deleteCachedAccessories(accessories) {
if (!this.configService.serviceMode) {
this.logger.error('The remove cached accessories command is only available in service mode.');
throw new common_1.BadRequestException('This command is only available in service mode.');
}
this.logger.warn(`Shutting down Homebridge before removing cached accessories ${accessories.map(x => x.uuid).join(', ')}.`);
await this.homebridgeIpcService.restartAndWaitForClose();
const accessoriesByCacheFile = new Map();
for (const { cacheFile, uuid } of accessories) {
const accessoryCacheFile = cacheFile || 'cachedAccessories';
if (!accessoriesByCacheFile.has(accessoryCacheFile)) {
accessoriesByCacheFile.set(accessoryCacheFile, []);
}
accessoriesByCacheFile.get(accessoryCacheFile).push({ uuid });
}
for (const [cacheFile, accessories] of accessoriesByCacheFile.entries()) {
const cachedAccessoriesPath = (0, node_path_1.resolve)(this.configService.storagePath, 'accessories', cacheFile);
const cachedAccessories = await (0, fs_extra_1.readJson)(cachedAccessoriesPath);
for (const { uuid } of accessories) {
try {
const accessoryIndex = cachedAccessories.findIndex(x => x.UUID === uuid);
if (accessoryIndex > -1) {
cachedAccessories.splice(accessoryIndex, 1);
this.logger.warn(`Removed cached accessory with UUID ${uuid} from file ${cacheFile}.`);
}
else {
this.logger.error(`Cannot find cached accessory with UUID ${uuid} from file ${cacheFile}.`);
}
}
catch (e) {
this.logger.error(`Failed to remove cached accessory with UUID ${uuid} from file ${cacheFile} as ${e.message}.`);
}
}
await (0, fs_extra_1.writeJson)(cachedAccessoriesPath, cachedAccessories);
}
return { ok: true };
}
async deleteAllCachedAccessories() {
if (!this.configService.serviceMode) {
this.logger.error('The remove all cached accessories command is only available in service mode.');
throw new common_1.BadRequestException('This command is only available in service mode.');
}
const cachedAccessoriesDir = (0, node_path_1.join)(this.configService.storagePath, 'accessories');
const cachedAccessoryPaths = (await (0, fs_extra_1.readdir)(cachedAccessoriesDir))
.filter(x => x.match(/cachedAccessories\.([A-F,0-9]+)/) || x === 'cachedAccessories' || x === '.cachedAccessories.bak')
.map(x => (0, node_path_1.resolve)(cachedAccessoriesDir, x));
const cachedAccessoriesPath = (0, node_path_1.resolve)(this.configService.storagePath, 'accessories', 'cachedAccessories');
await this.homebridgeIpcService.restartAndWaitForClose();
this.logger.warn('Shutting down Homebridge before removing cached accessories');
try {
this.logger.log('Clearing all cached accessories...');
for (const thisCachedAccessoriesPath of cachedAccessoryPaths) {
if (await (0, fs_extra_1.pathExists)(thisCachedAccessoriesPath)) {
await (0, fs_extra_1.unlink)(thisCachedAccessoriesPath);
this.logger.warn(`Removed ${thisCachedAccessoriesPath}.`);
}
}
}
catch (e) {
this.logger.error(`Failed to clear all cached accessories at ${cachedAccessoriesPath} as ${e.message}.`);
console.error(e);
throw new common_1.InternalServerErrorException('Failed to clear Homebridge accessory cache - see logs.');
}
return { ok: true };
}
async getSetupCode() {
if (this.setupCode) {
return this.setupCode;
}
else {
if (!await (0, fs_extra_1.pathExists)(this.accessoryInfoPath)) {
return null;
}
const accessoryInfo = await (0, fs_extra_1.readJson)(this.accessoryInfoPath);
this.setupCode = this.generateSetupCode(accessoryInfo);
return this.setupCode;
}
}
generateSetupCode(accessoryInfo) {
const buffer = node_buffer_1.Buffer.allocUnsafe(8);
let valueLow = Number.parseInt(accessoryInfo.pincode.replace(/-/g, ''), 10);
const valueHigh = accessoryInfo.category >> 1;
valueLow |= 1 << 28;
buffer.writeUInt32BE(valueLow, 4);
if (accessoryInfo.category & 1) {
buffer[4] = buffer[4] | 1 << 7;
}
buffer.writeUInt32BE(valueHigh, 0);
let encodedPayload = (buffer.readUInt32BE(4) + (buffer.readUInt32BE(0) * 2 ** 32)).toString(36).toUpperCase();
if (encodedPayload.length !== 9) {
for (let i = 0; i <= 9 - encodedPayload.length; i++) {
encodedPayload = `0${encodedPayload}`;
}
}
return `X-HM://${encodedPayload}${accessoryInfo.setupID}`;
}
async getBridgePairingInformation() {
if (!await (0, fs_extra_1.pathExists)(this.accessoryInfoPath)) {
return new common_1.ServiceUnavailableException('Pairing Information Not Available Yet');
}
const accessoryInfo = await (0, fs_extra_1.readJson)(this.accessoryInfoPath);
return {
displayName: accessoryInfo.displayName,
pincode: accessoryInfo.pincode,
setupCode: await this.getSetupCode(),
isPaired: accessoryInfo.pairedClients && Object.keys(accessoryInfo.pairedClients).length > 0,
};
}
async getSystemNetworkInterfaces() {
const fromCache = this.serverServiceCache.get('network-interfaces');
const interfaces = fromCache || (await (0, systeminformation_1.networkInterfaces)()).filter((adapter) => {
return !adapter.internal
&& (adapter.ip4 || (adapter.ip6));
});
if (!fromCache) {
this.serverServiceCache.set('network-interfaces', interfaces);
}
return interfaces;
}
async getHomebridgeNetworkInterfaces() {
const config = await this.configEditorService.getConfigFile();
if (!config.bridge?.bind) {
return [];
}
if (Array.isArray(config.bridge?.bind)) {
return config.bridge.bind;
}
if (typeof config.bridge?.bind === 'string') {
return [config.bridge.bind];
}
return [];
}
async getHomebridgeMdnsSetting() {
const config = await this.configEditorService.getConfigFile();
if (!config.bridge.advertiser) {
config.bridge.advertiser = 'bonjour-hap';
}
return {
advertiser: config.bridge.advertiser,
};
}
async setHomebridgeMdnsSetting(setting) {
const config = await this.configEditorService.getConfigFile();
config.bridge.advertiser = setting.advertiser;
await this.configEditorService.updateConfigFile(config);
}
async setHomebridgeNetworkInterfaces(adapters) {
const config = await this.configEditorService.getConfigFile();
if (!config.bridge) {
config.bridge = {};
}
if (!adapters.length) {
delete config.bridge.bind;
}
else {
config.bridge.bind = adapters;
}
await this.configEditorService.updateConfigFile(config);
}
async lookupUnusedPort() {
const randomPort = () => Math.floor(Math.random() * (60000 - 30000 + 1) + 30000);
let port = randomPort();
while (await (0, tcp_port_used_1.check)(port)) {
port = randomPort();
}
return { port };
}
async getHomebridgePort() {
const config = await this.configEditorService.getConfigFile();
return { port: config.bridge.port };
}
async setHomebridgeName(name) {
if (!name || !(/^[\p{L}\p{N}][\p{L}\p{N} ']*[\p{L}\p{N}]$/u).test(name)) {
throw new common_1.BadRequestException('Invalid name');
}
const config = await this.configEditorService.getConfigFile();
config.bridge.name = name;
await this.configEditorService.updateConfigFile(config);
}
async setHomebridgePort(port) {
if (!port || typeof port !== 'number' || !Number.isInteger(port) || port < 1025 || port > 65533) {
throw new common_1.BadRequestException('Invalid port number');
}
const config = await this.configEditorService.getConfigFile();
config.bridge.port = port;
await this.configEditorService.updateConfigFile(config);
}
async nodeVersionChanged() {
return new Promise((res) => {
let result = false;
const child = (0, node_child_process_1.spawn)(node_process_1.default.execPath, ['-v'], { shell: true });
child.stdout.once('data', (data) => {
result = data.toString().trim() !== node_process_1.default.version;
});
child.on('error', () => {
result = true;
});
child.on('close', () => {
return res(result);
});
});
}
async uploadWallpaper(data) {
const configFile = await this.configEditorService.getConfigFile();
const uiConfigBlock = configFile.platforms.find(x => x.platform === 'config');
if (uiConfigBlock) {
if (uiConfigBlock.wallpaper) {
const oldPath = (0, node_path_1.join)(this.configService.storagePath, uiConfigBlock.wallpaper);
if (await (0, fs_extra_1.pathExists)(oldPath)) {
try {
await (0, fs_extra_1.unlink)(oldPath);
this.logger.log(`Old wallpaper file ${oldPath} deleted successfully.`);
}
catch (e) {
this.logger.error(`Failed to delete old wallpaper ${oldPath} as ${e.message}.`);
}
}
}
const fileExtension = (0, node_path_1.extname)(data.filename);
const newPath = (0, node_path_1.join)(this.configService.storagePath, `ui-wallpaper${fileExtension}`);
await pump(data.file, (0, fs_extra_1.createWriteStream)(newPath));
uiConfigBlock.wallpaper = `ui-wallpaper${fileExtension}`;
await this.configEditorService.updateConfigFile(configFile);
this.logger.log('Wallpaper uploaded and set in the config file.');
}
}
async deleteWallpaper() {
const configFile = await this.configEditorService.getConfigFile();
const uiConfigBlock = configFile.platforms.find(x => x.platform === 'config');
const fullPath = (0, node_path_1.join)(this.configService.storagePath, uiConfigBlock.wallpaper);
if (uiConfigBlock && uiConfigBlock.wallpaper) {
if (await (0, fs_extra_1.pathExists)(fullPath)) {
try {
await (0, fs_extra_1.unlink)(fullPath);
this.logger.log(`Wallpaper file ${uiConfigBlock.wallpaper} deleted successfully.`);
}
catch (e) {
this.logger.error(`Failed to delete wallpaper file (${uiConfigBlock.wallpaper}) as ${e.message}.`);
}
}
delete uiConfigBlock.wallpaper;
await this.configEditorService.updateConfigFile(configFile);
this.configService.removeWallpaperCache();
this.logger.log('Wallpaper reference removed from the config file.');
}
}
};
exports.ServerService = ServerService;
exports.ServerService = ServerService = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [config_service_1.ConfigService,
config_editor_service_1.ConfigEditorService,
accessories_service_1.AccessoriesService,
homebridge_ipc_service_1.HomebridgeIpcService,
logger_service_1.Logger])
], ServerService);
//# sourceMappingURL=server.service.js.map
;