|
|
|
@ -114,6 +114,9 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst |
|
|
|
formValues: Record<string, any> = {}; |
|
|
|
entityOutputs = new Map<string, EntityStepOutput>(); |
|
|
|
transportVars: Record<string, string> = {}; |
|
|
|
// form-field key → declared type. Used by resolveTemplateJson() to emit BOOLEAN/INTEGER
|
|
|
|
// placeholders as raw JSON values instead of quoted strings.
|
|
|
|
private fieldTypes = new Map<string, FormFieldType>(); |
|
|
|
gatewayDockerComposeContent: string | null = null; |
|
|
|
|
|
|
|
resolveMarkdownVariable: (key: string) => string | undefined = this._resolveMarkdownVariable.bind(this); |
|
|
|
@ -499,6 +502,38 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst |
|
|
|
return undefined; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Resolve a JSON template, type-aware. A placeholder that fills an entire JSON string |
|
|
|
* token (e.g. "${mqttCleanSession}") whose form field is BOOLEAN or INTEGER is emitted as |
|
|
|
* a raw JSON boolean/number rather than a quoted string. Authored templates always quote |
|
|
|
* macros so the template file is valid JSON; the recorded form-field type decides the real |
|
|
|
* type at install time. Without this, "cleanSession": "${mqttCleanSession}" parses as the |
|
|
|
* string "false" (truthy) — so an unchecked boolean would be read as true. Placeholders |
|
|
|
* embedded in a larger string (e.g. "v3/${mqttUsername}/...") and STRING/PASSWORD/SELECT |
|
|
|
* fields are left quoted and handled by the regular resolveVariables() pass. |
|
|
|
*/ |
|
|
|
resolveTemplateJson(raw: string): any { |
|
|
|
const typed = raw.replace(/"\$\{([^"{}]+)}"/g, (whole, key) => { |
|
|
|
const type = this.fieldTypes.get(key); |
|
|
|
if (type !== FormFieldType.BOOLEAN && type !== FormFieldType.INTEGER) { |
|
|
|
return whole; |
|
|
|
} |
|
|
|
const res = this.resolveVariable(key); |
|
|
|
if (res === undefined) { |
|
|
|
return whole; |
|
|
|
} |
|
|
|
if (type === FormFieldType.BOOLEAN) { |
|
|
|
return String(res).trim().toLowerCase() === 'true' ? 'true' : 'false'; |
|
|
|
} |
|
|
|
// INTEGER: emit a raw JSON number only when the value really is an integer;
|
|
|
|
// otherwise leave it quoted for the regular pass (preserves prior behaviour).
|
|
|
|
const trimmed = String(res).trim(); |
|
|
|
const num = Number(trimmed); |
|
|
|
return trimmed !== '' && Number.isInteger(num) ? String(num) : whole; |
|
|
|
}); |
|
|
|
return JSON.parse(this.resolveVariables(typed)); |
|
|
|
} |
|
|
|
|
|
|
|
private _resolveMarkdownVariable(key: string): string | undefined { |
|
|
|
const res = this.resolveVariable(key); |
|
|
|
if (res) { |
|
|
|
@ -648,6 +683,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst |
|
|
|
// In review mode, use stored values; otherwise use defaults
|
|
|
|
const storedValues = this.reviewMode && this.data.installState?.[ws.label]?.formValues; |
|
|
|
for (const field of ws.formFields) { |
|
|
|
this.fieldTypes.set(field.key, field.type); |
|
|
|
const validators = []; |
|
|
|
if (field.required) { |
|
|
|
validators.push(Validators.required); |
|
|
|
@ -806,8 +842,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst |
|
|
|
if (!raw) { |
|
|
|
throw new Error(`Template file not found: ${step.template}`); |
|
|
|
} |
|
|
|
const resolved = this.resolveVariables(raw); |
|
|
|
const template = JSON.parse(resolved); |
|
|
|
const template = this.resolveTemplateJson(raw); |
|
|
|
|
|
|
|
switch (step.type) { |
|
|
|
case InstallStepType.DEVICE_PROFILE: { |
|
|
|
@ -915,7 +950,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst |
|
|
|
if (step.credentials) { |
|
|
|
const raw = this.zipFiles.get(step.credentials); |
|
|
|
if (raw) { |
|
|
|
const credTemplate = JSON.parse(this.resolveVariables(raw)); |
|
|
|
const credTemplate = this.resolveTemplateJson(raw); |
|
|
|
creds.credentialsType = credTemplate.credentialsType || creds.credentialsType; |
|
|
|
if (credTemplate.credentialsValue) { |
|
|
|
creds.credentialsValue = typeof credTemplate.credentialsValue === 'string' |
|
|
|
@ -964,8 +999,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst |
|
|
|
private async overwriteEntity(step: DeviceInstallStep, existing: EntityStepOutput): Promise<EntityStepOutput> { |
|
|
|
const raw = this.zipFiles.get(step.template); |
|
|
|
if (!raw) throw new Error(`Template file not found: ${step.template}`); |
|
|
|
const resolved = this.resolveVariables(raw); |
|
|
|
const template = JSON.parse(resolved); |
|
|
|
const template = this.resolveTemplateJson(raw); |
|
|
|
|
|
|
|
switch (step.type) { |
|
|
|
case InstallStepType.DEVICE_PROFILE: { |
|
|
|
@ -1016,8 +1050,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst |
|
|
|
private async preCheckEntity(step: DeviceInstallStep): Promise<EntityStepOutput | null> { |
|
|
|
const raw = this.zipFiles.get(step.template); |
|
|
|
if (!raw) return null; |
|
|
|
const resolved = this.resolveVariables(raw); |
|
|
|
const template = JSON.parse(resolved); |
|
|
|
const template = this.resolveTemplateJson(raw); |
|
|
|
|
|
|
|
switch (step.type) { |
|
|
|
case InstallStepType.DEVICE_PROFILE: |
|
|
|
@ -1095,7 +1128,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst |
|
|
|
if (step.serverAttributes) { |
|
|
|
const raw = this.zipFiles.get(step.serverAttributes); |
|
|
|
if (raw) { |
|
|
|
const resolved = JSON.parse(this.resolveVariables(raw)); |
|
|
|
const resolved = this.resolveTemplateJson(raw); |
|
|
|
const attrs = Object.entries(resolved).map(([key, value]) => ({ key, value })); |
|
|
|
await firstValueFrom(this.attributeService.saveEntityAttributes( |
|
|
|
entityId, AttributeScope.SERVER_SCOPE, attrs, {ignoreErrors: true} |
|
|
|
@ -1105,7 +1138,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst |
|
|
|
if (step.sharedAttributes) { |
|
|
|
const raw = this.zipFiles.get(step.sharedAttributes); |
|
|
|
if (raw) { |
|
|
|
const resolved = JSON.parse(this.resolveVariables(raw)); |
|
|
|
const resolved = this.resolveTemplateJson(raw); |
|
|
|
const attrs = Object.entries(resolved).map(([key, value]) => ({ key, value })); |
|
|
|
await firstValueFrom(this.attributeService.saveEntityAttributes( |
|
|
|
entityId, AttributeScope.SHARED_SCOPE, attrs, {ignoreErrors: true} |
|
|
|
|