Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .env.app.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ STORE_DIR=''
# Used as secret for session. If not provided the default one is used
SESSION_SECRET="zwavejsisawesome"

# Z-Wave controller port/address. When set, overrides the port configured in settings.json
# and disables automatic discovery of serial ports. Examples: /dev/ttyUSB0, COM3, tcp://192.168.1.100:5555
ZWAVE_PORT=''
# Path to a JSON file with externally provided settings for the driver and Z-Wave JS Server.
# When set, the settings in this file override those in settings.json and the UI controls for those settings are hidden.
ZWAVE_EXTERNAL_SETTINGS=''

# S0 Key
NETWORK_KEY=''
HTTPS=''
Expand All @@ -16,6 +23,6 @@ USE_SECURE_COOKIE=''
# https://zwave-js.github.io/node-zwave-js/#/usage/external-config?id=specifying-an-external-config-db-location
ZWAVEJS_EXTERNAL_CONFIG=''

# Browser preferred locale/tz to use for date/time formatting
# Browser preferred locale/tz to use for date/time formatting
TZ=''
LOCALE=''
28 changes: 25 additions & 3 deletions api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ import { createPlugin } from './lib/CustomPlugin.ts'
import { inboundEvents, socketEvents } from './lib/SocketEvents.ts'
import * as utils from './lib/utils.ts'
import backupManager from './lib/BackupManager.ts'
import {
getExternallyManagedPaths,
loadExternalSettings,
mergeExternalSettings,
} from './lib/externalSettings.ts'
import {
readFile,
realpath,
Expand Down Expand Up @@ -186,6 +191,12 @@ export async function startServer(port: number | string, host?: string) {

const settings = jsonStore.get(store.settings)

// Merge external settings into zwave config (if external settings exist)
if (loadExternalSettings()) {
settings.zwave ??= {}
mergeExternalSettings(settings.zwave as Record<string, unknown>)
}

// as the really first thing setup loggers so all logs will go to file if specified in settings
setupLogging(settings)

Expand Down Expand Up @@ -1105,19 +1116,29 @@ app.get(

const settings = jsonStore.get(store.settings)

const managedExternally: string[] = []
if (process.env.ZWAVE_PORT) {
managedExternally.push('zwave.port')
managedExternally.push('zwave.enabled')
}
// Add paths from external settings file
managedExternally.push(...getExternallyManagedPaths())

const data = {
success: true,
settings,
devices: gw?.zwave?.devices ?? {},
serial_ports: [],
scales: scales,
sslDisabled: sslDisabled(),
managedExternally,
tz: process.env.TZ,
locale: process.env.LOCALE,
deprecationWarning: process.env.TAG_NAME === 'zwavejs2mqtt',
}

if (process.platform !== 'sunos') {
// Only enumerate serial ports if ZWAVE_PORT is not set via env var
if (process.platform !== 'sunos' && !process.env.ZWAVE_PORT) {
try {
data.serial_ports = await Driver.enumerateSerialPorts({
local: true,
Expand All @@ -1127,8 +1148,9 @@ app.get(
logger.error(error)
data.serial_ports = []
}
res.json(data)
} else res.json(data)
}

res.json(data)
},
)

Expand Down
11 changes: 11 additions & 0 deletions api/lib/ZwaveClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
extractFirmware,
} from '@zwave-js/core'
import { createDefaultTransportFormat } from '@zwave-js/core/bindings/log/node'
import { applyExternalDriverSettings } from './externalSettings.ts'
import { JSONTransport } from '@zwave-js/log-transport-json'
import type {
AssociationAddress,
Expand Down Expand Up @@ -2162,6 +2163,12 @@ class ZwaveClient extends TypedEventEmitter<ZwaveClientEventCallbacks> {
* Method used to start Z-Wave connection using configuration `port`
*/
async connect() {
// When ZWAVE_PORT env var is set, force enable and override port
if (process.env.ZWAVE_PORT) {
this.cfg.enabled = true
this.cfg.port = process.env.ZWAVE_PORT
}

if (this.cfg.enabled === false) {
logger.info('Z-Wave driver DISABLED')
return
Expand Down Expand Up @@ -2327,6 +2334,10 @@ class ZwaveClient extends TypedEventEmitter<ZwaveClientEventCallbacks> {

utils.parseSecurityKeys(this.cfg, zwaveOptions)

// Apply driver-only external settings (storage, presets, logFilename, forceConsole).
// These are not in ZwaveConfig/settings.json, so they must be applied directly to driver options.
applyExternalDriverSettings(zwaveOptions)

const logTransport = new JSONTransport()
logTransport.format = createDefaultTransportFormat(true, false)

Expand Down
232 changes: 232 additions & 0 deletions api/lib/externalSettings.ts
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
}
2 changes: 0 additions & 2 deletions api/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,9 +426,7 @@ function hasDockerCGroup() {
let isDockerCached: boolean

export function isDocker(): boolean {
// TODO: Use `??=` when targeting Node.js 16.
isDockerCached ??= hasDockerEnv() || hasDockerCGroup()

return isDockerCached
}

Expand Down
Loading
Loading