UNPKG

homebridge-config-ui-x

Version:

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

853 lines • 37.6 kB
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 __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; import { randomInt } from 'node:crypto'; import { copyFile, readdir, readFile, unlink } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import dayjs from 'dayjs'; import { ensureDir, move, pathExists, readJson, remove } from 'fs-extra/esm'; import { gte } from 'semver'; import { ConfigService } from '../../core/config/config.service.js'; import { JsonFileStoreService } from '../../core/fs/json-file-store.service.js'; import { HomebridgeIpcService } from '../../core/homebridge-ipc/homebridge-ipc.service.js'; import { Logger } from '../../core/logger/logger.service.js'; import { RE_COLON, RE_CONFIG_BACKUP, RE_PIN, RE_PLUGIN_NAME, RE_SAFE_RESTART_CMD, RE_USERNAME } from '../../core/regex.constants.js'; import { SchedulerService } from '../../core/scheduler/scheduler.service.js'; import { BackupService } from '../backup/backup.service.js'; import { ChildBridgesService } from '../child-bridges/child-bridges.service.js'; import { PluginsService } from '../plugins/plugins.service.js'; let ConfigEditorService = class ConfigEditorService { logger; configService; schedulerService; pluginsService; homebridgeIpcService; childBridgesService; backupService; jsonStore; ready; resolveReady; constructor(logger, configService, schedulerService, pluginsService, homebridgeIpcService, childBridgesService, backupService, jsonStore) { this.logger = logger; this.configService = configService; this.schedulerService = schedulerService; this.pluginsService = pluginsService; this.homebridgeIpcService = homebridgeIpcService; this.childBridgesService = childBridgesService; this.backupService = backupService; this.jsonStore = jsonStore; this.ready = new Promise((res) => { this.resolveReady = res; }); this.scheduleConfigBackupCleanup(); } async onApplicationBootstrap() { await this.start(); this.resolveReady(); } async start() { await this.ensureBackupPathExists(); await this.migrateConfigBackups(); } scheduleConfigBackupCleanup() { const scheduleRule = new this.schedulerService.RecurrenceRule(); scheduleRule.hour = 1; scheduleRule.minute = 10; scheduleRule.second = Math.floor(Math.random() * 59) + 1; this.logger.debug(`Next config.json backup cleanup scheduled for ${scheduleRule.nextInvocationDate(new Date()).toString()}.`); this.schedulerService.scheduleJob('cleanup-config-backups', scheduleRule, () => { this.logger.log('Running job to cleanup config.json backup files older than 60 days...'); this.cleanupConfigBackups().catch((e) => { this.logger.error(`config.json backup cleanup failed as ${e?.message || e}.`); }); }); } async getConfigFile() { const config = await readJson(this.configService.configPath); if (!config.bridge || typeof config.bridge !== 'object') { config.bridge = {}; } if (!config.accessories || !Array.isArray(config.accessories)) { config.accessories = []; } if (!config.platforms || !Array.isArray(config.platforms)) { config.platforms = []; } return config; } assertRestartCommandsSafe(config) { const newUi = config.platforms?.find(p => p?.platform === 'config'); if (!newUi) { return; } const oldUi = this.configService.ui; const checks = [ { path: 'restart', newValue: newUi.restart, oldValue: oldUi?.restart }, { path: 'linux.restart', newValue: newUi.linux?.restart, oldValue: oldUi?.linux?.restart }, { path: 'linux.shutdown', newValue: newUi.linux?.shutdown, oldValue: oldUi?.linux?.shutdown }, ]; for (const { path, newValue, oldValue } of checks) { if (newValue === oldValue) { continue; } if (newValue === undefined || newValue === null || newValue === '') { continue; } if (typeof newValue !== 'string' || !RE_SAFE_RESTART_CMD.test(newValue)) { throw new BadRequestException(`Refusing to save unsafe restart/shutdown command for "${path}". The command must use systemctl, service, shutdown, reboot, poweroff, halt, or init, optionally prefixed with sudo, and may not contain shell metacharacters.`); } } } async updateConfigFile(config) { await this.ready; const now = new Date(); if (!config) { config = {}; } if (!config.bridge) { config.bridge = {}; } if (typeof config.bridge.port === 'string') { config.bridge.port = Number.parseInt(config.bridge.port, 10); } if (!config.bridge.port || typeof config.bridge.port !== 'number' || config.bridge.port > 65533 || config.bridge.port < 1025) { config.bridge.port = Math.floor(Math.random() * (52000 - 51000 + 1) + 51000); } if (!config.bridge.username) { config.bridge.username = this.generateUsername(); } if (!RE_USERNAME.test(config.bridge.username)) { if (RE_USERNAME.test(this.configService.homebridgeConfig.bridge.username)) { config.bridge.username = this.configService.homebridgeConfig.bridge.username; } else { config.bridge.username = this.generateUsername(); } } if (!config.bridge.pin) { config.bridge.pin = this.generatePin(); } if (!RE_PIN.test(config.bridge.pin)) { if (RE_PIN.test(this.configService.homebridgeConfig.bridge.pin)) { config.bridge.pin = this.configService.homebridgeConfig.bridge.pin; } else { config.bridge.pin = this.generatePin(); } } if (!config.bridge.name || typeof config.bridge.name !== 'string') { config.bridge.name = `Homebridge ${config.bridge.username.substring(config.bridge.username.length - 5).replace(RE_COLON, '')}`; } if (!config.accessories || !Array.isArray(config.accessories)) { config.accessories = []; } if (!config.platforms || !Array.isArray(config.platforms)) { config.platforms = []; } this.assertRestartCommandsSafe(config); if (config.plugins && Array.isArray(config.plugins)) { if (!config.plugins.length) { delete config.plugins; } } else if (config.plugins) { delete config.plugins; } if (config.mdns && typeof config.mdns !== 'object') { delete config.mdns; } if (config.disabledPlugins && !Array.isArray(config.disabledPlugins)) { delete config.disabledPlugins; } try { await copyFile(this.configService.configPath, resolve(this.configService.configBackupPath, `config.json.${now.getTime().toString()}`)); } catch (e) { if (e.code === 'ENOENT') { await this.ensureBackupPathExists(); } else { this.logger.warn(`Could not create a backup of the config.json file to ${this.configService.configBackupPath} as ${e.message}.`); } } await this.jsonStore.write(this.configService.configPath, config, { spaces: 4 }); this.logger.log('Changes to config.json saved.'); const configCopy = JSON.parse(JSON.stringify(config)); this.configService.parseConfig(configCopy); return config; } async getConfigForPlugin(pluginName) { const [plugin, config] = await Promise.all([ await this.pluginsService.getPluginAlias(pluginName), await this.getConfigFile(), ]); if (!plugin.pluginAlias) { throw new BadRequestException('Plugin alias could not be determined.'); } const arrayKey = plugin.pluginType === 'accessory' ? 'accessories' : 'platforms'; return config[arrayKey].filter((block) => { return block[plugin.pluginType] === plugin.pluginAlias || block[plugin.pluginType] === `${pluginName}.${plugin.pluginAlias}`; }); } async updateConfigForPlugin(pluginName, pluginConfig) { const [plugin, config] = await Promise.all([ await this.pluginsService.getPluginAlias(pluginName), await this.getConfigFile(), ]); if (!plugin.pluginAlias) { throw new BadRequestException('Plugin alias could not be determined.'); } const arrayKey = plugin.pluginType === 'accessory' ? 'accessories' : 'platforms'; if (!Array.isArray(pluginConfig)) { throw new BadRequestException('Plugin Config must be an array.'); } for (const block of pluginConfig) { if (typeof block !== 'object' || Array.isArray(block)) { throw new BadRequestException('Plugin config must be an array of objects.'); } block[plugin.pluginType] = plugin.pluginAlias; } let positionIndices; if (arrayKey === 'accessories') { config.accessories = config.accessories?.filter((block, index) => { if (block[plugin.pluginType] === plugin.pluginAlias || block[plugin.pluginType] === `${pluginName}.${plugin.pluginAlias}`) { positionIndices = index; return false; } else { return true; } }) || []; } else { config.platforms = config.platforms?.filter((block, index) => { if (block[plugin.pluginType] === plugin.pluginAlias || block[plugin.pluginType] === `${pluginName}.${plugin.pluginAlias}`) { positionIndices = index; return false; } else { return true; } }) || []; } pluginConfig.forEach((block) => { if (block._bridge) { if (plugin.pluginType === 'accessory' && block._bridge.matter) { this.logger.warn(`Removing Matter configuration from accessory-based plugin: ${pluginName}`); delete block._bridge.matter; } const isEnvObjAllowed = gte(this.configService.homebridgeVersion, '1.8.0'); Object.keys(block._bridge).forEach((key) => { if (key === 'env' && isEnvObjAllowed) { Object.keys(block._bridge.env).forEach((envKey) => { if (block._bridge.env[envKey] === undefined || typeof block._bridge.env[envKey] !== 'string' || block._bridge.env[envKey].trim() === '') { delete block._bridge.env[envKey]; } }); if (Object.keys(block._bridge.env).length === 0) { delete block._bridge.env; } } else { if (block._bridge[key] === undefined || (typeof block._bridge[key] === 'string' && block._bridge[key].trim() === '')) { delete block._bridge[key]; } } }); } }); if (arrayKey === 'accessories') { if (positionIndices !== undefined) { config.accessories?.splice(positionIndices, 0, ...pluginConfig); } else { config.accessories?.push(...pluginConfig); } } else { if (positionIndices !== undefined) { config.platforms?.splice(positionIndices, 0, ...pluginConfig); } else { config.platforms?.push(...pluginConfig); } } await this.updateConfigFile(config); return pluginConfig; } async getPropertyForUi(property) { const config = await this.getConfigFile(); const pluginConfig = config.platforms.find(x => x.platform === 'config'); if (property.includes('.')) { const properties = property.split('.'); let current = pluginConfig; for (const prop of properties) { if (current && typeof current === 'object') { current = current[prop]; } else { return undefined; } } return current; } return pluginConfig?.[property]; } async setPropertyForUi(property, value) { await this.setPropertiesForUi({ [property]: value }); } async setPropertiesForUi(properties) { if (!properties || typeof properties !== 'object' || Array.isArray(properties)) { throw new BadRequestException('Properties must be a key/value object.'); } const entries = Object.entries(properties); if (entries.length === 0) { return; } if (entries.some(([key]) => key === 'platform')) { throw new BadRequestException('Cannot update the platform property.'); } const forbiddenKeys = ['__proto__', 'constructor', 'prototype']; const offending = entries.find(([key]) => key.split('.').some(segment => forbiddenKeys.includes(segment))); if (offending) { throw new BadRequestException(`Property "${offending[0]}" contains a forbidden key segment.`); } await this.ready; const now = new Date(); await this.jsonStore.mutate(this.configService.configPath, (config) => { if (!config) { config = {}; } if (!config.platforms || !Array.isArray(config.platforms)) { config.platforms = []; } let pluginConfig = config.platforms.find(x => x.platform === 'config'); if (!pluginConfig) { pluginConfig = { platform: 'config' }; config.platforms.push(pluginConfig); } for (const [property, value] of entries) { this.applyPropertyToPluginConfig(pluginConfig, property, value); } config.platforms[config.platforms.findIndex(x => x.platform === 'config')] = this.cleanUpUiConfig(pluginConfig); this.assertRestartCommandsSafe(config); return config; }, { spaces: 4, backupTo: resolve(this.configService.configBackupPath, `config.json.${now.getTime().toString()}`), }); const updatedConfig = await this.getConfigFile(); this.configService.parseConfig(JSON.parse(JSON.stringify(updatedConfig))); this.logger.log('Changes to config.json saved.'); if (entries.some(([key]) => key === 'scheduledBackupDisable')) { this.backupService.refreshBackupSchedule(); } } applyPropertyToPluginConfig(pluginConfig, property, value) { if (property.includes('.')) { const properties = property.split('.'); let current = pluginConfig; for (let i = 0; i < properties.length - 1; i += 1) { if (!current[properties[i]]) { current[properties[i]] = {}; } current = current[properties[i]]; } current[properties.at(-1)] = value; } else { pluginConfig[property] = value; } } async setAccessoryControlInstanceBlacklist(value) { const config = await this.getConfigFile(); const pluginConfig = config.platforms.find(x => x.platform === 'config'); if (!pluginConfig.accessoryControl) { pluginConfig.accessoryControl = {}; } pluginConfig.accessoryControl.instanceBlacklist = (value || []) .filter(x => typeof x === 'string' && x.trim() !== '' && RE_USERNAME.test(x.trim())) .map(x => x.trim().toUpperCase()) .sort((a, b) => a.localeCompare(b)); config.platforms[config.platforms.findIndex(x => x.platform === 'config')] = this.cleanUpUiConfig(pluginConfig); await this.updateConfigFile(config); } async getPluginsHideUpdatesFor() { const config = await this.getConfigFile(); const pluginConfig = config.platforms.find(x => x.platform === 'config'); return pluginConfig?.plugins?.hideUpdatesFor || []; } async setPluginsHideUpdatesFor(value) { const config = await this.getConfigFile(); const pluginConfig = config.platforms.find(x => x.platform === 'config'); if (!pluginConfig.plugins) { pluginConfig.plugins = {}; } pluginConfig.plugins.hideUpdatesFor = (value || []) .filter(x => typeof x === 'string' && x.trim() !== '' && RE_PLUGIN_NAME.test(x.trim())) .map(x => x.trim().toLowerCase()); config.platforms[config.platforms.findIndex(x => x.platform === 'config')] = this.cleanUpUiConfig(pluginConfig); await this.updateConfigFile(config); } async getPluginsHideChildBridgeSetupFor() { const config = await this.getConfigFile(); const pluginConfig = config.platforms.find(x => x.platform === 'config'); return pluginConfig?.plugins?.hideChildBridgeSetupFor || []; } async setPluginsHideChildBridgeSetupFor(value) { const config = await this.getConfigFile(); const pluginConfig = config.platforms.find(x => x.platform === 'config'); if (!pluginConfig.plugins) { pluginConfig.plugins = {}; } pluginConfig.plugins.hideChildBridgeSetupFor = (value || []) .filter(x => typeof x === 'string' && x.trim() !== '' && RE_PLUGIN_NAME.test(x.trim())) .map(x => x.trim().toLowerCase()); config.platforms[config.platforms.findIndex(x => x.platform === 'config')] = this.cleanUpUiConfig(pluginConfig); await this.updateConfigFile(config); } async getBridge(username) { if (!username || !RE_USERNAME.test(username.trim())) { return null; } const config = await this.getConfigFile(); const pluginConfig = config.platforms.find(x => x.platform === 'config'); const normalizedUsername = username.trim().toUpperCase(); const bridge = pluginConfig?.bridges?.find((b) => b.username.toUpperCase() === normalizedUsername); return { username: normalizedUsername, hideHapAlert: bridge?.hideHapAlert || false, hideMatterAlert: bridge?.hideMatterAlert || false, scheduledRestartCron: bridge?.scheduledRestartCron || null, ...(bridge ? Object.keys(bridge).reduce((acc, key) => { if (key !== 'username' && key !== 'hideHapAlert' && key !== 'hideMatterAlert' && key !== 'scheduledRestartCron') { acc[key] = bridge[key]; } return acc; }, {}) : {}), }; } async updateBridgeProperty(username, property, value) { if (!username || !RE_USERNAME.test(username.trim())) { throw new NotFoundException('Invalid bridge username format'); } const config = await this.getConfigFile(); const pluginConfig = config.platforms.find(x => x.platform === 'config'); const normalizedUsername = username.trim().toUpperCase(); if (!pluginConfig.bridges) { pluginConfig.bridges = []; } let bridge = pluginConfig.bridges.find((b) => b.username.toUpperCase() === normalizedUsername); const shouldSet = value !== null && value !== undefined && value !== false && value !== '' && value !== 'never'; if (shouldSet) { if (!bridge) { bridge = { username: normalizedUsername }; pluginConfig.bridges.push(bridge); } bridge[property] = value; } else { if (bridge) { delete bridge[property]; const hasOtherProps = Object.keys(bridge).some(key => key !== 'username'); if (!hasOtherProps) { pluginConfig.bridges = pluginConfig.bridges.filter((b) => b.username.toUpperCase() !== normalizedUsername); } } } config.platforms[config.platforms.findIndex(x => x.platform === 'config')] = this.cleanUpUiConfig(pluginConfig); await this.updateConfigFile(config); } async setBridgeHideHapAlert(username, value) { await this.updateBridgeProperty(username, 'hideHapAlert', value); } async setBridgeHideMatterAlert(username, value) { await this.updateBridgeProperty(username, 'hideMatterAlert', value); } async setBridgeScheduledRestartCron(username, value) { await this.updateBridgeProperty(username, 'scheduledRestartCron', value); } async disablePlugin(pluginName) { if (pluginName === this.configService.name) { throw new BadRequestException('Disabling this plugin is now allowed.'); } const config = await this.getConfigFile(); if (!Array.isArray(config.disabledPlugins)) { config.disabledPlugins = []; } config.disabledPlugins.push(pluginName); await this.updateConfigFile(config); return config.disabledPlugins; } async enablePlugin(pluginName) { const config = await this.getConfigFile(); if (!Array.isArray(config.disabledPlugins)) { config.disabledPlugins = []; } const idx = config.disabledPlugins.findIndex(x => x === pluginName); if (idx > -1) { config.disabledPlugins.splice(idx, 1); await this.updateConfigFile(config); } return config.disabledPlugins; } async buildRestartInfo(payload, pluginName) { let affectedBridges = []; try { const allBridges = await this.childBridgesService.getChildBridges(); affectedBridges = pluginName === null ? allBridges : allBridges.filter(bridge => bridge.plugin === pluginName); } catch (e) { this.logger.warn(`Could not fetch child bridges for restart-info wrapper as ${e.message}.`); } return { config: payload, affectedBridges, }; } async updateConfigFileWithRestartInfo(config) { const saved = await this.updateConfigFile(config); return this.buildRestartInfo(saved, null); } async updateConfigForPluginWithRestartInfo(pluginName, pluginConfig) { const saved = await this.updateConfigForPlugin(pluginName, pluginConfig); return this.buildRestartInfo(saved, pluginName); } async disablePluginWithRestartInfo(pluginName) { const disabledPlugins = await this.disablePlugin(pluginName); return this.buildRestartInfo(disabledPlugins, pluginName); } async enablePluginWithRestartInfo(pluginName) { const disabledPlugins = await this.enablePlugin(pluginName); return this.buildRestartInfo(disabledPlugins, pluginName); } async listConfigBackups() { const dirContents = await readdir(this.configService.configBackupPath); return dirContents .filter(x => x.match(RE_CONFIG_BACKUP)) .map((x) => { const ext = x.split('.'); if (ext.length === 3 && !Number.isNaN(ext[2])) { return { id: ext[2], timestamp: new Date(Number.parseInt(ext[2], 10)), file: x, }; } else { return null; } }) .filter(x => x && !Number.isNaN(x.timestamp.getTime())) .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); } async getConfigBackup(backupId) { const requestedBackupPath = resolve(this.configService.configBackupPath, `config.json.${backupId}`); if (!await pathExists(requestedBackupPath)) { throw new NotFoundException(`Backup ${backupId} Not Found`); } return await readFile(requestedBackupPath); } async deleteConfigBackup(backupId) { const requestedBackupPath = resolve(this.configService.configBackupPath, `config.json.${backupId}`); if (!await pathExists(requestedBackupPath)) { throw new NotFoundException(`Backup ${backupId} Not Found`); } await unlink(resolve(this.configService.configBackupPath, `config.json.${backupId}`)); } async deleteAllConfigBackups() { const backups = await this.listConfigBackups(); for (const backupFile of backups) { await unlink(resolve(this.configService.configBackupPath, backupFile.file)); } } async ensureBackupPathExists() { try { await ensureDir(this.configService.configBackupPath); } catch (e) { this.logger.error(`Could not create directory for config backups ${this.configService.configBackupPath} as ${e.message}.`); this.logger.error(`Config backups will continue to use ${this.configService.storagePath}.`); this.configService.configBackupPath = this.configService.storagePath; } } async cleanupConfigBackups() { try { const backups = await this.listConfigBackups(); for (const backup of backups) { if (dayjs().diff(dayjs(backup.timestamp), 'day') >= 60) { await remove(resolve(this.configService.configBackupPath, backup.file)); } } } catch (e) { this.logger.warn(`Failed to cleanup old config.json backup files as ${e.message}`); } } async migrateConfigBackups() { try { if (this.configService.configBackupPath === this.configService.storagePath) { this.logger.error('Skipping migration of existing config.json backups...'); return; } const dirContents = await readdir(this.configService.storagePath); const backups = dirContents .filter(x => x.match(RE_CONFIG_BACKUP)) .sort((a, b) => { const ta = Number.parseInt(a.split('.')[2], 10); const tb = Number.parseInt(b.split('.')[2], 10); return tb - ta; }); for (const backupFileName of backups.splice(0, 100)) { const sourcePath = resolve(this.configService.storagePath, backupFileName); const targetPath = resolve(this.configService.configBackupPath, backupFileName); await move(sourcePath, targetPath, { overwrite: true }); } for (const backupFileName of backups) { const sourcePath = resolve(this.configService.storagePath, backupFileName); await remove(sourcePath); } } catch (e) { this.logger.warn(`Migrating config.json backups to new location failed as ${e.message}.`); } } generatePin() { let code = `${randomInt(10000000, 100000000)}`; code = code.split(''); code.splice(3, 0, '-'); code.splice(6, 0, '-'); code = code.join(''); return code; } generateUsername() { const hexDigits = '0123456789ABCDEF'; let username = '0E:'; for (let i = 0; i < 5; i += 1) { username += hexDigits.charAt(randomInt(0, 16)); username += hexDigits.charAt(randomInt(0, 16)); if (i !== 4) { username += ':'; } } return username; } removeEmpty(obj) { Object.keys(obj).forEach((key) => { const value = obj[key]; if (value === '' || value === null || value === undefined || value === false || (Array.isArray(value) && value.length === 0)) { delete obj[key]; } else if (typeof value === 'object') { this.removeEmpty(value); if (Object.keys(value).length === 0) { delete obj[key]; } } }); } cleanUpUiConfig(uiConfig) { const { name, platform, ...rest } = uiConfig; const cleanedUiConfig = { name, platform: platform || 'config', ...rest, }; if (Array.isArray(cleanedUiConfig.bridges)) { cleanedUiConfig.bridges = cleanedUiConfig.bridges.filter(bridge => bridge && bridge.username); } this.removeEmpty(cleanedUiConfig); return cleanedUiConfig; } async getMatterPortRange() { const config = await this.getConfigFile(); return { start: config.matterPorts?.start, end: config.matterPorts?.end, }; } async setMatterPortRange(value) { this.validateMatterPortRange(value); let config = await this.getConfigFile(); if (value.start === null || value.start === undefined) { delete value.start; } if (value.end === null || value.end === undefined) { delete value.end; } if (!value.start && !value.end) { delete config.matterPorts; } else { config.matterPorts = {}; if (value.start) { config.matterPorts.start = value.start; } if (value.end) { config.matterPorts.end = value.end; } } const { bridge, ports, matterPorts, ...rest } = config; config = matterPorts ? (ports ? { bridge, ports, matterPorts, ...rest } : { bridge, matterPorts, ...rest }) : (ports ? { bridge, ports, ...rest } : { bridge, ...rest }); await this.updateConfigFile(config); } validateMatterPortRange(value) { if (value.start !== null && value.start !== undefined) { if (typeof value.start !== 'number' || value.start < 1025 || value.start > 65533) { throw new BadRequestException('Matter port range start must be a number between 1025 and 65533.'); } } if (value.end !== null && value.end !== undefined) { if (typeof value.end !== 'number' || value.end < 1025 || value.end > 65533) { throw new BadRequestException('Matter port range end must be a number between 1025 and 65533.'); } } if (value.start && value.end && value.start >= value.end) { throw new BadRequestException('Matter port range start must be less than end.'); } } async getMatterConfig() { const config = await this.getConfigFile(); return config.bridge.matter || null; } async updateMatterConfig(matterConfig) { this.validateMatterConfig(matterConfig); const config = await this.getConfigFile(); config.bridge.matter = matterConfig; await this.updateConfigFile(config); return matterConfig; } async setMatterEnabled(enabled, restart = true, externalsOnly = false) { const config = await this.getConfigFile(); if (!config.bridge?.matter) { throw new BadRequestException('Matter is not configured on the main bridge.'); } const useExternalsOnly = this.configService.getFeatureFlags().protocolExternalsOnly === true; const currentlyEnabled = config.bridge.matter.enabled !== false; const currentlyExternalsOnly = config.bridge.matter.externalsOnly === true; const targetExternalsOnly = useExternalsOnly && !enabled && externalsOnly; if (enabled === currentlyEnabled && targetExternalsOnly === currentlyExternalsOnly) { return { enabled, externalsOnly: currentlyExternalsOnly }; } if (restart) { await this.homebridgeIpcService.restartAndWaitForClose(); } if (enabled) { delete config.bridge.matter.enabled; delete config.bridge.matter.externalsOnly; } else { config.bridge.matter.enabled = false; if (targetExternalsOnly) { config.bridge.matter.externalsOnly = true; } else { delete config.bridge.matter.externalsOnly; } } await this.updateConfigFile(config); return { enabled, externalsOnly: targetExternalsOnly }; } async deleteMatterConfig() { const config = await this.getConfigFile(); const deviceId = config.bridge.username.replace(RE_COLON, '').toUpperCase(); await this.homebridgeIpcService.restartAndWaitForClose(); delete config.bridge.matter; await this.updateConfigFile(config); const matterPath = join(this.configService.storagePath, 'matter', deviceId); if (await pathExists(matterPath)) { await remove(matterPath); this.logger.warn(`Bridge ${deviceId} reset: removed Matter bridge storage at ${matterPath}.`); } } async getHapEnabled() { const config = await this.getConfigFile(); const hap = config.bridge?.hap; if (hap === false) { return { enabled: false, externalsOnly: false }; } if (typeof hap === 'object' && hap !== null) { const enabled = hap.enabled !== false; const externalsOnly = hap.externalsOnly === true; return { enabled, externalsOnly }; } return { enabled: true, externalsOnly: false }; } async setHapEnabled(enabled, restart = true, externalsOnly = false) { const config = await this.getConfigFile(); const useNestedShape = this.configService.getFeatureFlags().protocolExternalsOnly === true; if (!enabled) { if (restart) { await this.homebridgeIpcService.restartAndWaitForClose(); } if (useNestedShape) { config.bridge.hap = externalsOnly ? { enabled: false, externalsOnly: true } : { enabled: false }; } else { config.bridge.hap = false; } await this.updateConfigFile(config); } else { const hap = config.bridge?.hap; if (hap === false || (typeof hap === 'object' && hap !== null)) { delete config.bridge.hap; await this.updateConfigFile(config); } } return { enabled, externalsOnly: useNestedShape && !enabled && externalsOnly }; } validateMatterConfig(matterConfig) { if (matterConfig.port !== undefined) { if (typeof matterConfig.port !== 'number' || !Number.isInteger(matterConfig.port) || matterConfig.port < 1024 || matterConfig.port > 65535) { throw new BadRequestException('Port must be an integer between 1024 and 65535'); } if ([5353, 8080, 8443].includes(matterConfig.port)) { throw new BadRequestException('Port 5353, 8080, and 8443 are reserved and cannot be used'); } } } }; ConfigEditorService = __decorate([ Injectable(), __param(0, Inject(Logger)), __param(1, Inject(ConfigService)), __param(2, Inject(SchedulerService)), __param(3, Inject(PluginsService)), __param(4, Inject(HomebridgeIpcService)), __param(5, Inject(ChildBridgesService)), __param(6, Inject(BackupService)), __param(7, Inject(JsonFileStoreService)), __metadata("design:paramtypes", [Logger, ConfigService, SchedulerService, PluginsService, HomebridgeIpcService, ChildBridgesService, BackupService, JsonFileStoreService]) ], ConfigEditorService); export { ConfigEditorService }; //# sourceMappingURL=config-editor.service.js.map