UNPKG

homebridge-config-ui-x

Version:

A web based management, configuration and control platform for Homebridge.

528 lines • 28 kB
"use strict"; 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