-
-
Notifications
You must be signed in to change notification settings - Fork 233
feat: support embedding Z-Wave JS UI in other software #4520
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+685
−104
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
563f681
feat: support setting controller port through env variable `ZWAVE_PORT`
AlCalzone ca27955
fix lint
AlCalzone f2cf243
fix: env variable name
AlCalzone ec54863
test: check in tarball for installation tests
AlCalzone 33713af
do not allow disabling Z-Wave controller if port is managed externally
AlCalzone 660b647
fix formatting
AlCalzone 28aa5a0
Support managing logging, RF settings and keys externally
AlCalzone bf090b3
hide all externally managed settings
AlCalzone 4b26925
Optimize layout when setting are hidden
AlCalzone 80ab783
externally manage Z-Wave JS Server settings
AlCalzone 90e19f7
fix: some settings weren't handled correctly
AlCalzone 061eb5f
Update .env.app.example
AlCalzone 96966b6
delete tarball
AlCalzone 39271c7
docs: add embedding documentation to usage/setup section (#4533)
Copilot c6e951a
Merge branch 'master' into external-port-setting
robertsLando File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,232 @@ | ||
| import { readFileSync, existsSync } from 'node:fs' | ||
| import type { PartialZWaveOptions } from 'zwave-js' | ||
| import { driverPresets } from 'zwave-js' | ||
| import { module } from './logger.ts' | ||
|
|
||
| const logger = module('ExternalSettings') | ||
|
|
||
| export interface ExternalZwaveSettings { | ||
| // Logging (with UI configuration) | ||
| logEnabled?: boolean | ||
| logLevel?: string | ||
| logToFile?: boolean | ||
| maxFiles?: number | ||
|
|
||
| // Logging (without UI configuration) | ||
| logFilename?: string | ||
| forceConsole?: boolean | ||
|
|
||
| // RF settings | ||
| rf?: { | ||
| region?: number | ||
| autoPowerlevels?: boolean | ||
| } | ||
|
|
||
| // Storage | ||
| storage?: { | ||
| cacheDir?: string | ||
| throttle?: 'normal' | 'slow' | 'fast' | ||
| } | ||
|
|
||
| // Security keys | ||
| securityKeys?: { | ||
| S0_Legacy?: string | ||
| S2_Unauthenticated?: string | ||
| S2_Authenticated?: string | ||
| S2_AccessControl?: string | ||
| } | ||
| securityKeysLongRange?: { | ||
| S2_Authenticated?: string | ||
| S2_AccessControl?: string | ||
| } | ||
|
|
||
| // Features | ||
| enableSoftReset?: boolean | ||
|
|
||
| // Z-Wave JS Server settings | ||
| serverEnabled?: boolean | ||
| serverPort?: number | ||
| serverHost?: string | ||
| serverServiceDiscoveryDisabled?: boolean | ||
|
|
||
| // Presets | ||
| presets?: string[] | ||
| } | ||
|
|
||
| let cachedSettings: ExternalZwaveSettings | null = null | ||
| let settingsLoaded = false | ||
|
|
||
| export function loadExternalSettings(): ExternalZwaveSettings | null { | ||
| if (settingsLoaded) return cachedSettings | ||
| settingsLoaded = true | ||
|
|
||
| const filePath = process.env.ZWAVE_EXTERNAL_SETTINGS | ||
| if (!filePath) return null | ||
|
|
||
| if (!existsSync(filePath)) { | ||
| logger.warn(`External settings file not found: ${filePath}`) | ||
| return null | ||
| } | ||
|
|
||
| try { | ||
| cachedSettings = JSON.parse( | ||
| readFileSync(filePath, 'utf-8'), | ||
| ) as ExternalZwaveSettings | ||
| logger.info(`Loaded external Z-Wave settings from: ${filePath}`) | ||
| return cachedSettings | ||
| } catch (error) { | ||
| logger.error( | ||
| `Failed to load external settings: ${(error as Error).message}`, | ||
| ) | ||
| return null | ||
| } | ||
| } | ||
|
|
||
| export function getExternallyManagedPaths(): string[] { | ||
| const settings = loadExternalSettings() | ||
| if (!settings) return [] | ||
|
|
||
| const paths: string[] = [] | ||
|
|
||
| // Logging (logFilename and forceConsole are driver-only, no UI mapping) | ||
| if (settings.logEnabled !== undefined) paths.push('zwave.logEnabled') | ||
| if (settings.logLevel !== undefined) paths.push('zwave.logLevel') | ||
| if (settings.logToFile !== undefined) paths.push('zwave.logToFile') | ||
| if (settings.maxFiles !== undefined) paths.push('zwave.maxFiles') | ||
|
|
||
| // RF settings | ||
| if (settings.rf?.region !== undefined) paths.push('zwave.rf.region') | ||
| if (settings.rf?.autoPowerlevels !== undefined) | ||
| paths.push('zwave.rf.autoPowerlevels') | ||
|
|
||
| // Security keys (check each specific key) | ||
| if (settings.securityKeys?.S0_Legacy !== undefined) | ||
| paths.push('zwave.securityKeys.S0_Legacy') | ||
| if (settings.securityKeys?.S2_Unauthenticated !== undefined) | ||
| paths.push('zwave.securityKeys.S2_Unauthenticated') | ||
| if (settings.securityKeys?.S2_Authenticated !== undefined) | ||
| paths.push('zwave.securityKeys.S2_Authenticated') | ||
| if (settings.securityKeys?.S2_AccessControl !== undefined) | ||
| paths.push('zwave.securityKeys.S2_AccessControl') | ||
| if (settings.securityKeysLongRange?.S2_Authenticated !== undefined) | ||
| paths.push('zwave.securityKeysLongRange.S2_Authenticated') | ||
| if (settings.securityKeysLongRange?.S2_AccessControl !== undefined) | ||
| paths.push('zwave.securityKeysLongRange.S2_AccessControl') | ||
|
|
||
| // Features | ||
| if (settings.enableSoftReset !== undefined) | ||
| paths.push('zwave.enableSoftReset') | ||
|
|
||
| // Home Assistant / Z-Wave JS Server settings | ||
| if (settings.serverEnabled !== undefined) paths.push('zwave.serverEnabled') | ||
| if (settings.serverPort !== undefined) paths.push('zwave.serverPort') | ||
| if (settings.serverHost !== undefined) paths.push('zwave.serverHost') | ||
| if (settings.serverServiceDiscoveryDisabled !== undefined) | ||
| paths.push('zwave.serverServiceDiscoveryDisabled') | ||
|
|
||
| // Presets (driver-only, no UI mapping) | ||
|
|
||
| return paths | ||
| } | ||
|
|
||
| export function applyExternalDriverSettings( | ||
| zwaveOptions: PartialZWaveOptions, | ||
| ): void { | ||
| const settings = loadExternalSettings() | ||
| if (!settings) return | ||
|
|
||
| if ( | ||
| settings.logFilename !== undefined || | ||
| settings.forceConsole !== undefined | ||
| ) { | ||
| zwaveOptions.logConfig = zwaveOptions.logConfig || {} | ||
| if (settings.logFilename !== undefined) | ||
| zwaveOptions.logConfig.filename = settings.logFilename | ||
| if (settings.forceConsole !== undefined) | ||
| zwaveOptions.logConfig.forceConsole = settings.forceConsole | ||
| } | ||
|
|
||
| if (settings.storage) { | ||
| zwaveOptions.storage = zwaveOptions.storage || {} | ||
| if (settings.storage.cacheDir !== undefined) | ||
| zwaveOptions.storage.cacheDir = settings.storage.cacheDir | ||
| if (settings.storage.throttle !== undefined) | ||
| zwaveOptions.storage.throttle = settings.storage.throttle | ||
| } | ||
|
|
||
| if (settings.presets && settings.presets.length > 0) { | ||
| for (const presetName of settings.presets) { | ||
| const preset = | ||
| driverPresets[presetName as keyof typeof driverPresets] | ||
| if (preset) { | ||
| Object.assign(zwaveOptions, preset) | ||
| } else { | ||
| logger.warn(`Unknown driver preset: ${presetName}`) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Merge external settings into ZwaveConfig. | ||
| * This should be called once in app.ts before passing settings to ZwaveClient. | ||
| */ | ||
| export function mergeExternalSettings( | ||
| zwaveConfig: Record<string, unknown>, | ||
| ): void { | ||
| const settings = loadExternalSettings() | ||
| if (!settings) return | ||
|
|
||
| // Server settings | ||
| if (settings.serverEnabled !== undefined) | ||
| zwaveConfig.serverEnabled = settings.serverEnabled | ||
| if (settings.serverPort !== undefined) | ||
| zwaveConfig.serverPort = settings.serverPort | ||
| if (settings.serverHost !== undefined) | ||
| zwaveConfig.serverHost = settings.serverHost | ||
| if (settings.serverServiceDiscoveryDisabled !== undefined) | ||
| zwaveConfig.serverServiceDiscoveryDisabled = | ||
| settings.serverServiceDiscoveryDisabled | ||
|
|
||
| // Logging settings | ||
| if (settings.logEnabled !== undefined) | ||
| zwaveConfig.logEnabled = settings.logEnabled | ||
| if (settings.logLevel !== undefined) | ||
| zwaveConfig.logLevel = settings.logLevel | ||
| if (settings.logToFile !== undefined) | ||
| zwaveConfig.logToFile = settings.logToFile | ||
| if (settings.maxFiles !== undefined) | ||
| zwaveConfig.maxFiles = settings.maxFiles | ||
|
|
||
| // RF settings | ||
| if (settings.rf) { | ||
| zwaveConfig.rf = zwaveConfig.rf || {} | ||
| const rf = zwaveConfig.rf as Record<string, unknown> | ||
| if (settings.rf.region !== undefined) rf.region = settings.rf.region | ||
| if (settings.rf.autoPowerlevels !== undefined) | ||
| rf.autoPowerlevels = settings.rf.autoPowerlevels | ||
| } | ||
|
|
||
| // Security keys (stored as hex strings, converted to Buffers later by ZwaveClient) | ||
| if (settings.securityKeys) { | ||
| zwaveConfig.securityKeys = zwaveConfig.securityKeys || {} | ||
| const keys = zwaveConfig.securityKeys as Record<string, string> | ||
| for (const [key, value] of Object.entries(settings.securityKeys)) { | ||
| if (value) keys[key] = value | ||
| } | ||
| } | ||
| if (settings.securityKeysLongRange) { | ||
| zwaveConfig.securityKeysLongRange = | ||
| zwaveConfig.securityKeysLongRange || {} | ||
| const keys = zwaveConfig.securityKeysLongRange as Record<string, string> | ||
| for (const [key, value] of Object.entries( | ||
| settings.securityKeysLongRange, | ||
| )) { | ||
| if (value) keys[key] = value | ||
| } | ||
| } | ||
|
|
||
| // Features | ||
| if (settings.enableSoftReset !== undefined) | ||
| zwaveConfig.enableSoftReset = settings.enableSoftReset | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.