homebridge-config-ui-x
Version:
A web based management, configuration and control platform for Homebridge.
853 lines • 37.6 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 __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