@iotize/ionic
Version:
Iotize specific building blocks on top of @ionic/angular.
1,186 lines (1,178 loc) • 86.4 kB
JavaScript
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