homebridge-config-ui-x
Version:
A web based management, configuration and control platform for Homebridge.
356 lines • 16.1 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);
};
import { createHash, randomBytes } from 'node:crypto';
import { createReadStream } from 'node:fs';
import { stat } from 'node:fs/promises';
import { homedir, platform, totalmem } from 'node:os';
import { resolve } from 'node:path';
import process from 'node:process';
import { isDeepStrictEqual } from 'node:util';
import { Injectable } from '@nestjs/common';
import { pathExists, pathExistsSync, readJson, readJSONSync, readJsonSync, writeJsonSync } from 'fs-extra/esm';
import { satisfies } from 'semver';
import { FEATURE_FLAGS } from '../feature-flags/feature-flags.registry.js';
let ConfigService = class ConfigService {
name = 'homebridge-config-ui-x';
configPath = process.env.UIX_CONFIG_PATH || resolve(homedir(), '.homebridge/config.json');
storagePath = process.env.UIX_STORAGE_PATH || resolve(homedir(), '.homebridge');
customPluginPath = process.env.UIX_CUSTOM_PLUGIN_PATH;
strictPluginResolution = (process.env.UIX_STRICT_PLUGIN_RESOLUTION === '1');
secretPath = resolve(this.storagePath, '.uix-secrets');
authPath = resolve(this.storagePath, 'auth.json');
accessoryLayoutPath = resolve(this.storagePath, 'accessories', 'uiAccessoriesLayout.json');
configBackupPath = resolve(this.storagePath, 'backups/config-backups');
instanceBackupPath = resolve(this.storagePath, 'backups/instance-backups');
homebridgeInsecureMode = Boolean(process.env.UIX_INSECURE_MODE === '1');
homebridgeVersion;
runningHomebridgeModulePath;
minimumNodeVersion = '22.12.0';
runningInDocker = Boolean(process.env.HOMEBRIDGE_CONFIG_UI === '1');
runningInSynologyPackage = Boolean(process.env.HOMEBRIDGE_SYNOLOGY_PACKAGE === '1');
runningInPackageMode = Boolean(process.env.HOMEBRIDGE_APT_PACKAGE === '1');
runningInLinux = (!this.runningInDocker && !this.runningInSynologyPackage && !this.runningInPackageMode && platform() === 'linux');
runningInFreeBSD = (platform() === 'freebsd');
canShutdownRestartHost = (this.runningInLinux || process.env.UIX_CAN_SHUTDOWN_RESTART_HOST === '1');
enableTerminalAccess = (this.runningInDocker && process.env.HOMEBRIDGE_CONFIG_UI_TERMINAL !== '0')
|| (this.runningInPackageMode && process.env.HOMEBRIDGE_CONFIG_UI_TERMINAL !== '0')
|| this.runningInSynologyPackage
|| Boolean(process.env.HOMEBRIDGE_CONFIG_UI_TERMINAL === '1');
usePluginBundles = (process.env.UIX_USE_PLUGIN_BUNDLES === '1');
recommendChildBridges = (totalmem() > 2e+9);
runningOnRaspberryPi = false;
runningOnRaspbianImage = false;
startupScript = resolve(this.storagePath, 'startup.sh');
dockerOfflineUpdate = this.runningInDocker && satisfies(process.env.CONFIG_UI_VERSION, '>=4.6.2 <=4.44.1', { includePrerelease: true });
package = readJsonSync(resolve(process.env.UIX_BASE_PATH, 'package.json'));
hbStartupSettings = pathExistsSync(resolve(this.storagePath, '.uix-hb-service-homebridge-startup.json'))
? readJsonSync(resolve(this.storagePath, '.uix-hb-service-homebridge-startup.json'))
: {};
setupWizardComplete = true;
customWallpaperHash;
hbServiceUiRestartRequired = false;
homebridgeConfig;
ui;
bridgeFreeze;
uiFreeze;
secrets;
instanceId;
constructor() {
const homebridgeConfig = readJSONSync(this.configPath);
this.parseConfig(homebridgeConfig);
this.checkIfRunningOnRaspberryPi();
this.checkIfRunningOnRaspbianImage();
}
parseConfig(homebridgeConfig) {
this.homebridgeConfig = homebridgeConfig;
if (!this.homebridgeConfig.bridge) {
this.homebridgeConfig.bridge = {};
}
this.ui = Array.isArray(this.homebridgeConfig.platforms) ? this.homebridgeConfig.platforms.find(x => x.platform === 'config') : undefined;
if (!this.ui) {
this.ui = {
name: 'Config',
};
}
process.env.UIX_PLUGIN_NAME = this.ui.name || 'homebridge-config-ui-x';
if (this.runningInDocker) {
this.setConfigForDocker();
}
this.setConfig();
if (!this.ui.port) {
this.ui.port = 8080;
}
if (!this.ui.sessionTimeout) {
this.ui.sessionTimeout = this.ui.auth === 'none' ? 1296000 : 28800;
}
if (this.ui.scheduledBackupPath) {
this.instanceBackupPath = this.ui.scheduledBackupPath;
}
else {
this.instanceBackupPath = resolve(this.storagePath, 'backups/instance-backups');
}
this.secrets = this.getSecrets();
this.instanceId = this.getInstanceId();
this.freezeUiSettings();
this.getCustomWallpaperHash();
}
uiSettings(authorized = false) {
const toReturn = {
env: {
canShutdownRestartHost: this.canShutdownRestartHost,
customWallpaperHash: this.customWallpaperHash,
dockerOfflineUpdate: this.dockerOfflineUpdate,
featureFlags: this.getFeatureFlags(),
homebridgeVersion: this.homebridgeVersion || null,
homebridgeInstanceName: this.homebridgeConfig.bridge.name,
instanceId: this.instanceId,
lang: this.ui.lang === 'auto' ? null : this.ui.lang,
packageName: this.package.name,
packageVersion: this.package.version,
platform: platform(),
port: this.ui.port,
setupWizardComplete: this.setupWizardComplete,
scheduledBackupDisable: Boolean(this.ui.scheduledBackupDisable),
scheduledBackupPath: this.ui.scheduledBackupPath || this.instanceBackupPath,
},
formAuth: Boolean(this.ui.auth !== 'none'),
sessionTimeout: this.ui.sessionTimeout || 28800,
sessionTimeoutInactivityBased: Boolean(this.ui.sessionTimeoutInactivityBased),
lightingMode: this.ui.lightingMode || 'auto',
serverTimestamp: new Date().toISOString(),
theme: this.ui.theme || 'deep-purple',
menuMode: this.ui.menuMode || 'default',
};
if (!authorized) {
return toReturn;
}
return {
...toReturn,
env: {
...toReturn.env,
enableAccessories: this.homebridgeInsecureMode,
enableTerminalAccess: this.enableTerminalAccess,
enableMdnsAdvertise: Boolean(this.ui.enableMdnsAdvertise),
nodeVersion: process.version,
recommendChildBridges: this.recommendChildBridges,
runningInDocker: this.runningInDocker,
runningInSynologyPackage: this.runningInSynologyPackage,
runningInPackageMode: this.runningInPackageMode,
runningInLinux: this.runningInLinux,
runningInFreeBSD: this.runningInFreeBSD,
runningOnRaspberryPi: this.runningOnRaspberryPi,
runningOnRaspbianImage: this.runningOnRaspbianImage,
temperatureUnits: this.ui.tempUnits || 'c',
temp: this.ui.temp,
log: {
maxSize: this.ui.log?.maxSize,
truncateSize: this.ui.log?.truncateSize,
},
ssl: {
key: this.ui.ssl?.key,
cert: this.ui.ssl?.cert,
pfx: this.ui.ssl?.pfx,
hasPassphrase: Boolean(this.ui.ssl?.passphrase),
selfSigned: this.ui.ssl?.selfSigned,
selfSignedHostnames: this.ui.ssl?.selfSignedHostnames,
},
accessoryControl: {
debug: this.ui.accessoryControl?.debug,
instanceBlacklist: this.ui.accessoryControl?.instanceBlacklist || [],
},
plugins: {
hideUpdatesFor: this.ui.plugins?.hideUpdatesFor || [],
showBetasFor: this.ui.plugins?.showBetasFor || [],
hideChildBridgeSetupFor: this.ui.plugins?.hideChildBridgeSetupFor || [],
},
nodeUpdatePolicy: this.ui.nodeUpdatePolicy || 'all',
homebridgeUpdatePolicy: this.ui.homebridgeUpdatePolicy || 'all',
homebridgeUiUpdatePolicy: this.ui.homebridgeUiUpdatePolicy || 'all',
scheduledRestartCron: this.ui.scheduledRestartCron || null,
bridges: this.ui.bridges || [],
linux: {
shutdown: this.ui.linux?.shutdown,
restart: this.ui.linux?.restart,
},
terminal: {
persistence: this.ui.terminal?.persistence,
hideWarning: this.ui.terminal?.hideWarning,
bufferSize: this.ui.terminal?.bufferSize || globalThis.terminal.bufferSize,
fontSize: this.ui.terminal?.fontSize,
fontWeight: this.ui.terminal?.fontWeight,
lightingMode: this.ui.terminal?.lightingMode,
},
},
menuMode: this.ui.menuMode || 'default',
wallpaper: this.ui.wallpaper,
host: this.ui.host || '',
proxyHost: this.ui.proxyHost || '',
homebridgePackagePath: this.ui.homebridgePackagePath,
disableServerMetricsMonitoring: this.ui.disableServerMetricsMonitoring,
keepOrphans: this.hbStartupSettings?.keepOrphans || false,
};
}
async uiRestartRequired() {
if (this.hbServiceUiRestartRequired) {
return true;
}
const currentPackage = await readJson(resolve(process.env.UIX_BASE_PATH, 'package.json'));
if (currentPackage.version !== this.package.version) {
return true;
}
return !(isDeepStrictEqual(this.ui, this.uiFreeze) && isDeepStrictEqual(this.homebridgeConfig.bridge, this.bridgeFreeze));
}
freezeUiSettings() {
if (!this.uiFreeze) {
this.uiFreeze = {};
Object.assign(this.uiFreeze, this.ui);
}
if (!this.bridgeFreeze) {
this.bridgeFreeze = {};
Object.assign(this.bridgeFreeze, this.homebridgeConfig.bridge);
}
}
setConfigForDocker() {
this.ui.restart = 'killall -15 homebridge; sleep 5.1; killall -9 homebridge; kill -9 $(pidof homebridge-config-ui-x);';
this.homebridgeInsecureMode = Boolean(process.env.HOMEBRIDGE_INSECURE === '1');
this.ui.sudo = false;
this.ui.log = {
method: 'file',
path: '/homebridge/logs/homebridge.log',
maxSize: this.ui.log?.maxSize,
truncateSize: this.ui.log?.truncateSize,
};
if (!this.ui.port && process.env.HOMEBRIDGE_CONFIG_UI_PORT) {
this.ui.port = Number.parseInt(process.env.HOMEBRIDGE_CONFIG_UI_PORT, 10);
}
this.ui.theme = this.ui.theme || process.env.HOMEBRIDGE_CONFIG_UI_THEME || 'deep-purple';
this.ui.auth = this.ui.auth || process.env.HOMEBRIDGE_CONFIG_UI_AUTH || 'form';
this.ui.wallpaper = this.ui.wallpaper || process.env.HOMEBRIDGE_CONFIG_UI_LOGIN_WALLPAPER || undefined;
}
setConfig() {
this.homebridgeInsecureMode = Boolean(process.env.UIX_INSECURE_MODE === '1');
this.ui.restart = undefined;
this.ui.sudo = (platform() === 'linux' && !this.runningInDocker && !this.runningInSynologyPackage && !this.runningInPackageMode) || platform() === 'freebsd';
this.ui.log = {
method: 'native',
path: resolve(this.storagePath, 'homebridge.log'),
maxSize: this.ui.log?.maxSize,
truncateSize: this.ui.log?.truncateSize,
};
}
getSecrets() {
if (pathExistsSync(this.secretPath)) {
try {
const secrets = readJsonSync(this.secretPath);
if (!secrets.secretKey) {
return this.generateSecretToken();
}
else {
return secrets;
}
}
catch (e) {
return this.generateSecretToken();
}
}
else {
return this.generateSecretToken();
}
}
generateSecretToken() {
const secrets = {
secretKey: randomBytes(32).toString('hex'),
};
writeJsonSync(this.secretPath, secrets);
return secrets;
}
getInstanceId() {
return createHash('sha256').update(this.secrets.secretKey).digest('hex');
}
async getCustomWallpaperHash() {
try {
if (this.ui.wallpaper) {
const filePath = resolve(this.storagePath, this.ui.wallpaper);
const fileStat = await stat(filePath);
const hash = createHash('sha256');
hash.update(`${fileStat.birthtime}${fileStat.ctime}${fileStat.size}${fileStat.blocks}`);
this.customWallpaperHash = `${hash.digest('hex')}.jpg`;
}
}
catch (e) {
}
}
removeWallpaperCache() {
this.customWallpaperHash = undefined;
}
streamCustomWallpaper() {
return createReadStream(resolve(this.storagePath, this.ui.wallpaper));
}
async checkIfRunningOnRaspberryPi() {
try {
this.runningOnRaspberryPi = await pathExists('/usr/bin/vcgencmd') && await pathExists('/usr/bin/raspi-config');
}
catch (e) {
this.runningOnRaspberryPi = false;
}
}
async checkIfRunningOnRaspbianImage() {
try {
this.runningOnRaspbianImage = platform() === 'linux' && await pathExists('/etc/hb-ui-port');
}
catch (e) {
this.runningOnRaspbianImage = false;
}
}
getFeatureFlags() {
const featureFlags = {};
if (!this.homebridgeVersion) {
FEATURE_FLAGS.forEach((flag) => {
featureFlags[flag.key] = false;
});
return featureFlags;
}
FEATURE_FLAGS.forEach((flag) => {
try {
featureFlags[flag.key] = satisfies(this.homebridgeVersion, flag.range, { includePrerelease: true });
}
catch (error) {
featureFlags[flag.key] = false;
}
});
return featureFlags;
}
isMatterEnabled() {
const config = this.homebridgeConfig;
if (!config) {
return false;
}
const matterEnabled = (matter) => !!matter && matter.enabled !== false;
if (matterEnabled(config.bridge?.matter)) {
return true;
}
if (Array.isArray(config.platforms) && config.platforms.some(p => matterEnabled(p?._bridge?.matter))) {
return true;
}
if (Array.isArray(config.accessories) && config.accessories.some(a => matterEnabled(a?._bridge?.matter))) {
return true;
}
return false;
}
getNodeUpdatePolicy() {
return this.ui?.nodeUpdatePolicy || 'all';
}
};
ConfigService = __decorate([
Injectable(),
__metadata("design:paramtypes", [])
], ConfigService);
export { ConfigService };
//# sourceMappingURL=config.service.js.map