Commit 75801045cfcdda85b90973d7e3079b22b747de52

Authored by Vladyslav_Prykhodko
1 parent eec4f5a4

UI: Add SNMP device profile configuration

Showing 15 changed files with 834 additions and 43 deletions
... ... @@ -99,7 +99,6 @@ import { DeviceProfileDialogComponent } from '@home/components/profile/device-pr
99 99 import { DeviceProfileAutocompleteComponent } from '@home/components/profile/device-profile-autocomplete.component';
100 100 import { MqttDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/mqtt-device-profile-transport-configuration.component';
101 101 import { CoapDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/coap-device-profile-transport-configuration.component';
102   -import { SnmpDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/snmp-device-profile-transport-configuration.component';
103 102 import { DeviceProfileAlarmsComponent } from '@home/components/profile/alarm/device-profile-alarms.component';
104 103 import { DeviceProfileAlarmComponent } from '@home/components/profile/alarm/device-profile-alarm.component';
105 104 import { CreateAlarmRulesComponent } from '@home/components/profile/alarm/create-alarm-rules.component';
... ... @@ -143,6 +142,7 @@ import { SecurityConfigLwm2mComponent } from '@home/components/device/security-c
143 142 import { SecurityConfigLwm2mServerComponent } from '@home/components/device/security-config-lwm2m-server.component';
144 143 import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component';
145 144 import { WidgetContainerComponent } from '@home/components/widget/widget-container.component';
  145 +import { SnmpDeviceProfileTransportModule } from '@home/components/profile/device/snpm/snmp-device-profile-transport.module';
146 146
147 147 @NgModule({
148 148 declarations:
... ... @@ -228,7 +228,6 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain
228 228 DefaultDeviceProfileTransportConfigurationComponent,
229 229 MqttDeviceProfileTransportConfigurationComponent,
230 230 CoapDeviceProfileTransportConfigurationComponent,
231   - SnmpDeviceProfileTransportConfigurationComponent,
232 231 DeviceProfileTransportConfigurationComponent,
233 232 CreateAlarmRulesComponent,
234 233 AlarmRuleComponent,
... ... @@ -272,6 +271,7 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain
272 271 SharedModule,
273 272 SharedHomeComponentsModule,
274 273 Lwm2mProfileComponentsModule,
  274 + SnmpDeviceProfileTransportModule,
275 275 StatesControllerModule
276 276 ],
277 277 exports: [
... ... @@ -339,7 +339,6 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain
339 339 DefaultDeviceProfileTransportConfigurationComponent,
340 340 MqttDeviceProfileTransportConfigurationComponent,
341 341 CoapDeviceProfileTransportConfigurationComponent,
342   - SnmpDeviceProfileTransportConfigurationComponent,
343 342 DeviceProfileTransportConfigurationComponent,
344 343 CreateAlarmRulesComponent,
345 344 AlarmRuleComponent,
... ...
... ... @@ -89,7 +89,9 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu
89 89 if (configuration) {
90 90 delete configuration.type;
91 91 }
92   - this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false});
  92 + setTimeout(() => {
  93 + this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false});
  94 + });
93 95 }
94 96
95 97 private updateModel() {
... ...
1   -<!--
2   -
3   - Copyright © 2016-2021 The Thingsboard Authors
4   -
5   - Licensed under the Apache License, Version 2.0 (the "License");
6   - you may not use this file except in compliance with the License.
7   - You may obtain a copy of the License at
8   -
9   - http://www.apache.org/licenses/LICENSE-2.0
10   -
11   - Unless required by applicable law or agreed to in writing, software
12   - distributed under the License is distributed on an "AS IS" BASIS,
13   - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   - See the License for the specific language governing permissions and
15   - limitations under the License.
16   -
17   --->
18   -<form [formGroup]="snmpDeviceProfileTransportConfigurationFormGroup" style="padding-bottom: 16px;">
19   - <tb-json-object-edit
20   - required
21   - formControlName="configuration">
22   - </tb-json-object-edit>
23   -</form>
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div fxLayout="column">
  19 + <div *ngFor="let deviceProfileCommunication of communicationConfigFormArray().controls; let $index = index;
  20 + last as isLast;" fxLayout="row" fxLayoutAlign="start center"
  21 + fxLayoutGap="8px" class="scope-row" [formGroup]="deviceProfileCommunication">
  22 + <div class="communication-config" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
  23 + <mat-form-field class="spec mat-block" floatLabel="always" hideRequiredMarker>
  24 + <mat-label translate>device-profile.snmp.scope</mat-label>
  25 + <mat-select formControlName="spec" required>
  26 + <mat-option *ngFor="let snmpSpecType of snmpSpecTypes" [value]="snmpSpecType"
  27 + [disabled]="isDisabledSeverity(snmpSpecType, $index)">
  28 + {{ snmpSpecTypeTranslationMap.get(snmpSpecType) }}
  29 + </mat-option>
  30 + </mat-select>
  31 + <mat-error *ngIf="deviceProfileCommunication.get('spec').hasError('required')">
  32 + {{ 'device-profile.snmp.scope-required' | translate }}
  33 + </mat-error>
  34 + </mat-form-field>
  35 + <mat-divider vertical></mat-divider>
  36 + <section fxFlex fxLayout="column">
  37 + <mat-form-field *ngIf="isShowFrequency(deviceProfileCommunication.get('spec').value)">
  38 + <mat-label translate>device-profile.snmp.querying-frequency</mat-label>
  39 + <input matInput formControlName="queryingFrequencyMs" type="number" min="0" required/>
  40 + <mat-error *ngIf="deviceProfileCommunication.get('queryingFrequencyMs').hasError('required')">
  41 + {{ 'device-profile.snmp.querying-frequency-required' | translate }}
  42 + </mat-error>
  43 + <mat-error *ngIf="deviceProfileCommunication.get('queryingFrequencyMs').hasError('pattern') ||
  44 + deviceProfileCommunication.get('queryingFrequencyMs').hasError('min')">
  45 + {{ 'device-profile.snmp.querying-frequency-invalid-format' | translate }}
  46 + </mat-error>
  47 + </mat-form-field>
  48 + <tb-snmp-device-profile-mapping formControlName="mappings">
  49 + </tb-snmp-device-profile-mapping>
  50 + </section>
  51 + </div>
  52 + <button *ngIf="!disabled"
  53 + mat-icon-button color="primary" style="min-width: 40px;"
  54 + type="button"
  55 + (click)="removeCommunicationConfig($index)"
  56 + matTooltip="{{ 'action.remove' | translate }}"
  57 + matTooltipPosition="above">
  58 + <mat-icon>remove_circle_outline</mat-icon>
  59 + </button>
  60 + </div>
  61 + <div *ngIf="!communicationConfigFormArray().controls.length && !disabled">
  62 + <span fxLayoutAlign="center center" class="tb-prompt required required-text" translate>device-profile.snmp.please-add-communication-config</span>
  63 + </div>
  64 + <div *ngIf="!disabled && isAddEnabled">
  65 + <button mat-stroked-button color="primary"
  66 + type="button"
  67 + (click)="addCommunicationConfig()">
  68 + <mat-icon class="button-icon">add_circle_outline</mat-icon>
  69 + {{ 'device-profile.snmp.add-communication-config' | translate }}
  70 + </button>
  71 + </div>
  72 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .communication-config {
  18 + border: 2px groove rgba(0, 0, 0, 0.25);
  19 + border-radius: 4px;
  20 + padding: 8px;
  21 + min-width: 0;
  22 + }
  23 +
  24 + .scope-row {
  25 + padding-bottom: 8px;
  26 + }
  27 +
  28 + .required-text {
  29 + margin: 16px 0
  30 + }
  31 +}
  32 +
  33 +:host ::ng-deep {
  34 + .mat-form-field.spec {
  35 + .mat-form-field-infix {
  36 + width: 160px;
  37 + }
  38 + }
  39 + .button-icon{
  40 + font-size: 20px;
  41 + width: 20px;
  42 + height: 20px;
  43 + }
  44 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
  18 +import {
  19 + AbstractControl,
  20 + ControlValueAccessor,
  21 + FormArray,
  22 + FormBuilder,
  23 + FormGroup,
  24 + NG_VALIDATORS,
  25 + NG_VALUE_ACCESSOR,
  26 + Validator,
  27 + Validators
  28 +} from '@angular/forms';
  29 +import { SnmpCommunicationConfig, SnmpSpecType, SnmpSpecTypeTranslationMap } from '@shared/models/device.models';
  30 +import { Subject, Subscription } from 'rxjs';
  31 +import { isUndefinedOrNull } from '@core/utils';
  32 +import { takeUntil } from 'rxjs/operators';
  33 +
  34 +@Component({
  35 + selector: 'tb-snmp-device-profile-communication-config',
  36 + templateUrl: './snmp-device-profile-communication-config.component.html',
  37 + styleUrls: ['./snmp-device-profile-communication-config.component.scss'],
  38 + providers: [
  39 + {
  40 + provide: NG_VALUE_ACCESSOR,
  41 + useExisting: forwardRef(() => SnmpDeviceProfileCommunicationConfigComponent),
  42 + multi: true
  43 + },
  44 + {
  45 + provide: NG_VALIDATORS,
  46 + useExisting: forwardRef(() => SnmpDeviceProfileCommunicationConfigComponent),
  47 + multi: true
  48 + }]
  49 +})
  50 +export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
  51 +
  52 + snmpSpecTypes = Object.values(SnmpSpecType);
  53 + snmpSpecTypeTranslationMap = SnmpSpecTypeTranslationMap;
  54 +
  55 + deviceProfileCommunicationConfig: FormGroup;
  56 +
  57 + @Input()
  58 + disabled: boolean;
  59 +
  60 + private usedSpecType: SnmpSpecType[] = [];
  61 + private valueChange$: Subscription = null;
  62 + private destroy$ = new Subject();
  63 + private propagateChange = (v: any) => { };
  64 +
  65 + constructor(private fb: FormBuilder) { }
  66 +
  67 + ngOnInit(): void {
  68 + this.deviceProfileCommunicationConfig = this.fb.group({
  69 + communicationConfig: this.fb.array([])
  70 + });
  71 + }
  72 +
  73 + ngOnDestroy() {
  74 + if (this.valueChange$) {
  75 + this.valueChange$.unsubscribe();
  76 + }
  77 + this.destroy$.next();
  78 + this.destroy$.complete();
  79 + }
  80 +
  81 + communicationConfigFormArray(): FormArray {
  82 + return this.deviceProfileCommunicationConfig.get('communicationConfig') as FormArray;
  83 + }
  84 +
  85 + registerOnChange(fn: any): void {
  86 + this.propagateChange = fn;
  87 + }
  88 +
  89 + registerOnTouched(fn: any): void {
  90 + }
  91 +
  92 + setDisabledState(isDisabled: boolean) {
  93 + this.disabled = isDisabled;
  94 + if (this.disabled) {
  95 + this.deviceProfileCommunicationConfig.disable({emitEvent: false});
  96 + } else {
  97 + this.deviceProfileCommunicationConfig.enable({emitEvent: false});
  98 + }
  99 + }
  100 +
  101 + writeValue(communicationConfig: SnmpCommunicationConfig[]) {
  102 + if (this.valueChange$) {
  103 + this.valueChange$.unsubscribe();
  104 + }
  105 + const communicationConfigControl: Array<AbstractControl> = [];
  106 + if (communicationConfig) {
  107 + communicationConfig.forEach((config) => {
  108 + communicationConfigControl.push(this.createdFormGroup(config));
  109 + });
  110 + }
  111 + this.deviceProfileCommunicationConfig.setControl('communicationConfig', this.fb.array(communicationConfigControl));
  112 + if (!communicationConfig || !communicationConfig.length) {
  113 + this.addCommunicationConfig();
  114 + }
  115 + if (this.disabled) {
  116 + this.deviceProfileCommunicationConfig.disable({emitEvent: false});
  117 + } else {
  118 + this.deviceProfileCommunicationConfig.enable({emitEvent: false});
  119 + }
  120 + this.valueChange$ = this.deviceProfileCommunicationConfig.valueChanges.subscribe(() => {
  121 + this.updateModel();
  122 + });
  123 + this.updateUsedSpecType();
  124 + if (!this.disabled && !this.deviceProfileCommunicationConfig.valid) {
  125 + this.updateModel();
  126 + }
  127 + }
  128 +
  129 + public validate() {
  130 + return this.deviceProfileCommunicationConfig.valid && this.deviceProfileCommunicationConfig.value.communicationConfig.length ? null : {
  131 + communicationConfig: false
  132 + };
  133 + }
  134 +
  135 + public removeCommunicationConfig(index: number) {
  136 + this.communicationConfigFormArray().removeAt(index);
  137 + }
  138 +
  139 +
  140 + get isAddEnabled(): boolean {
  141 + return this.communicationConfigFormArray().length !== Object.keys(SnmpSpecType).length;
  142 + }
  143 +
  144 + public addCommunicationConfig() {
  145 + this.communicationConfigFormArray().push(this.createdFormGroup());
  146 + this.deviceProfileCommunicationConfig.updateValueAndValidity();
  147 + if (!this.deviceProfileCommunicationConfig.valid) {
  148 + this.updateModel();
  149 + }
  150 + }
  151 +
  152 + private getFirstUnusedSeverity(): SnmpSpecType {
  153 + for (const type of Object.values(SnmpSpecType)) {
  154 + if (this.usedSpecType.indexOf(type) === -1) {
  155 + return type;
  156 + }
  157 + }
  158 + return null;
  159 + }
  160 +
  161 + public isDisabledSeverity(type: SnmpSpecType, index: number): boolean {
  162 + const usedIndex = this.usedSpecType.indexOf(type);
  163 + return usedIndex > -1 && usedIndex !== index;
  164 + }
  165 +
  166 + public isShowFrequency(type: SnmpSpecType): boolean {
  167 + return type === SnmpSpecType.TELEMETRY_QUERYING || type === SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING;
  168 + }
  169 +
  170 + private updateUsedSpecType() {
  171 + this.usedSpecType = [];
  172 + const value: SnmpCommunicationConfig[] = this.deviceProfileCommunicationConfig.get('communicationConfig').value;
  173 + value.forEach((rule, index) => {
  174 + this.usedSpecType[index] = rule.spec;
  175 + });
  176 + }
  177 +
  178 + private createdFormGroup(value?: SnmpCommunicationConfig): FormGroup {
  179 + if (isUndefinedOrNull(value)) {
  180 + value = {
  181 + spec: this.getFirstUnusedSeverity(),
  182 + queryingFrequencyMs: 0,
  183 + mappings: null
  184 + };
  185 + }
  186 + const form = this.fb.group({
  187 + spec: [value.spec, Validators.required],
  188 + mappings: [value.mappings]
  189 + });
  190 + if (this.isShowFrequency(value.spec)) {
  191 + form.addControl('queryingFrequencyMs',
  192 + this.fb.control(value.queryingFrequencyMs, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]));
  193 + }
  194 + form.get('spec').valueChanges.pipe(
  195 + takeUntil(this.destroy$)
  196 + ).subscribe(spec => {
  197 + if (this.isShowFrequency(spec)) {
  198 + form.addControl('queryingFrequencyMs',
  199 + this.fb.control(0, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]));
  200 + } else {
  201 + form.removeControl('queryingFrequencyMs');
  202 + }
  203 + });
  204 + return form;
  205 + }
  206 +
  207 + private updateModel() {
  208 + const value: SnmpCommunicationConfig[] = this.deviceProfileCommunicationConfig.get('communicationConfig').value;
  209 + value.forEach(config => {
  210 + if (!this.isShowFrequency(config.spec)) {
  211 + delete config.queryingFrequencyMs;
  212 + }
  213 + });
  214 + this.updateUsedSpecType();
  215 + this.propagateChange(value);
  216 + }
  217 +
  218 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div fxFlex fxLayout="column" class="mapping-config">
  19 + <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex="100">
  20 + <div fxFlex fxLayout="row" fxLayoutGap="8px">
  21 + <label fxFlex="26" class="tb-title no-padding" translate>device-profile.snmp.data-type</label>
  22 + <label fxFlex="37" class="tb-title no-padding" translate>device-profile.snmp.data-key</label>
  23 + <label fxFlex="37" class="tb-title no-padding" translate>device-profile.snmp.oid</label>
  24 + <span style="min-width: 40px" [fxShow]="!disabled"></span>
  25 + </div>
  26 + </div>
  27 + <mat-divider></mat-divider>
  28 + <div *ngFor="let mappingConfig of mappingsConfigFormArray().controls; let $index = index;
  29 + last as isLast;" fxLayout="row" fxLayoutAlign="start center"
  30 + fxLayoutGap="8px" [formGroup]="mappingConfig" class="mapping-list">
  31 + <div fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
  32 + <mat-form-field fxFlex="26" floatLabel="always" hideRequiredMarker>
  33 + <mat-label></mat-label>
  34 + <mat-select formControlName="dataType" required>
  35 + <mat-option *ngFor="let dataType of dataTypes" [value]="dataType">
  36 + {{ dataTypesTranslationMap.get(dataType) | translate }}
  37 + </mat-option>
  38 + </mat-select>
  39 + <mat-error *ngIf="mappingConfig.get('dataType').hasError('required')">
  40 + {{ 'device-profile.snmp.data-type-required' | translate }}
  41 + </mat-error>
  42 + </mat-form-field>
  43 + <mat-form-field floatLabel="always" hideRequiredMarker fxFlex="37">
  44 + <mat-label></mat-label>
  45 + <input matInput formControlName="key" required/>
  46 + <mat-error *ngIf="mappingConfig.get('key').hasError('required')">
  47 + {{ 'device-profile.snmp.data-key-required' | translate }}
  48 + </mat-error>
  49 + </mat-form-field>
  50 + <mat-form-field floatLabel="always" hideRequiredMarker fxFlex="37">
  51 + <mat-label></mat-label>
  52 + <input matInput formControlName="oid" required/>
  53 + <mat-error *ngIf="mappingConfig.get('oid').hasError('required')">
  54 + {{ 'device-profile.snmp.oid-required' | translate }}
  55 + </mat-error>
  56 + <mat-error *ngIf="mappingConfig.get('oid').hasError('pattern')">
  57 + {{ 'device-profile.snmp.oid-pattern' | translate }}
  58 + </mat-error>
  59 + </mat-form-field>
  60 + <button *ngIf="!disabled"
  61 + mat-icon-button color="primary"
  62 + type="button"
  63 + (click)="removeMappingConfig($index)"
  64 + matTooltip="{{ 'action.remove' | translate }}"
  65 + matTooltipPosition="above">
  66 + <mat-icon>close</mat-icon>
  67 + </button>
  68 + </div>
  69 + </div>
  70 + <div *ngIf="!mappingsConfigFormArray().controls.length && !disabled">
  71 + <span fxLayoutAlign="center center" class="tb-prompt required required-text" translate>device-profile.snmp.please-add-mapping-config</span>
  72 + </div>
  73 + <div *ngIf="!disabled">
  74 + <button mat-stroked-button color="primary"
  75 + type="button"
  76 + (click)="addMappingConfig()">
  77 + <mat-icon class="button-icon">add_circle_outline</mat-icon>
  78 + {{ 'device-profile.snmp.add-mapping' | translate }}
  79 + </button>
  80 + </div>
  81 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .mapping-config {
  18 + min-width: 518px;
  19 + }
  20 + .mapping-list {
  21 + padding-bottom: 8px;
  22 + height: 46px;
  23 + }
  24 +
  25 + .required-text {
  26 + margin: 14px 0;
  27 + }
  28 +}
  29 +
  30 +:host ::ng-deep {
  31 + .mapping-list {
  32 + mat-form-field {
  33 + .mat-form-field-wrapper {
  34 + padding-bottom: 0;
  35 + .mat-form-field-infix {
  36 + border-top-width: 0.2em;
  37 + width: auto;
  38 + min-width: auto;
  39 + }
  40 + .mat-form-field-underline {
  41 + bottom: 0;
  42 + }
  43 + }
  44 + }
  45 + }
  46 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
  18 +import {
  19 + AbstractControl,
  20 + ControlValueAccessor,
  21 + FormArray,
  22 + FormBuilder,
  23 + FormGroup,
  24 + NG_VALIDATORS,
  25 + NG_VALUE_ACCESSOR,
  26 + ValidationErrors,
  27 + Validator,
  28 + Validators
  29 +} from '@angular/forms';
  30 +import { SnmpMapping } from '@shared/models/device.models';
  31 +import { Subscription } from 'rxjs';
  32 +import { DataType, DataTypeTranslationMap } from '@shared/models/constants';
  33 +import { isUndefinedOrNull } from '@core/utils';
  34 +
  35 +@Component({
  36 + selector: 'tb-snmp-device-profile-mapping',
  37 + templateUrl: './snmp-device-profile-mapping.component.html',
  38 + styleUrls: ['./snmp-device-profile-mapping.component.scss'],
  39 + providers: [
  40 + {
  41 + provide: NG_VALUE_ACCESSOR,
  42 + useExisting: forwardRef(() => SnmpDeviceProfileMappingComponent),
  43 + multi: true
  44 + },
  45 + {
  46 + provide: NG_VALIDATORS,
  47 + useExisting: forwardRef(() => SnmpDeviceProfileMappingComponent),
  48 + multi: true
  49 + }]
  50 +})
  51 +export class SnmpDeviceProfileMappingComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
  52 +
  53 + mappingsConfigForm: FormGroup;
  54 +
  55 + dataTypes = Object.values(DataType);
  56 + dataTypesTranslationMap = DataTypeTranslationMap;
  57 +
  58 + @Input()
  59 + disabled: boolean;
  60 +
  61 + private readonly oidPattern: RegExp = /^\.?([0-2])((\.0)|(\.[1-9][0-9]*))*$/;
  62 +
  63 + private valueChange$: Subscription = null;
  64 + private propagateChange = (v: any) => { };
  65 +
  66 + constructor(private fb: FormBuilder) { }
  67 +
  68 + ngOnInit() {
  69 + this.mappingsConfigForm = this.fb.group({
  70 + mappings: this.fb.array([])
  71 + });
  72 + }
  73 +
  74 + ngOnDestroy() {
  75 + if (this.valueChange$) {
  76 + this.valueChange$.unsubscribe();
  77 + }
  78 + }
  79 +
  80 + registerOnChange(fn: any) {
  81 + this.propagateChange = fn;
  82 + }
  83 +
  84 + registerOnTouched(fn: any) {
  85 + }
  86 +
  87 + setDisabledState(isDisabled: boolean) {
  88 + this.disabled = isDisabled;
  89 + if (this.disabled) {
  90 + this.mappingsConfigForm.disable({emitEvent: false});
  91 + } else {
  92 + this.mappingsConfigForm.enable({emitEvent: false});
  93 + }
  94 + }
  95 +
  96 + validate(): ValidationErrors | null {
  97 + return this.mappingsConfigForm.valid && this.mappingsConfigForm.value.mappings.length ? null : {
  98 + mapping: false
  99 + };
  100 + }
  101 +
  102 + writeValue(mappings: SnmpMapping[]) {
  103 + if (this.valueChange$) {
  104 + this.valueChange$.unsubscribe();
  105 + }
  106 + const mappingsControl: Array<AbstractControl> = [];
  107 + if (mappings) {
  108 + mappings.forEach((config) => {
  109 + mappingsControl.push(this.createdFormGroup(config));
  110 + });
  111 + }
  112 + this.mappingsConfigForm.setControl('mappings', this.fb.array(mappingsControl));
  113 + if (!mappings || !mappings.length) {
  114 + this.addMappingConfig();
  115 + }
  116 + if (this.disabled) {
  117 + this.mappingsConfigForm.disable({emitEvent: false});
  118 + } else {
  119 + this.mappingsConfigForm.enable({emitEvent: false});
  120 + }
  121 + this.valueChange$ = this.mappingsConfigForm.valueChanges.subscribe(() => {
  122 + this.updateModel();
  123 + });
  124 + if (!this.disabled && !this.mappingsConfigForm.valid) {
  125 + this.updateModel();
  126 + }
  127 + }
  128 +
  129 + mappingsConfigFormArray(): FormArray {
  130 + return this.mappingsConfigForm.get('mappings') as FormArray;
  131 + }
  132 +
  133 + public addMappingConfig() {
  134 + this.mappingsConfigFormArray().push(this.createdFormGroup());
  135 + this.mappingsConfigForm.updateValueAndValidity();
  136 + if (!this.mappingsConfigForm.valid) {
  137 + this.updateModel();
  138 + }
  139 + }
  140 +
  141 + public removeMappingConfig(index: number) {
  142 + this.mappingsConfigFormArray().removeAt(index);
  143 + }
  144 +
  145 + private createdFormGroup(value?: SnmpMapping): FormGroup {
  146 + if (isUndefinedOrNull(value)) {
  147 + value = {
  148 + dataType: DataType.STRING,
  149 + key: '',
  150 + oid: ''
  151 + };
  152 + }
  153 + return this.fb.group({
  154 + dataType: [value.dataType, Validators.required],
  155 + key: [value.key, Validators.required],
  156 + oid: [value.oid, [Validators.required, Validators.pattern(this.oidPattern)]]
  157 + });
  158 + }
  159 +
  160 + private updateModel() {
  161 + const value: SnmpMapping[] = this.mappingsConfigForm.get('mappings').value;
  162 + this.propagateChange(value);
  163 + }
  164 +
  165 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form [formGroup]="snmpDeviceProfileTransportConfigurationFormGroup" style="padding: 8px 0 16px;">
  19 + <section fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column">
  20 + <mat-form-field fxFlex>
  21 + <mat-label translate>device-profile.snmp.timeout-ms</mat-label>
  22 + <input matInput formControlName="timeoutMs" type="number" min="0" required/>
  23 + <mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('required')">
  24 + {{ 'device-profile.snmp.timeout-ms-required' | translate }}
  25 + </mat-error>
  26 + <mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('pattern') ||
  27 + snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('min')">
  28 + {{ 'device-profile.snmp.timeout-ms-invalid-format' | translate }}
  29 + </mat-error>
  30 + </mat-form-field>
  31 + <mat-form-field fxFlex>
  32 + <mat-label translate>device-profile.snmp.retries</mat-label>
  33 + <input matInput formControlName="retries" type="number" min="0" required/>
  34 + <mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('required')">
  35 + {{ 'device-profile.snmp.retries-required' | translate }}
  36 + </mat-error>
  37 + <mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('pattern') ||
  38 + snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('min')">
  39 + {{ 'device-profile.snmp.retries-invalid-format' | translate }}
  40 + </mat-error>
  41 + </mat-form-field>
  42 + </section>
  43 + <div class="tb-small" style="padding-bottom: 8px" translate>device-profile.snmp.communication-configs</div>
  44 + <tb-snmp-device-profile-communication-config formControlName="communicationConfigs">
  45 + </tb-snmp-device-profile-communication-config>
  46 +</form>
... ...
ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport-configuration.component.ts renamed from ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.ts
... ... @@ -15,9 +15,16 @@
15 15 ///
16 16
17 17 import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
18   -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
19   -import { Store } from '@ngrx/store';
20   -import { AppState } from '@app/core/core.state';
  18 +import {
  19 + ControlValueAccessor,
  20 + FormBuilder,
  21 + FormGroup,
  22 + NG_VALIDATORS,
  23 + NG_VALUE_ACCESSOR,
  24 + ValidationErrors,
  25 + Validator,
  26 + Validators
  27 +} from '@angular/forms';
21 28 import { coerceBooleanProperty } from '@angular/cdk/coercion';
22 29 import {
23 30 DeviceProfileTransportConfiguration,
... ... @@ -40,19 +47,24 @@ export interface OidMappingConfiguration {
40 47 selector: 'tb-snmp-device-profile-transport-configuration',
41 48 templateUrl: './snmp-device-profile-transport-configuration.component.html',
42 49 styleUrls: [],
43   - providers: [{
44   - provide: NG_VALUE_ACCESSOR,
45   - useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent),
46   - multi: true
47   - }]
  50 + providers: [
  51 + {
  52 + provide: NG_VALUE_ACCESSOR,
  53 + useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent),
  54 + multi: true
  55 + },
  56 + {
  57 + provide: NG_VALIDATORS,
  58 + useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent),
  59 + multi: true
  60 + }]
48 61 })
49   -export class SnmpDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy {
  62 +export class SnmpDeviceProfileTransportConfigurationComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
50 63
51 64 snmpDeviceProfileTransportConfigurationFormGroup: FormGroup;
52 65
53 66 private destroy$ = new Subject();
54 67 private requiredValue: boolean;
55   - private configuration = [];
56 68
57 69 get required(): boolean {
58 70 return this.requiredValue;
... ... @@ -69,12 +81,14 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements Control
69 81 private propagateChange = (v: any) => {
70 82 }
71 83
72   - constructor(private store: Store<AppState>, private fb: FormBuilder) {
  84 + constructor(private fb: FormBuilder) {
73 85 }
74 86
75 87 ngOnInit(): void {
76 88 this.snmpDeviceProfileTransportConfigurationFormGroup = this.fb.group({
77   - configuration: [null, Validators.required]
  89 + timeoutMs: [0, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
  90 + retries: [0, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
  91 + communicationConfigs: [null, Validators.required],
78 92 });
79 93 this.snmpDeviceProfileTransportConfigurationFormGroup.valueChanges.pipe(
80 94 takeUntil(this.destroy$)
... ... @@ -95,18 +109,33 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements Control
95 109 registerOnTouched(fn: any): void {
96 110 }
97 111
  112 + setDisabledState(isDisabled: boolean) {
  113 + this.disabled = isDisabled;
  114 + if (this.disabled) {
  115 + this.snmpDeviceProfileTransportConfigurationFormGroup.disable({emitEvent: false});
  116 + } else {
  117 + this.snmpDeviceProfileTransportConfigurationFormGroup.enable({emitEvent: false});
  118 + }
  119 + }
  120 +
98 121 writeValue(value: SnmpDeviceProfileTransportConfiguration | null): void {
99 122 if (isDefinedAndNotNull(value)) {
100   - this.snmpDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false});
  123 + this.snmpDeviceProfileTransportConfigurationFormGroup.patchValue(value, {emitEvent: !value.communicationConfigs});
101 124 }
102 125 }
103 126
104 127 private updateModel() {
105 128 let configuration: DeviceProfileTransportConfiguration = null;
106 129 if (this.snmpDeviceProfileTransportConfigurationFormGroup.valid) {
107   - configuration = this.snmpDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration;
  130 + configuration = this.snmpDeviceProfileTransportConfigurationFormGroup.getRawValue();
108 131 configuration.type = DeviceTransportType.SNMP;
109 132 }
110 133 this.propagateChange(configuration);
111 134 }
  135 +
  136 + validate(): ValidationErrors | null {
  137 + return this.snmpDeviceProfileTransportConfigurationFormGroup.valid ? null : {
  138 + snmpDeviceProfileTransportConfiguration: false
  139 + };
  140 + }
112 141 }
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { SharedModule } from '@shared/shared.module';
  19 +import { CommonModule } from '@angular/common';
  20 +import { SnmpDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/snpm/snmp-device-profile-transport-configuration.component';
  21 +import { SnmpDeviceProfileCommunicationConfigComponent } from './snmp-device-profile-communication-config.component';
  22 +import { SnmpDeviceProfileMappingComponent } from './snmp-device-profile-mapping.component';
  23 +
  24 +@NgModule({
  25 + declarations: [
  26 + SnmpDeviceProfileTransportConfigurationComponent,
  27 + SnmpDeviceProfileCommunicationConfigComponent,
  28 + SnmpDeviceProfileMappingComponent
  29 + ],
  30 + imports: [
  31 + CommonModule,
  32 + SharedModule
  33 + ],
  34 + exports: [
  35 + SnmpDeviceProfileTransportConfigurationComponent
  36 + ]
  37 +})
  38 +export class SnmpDeviceProfileTransportModule { }
... ...
... ... @@ -147,6 +147,22 @@ export enum ValueType {
147 147 JSON = 'JSON'
148 148 }
149 149
  150 +export enum DataType {
  151 + STRING = 'STRING',
  152 + LONG = 'LONG',
  153 + BOOLEAN = 'BOOLEAN',
  154 + DOUBLE = 'DOUBLE',
  155 + JSON = 'JSON'
  156 +}
  157 +
  158 +export const DataTypeTranslationMap = new Map([
  159 + [DataType.STRING, 'value.string'],
  160 + [DataType.LONG, 'value.integer'],
  161 + [DataType.BOOLEAN, 'value.boolean'],
  162 + [DataType.DOUBLE, 'value.double'],
  163 + [DataType.JSON, 'value.json']
  164 +]);
  165 +
150 166 export const valueTypesMap = new Map<ValueType, ValueTypeData>(
151 167 [
152 168 [
... ...
... ... @@ -29,6 +29,7 @@ import * as _moment from 'moment';
29 29 import { AbstractControl, ValidationErrors } from '@angular/forms';
30 30 import { OtaPackageId } from '@shared/models/id/ota-package-id';
31 31 import { DashboardId } from '@shared/models/id/dashboard-id';
  32 +import { DataType } from '@shared/models/constants';
32 33
33 34 export enum DeviceProfileType {
34 35 DEFAULT = 'DEFAULT',
... ... @@ -257,7 +258,35 @@ export interface Lwm2mDeviceProfileTransportConfiguration {
257 258 }
258 259
259 260 export interface SnmpDeviceProfileTransportConfiguration {
260   - [key: string]: any;
  261 + timeoutMs?: number;
  262 + retries?: number;
  263 + communicationConfigs?: SnmpCommunicationConfig[];
  264 +}
  265 +
  266 +export enum SnmpSpecType {
  267 + TELEMETRY_QUERYING = 'TELEMETRY_QUERYING',
  268 + CLIENT_ATTRIBUTES_QUERYING = 'CLIENT_ATTRIBUTES_QUERYING',
  269 + SHARED_ATTRIBUTES_SETTING = 'SHARED_ATTRIBUTES_SETTING',
  270 + TO_DEVICE_RPC_REQUEST = 'TO_DEVICE_RPC_REQUEST'
  271 +}
  272 +
  273 +export const SnmpSpecTypeTranslationMap = new Map<SnmpSpecType, string>([
  274 + [SnmpSpecType.TELEMETRY_QUERYING, ' Telemetry'],
  275 + [SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING, 'Client attributes'],
  276 + [SnmpSpecType.SHARED_ATTRIBUTES_SETTING, 'Shared attributes'],
  277 + [SnmpSpecType.TO_DEVICE_RPC_REQUEST, 'RPC request']
  278 +]);
  279 +
  280 +export interface SnmpCommunicationConfig {
  281 + spec: SnmpSpecType;
  282 + mappings: SnmpMapping[];
  283 + queryingFrequencyMs?: number;
  284 +}
  285 +
  286 +export interface SnmpMapping {
  287 + oid: string;
  288 + key: string;
  289 + dataType: DataType;
261 290 }
262 291
263 292 export type DeviceProfileTransportConfigurations = DefaultDeviceProfileTransportConfiguration &
... ... @@ -332,7 +361,11 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT
332 361 transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M};
333 362 break;
334 363 case DeviceTransportType.SNMP:
335   - const snmpTransportConfiguration: SnmpDeviceProfileTransportConfiguration = {};
  364 + const snmpTransportConfiguration: SnmpDeviceProfileTransportConfiguration = {
  365 + timeoutMs: 0,
  366 + retries: 0,
  367 + communicationConfigs: null
  368 + };
336 369 transportConfiguration = {...snmpTransportConfiguration, type: DeviceTransportType.SNMP};
337 370 break;
338 371 }
... ...
... ... @@ -1297,6 +1297,31 @@
1297 1297 "sw-update-recourse": "Software update CoAP recourse",
1298 1298 "sw-update-recourse-required": "Software update CoAP recourse is required.",
1299 1299 "config-json-tab": "Json Config Profile Device"
  1300 + },
  1301 + "snmp": {
  1302 + "add-communication-config": "Add communication config",
  1303 + "add-mapping": "Add mapping",
  1304 + "communication-configs": "Communication configs",
  1305 + "data-key": "Data key",
  1306 + "data-key-required": "Data key is required.",
  1307 + "data-type": "Data type",
  1308 + "data-type-required": "Data type is required.",
  1309 + "oid": "OID",
  1310 + "oid-pattern": "Invalid OID format.",
  1311 + "oid-required": "OID is required.",
  1312 + "please-add-communication-config": "Please add communication config",
  1313 + "please-add-mapping-config": "Please add mapping config",
  1314 + "querying-frequency": "Querying frequency, ms",
  1315 + "querying-frequency-invalid-format": "Querying frequency must be a positive integer.",
  1316 + "querying-frequency-required": "Querying frequency is required.",
  1317 + "retries": "Retries",
  1318 + "retries-invalid-format": "Retries must be a positive integer.",
  1319 + "retries-required": "Retries is required.",
  1320 + "scope": "Scope",
  1321 + "scope-required": "Scope is required.",
  1322 + "timeout-ms": "Timeout, ms",
  1323 + "timeout-ms-invalid-format": "Timeout must be a positive integer.",
  1324 + "timeout-ms-required": "Timeout is required."
1300 1325 }
1301 1326 },
1302 1327 "dialog": {
... ...