UNPKG

@iotize/ionic

Version:

Iotize specific building blocks on top of @ionic/angular.

1,186 lines (1,178 loc) 86.4 kB
import * as i1$2 from '@angular/forms'; import { Validators, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { bufferToHexString, hexStringToBuffer } from '@iotize/common/byte-converter'; import * as i1 from '@iotize/ionic'; import { TapInfo, FACTORY_RESET_MODE_VERSION, TAP_MANAGER_APP_ID, interfaceServiceGetAppPathResolved, createCacheKey, toTapInfoKeyObject, LibError, isTapResultCodeError, PendingCallManager, LibCommonModule, CONFIGURATION_MODE_VERSION } from '@iotize/ionic'; import { HostProtocol, TargetProtocol, TapResponse, SpecialFeature, TapError, TapResponseStatusError, PathParameter } from '@iotize/tap'; import { converters } from '@iotize/tap/service/core'; import { NfcConnectionPriority, NfcPairingMode } from '@iotize/tap/service/impl/interface'; import { WifiSSIDVisibility, WifiKeyVisibility, WifiMode } from '@iotize/tap/service/impl/wifi'; import { combineLatest, of, BehaviorSubject, Subject, defer, concat } from 'rxjs'; import { map, switchMap, distinctUntilChanged, shareReplay, tap, filter, first } from 'rxjs/operators'; import * as i0 from '@angular/core'; import { Injectable, Pipe, EventEmitter, Component, Output, Input, NgModule } from '@angular/core'; import { deepEqual, deepCopy, listEnumValues } from '@iotize/common/utility'; import * as i3 from '@ionic/angular'; import { ResultCode, TapRequestFrame } from '@iotize/tap/client/api'; import { createDebugger } from '@iotize/common/debug'; import * as i5 from '@angular/common'; import * as i6 from '@iotize/app-common'; import { FormErrorItemModule } from '@iotize/app-common'; import { isCodeError } from '@iotize/common/error'; import { tapResponseStatusToString } from '@iotize/tap/client/impl'; import * as i1$1 from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core'; import { TapResourceMetaData } from '@iotize/tap/service/all'; import { DotAccessor } from '@iotize/common/data-accessor'; import { xmlToJson } from '@iotize/common/xml'; import { IoTizeStudioToTapConfigConverter } from '@iotize/tap/config/iotize-studio'; import { ADMIN_USER } from '@iotize/tap/configurator'; import { ConnectionState } from '@iotize/tap/protocol/api'; function enumToOptions(mapping) { const keys = Object.keys(mapping).filter((k) => typeof mapping[k] === 'number'); return keys.map((key) => { return { key: mapping[key], text: key }; }); } function tapResourceTranslateKey(path, subKey) { return `tap.lwm2m.paths.${path}.${subKey}`; } const TAP_CONFIG_PATTERN = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/; const IPV4_PATTERN = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/; const IPV4_MASK_PATTERN = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/; const SSID_PATTERN = /^[^ !#;+\]\/"\t][^+\]\/"\t]{0,30}[^ !#;+\]\/"\t]$|^[^ !#;+\]\/"\t]$/; function regexValidator(pattern, message) { return (control) => { const value = control.value; const valueMatchPattern = pattern.test(value); return !valueMatchPattern ? { invalidFormat: { value: control.value, message } } : null; }; } const validateTapVersion = regexValidator(TAP_CONFIG_PATTERN, `Must be a valid semantic version (Major.Minor.Patch).`); function enumFormatter(data) { return (value) => { if (Array.isArray(value)) { if (value.length === 0) { return '-'; } return value.map((key) => HostProtocol[key]).join(', '); } else { return data[value]; } }; } function isTapConfigured(version) { return !(version.major === 0 && version.minor === 0 && version.patch === 0); } function stringToEnumArray(v, mapping) { return v .split(',') .map((item) => item.trim()) .map((item) => { if (!(item in mapping)) { throw new Error(`Invalid value "${item}". Must one of the following: ${Object.keys(mapping).join(', ')}`); } return mapping[item]; }); } var WifiTxPowerEnum; (function (WifiTxPowerEnum) { WifiTxPowerEnum[WifiTxPowerEnum["default"] = 0] = "default"; WifiTxPowerEnum[WifiTxPowerEnum["10 dBm"] = 40] = "10 dBm"; WifiTxPowerEnum[WifiTxPowerEnum["12.5 dBm"] = 50] = "12.5 dBm"; WifiTxPowerEnum[WifiTxPowerEnum["15 dBm"] = 60] = "15 dBm"; WifiTxPowerEnum[WifiTxPowerEnum["17.5 dBm"] = 70] = "17.5 dBm"; WifiTxPowerEnum[WifiTxPowerEnum["20 dBm"] = 80] = "20 dBm"; })(WifiTxPowerEnum || (WifiTxPowerEnum = {})); const IPV4_VALIDATOR = regexValidator(IPV4_PATTERN, `Not a valid IPv4 address (eg 192.168.20.1)`); const defaultInfoResolverConfig = [ { key: TapInfo.HostProtocol, input: createEnumField('HostProtocol', HostProtocol), viewFormatter: enumFormatter(HostProtocol), }, { key: TapInfo.configVersion, input: { formValidators: [validateTapVersion], }, viewFormatter: (v) => { if (v === FACTORY_RESET_MODE_VERSION) { return 'FACTORY RESET'; } else if (v === '255.255.65535') { return 'CONFIGURATION MODE'; } else if (v) { return `v${v}`; } else { return 'UNKNOWN'; } }, editFormatter: { read: (input) => { return input; }, write: (v) => { return v || FACTORY_RESET_MODE_VERSION; }, }, }, { key: TapInfo.CloudEndpoint, getValue: async (tap) => { const hostname = (await tap.service.mqtt.getBrokerHostname()).body(); const port = (await tap.service.mqtt.getBrokerPort()).body(); return `${hostname}:${port}`; }, putValue: async (tap, value) => { const [hostname, port] = value.split(':'); const setPort = await tap.service.mqtt.putBrokerPort(port || '1883'); setPort.successful(); return tap.service.mqtt.putBrokerHostname(hostname); }, }, { key: TapInfo.TargetProtocolConfiguration, viewFormatter: (v) => `${bufferToHexString(v)}`, editFormatter: { read: (input) => { return input ? hexStringToBuffer(input) : new Uint8Array(); }, write: (v) => { return v ? bufferToHexString(v) : ''; }, }, getValue: async (tap) => { const result = await tap.service.target.getModbusTcpConfiguration(); result.successful(); return result.rawBody(); }, putValue: async (tap, value) => { return await tap.lwm2m.put('/1027//21', value); }, }, { input: { type: 'toggle', }, key: TapInfo.NFCConnectionPriority, getValue: async (tap) => { const value = (await tap.service.interface.getNfcConnectionPriority()).body(); return value === NfcConnectionPriority.NFC_PRIORITY; }, putValue: (tap, val) => { const mode = val ? NfcConnectionPriority.NFC_NON_PRIORITY : NfcConnectionPriority.NFC_PRIORITY; return tap.service.interface.putNfcConnectionPriority(mode); }, }, { key: TapInfo.NFCPairingMode, input: createEnumField('NfcPairingMode', NfcPairingMode), }, { key: TapInfo.DataLogMaxPacketCount, viewFormatter: (v) => { return v === 0 ? 'NO LIMIT' : v.toString(); }, input: { type: 'number', }, }, { key: TapInfo.IsTargetConnected, // viewFormatter: (v: boolean) => v ? 'YES' : 'NO', getValue: (tap) => tap.service.target.isConnected(), putValue: (tap, enabled) => { if (enabled) { return tap.service.target.connect(); } else { return tap.service.target.disconnect(); } }, input: { type: 'toggle', }, }, { key: TapInfo.universalLink, icon: 'link', }, { key: TapInfo.androidApplicationRecord, // TODO replace with getAndroidApplicationId() getValue: async (tap) => { const appPath = (await tap.service.interface.getAppPath()).body(); if (appPath.startsWith('$4/')) { return appPath.substring(3); } else { return TAP_MANAGER_APP_ID; } }, viewFormatter: (input) => { if (input === TAP_MANAGER_APP_ID) { return `${input} (Tap Manager)`; } return input; }, putValue: async (tap, value) => { return tap.service.interface.putAppPath(`$4/${value}`); }, }, { key: TapInfo.useEncryption, input: { type: 'toggle', }, editable: false, getValue: async (tap) => { const lockOptions = (await tap.service.interface.getSecurityOptions()).body(); return lockOptions.scramActivated; }, }, { key: TapInfo.lockFactoryReset, input: { type: 'toggle', }, editable: false, getValue: async (tap) => { const lockOptions = (await tap.service.interface.getSecurityOptions()).body(); return !lockOptions.disableHardwareFactoryReset; }, }, { key: TapInfo.InterfaceSecurityScramActivated, input: { type: 'toggle', }, editable: false, getValue: async (tap) => { const lockOptions = (await tap.service.interface.getSecurityOptions()).body(); return lockOptions.scramActivated; }, }, { key: TapInfo.InterfaceSecurityDisableHardwareFactoryReset, input: { type: 'toggle', }, editable: false, getValue: async (tap) => { const lockOptions = (await tap.service.interface.getSecurityOptions()).body(); return lockOptions.disableHardwareFactoryReset; }, }, { key: TapInfo.InterfaceSecurityDisabledResourceFactoryReset, input: { type: 'toggle', }, editable: false, getValue: async (tap) => { const lockOptions = (await tap.service.interface.getSecurityOptions()).body(); return lockOptions.disableResourceFactoryReset; }, }, { key: TapInfo.InterfaceSecurityDisabledLoginWithUID, input: { type: 'toggle', }, editable: false, getValue: async (tap) => { const lockOptions = (await tap.service.interface.getSecurityOptions()).body(); return lockOptions.disableLoginWithUID; }, }, { key: TapInfo.hashPassword, input: { type: 'toggle', }, editable: false, getValue: async (tap) => { const lockOptions = (await tap.service.interface.getSecurityOptions()).body(); return lockOptions.hashPassword; }, }, { key: TapInfo.isLoginWithUIDEnabled, input: { type: 'toggle', }, editable: false, getValue: async (tap) => { const lockOptions = (await tap.service.interface.getSecurityOptions()).body(); return !lockOptions.disableLoginWithUID; }, }, { key: TapInfo.isLWM2MFactoryResetEnabled, input: { type: 'toggle', }, editable: false, getValue: async (tap) => { const lockOptions = (await tap.service.interface.getSecurityOptions()).body(); return !lockOptions.disableResourceFactoryReset; }, }, { key: TapInfo.appPath, putValue: (tap, value) => { return tap.service.interface.putAppPath(value); }, getValue: (tap) => interfaceServiceGetAppPathResolved.call(tap.service.interface), }, { key: TapInfo.TargetProtocol, getValue: (tap) => { return tap.service.target.getProtocol(); }, putValue: (tap, protocol) => { return tap.service.target.putProtocol(protocol); }, setValue: (tap, protocol) => { return tap.service.target.setProtocol(protocol); }, viewFormatter: enumFormatter(TargetProtocol), input: { type: 'select', options: enumToOptions(TargetProtocol).filter((f) => { return f.key !== TargetProtocol.JTAG; }), }, }, { key: '/interface/available-host-protocols', viewFormatter: enumFormatter(HostProtocol), editFormatter: { write: (v) => { // console.log('Host protocols: ', v); if (!v || v.length === 0) { return 'NONE'; } return v.map((key) => HostProtocol[key]).join(', '); }, read: (v) => { return stringToEnumArray(v, HostProtocol); }, }, }, { key: TapInfo.authorizedHostProtocols, input: { type: 'select', multiple: true, options: [], }, viewFormatter: enumFormatter(HostProtocol), init: async (_service, config, infoResolver) => { const hostProtocols = await infoResolver.getValue(TapInfo.availableHostProtocols); if (config.input !== undefined) { config.input.options = hostProtocols.map((key) => { return { key: key, text: HostProtocol[key] }; }); } }, }, { key: TapInfo.AdpVersion, getValue: (tap) => tap.service.tapnpass.getStatus(), viewFormatter: (stats) => { const v = stats.header.version; return `${v.major}.${v.minor}.${v.patch}`; }, }, { key: TapInfo.WifiHostname, viewFormatter: (v) => { if (v === '0.0.0.0') { return 'NO IP'; } return v; }, }, { key: TapInfo.WifiSSID, input: { formValidators: [ regexValidator(SSID_PATTERN, `This is not a valid SSID. It can be any alphanumeric, case-sensitive entry from 1 to 32 characters. Trailing or leading spaces are not allowed.`), ], }, }, { key: TapInfo.WifiSSIDVisibility, input: createEnumField('WifiSSIDVisibility', WifiSSIDVisibility), viewFormatter: enumFormatter(WifiSSIDVisibility), }, { key: TapInfo.WifiKeyVisibility, input: createEnumField('WifiKeyVisibility', WifiKeyVisibility), viewFormatter: enumFormatter(WifiKeyVisibility), }, { key: TapInfo.WifiTxPower, viewFormatter: enumFormatter(WifiTxPowerEnum), input: createEnumField('WifiTxPowerEnum', WifiTxPowerEnum), }, { key: TapInfo.NetworkInfraIp, input: { formValidators: [IPV4_VALIDATOR], }, viewFormatter: (value) => { if (value === '0.0.0.0') { return 'Dynamic (DHCP)'; } return value; }, }, { key: TapInfo.NetworkIpMask, input: { formValidators: [ regexValidator(IPV4_MASK_PATTERN, `Not a valid IPv4 mask (eg 192.168.20.1)`), ], }, isDisplayed: ({ tapConfigItemStateService }) => { return combineLatest([ tapConfigItemStateService.valueChange(TapInfo.WifiMode), tapConfigItemStateService.valueChange(TapInfo.NetworkInfraIp), ]).pipe(map(([mode, ip]) => { return mode === WifiMode.NETWORK && ip !== '0.0.0.0'; })); }, }, { key: TapInfo.NetworkGatewayIp, input: { formValidators: [IPV4_VALIDATOR], }, isDisplayed: ({ tapConfigItemStateService }) => { return combineLatest([ tapConfigItemStateService.valueChange(TapInfo.WifiMode), tapConfigItemStateService.valueChange(TapInfo.NetworkInfraIp), ]).pipe(map(([mode, ip]) => { return mode === WifiMode.NETWORK && ip !== '0.0.0.0'; })); }, }, { key: TapInfo.NetworkDNSIp, input: { formValidators: [IPV4_VALIDATOR], }, isDisplayed: ({ tapConfigItemStateService }) => { return combineLatest([ tapConfigItemStateService.valueChange(TapInfo.WifiMode), tapConfigItemStateService.valueChange(TapInfo.NetworkInfraIp), ]).pipe(map(([mode, ip]) => { return mode === WifiMode.NETWORK && ip !== '0.0.0.0'; })); }, }, { key: TapInfo.WifiMode, input: createEnumField('WifiMode', WifiMode), }, { key: TapInfo.isHostProtocolAuthorized, input: { type: 'toggle', }, getValue: async (tap, protocol) => { if (protocol === undefined) { throw new Error('Illegal argument error: missing protocol'); } const protocols = (await tap.service.interface.getAuthorizedHostProtocol()).body(); return protocols.find((p) => p === protocol) !== undefined; }, putValue: async (tap, value, protocol) => { const protocols = (await tap.service.interface.getAuthorizedHostProtocol()).body(); if (protocol === undefined) { throw new Error('Illegal argument error: missing protocol'); } const indexOfProtocol = protocols.findIndex((p) => p === protocol); if (value) { if (indexOfProtocol === -1) { protocols.push(protocol); } } else { if (indexOfProtocol >= 0) { protocols.splice(indexOfProtocol, 1); } } return tap.service.interface.putAuthorizedHostProtocol(protocols); }, }, { key: TapInfo.WifiKey, input: { type: 'password', formValidators: [Validators.minLength(8), Validators.maxLength(128)], }, }, { key: TapInfo.variableMetaData, input: {}, getValue: async (tap, variableId) => { const metaData = (await tap.service.variable.getRawMeta(variableId)).body(); return converters.ascii.decode(metaData); }, putValue: (tap, value, bundleId) => { const data = converters.ascii.encode(value); return tap.service.variable.putRawMeta(bundleId, data); }, }, { key: TapInfo.profilePassword, input: { type: 'password', formValidators: [Validators.minLength(1), Validators.maxLength(16)], }, viewFormatter: () => { return '********'; }, putValue: async (tap, value, profileId) => { await tap.auth.changePassword(value, profileId); return TapResponse.SUCCESS(); }, }, { key: TapInfo.DeviceMemoryFree, viewFormatter(value) { return Math.floor((value / 1024) * 100) / 100 + ' KB'; }, }, { key: TapInfo.TimeLocalTime, viewFormatter(localTime) { if (!localTime) { return ''; } const date = new Date(Date.UTC(localTime.year, 0, localTime.dayOfYear)); date.setHours(localTime.hours); date.setMinutes(localTime.minutes); date.setSeconds(localTime.seconds); // {"seconds":54,"minutes":3,"hours":8,"dayOfMonth":8,"month":8,"year":122,"dayOfWeek":4,"dayOfYear":250,"isdst":0} return `${date.toUTCString()}`; }, }, { key: TapInfo.InterfaceSpecialFeatureSWDDirect, ...createSpecialFeatureDAO(SpecialFeature.SWD_DIRECT_ACCESS), }, { key: TapInfo.InterfaceSpecialFeatureSerial, ...createSpecialFeatureDAO(SpecialFeature.SERIAL_ACCESS), }, { key: TapInfo.InterfaceSpecialFeatureModbusDirect, ...createSpecialFeatureDAO(SpecialFeature.MODBUS_DIRECT_ACCESS), }, { key: '/wifi/enabled', input: { type: 'toggle', }, getValue: async (tap) => { const response = await tap.service.wifi.getDisabled(); return !response.body(); }, putValue: (tap, enabled) => { return tap.service.wifi.putDisabled(!enabled); }, }, ]; function createSpecialFeatureDAO(feature) { return { input: { type: 'number', formValidators: [Validators.min(0), Validators.max(65535)], }, getValue: async (tap) => { const result = (await tap.service.interface.getSpecialFeatureProfile(feature)).body(); if (result === 65535 - (feature - 100)) { return 'UNAUTHORIZED'; } else { return result; } }, putValue: async (tap, body) => { return await tap.service.interface.putSpecialFeatureProfile(feature, body); }, }; } function createEnumField(enumId, enumData) { return { type: 'select', enum: { id: enumId, data: enumData, }, }; } class TapConfigItemStateService { /** * Value change taking into acccount the edited value. * If no editaded value for the key, it will take the lasted fetched value from the Tap * @param key * @returns */ valueChange(key) { return this._pendingChanges.pipe(switchMap((changes) => { const hashKey = createCacheKey(toTapInfoKeyObject(key)); const editedValue = changes.find((entry) => createCacheKey(entry.key) === hashKey); if (editedValue !== undefined) { return of(editedValue.value); } else { return this.tapInfoCacheService.valueChange(key); } }), distinctUntilChanged(deepEqual), shareReplay(1)); } getPendingChangeForKey(key) { return this._pendingChanges.pipe(map((changes) => { const hashKey = createCacheKey(toTapInfoKeyObject(key)); return changes.find((entry) => createCacheKey(entry.key) === hashKey) ?.value; }), distinctUntilChanged(deepEqual), shareReplay(1)); } get pendingChangesChange() { return this._pendingChanges.asObservable(); } get saveProgress() { return this._saveProgress.asObservable(); } constructor(tapInfoDAOService, tapInfoCacheService, tapInfoConfigService) { this.tapInfoDAOService = tapInfoDAOService; this.tapInfoCacheService = tapInfoCacheService; this.tapInfoConfigService = tapInfoConfigService; this._pendingChanges = new BehaviorSubject([]); this.loading = false; this.hasPendingChanges = this._pendingChanges.pipe(map((changes) => changes.length > 0)); this.validationErrorsChange = this._pendingChanges.pipe(map((changes) => { return changes.reduce((acc, change) => { const tapConfigItem = this.tapInfoConfigService.findByKey(change.key); if (!tapConfigItem) { console.warn(`Failed to find Tap Config Item Key "${change.key}"`); return acc; } tapConfigItem.input?.formValidators?.forEach((validator) => { const error = validator(new FormControl(change.value)); if (error) { acc.set(change.key, { value: change.value, errors: error, }); } }); return acc; }, new Map()); })); this.validationErrorsCountChange = this.validationErrorsChange.pipe(map((changeErrors) => { return Array.from(changeErrors.keys()).length; }), shareReplay({ bufferSize: 1, refCount: true })); this.hasValidationErrors = this.validationErrorsCountChange.pipe(map((count) => { return count > 0; })); this._saveProgress = new Subject(); } async savePendingChanges() { try { this.loading = true; await this._savePendingChangesWithProgress().toPromise(); } finally { this.loading = false; } } _savePendingChangesWithProgress() { const configItemStateChanges = this.getPendingChangesSnapshot().filter(({ key }) => { return key.key !== TapInfo.configVersion; }); const observables = configItemStateChanges.map(({ key, value }) => { return defer(async () => { await this.tapInfoDAOService.put(key, value); this.tapInfoDAOService.get(key).catch((err) => { }); }); }); const total = configItemStateChanges.length; const saveProgress = concat(...observables).pipe(map((info, index) => { return { total, loaded: index + 1, payload: info, }; }), tap(this._saveProgress)); return saveProgress; } clearPendingChanges() { this._pendingChanges.next([]); } getPendingChangesSnapshot() { return deepCopy(this._pendingChanges.value); } clearPendingChange(tapInfoKey) { const hashKey = createCacheKey(tapInfoKey); const pendingChangesSnapshot = this._pendingChanges.value; const pendingChangeIndex = pendingChangesSnapshot.findIndex((entry) => createCacheKey(entry.key) === hashKey); if (pendingChangeIndex >= 0) { pendingChangesSnapshot.splice(pendingChangeIndex, 1); this._pendingChanges.next(pendingChangesSnapshot); } } setPendingChange(info, newValue) { // Make sure we don't store any other info otherwise deepCopy may failed info = { key: info.key, params: info.params, }; const hashKey = createCacheKey(info); const pendingChangesSnapshot = this._pendingChanges.value; const item = pendingChangesSnapshot.find((entry) => createCacheKey(entry.key) === hashKey); if (!item) { pendingChangesSnapshot.push({ key: info, value: newValue, }); this._pendingChanges.next(pendingChangesSnapshot); } else { if (!deepEqual(item.value, newValue)) { item.value = newValue; this._pendingChanges.next(pendingChangesSnapshot); } } } removePendingChange(info) { const hashKey = createCacheKey(info); const pendingChangesSnapshot = this._pendingChanges.value; const indexOfItem = pendingChangesSnapshot.findIndex((entry) => createCacheKey(entry.key) === hashKey); if (indexOfItem >= 0) { pendingChangesSnapshot.splice(indexOfItem, 1); this._pendingChanges.next(pendingChangesSnapshot); } } } /** @nocollapse */ TapConfigItemStateService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TapConfigItemStateService, deps: [{ token: i1.TapInfoDAOService }, { token: i1.TapInfoCacheService }, { token: i1.TapInfoConfigService }], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ TapConfigItemStateService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TapConfigItemStateService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TapConfigItemStateService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: function () { return [{ type: i1.TapInfoDAOService }, { type: i1.TapInfoCacheService }, { type: i1.TapInfoConfigService }]; } }); const debug = createDebugger('@iotize/ionic:config'); class TapRequestErrorToStringPipe { transform(value, action = 'read') { if (!value) { return undefined; } return userFriendlyError(value, action).message; } } /** @nocollapse */ TapRequestErrorToStringPipe.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TapRequestErrorToStringPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); /** @nocollapse */ TapRequestErrorToStringPipe.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: TapRequestErrorToStringPipe, name: "tapRequestErrorToString" }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TapRequestErrorToStringPipe, decorators: [{ type: Pipe, args: [{ name: 'tapRequestErrorToString', }] }] }); function userFriendlyError(error, action) { if (isCodeError(TapError.Code.ExecuteRequestError, error)) { error = error.cause || error; } if (isCodeError(TapResponseStatusError.Code.ResponseStatusError, error)) { const tapResponseStatusError = error; const statusCode = tapResponseStatusError.response.status; let message; switch (statusCode) { case ResultCode.UNAUTHORIZED: message = `You are not authorized to ${action} this value. Try to login first.`; break; case ResultCode.RESOURCE_LOCKED: message = `This value is ${action} protected`; break; case ResultCode.NOT_IMPLEMENTED: message = `This configuration is not available with your current firmware version or Tap model`; break; default: message = tapResponseStatusToString(statusCode); } error = new Error(message); } return error; } class TapResourceEnumTranslatePipe { transform(enumKey, part = 'title', meta) { if (meta) { let enumKeyString = typeof enumKey === 'number' ? meta.data[enumKey] : enumKey; const translationKey = `tap.lwm2m.enums.${meta.id}.${enumKeyString}.${part}`; return this.translate.stream(translationKey).pipe(map((v) => { return v === translationKey ? enumKeyString : v; })); } else { return of(enumKey.toString()); } } constructor(translate) { this.translate = translate; } } /** @nocollapse */ TapResourceEnumTranslatePipe.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TapResourceEnumTranslatePipe, deps: [{ token: i1$1.TranslateService }], target: i0.ɵɵFactoryTarget.Pipe }); /** @nocollapse */ TapResourceEnumTranslatePipe.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: TapResourceEnumTranslatePipe, name: "tapResourceEnumTranslate" }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TapResourceEnumTranslatePipe, decorators: [{ type: Pipe, args: [{ name: 'tapResourceEnumTranslate', }] }], ctorParameters: function () { return [{ type: i1$1.TranslateService }]; } }); class ListEnumValuesPipe { transform(enumData) { if (!enumData) { return []; } return listEnumValues(enumData); } } /** @nocollapse */ ListEnumValuesPipe.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ListEnumValuesPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); /** @nocollapse */ ListEnumValuesPipe.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: ListEnumValuesPipe, name: "listEnumValues" }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ListEnumValuesPipe, decorators: [{ type: Pipe, args: [{ name: 'listEnumValues', }] }] }); class TapConfigItemComponent { set info(configOrKey) { this._resetErrors(); let config; if (typeof configOrKey === 'object') { const defaultConfig = this.tapInfoConfigService.findByKey(configOrKey); if (defaultConfig) { config = { ...defaultConfig, ...configOrKey }; } else { config = configOrKey; } } else { config = this.tapInfoConfigService.findByKey({ key: configOrKey, }); if (!config) { console.warn(`Failed to find configuration with key "${configOrKey}"`); return; } } this.onConfigChange(config); this.updateDisplayValue(); // this.fetchValueIfRequired(); } get info() { return this.config; } get originalValue() { return this.config.resolvedValue?.value; } isEdited() { return this.field.value !== this.originalValue; } get config() { if (!this._config) { throw LibError.componentArgumentRequired('TapConfigItemComponent', 'info'); } return this._config; } get inputOptions() { return this.info.input || {}; } get isEditable() { return (this._config?.editable && this.editable && (!this.readError || !isTapResultCodeError(this.readError, ResultCode.NOT_IMPLEMENTED))); } constructor(formBuilder, tapService, toastController, infoResolver, tapInfoDAOService, tapInfoCacheService, tapConfigItemState, tapInfoConfigService) { this.tapService = tapService; this.toastController = toastController; this.infoResolver = infoResolver; this.tapInfoDAOService = tapInfoDAOService; this.tapInfoCacheService = tapInfoCacheService; this.tapConfigItemState = tapConfigItemState; this.tapInfoConfigService = tapInfoConfigService; this.focusOut = new EventEmitter(); this.onSubmit = new EventEmitter(); this.refresh = false; this.editMode = false; this.globalSubmit = false; this.loading = false; this.placeholder = ''; this.ressourceAvailable = true; const form = formBuilder.nonNullable.group({ field: [''], }); this.field = form.controls.field; this.pendingCallManager = PendingCallManager.create(this.tapService); } _resetErrors() { this.setReadError(undefined); this.writeError = undefined; this.ressourceAvailable = true; } get pendingEditCall() { return this.pendingCallManager.pendingCall; } onConfigChange(config) { this._valueSubscription?.unsubscribe(); this._cancelChangeSubscription?.unsubscribe(); this._config = config; this._cancelChangeSubscription = this.tapConfigItemState .getPendingChangeForKey(config) .subscribe((v) => { if (v === undefined) { this.setFieldValue(this.originalValue, { emitEvent: false, }); } }); this.initializeConfig(); if (config.input && config.input.type === 'password') { this.setDisplayValue(''); } if (config.input?.formValidators) { this.field.setValidators(config.input.formValidators); } this._valueSubscription = this.tapInfoCacheService .infoChange(config) .subscribe((newValue) => { config.resolvedValue = newValue; if (newValue.error !== undefined) { this.setReadError(newValue.error); } else { this.setReadError(undefined); if (newValue.value !== undefined) { this.setDisplayValue(newValue.value); this.setFieldValue(newValue.value); } else { this.displayedValue = undefined; } } }); } initializeConfig() { if (this.config.init) { this.config .init(this.tapService, this.config, this.infoResolver) .catch((err) => { this.setReadError(err); }); } } setWriteError(error) { this.writeError = error; } setReadError(error) { this.readError = error; this.ressourceAvailable = error ? !isTapResultCodeError(error, ResultCode.NOT_IMPLEMENTED) : true; } ngOnInit() { this._formValueSubscription = this.field.valueChanges.subscribe((newValue) => { if (this.isEdited()) { this.tapConfigItemState.setPendingChange(this.info, newValue); } else { this.tapConfigItemState.removePendingChange(this.info); } }); } ngOnDestroy() { this._valueSubscription?.unsubscribe(); this._formValueSubscription?.unsubscribe(); this._cancelChangeSubscription?.unsubscribe(); this.pendingCallManager.destroy(); } submit($event) { if (!this.globalSubmit) { debug('submit', $event); this.field.updateValueAndValidity(); if (!this.field.valid) { this.writeError = new Error('Invalid value'); return; } // this.onSubmit.emit(event); const value = this.getFieldValue(); // debug(`Creating call new value:`, value, `valid: ${this.field.valid}`) const call = async () => { await this.tapInfoDAOService.put(this.info, value); if (this.info.getValue) { this.tapInfoDAOService.get(this.info).catch((err) => { }); } return value; }; this.runCall(call); } } runCall(call) { this.loading = true; this.pendingCallManager .exec(call) .then(async (value) => { await this.showToast('Value saved!', 'success'); this.config.resolvedValue = { loadDate: new Date(), value: value, }; this.writeError = undefined; this.editMode = false; this.updateDisplayValue(); }) .catch((err) => { this.setWriteError(err); this.field.setErrors({ write: err.message, }); }) .finally(() => { this.loading = false; }); } onValueClick($event) { $event.preventDefault(); if (this.isEditable) { this.editMode = true; } } async explainPendingCall() { await this.showToast('Please reconnect your Tap to save pending changes'); } onFocusOut() { this.focusOut.emit(this.field.value); } cancelEdit($event) { debug(this.config.key, 'cancel edit', $event); if (this.globalSubmit) { this.restaureValue(); } else { this.writeError = undefined; if (this.pendingCallManager.hasPendingCall()) { this.pendingCallManager.cancel(); } else { this.editMode = false; this.restaureValue(); } } } getFieldValue() { let fieldValue = this.field.value; if (this.config.editFormatter) { fieldValue = this.config.editFormatter.read(fieldValue); } return fieldValue; } async showToast(msg, color) { const toast = await this.toastController.create({ message: msg, duration: 2000, position: 'bottom', color: color, }); await toast.present(); } refreshValue() { return this._execRefreshKey(); } _execRefreshKey() { return this.infoResolver.refreshKey(this.config); } restaureValue() { this.setFieldValue(this.originalValue); this.tapConfigItemState.removePendingChange(this.config); } setFieldValue(value, options) { const formattedValue = this.config.editFormatter ? this.config.editFormatter.write(value) : value; debug(this.config.key, 'setFieldValue with', formattedValue); this.field.setValue(formattedValue, options); } updateDisplayValue() { if (this.config.resolvedValue) { if (this.config.resolvedValue.error) { this.displayedValue = this.config.resolvedValue.error.message; } else { const value = this.config.resolvedValue.value; this.setDisplayValue(value); this.setFieldValue(value); } } else { debug(this.config.key, 'Value is not loaded yet', this.config.resolvedValue); this.displayedValue = undefined; } } setDisplayValue(value) { if (value instanceof Error) { return value.message; } else { let stringValue; if (this.config.viewFormatter) { stringValue = this.config.viewFormatter(value); } else if (typeof value === 'object') { stringValue = JSON.stringify(value); } else if (this.config.input?.type === 'select') { stringValue = this.getSelectedOption(value)?.text || (value !== undefined ? value.toString() : ''); } else { stringValue = value !== undefined ? value.toString() : ''; } // debug(`${this.config.key} setDisplayValue with`, stringValue); this.displayedValue = stringValue; return stringValue; } } getSelectedOption(value) { return this.config.input?.options?.find((option) => option.key === value); } get selectedOption() { return this.getSelectedOption(this.config.resolvedValue?.value); } async fetchValue() { if (this.config.getValue) { this.infoResolver.refreshKey(this.config); const result = await this.tapInfoCacheService .infoChange(this.config) .pipe(filter((v) => !!v.loadDate), first()) .toPromise(); } } } /** @nocollapse */ TapConfigItemComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: TapConfigItemComponent, deps: [{ token: i1$2.FormBuilder }, { token: i1.CurrentDeviceService }, { token: i3.ToastController }, { token: i1.TapInfoRequestService }, { token: i1.TapInfoDAOService }, { token: i1.TapInfoCacheService }, { token: TapConfigItemStateService }, { token: i1.TapInfoConfigService }], target: i0.ɵɵFactoryTarget.Component }); /** @nocollapse */ TapConfigItemComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: TapConfigItemComponent, selector: "tap-config-item", inputs: { editable: "editable", info: "info", refresh: "refresh", editMode: "editMode", globalSubmit: "globalSubmit" }, outputs: { focusOut: "focusOut", onSubmit: "onSubmit" }, ngImport: i0, template: "<ion-item *ngIf=\"_config\" [lines]=\"isEditable ? '' : 'none'\">\n <ion-skeleton-text\n animated\n *ngIf=\"\n displayedValue === undefined && readError === undefined;\n else valueFetchedTemplate\n \"\n ></ion-skeleton-text>\n <ng-template #valueFetchedTemplate>\n <ng-container\n *ngIf=\"ressourceAvailable; else ressourceNotAvailableTemplate\"\n >\n <ng-container *ngIf=\"!editMode || !isEditable; else editModeTemplate\">\n <ion-text\n color=\"warning\"\n class=\"read-value-error\"\n (click)=\"onValueClick($event)\"\n [class]=\"isEditable ? 'cell' : ''\"\n *ngIf=\"readError; else noReadErrorTemplate\"\n >\n <ion-icon name=\"alert-circle\"></ion-icon>\n {{ readError | tapRequestErrorToString }}\n </ion-text>\n <ng-template #noReadErrorTemplate>\n <ng-container [ngSwitch]=\"inputOptions.type\">\n <ng-container *ngSwitchCase=\"'toggle'\">\n <ion-toggle [disabled]=\"true\" [checked]=\"field.value\"></ion-toggle\n ></ng-container>\n <div\n (click)=\"onValueClick($event)\"\n *ngSwitchCase=\"'select'\"\n class=\"item-value\"\n [class]=\"isEditable ? 'cell' : ''\"\n >\n <ion-icon\n *ngIf=\"selectedOption?.icon\"\n [name]=\"selectedOption?.icon\"\n ></ion-icon>\n <ion-text class=\"tap-config-item-value\">\n {{ displayedValue }}\n </ion-text>\n </div>\n <ion-text\n (click)=\"onValueClick($event)\"\n class=\"item-value tap-config-item-value\"\n *ngSwitchDefault\n [class]=\"isEditable ? 'cell' : ''\"\n >\n {{ displayedValue }}\n </ion-text>\n </ng-container>\n </ng-template>\n </ng-container>\n <ng-template #editModeTemplate>\n <ng-container [ngSwitch]=\"inputOptions.type\">\n <ion-toggle\n *ngSwitchCase=\"'toggle'\"\n [disabled]=\"loading || !!pendingEditCall\"\n [formControl]=\"field\"\n ></ion-toggle>\n <ion-select\n style=\"max-width: 100% !important\"\n *ngSwitchCase=\"'select'\"\n [formControl]=\"field\"\n [multiple]=\"inputOptions.multiple\"\n >\n <ion-select-option\n *ngFor=\"let option of inputOptions.options\"\n [value]=\"option.key\"\n >\n <ion-icon *ngIf=\"option.icon\" [name]=\"option.icon\"></ion-icon>\n {{ option.text }}\n </ion-select-option>\n <ion-select-option\n *ngFor=\"let enumValue of inputOptions.enum?.data | listEnumValues\"\n [value]=\"enumValue\"\n >\n <ion-icon\n [name]=\"\n enumValue\n | tapResourceEnumTranslate : 'icon' : inputOptions.enum\n | async\n \"\n ></ion-icon>\n {{\n enumValue\n | tapResourceEnumTranslate : 'title' : inputOptions.enum\n | async\n }}\n </ion-select-option>\n </ion-select>\n <ion-input\n *ngSwitchDefault\n (keydown.enter)=\"submit($event)\"\n (keydown.escape)=\"cancelEdit($event)\"\n [disabled]=\"!!pendingEditCall\"\n [maxlength]=\"inputOptions.maxLength\"\n [minlength]=\"inputOptions.minLength\"\n [max]=\"inputOptions.max\"\n [min]=\"inputOptions.min\"\n [pattern]=\"inputOptions.pattern\"\n [placeholder]=\"placeholder\"\n [type]=\"inputOptions.type\"\n appAutofocus\n class=\"cellInput\"\n [formControl]=\"field\"\n >\n </ion-input>\n </ng-container>\n </ng-template>\n <ion-buttons slot=\"end\">\n <ion-button\n (click)=\"explainPendingCall()\"\n *ngIf=\"pendingEditCall\"\n [disabled]=\"false\"\n color=\"warning\"\n >\n <ion-icon name=\"alert-circle\"></ion-icon>\n </ion-button>\n <ion-button\n (click)=\"editMode = true\"\n *ngIf=\"!globalSubmit && !editMode && info.putValue\"\n >\n <ion-icon name=\"create\"></ion-icon>\n </ion-button>\n <io