107 changed files with 7027 additions and 1114 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,322 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.queue.consumer; |
|||
|
|||
import lombok.Builder; |
|||
import lombok.Getter; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.common.util.ThingsBoardThreadFactory; |
|||
import org.thingsboard.server.common.data.queue.QueueConfig; |
|||
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; |
|||
import org.thingsboard.server.queue.TbQueueConsumer; |
|||
import org.thingsboard.server.queue.TbQueueMsg; |
|||
import org.thingsboard.server.queue.discovery.QueueKey; |
|||
import org.thingsboard.server.service.queue.ruleengine.QueueEvent; |
|||
import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerManagerTask; |
|||
import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerTask; |
|||
|
|||
import java.util.Collection; |
|||
import java.util.Collections; |
|||
import java.util.HashMap; |
|||
import java.util.HashSet; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
import java.util.concurrent.ConcurrentLinkedQueue; |
|||
import java.util.concurrent.ExecutorService; |
|||
import java.util.concurrent.Future; |
|||
import java.util.concurrent.ScheduledExecutorService; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.locks.ReentrantLock; |
|||
import java.util.function.Function; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Slf4j |
|||
public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfig> { |
|||
|
|||
protected final QueueKey queueKey; |
|||
@Getter |
|||
protected C config; |
|||
protected final MsgPackProcessor<M, C> msgPackProcessor; |
|||
protected final Function<C, TbQueueConsumer<M>> consumerCreator; |
|||
protected final ExecutorService consumerExecutor; |
|||
protected final ScheduledExecutorService scheduler; |
|||
protected final ExecutorService taskExecutor; |
|||
|
|||
private final java.util.Queue<TbQueueConsumerManagerTask> tasks = new ConcurrentLinkedQueue<>(); |
|||
private final ReentrantLock lock = new ReentrantLock(); |
|||
|
|||
@Getter |
|||
private volatile Set<TopicPartitionInfo> partitions; |
|||
protected volatile ConsumerWrapper<M> consumerWrapper; |
|||
protected volatile boolean stopped; |
|||
|
|||
@Builder |
|||
public MainQueueConsumerManager(QueueKey queueKey, C config, |
|||
MsgPackProcessor<M, C> msgPackProcessor, |
|||
Function<C, TbQueueConsumer<M>> consumerCreator, |
|||
ExecutorService consumerExecutor, |
|||
ScheduledExecutorService scheduler, |
|||
ExecutorService taskExecutor) { |
|||
this.queueKey = queueKey; |
|||
this.config = config; |
|||
this.msgPackProcessor = msgPackProcessor; |
|||
this.consumerCreator = consumerCreator; |
|||
this.consumerExecutor = consumerExecutor; |
|||
this.scheduler = scheduler; |
|||
this.taskExecutor = taskExecutor; |
|||
if (config != null) { |
|||
init(config); |
|||
} |
|||
} |
|||
|
|||
public void init(C config) { |
|||
this.config = config; |
|||
if (config.isConsumerPerPartition()) { |
|||
this.consumerWrapper = new ConsumerPerPartitionWrapper(); |
|||
} else { |
|||
this.consumerWrapper = new SingleConsumerWrapper(); |
|||
} |
|||
log.debug("[{}] Initialized consumer for queue: {}", queueKey, config); |
|||
} |
|||
|
|||
public void update(C config) { |
|||
addTask(TbQueueConsumerManagerTask.configUpdate(config)); |
|||
} |
|||
|
|||
public void update(Set<TopicPartitionInfo> partitions) { |
|||
addTask(TbQueueConsumerManagerTask.partitionChange(partitions)); |
|||
} |
|||
|
|||
protected void addTask(TbQueueConsumerManagerTask todo) { |
|||
if (stopped) { |
|||
return; |
|||
} |
|||
tasks.add(todo); |
|||
log.trace("[{}] Added task: {}", queueKey, todo); |
|||
tryProcessTasks(); |
|||
} |
|||
|
|||
private void tryProcessTasks() { |
|||
taskExecutor.submit(() -> { |
|||
if (lock.tryLock()) { |
|||
try { |
|||
C newConfig = null; |
|||
Set<TopicPartitionInfo> newPartitions = null; |
|||
while (!stopped) { |
|||
TbQueueConsumerManagerTask task = tasks.poll(); |
|||
if (task == null) { |
|||
break; |
|||
} |
|||
log.trace("[{}] Processing task: {}", queueKey, task); |
|||
|
|||
if (task.getEvent() == QueueEvent.PARTITION_CHANGE) { |
|||
newPartitions = task.getPartitions(); |
|||
} else if (task.getEvent() == QueueEvent.CONFIG_UPDATE) { |
|||
newConfig = (C) task.getConfig(); |
|||
} else { |
|||
processTask(task); |
|||
} |
|||
} |
|||
if (stopped) { |
|||
return; |
|||
} |
|||
if (newConfig != null) { |
|||
doUpdate(newConfig); |
|||
} |
|||
if (newPartitions != null) { |
|||
doUpdate(newPartitions); |
|||
} |
|||
} catch (Exception e) { |
|||
log.error("[{}] Failed to process tasks", queueKey, e); |
|||
} finally { |
|||
lock.unlock(); |
|||
} |
|||
} else { |
|||
log.trace("[{}] Failed to acquire lock", queueKey); |
|||
scheduler.schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
protected void processTask(TbQueueConsumerManagerTask task) { |
|||
} |
|||
|
|||
private void doUpdate(C newConfig) { |
|||
log.info("[{}] Processing queue update: {}", queueKey, newConfig); |
|||
var oldConfig = this.config; |
|||
this.config = newConfig; |
|||
if (log.isTraceEnabled()) { |
|||
log.trace("[{}] Old queue configuration: {}", queueKey, oldConfig); |
|||
log.trace("[{}] New queue configuration: {}", queueKey, newConfig); |
|||
} |
|||
|
|||
if (oldConfig == null) { |
|||
init(config); |
|||
} else if (newConfig.isConsumerPerPartition() != oldConfig.isConsumerPerPartition()) { |
|||
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop); |
|||
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion); |
|||
|
|||
init(config); |
|||
if (partitions != null) { |
|||
doUpdate(partitions); // even if partitions number was changed, there can be no partition change event
|
|||
} |
|||
} else { |
|||
log.trace("[{}] Silently applied new config, because consumer-per-partition not changed", queueKey); |
|||
// do nothing, because partitions change (if they changed) will be handled on PartitionChangeEvent,
|
|||
// and changes to other config values will be picked up by consumer on the fly,
|
|||
// and queue topic and name are immutable
|
|||
} |
|||
} |
|||
|
|||
private void doUpdate(Set<TopicPartitionInfo> partitions) { |
|||
this.partitions = partitions; |
|||
consumerWrapper.updatePartitions(partitions); |
|||
} |
|||
|
|||
private void launchConsumer(TbQueueConsumerTask<M> consumerTask) { |
|||
log.info("[{}] Launching consumer", consumerTask.getKey()); |
|||
Future<?> consumerLoop = consumerExecutor.submit(() -> { |
|||
ThingsBoardThreadFactory.updateCurrentThreadName(consumerTask.getKey().toString()); |
|||
try { |
|||
consumerLoop(consumerTask.getConsumer()); |
|||
} catch (Throwable e) { |
|||
log.error("Failure in consumer loop", e); |
|||
} |
|||
log.info("[{}] Consumer stopped", consumerTask.getKey()); |
|||
}); |
|||
consumerTask.setTask(consumerLoop); |
|||
} |
|||
|
|||
private void consumerLoop(TbQueueConsumer<M> consumer) { |
|||
while (!stopped && !consumer.isStopped()) { |
|||
try { |
|||
List<M> msgs = consumer.poll(config.getPollInterval()); |
|||
if (msgs.isEmpty()) { |
|||
continue; |
|||
} |
|||
processMsgs(msgs, consumer, config); |
|||
} catch (Exception e) { |
|||
if (!consumer.isStopped()) { |
|||
log.warn("Failed to process messages from queue", e); |
|||
try { |
|||
Thread.sleep(config.getPollInterval()); |
|||
} catch (InterruptedException e2) { |
|||
log.trace("Failed to wait until the server has capacity to handle new requests", e2); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (consumer.isStopped()) { |
|||
consumer.unsubscribe(); |
|||
} |
|||
} |
|||
|
|||
protected void processMsgs(List<M> msgs, TbQueueConsumer<M> consumer, C config) throws Exception { |
|||
msgPackProcessor.process(msgs, consumer, config); |
|||
} |
|||
|
|||
public void stop() { |
|||
log.debug("[{}] Stopping consumers", queueKey); |
|||
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop); |
|||
stopped = true; |
|||
} |
|||
|
|||
public void awaitStop() { |
|||
log.debug("[{}] Waiting for consumers to stop", queueKey); |
|||
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion); |
|||
log.debug("[{}] Unsubscribed and stopped consumers", queueKey); |
|||
} |
|||
|
|||
private static String partitionsToString(Collection<TopicPartitionInfo> partitions) { |
|||
return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.joining(", ", "[", "]")); |
|||
} |
|||
|
|||
public interface MsgPackProcessor<M extends TbQueueMsg, C extends QueueConfig> { |
|||
void process(List<M> msgs, TbQueueConsumer<M> consumer, C config) throws Exception; |
|||
} |
|||
|
|||
public interface ConsumerWrapper<M extends TbQueueMsg> { |
|||
|
|||
void updatePartitions(Set<TopicPartitionInfo> partitions); |
|||
|
|||
Collection<TbQueueConsumerTask<M>> getConsumers(); |
|||
|
|||
} |
|||
|
|||
class ConsumerPerPartitionWrapper implements ConsumerWrapper<M> { |
|||
private final Map<TopicPartitionInfo, TbQueueConsumerTask<M>> consumers = new HashMap<>(); |
|||
|
|||
@Override |
|||
public void updatePartitions(Set<TopicPartitionInfo> partitions) { |
|||
Set<TopicPartitionInfo> addedPartitions = new HashSet<>(partitions); |
|||
addedPartitions.removeAll(consumers.keySet()); |
|||
|
|||
Set<TopicPartitionInfo> removedPartitions = new HashSet<>(consumers.keySet()); |
|||
removedPartitions.removeAll(partitions); |
|||
log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, partitionsToString(addedPartitions), partitionsToString(removedPartitions)); |
|||
|
|||
removedPartitions.forEach((tpi) -> consumers.get(tpi).initiateStop()); |
|||
removedPartitions.forEach((tpi) -> consumers.remove(tpi).awaitCompletion()); |
|||
|
|||
addedPartitions.forEach((tpi) -> { |
|||
String key = queueKey + "-" + tpi.getPartition().orElse(-1); |
|||
TbQueueConsumerTask<M> consumer = new TbQueueConsumerTask<>(key, consumerCreator.apply(config)); |
|||
consumers.put(tpi, consumer); |
|||
consumer.subscribe(Set.of(tpi)); |
|||
launchConsumer(consumer); |
|||
}); |
|||
} |
|||
|
|||
@Override |
|||
public Collection<TbQueueConsumerTask<M>> getConsumers() { |
|||
return consumers.values(); |
|||
} |
|||
} |
|||
|
|||
class SingleConsumerWrapper implements ConsumerWrapper<M> { |
|||
private TbQueueConsumerTask<M> consumer; |
|||
|
|||
@Override |
|||
public void updatePartitions(Set<TopicPartitionInfo> partitions) { |
|||
log.info("[{}] New partitions: {}", queueKey, partitionsToString(partitions)); |
|||
if (partitions.isEmpty()) { |
|||
if (consumer != null && consumer.isRunning()) { |
|||
consumer.initiateStop(); |
|||
consumer.awaitCompletion(); |
|||
} |
|||
consumer = null; |
|||
return; |
|||
} |
|||
|
|||
if (consumer == null) { |
|||
consumer = new TbQueueConsumerTask<>(queueKey, consumerCreator.apply(config)); |
|||
} |
|||
consumer.subscribe(partitions); |
|||
if (!consumer.isRunning()) { |
|||
launchConsumer(consumer); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Collection<TbQueueConsumerTask<M>> getConsumers() { |
|||
if (consumer == null) { |
|||
return Collections.emptyList(); |
|||
} |
|||
return List.of(consumer); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.queue; |
|||
|
|||
public interface QueueConfig { |
|||
|
|||
boolean isConsumerPerPartition(); |
|||
|
|||
int getPollInterval(); |
|||
|
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-form-row space-between same-padding tb-flex column" [formGroup]="securityFormGroup"> |
|||
<div class="tb-flex row space-between align-center no-gap fill-width"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.security</div> |
|||
<tb-toggle-select formControlName="type" appearance="fill"> |
|||
<tb-toggle-option *ngFor="let type of securityTypes" [value]="type"> |
|||
{{ SecurityTypeTranslationsMap.get(type) | translate }} |
|||
</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
<ng-container [ngSwitch]="securityFormGroup.get('type').value"> |
|||
<ng-template [ngSwitchCase]="BrokerSecurityType.BASIC"> |
|||
<div class="tb-form-row space-between tb-flex fill-width"> |
|||
<div class="fixed-title-width" translate>gateway.username</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="username" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.username-required') | translate" |
|||
*ngIf="securityFormGroup.get('username').hasError('required') |
|||
&& securityFormGroup.get('username').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row space-between tb-flex fill-width"> |
|||
<div class="fixed-title-width" translate>gateway.password</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="password" name="value" formControlName="password" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<div class="tb-flex no-gap align-center fill-height" matSuffix> |
|||
<tb-toggle-password class="tb-flex align-center fill-height"></tb-toggle-password> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="BrokerSecurityType.CERTIFICATES"> |
|||
<div class="tb-form-hint tb-primary-fill">{{ 'gateway.path-hint' | translate }}</div> |
|||
<div class="tb-form-row space-between tb-flex fill-width"> |
|||
<div class="fixed-title-width" translate>gateway.CA-certificate-path</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="pathToCACert" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row space-between tb-flex fill-width"> |
|||
<div class="fixed-title-width" translate>gateway.private-key-path</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="pathToPrivateKey" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row space-between tb-flex fill-width"> |
|||
<div class="fixed-title-width" translate>gateway.client-cert-path</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="pathToClientCert" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
</ng-container> |
|||
</div> |
|||
@ -0,0 +1,20 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: block; |
|||
} |
|||
@ -0,0 +1,165 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
ElementRef, |
|||
forwardRef, |
|||
NgZone, |
|||
OnDestroy, |
|||
ViewContainerRef |
|||
} from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { Subject } from 'rxjs'; |
|||
import { Overlay } from '@angular/cdk/overlay'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormGroup, |
|||
ValidationErrors, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { |
|||
BrokerSecurityType, |
|||
BrokerSecurityTypeTranslationsMap, |
|||
noLeadTrailSpacesRegex |
|||
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-broker-security', |
|||
templateUrl: './broker-security.component.html', |
|||
styleUrls: ['./broker-security.component.scss'], |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => BrokerSecurityComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => BrokerSecurityComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class BrokerSecurityComponent extends PageComponent implements ControlValueAccessor, Validator, OnDestroy { |
|||
|
|||
BrokerSecurityType = BrokerSecurityType; |
|||
|
|||
securityTypes = Object.values(BrokerSecurityType); |
|||
|
|||
SecurityTypeTranslationsMap = BrokerSecurityTypeTranslationsMap; |
|||
|
|||
securityFormGroup: UntypedFormGroup; |
|||
|
|||
private destroy$ = new Subject<void>(); |
|||
private propagateChange = (v: any) => {}; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
public translate: TranslateService, |
|||
public dialog: MatDialog, |
|||
private overlay: Overlay, |
|||
private viewContainerRef: ViewContainerRef, |
|||
private dialogService: DialogService, |
|||
private entityService: EntityService, |
|||
private utils: UtilsService, |
|||
private zone: NgZone, |
|||
private cd: ChangeDetectorRef, |
|||
private elementRef: ElementRef, |
|||
private fb: FormBuilder) { |
|||
super(store); |
|||
this.securityFormGroup = this.fb.group({ |
|||
type: [BrokerSecurityType.ANONYMOUS, []], |
|||
username: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
pathToCACert: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
pathToPrivateKey: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
pathToClientCert: ['', [Validators.pattern(noLeadTrailSpacesRegex)]] |
|||
}); |
|||
this.securityFormGroup.valueChanges.pipe( |
|||
takeUntil(this.destroy$) |
|||
).subscribe((value) => { |
|||
this.updateView(value); |
|||
}); |
|||
this.securityFormGroup.get('type').valueChanges.pipe( |
|||
takeUntil(this.destroy$) |
|||
).subscribe((type) => { |
|||
this.updateValidators(type); |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
super.ngOnDestroy(); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void {} |
|||
|
|||
writeValue(deviceInfo: any) { |
|||
if (!deviceInfo.type) { |
|||
deviceInfo.type = BrokerSecurityType.ANONYMOUS; |
|||
} |
|||
this.securityFormGroup.reset(deviceInfo); |
|||
this.updateView(deviceInfo); |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
return this.securityFormGroup.valid ? null : { |
|||
securityForm: { valid: false } |
|||
}; |
|||
} |
|||
|
|||
updateView(value: any) { |
|||
this.propagateChange(value); |
|||
} |
|||
|
|||
private updateValidators(type) { |
|||
if (type) { |
|||
this.securityFormGroup.get('username').disable({emitEvent: false}); |
|||
this.securityFormGroup.get('password').disable({emitEvent: false}); |
|||
this.securityFormGroup.get('pathToCACert').disable({emitEvent: false}); |
|||
this.securityFormGroup.get('pathToPrivateKey').disable({emitEvent: false}); |
|||
this.securityFormGroup.get('pathToClientCert').disable({emitEvent: false}); |
|||
if (type === BrokerSecurityType.BASIC) { |
|||
this.securityFormGroup.get('username').enable({emitEvent: false}); |
|||
this.securityFormGroup.get('password').enable({emitEvent: false}); |
|||
} else if (type === BrokerSecurityType.CERTIFICATES) { |
|||
this.securityFormGroup.get('pathToCACert').enable({emitEvent: false}); |
|||
this.securityFormGroup.get('pathToPrivateKey').enable({emitEvent: false}); |
|||
this.securityFormGroup.get('pathToClientCert').enable({emitEvent: false}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-form-panel stroked" [formGroup]="mappingFormGroup"> |
|||
<div class="tb-form-panel-title" [class.tb-required]="required" translate>device.device</div> |
|||
<div class="tb-form-table no-padding no-gap"> |
|||
<div class="tb-form-table-header"> |
|||
<div class="tb-form-table-header-cell table-name-column" translate>gateway.device-info.entity-field</div> |
|||
<div *ngIf="useSource" class="tb-form-table-header-cell table-column" translate>gateway.device-info.source</div> |
|||
<div class="tb-form-table-header-cell table-column" translate> |
|||
gateway.device-info.expression |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-table-body no-gap"> |
|||
<div class="tb-form-table-row tb-form-row no-border same-padding top-same-padding" |
|||
[class.bottom-same-padding]="deviceInfoType !== DeviceInfoType.FULL"> |
|||
<div class="fixed-title-width" translate>gateway.device-info.name</div> |
|||
<div class="tb-flex no-gap raw-value-option" *ngIf="useSource"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="deviceNameExpressionSource"> |
|||
<mat-option *ngFor="let type of sourceTypes" [value]="type"> |
|||
{{ SourceTypeTranslationsMap.get(type) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-table-row-cell tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="deviceNameExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.device-info.device-name-expression-required') | translate" |
|||
*ngIf="mappingFormGroup.get('deviceNameExpression').hasError('required') && |
|||
mappingFormGroup.get('deviceNameExpression').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<div matSuffix |
|||
class="see-example" |
|||
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-table-row tb-form-row no-border same-padding bottom-same-padding" |
|||
*ngIf="deviceInfoType === DeviceInfoType.FULL"> |
|||
<div class="fixed-title-width" translate>gateway.device-info.profile-name</div> |
|||
<div class="tb-flex no-gap raw-value-option" *ngIf="useSource"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="deviceProfileExpressionSource"> |
|||
<mat-option *ngFor="let type of sourceTypes" [value]="type"> |
|||
{{ SourceTypeTranslationsMap.get(type) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-table-row-cell tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="deviceProfileExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.device-info.device-profile-expression-required') | translate" |
|||
*ngIf="mappingFormGroup.get('deviceProfileExpression').hasError('required') && |
|||
mappingFormGroup.get('deviceProfileExpression').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<div matSuffix |
|||
class="see-example" |
|||
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,57 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: block; |
|||
|
|||
.tb-form-row { |
|||
&.bottom-same-padding { |
|||
padding-bottom: 16px; |
|||
} |
|||
|
|||
&.top-same-padding { |
|||
padding-top: 16px; |
|||
} |
|||
|
|||
.fixed-title-width { |
|||
width: 19%; |
|||
} |
|||
} |
|||
|
|||
.table-column { |
|||
width: 40%; |
|||
} |
|||
|
|||
.table-name-column { |
|||
width: 20%; |
|||
} |
|||
|
|||
.raw-name { |
|||
width: 19%; |
|||
} |
|||
|
|||
.raw-value-option { |
|||
max-width: 40%; |
|||
} |
|||
|
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.mat-mdc-form-field-icon-suffix { |
|||
display: flex; |
|||
} |
|||
} |
|||
@ -0,0 +1,179 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
ElementRef, |
|||
forwardRef, |
|||
Input, |
|||
NgZone, |
|||
OnDestroy, |
|||
OnInit, |
|||
ViewContainerRef |
|||
} from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { Subject } from 'rxjs'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
import { Overlay } from '@angular/cdk/overlay'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormGroup, |
|||
ValidationErrors, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { |
|||
DeviceInfoType, |
|||
noLeadTrailSpacesRegex, |
|||
SourceTypes, |
|||
SourceTypeTranslationsMap |
|||
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
|||
import { coerceBoolean } from '@shared/decorators/coercion'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-device-info-table', |
|||
templateUrl: './device-info-table.component.html', |
|||
styleUrls: ['./device-info-table.component.scss'], |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => DeviceInfoTableComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => DeviceInfoTableComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class DeviceInfoTableComponent extends PageComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { |
|||
|
|||
SourceTypeTranslationsMap = SourceTypeTranslationsMap; |
|||
|
|||
DeviceInfoType = DeviceInfoType; |
|||
|
|||
@coerceBoolean() |
|||
@Input() |
|||
useSource = true; |
|||
|
|||
@coerceBoolean() |
|||
@Input() |
|||
required = false; |
|||
|
|||
@Input() |
|||
sourceTypes: Array<SourceTypes> = Object.values(SourceTypes); |
|||
|
|||
deviceInfoTypeValue: any; |
|||
|
|||
get deviceInfoType(): any { |
|||
return this.deviceInfoTypeValue; |
|||
} |
|||
|
|||
@Input() |
|||
set deviceInfoType(value: any) { |
|||
if (this.deviceInfoTypeValue !== value) { |
|||
this.deviceInfoTypeValue = value; |
|||
} |
|||
} |
|||
|
|||
mappingFormGroup: UntypedFormGroup; |
|||
|
|||
private destroy$ = new Subject<void>(); |
|||
private propagateChange = (v: any) => {}; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
public translate: TranslateService, |
|||
public dialog: MatDialog, |
|||
private overlay: Overlay, |
|||
private viewContainerRef: ViewContainerRef, |
|||
private dialogService: DialogService, |
|||
private entityService: EntityService, |
|||
private utils: UtilsService, |
|||
private zone: NgZone, |
|||
private cd: ChangeDetectorRef, |
|||
private elementRef: ElementRef, |
|||
private fb: FormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.mappingFormGroup = this.fb.group({ |
|||
deviceNameExpression: ['', this.required ? |
|||
[Validators.required, Validators.pattern(noLeadTrailSpacesRegex)] : [Validators.pattern(noLeadTrailSpacesRegex)]] |
|||
}); |
|||
|
|||
if (this.useSource) { |
|||
this.mappingFormGroup.addControl('deviceNameExpressionSource', |
|||
this.fb.control(SourceTypes.MSG, [])); |
|||
} |
|||
|
|||
if (this.deviceInfoType === DeviceInfoType.FULL) { |
|||
if (this.useSource) { |
|||
this.mappingFormGroup.addControl('deviceProfileExpressionSource', |
|||
this.fb.control(SourceTypes.MSG, [])); |
|||
} |
|||
this.mappingFormGroup.addControl('deviceProfileExpression', |
|||
this.fb.control('', this.required ? |
|||
[Validators.required, Validators.pattern(noLeadTrailSpacesRegex)] : [Validators.pattern(noLeadTrailSpacesRegex)])); |
|||
} |
|||
|
|||
this.mappingFormGroup.valueChanges.pipe( |
|||
takeUntil(this.destroy$) |
|||
).subscribe((value) => { |
|||
this.updateView(value); |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void {} |
|||
|
|||
writeValue(deviceInfo: any) { |
|||
this.mappingFormGroup.patchValue(deviceInfo, {emitEvent: false}); |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
return this.mappingFormGroup.valid ? null : { |
|||
mappingForm: { valid: false } |
|||
}; |
|||
} |
|||
|
|||
updateView(value: any) { |
|||
this.propagateChange(value); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,128 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
Directive, |
|||
ElementRef, |
|||
Inject, |
|||
Input, |
|||
OnDestroy, |
|||
Renderer2 |
|||
} from '@angular/core'; |
|||
import { isEqual } from '@core/utils'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { WINDOW } from '@core/services/window.service'; |
|||
import { fromEvent, Subject } from 'rxjs'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
|
|||
@Directive({ |
|||
// eslint-disable-next-line @angular-eslint/directive-selector
|
|||
selector: '[tb-ellipsis-chip-list]' |
|||
}) |
|||
export class EllipsisChipListDirective implements OnDestroy { |
|||
|
|||
chipsValue: string[]; |
|||
|
|||
private destroy$ = new Subject<void>(); |
|||
|
|||
@Input('tb-ellipsis-chip-list') |
|||
set chips(value: string[]) { |
|||
if (!isEqual(this.chipsValue, value)) { |
|||
this.chipsValue = value; |
|||
setTimeout(() => { |
|||
this.adjustChips(); |
|||
}, 0); |
|||
} |
|||
} |
|||
|
|||
constructor(private el: ElementRef, |
|||
private renderer: Renderer2, |
|||
private translate: TranslateService, |
|||
@Inject(WINDOW) private window: Window) { |
|||
this.renderer.setStyle(this.el.nativeElement, 'max-height', '48px'); |
|||
this.renderer.setStyle(this.el.nativeElement, 'overflow', 'auto'); |
|||
fromEvent(window, 'resize').pipe( |
|||
takeUntil(this.destroy$) |
|||
).subscribe(() => { |
|||
this.adjustChips(); |
|||
}); |
|||
} |
|||
|
|||
private adjustChips(): void { |
|||
const chipListElement = this.el.nativeElement; |
|||
const ellipsisChip = this.el.nativeElement.querySelector('.ellipsis-chip'); |
|||
const margin = parseFloat(this.window.getComputedStyle(ellipsisChip).marginLeft) || 0; |
|||
const chipNodes = chipListElement.querySelectorAll('mat-chip:not(.ellipsis-chip)'); |
|||
|
|||
if (this.chipsValue.length > 1) { |
|||
const ellipsisText = this.el.nativeElement.querySelector('.ellipsis-text'); |
|||
this.renderer.setStyle(ellipsisChip, 'display', 'inline-flex'); |
|||
ellipsisText.innerHTML = this.translate.instant('gateway.ellipsis-chips-text', |
|||
{count: (this.chipsValue.length)}); |
|||
|
|||
const availableWidth = chipListElement.offsetWidth - (ellipsisChip.offsetWidth + margin); |
|||
let usedWidth = 0; |
|||
let visibleChipsCount = 0; |
|||
|
|||
chipNodes.forEach((chip) => { |
|||
this.renderer.setStyle(chip, 'display', 'inline-flex'); |
|||
const textLabelContainer = chip.querySelector('.mdc-evolution-chip__text-label'); |
|||
|
|||
this.applyMaxChipTextWidth(textLabelContainer, (availableWidth / 3)); |
|||
|
|||
if ((usedWidth + (chip.offsetWidth + margin) <= availableWidth) && (visibleChipsCount < this.chipsValue.length)) { |
|||
visibleChipsCount++; |
|||
usedWidth += chip.offsetWidth + margin; |
|||
} else { |
|||
this.renderer.setStyle(chip, 'display', 'none'); |
|||
} |
|||
}); |
|||
|
|||
ellipsisText.innerHTML = this.translate.instant('gateway.ellipsis-chips-text', |
|||
{count: (this.chipsValue.length - visibleChipsCount)}); |
|||
|
|||
if (visibleChipsCount === this.chipsValue?.length) { |
|||
this.renderer.setStyle(ellipsisChip, 'display', 'none'); |
|||
} |
|||
} else if (this.chipsValue.length === 1) { |
|||
const chipLabelContainer = chipNodes[0].querySelector('.mdc-evolution-chip__action'); |
|||
const textLabelContainer = chipLabelContainer.querySelector('.mdc-evolution-chip__text-label'); |
|||
const leftPadding = parseFloat(this.window.getComputedStyle(chipLabelContainer).paddingLeft) || 0; |
|||
const rightPadding = parseFloat(this.window.getComputedStyle(chipLabelContainer).paddingRight) || 0; |
|||
const computedTextWidth = chipListElement.offsetWidth - margin - |
|||
(leftPadding + rightPadding); |
|||
|
|||
this.renderer.setStyle(ellipsisChip, 'display', 'none'); |
|||
this.renderer.setStyle(chipNodes[0], 'display', 'inline-flex'); |
|||
|
|||
this.applyMaxChipTextWidth(textLabelContainer, computedTextWidth); |
|||
} else { |
|||
this.renderer.setStyle(ellipsisChip, 'display', 'none'); |
|||
} |
|||
} |
|||
|
|||
private applyMaxChipTextWidth(element: HTMLElement, widthLimit: number): void { |
|||
this.renderer.setStyle(element, 'max-width', widthLimit + 'px'); |
|||
this.renderer.setStyle(element, 'overflow', 'hidden'); |
|||
this.renderer.setStyle(element, 'text-overflow', 'ellipsis'); |
|||
this.renderer.setStyle(element, 'white-space', 'nowrap'); |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
} |
|||
} |
|||
@ -0,0 +1,195 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-mapping-keys-panel"> |
|||
<div class="tb-form-panel no-border no-padding"> |
|||
<div class="tb-form-panel-title">{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}</div> |
|||
<div class="tb-form-panel no-border no-padding key-panel" *ngIf="keysListFormArray.controls.length; else noKeys"> |
|||
<div class="tb-form-panel no-border no-padding tb-flex no-flex row center fill-width" |
|||
*ngFor="let keyControl of keysListFormArray.controls; trackBy: trackByKey; let $index = index; let last = last;"> |
|||
<div class="tb-form-panel stroked tb-flex"> |
|||
<ng-container [formGroup]="keyControl"> |
|||
<mat-expansion-panel class="tb-settings" [expanded]="last"> |
|||
<mat-expansion-panel-header fxLayout="row wrap"> |
|||
<mat-panel-title> |
|||
<div class="title-container">{{ keyControl.get('key').value }}</div> |
|||
{{ '-' }} |
|||
<div class="title-container">{{ valueTitle(keyControl.get('value').value) }}</div> |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<ng-template matExpansionPanelContent> |
|||
<div class="tb-form-panel no-border no-padding" |
|||
*ngIf="keysType !== MappingKeysType.CUSTOM; else customPanel"> |
|||
<div class="tb-form-panel stroked"> |
|||
<div class="tb-form-panel-title" translate>gateway.platform-side</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" |
|||
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate> |
|||
gateway.key |
|||
</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.key-required') | translate" |
|||
*ngIf="keyControl.get('key').hasError('required') && |
|||
keyControl.get('key').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<div matSuffix |
|||
class="see-example" |
|||
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-panel stroked"> |
|||
<div class="tb-form-panel-title" translate>gateway.connector-side</div> |
|||
<div class="tb-form-row"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.type</div> |
|||
<mat-form-field class="tb-flex no-gap fill-width" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select name="valueType" formControlName="type"> |
|||
<mat-select-trigger *ngIf="!rawData"> |
|||
<div class="tb-flex align-center"> |
|||
<mat-icon class="tb-mat-18" [svgIcon]="valueTypes.get(keyControl.get('type').value)?.icon"> |
|||
</mat-icon> |
|||
<span> |
|||
{{ (rawData ? 'gateway.raw' : valueTypes.get(keyControl.get('type').value)?.name) | translate}} |
|||
</span> |
|||
</div> |
|||
</mat-select-trigger> |
|||
<ng-container *ngIf="!rawData; else rawOption"> |
|||
<mat-option *ngFor="let valueType of valueTypeKeys" [value]="valueType"> |
|||
<mat-icon class="tb-mat-20" svgIcon="{{ valueTypes.get(valueType).icon }}"> |
|||
</mat-icon> |
|||
<span>{{ valueTypes.get(valueType).name | translate }}</span> |
|||
</mat-option> |
|||
</ng-container> |
|||
<ng-template #rawOption> |
|||
<mat-option [value]="'raw'"> |
|||
<span>{{ 'gateway.raw' | translate }}</span> |
|||
</mat-option> |
|||
</ng-template> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" |
|||
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate> |
|||
gateway.value |
|||
</div> |
|||
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-flex no-gap"> |
|||
<input matInput required formControlName="value" |
|||
placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.value-required') | translate" |
|||
*ngIf="keyControl.get('value').hasError('required') && |
|||
keyControl.get('value').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<div matSuffix |
|||
class="see-example" |
|||
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<ng-template #customPanel> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.key</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.key-required') | translate" |
|||
*ngIf="keyControl.get('key').hasError('required') && keyControl.get('key').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.value</div> |
|||
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-inline-field flex tb-suffix-absolute"> |
|||
<input matInput required formControlName="value" |
|||
placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.value-required') | translate" |
|||
*ngIf="keyControl.get('value').hasError('required') && keyControl.get('value').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</ng-template> |
|||
</ng-template> |
|||
</mat-expansion-panel> |
|||
</ng-container> |
|||
</div> |
|||
<button type="button" |
|||
mat-icon-button |
|||
(click)="deleteKey($event, $index)" |
|||
[matTooltip]="deleteKeyTitle | translate" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div> |
|||
<button type="button" mat-stroked-button color="primary" (click)="addKey()"> |
|||
{{ addKeyTitle | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<ng-template #noKeys> |
|||
<div class="tb-flex no-flex center align-center key-panel"> |
|||
<span class="tb-prompt" translate>{{ noKeysText }}</span> |
|||
</div> |
|||
</ng-template> |
|||
<div class="tb-flex flex-end"> |
|||
<button mat-button |
|||
color="primary" |
|||
type="button" |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button |
|||
color="primary" |
|||
type="button" |
|||
(click)="applyKeysData()" |
|||
[disabled]="keysListFormArray.invalid || !keysListFormArray.dirty"> |
|||
{{ 'action.apply' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,60 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
:host { |
|||
.tb-mapping-keys-panel { |
|||
width: 77vw; |
|||
max-width: 700px; |
|||
|
|||
.title-container { |
|||
max-width: 11vw; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap |
|||
} |
|||
|
|||
.key-panel { |
|||
height: 500px; |
|||
overflow: auto; |
|||
} |
|||
|
|||
tb-value-input { |
|||
width: 100%; |
|||
} |
|||
|
|||
.tb-form-panel { |
|||
.mat-mdc-icon-button { |
|||
width: 56px; |
|||
height: 56px; |
|||
padding: 16px; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
} |
|||
} |
|||
|
|||
.see-example { |
|||
width: 32px; |
|||
height: 32px; |
|||
margin: 4px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.mat-mdc-form-field-icon-suffix { |
|||
display: flex; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,173 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
Component, |
|||
EventEmitter, |
|||
Input, |
|||
OnInit, |
|||
Output |
|||
} from '@angular/core'; |
|||
import { |
|||
AbstractControl, |
|||
UntypedFormArray, |
|||
UntypedFormBuilder, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { coerceBoolean } from '@shared/decorators/coercion'; |
|||
import { TbPopoverComponent } from '@shared/components/popover.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { isDefinedAndNotNull } from '@core/utils'; |
|||
import { |
|||
MappingDataKey, |
|||
MappingKeysType, |
|||
MappingValueType, |
|||
mappingValueTypesMap, |
|||
noLeadTrailSpacesRegex |
|||
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-mapping-data-keys-panel', |
|||
templateUrl: './mapping-data-keys-panel.component.html', |
|||
styleUrls: ['./mapping-data-keys-panel.component.scss'], |
|||
providers: [] |
|||
}) |
|||
export class MappingDataKeysPanelComponent extends PageComponent implements OnInit { |
|||
|
|||
@Input() |
|||
panelTitle: string; |
|||
|
|||
@Input() |
|||
addKeyTitle: string; |
|||
|
|||
@Input() |
|||
deleteKeyTitle: string; |
|||
|
|||
@Input() |
|||
noKeysText: string; |
|||
|
|||
@Input() |
|||
keys: Array<MappingDataKey> | {[key: string]: any}; |
|||
|
|||
@Input() |
|||
keysType: string; |
|||
|
|||
@Input() |
|||
@coerceBoolean() |
|||
rawData = false; |
|||
|
|||
@Input() |
|||
popover: TbPopoverComponent<MappingDataKeysPanelComponent>; |
|||
|
|||
@Output() |
|||
keysDataApplied = new EventEmitter<Array<MappingDataKey> | {[key: string]: any}>(); |
|||
|
|||
valueTypeKeys = Object.values(MappingValueType); |
|||
|
|||
MappingKeysType = MappingKeysType; |
|||
|
|||
valueTypeEnum = MappingValueType; |
|||
|
|||
valueTypes = mappingValueTypesMap; |
|||
|
|||
dataKeyType: DataKeyType; |
|||
|
|||
keysListFormArray: UntypedFormArray; |
|||
|
|||
errorText = ''; |
|||
|
|||
constructor(private fb: UntypedFormBuilder, |
|||
protected store: Store<AppState>) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.keysListFormArray = this.prepareKeysFormArray(this.keys) |
|||
} |
|||
|
|||
trackByKey(index: number, keyControl: AbstractControl): any { |
|||
return keyControl; |
|||
} |
|||
|
|||
addKey(): void { |
|||
const dataKeyFormGroup = this.fb.group({ |
|||
key: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
value: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]] |
|||
}); |
|||
if (this.keysType !== MappingKeysType.CUSTOM) { |
|||
dataKeyFormGroup.addControl('type', this.fb.control(this.rawData ? 'raw' : MappingValueType.STRING)); |
|||
} |
|||
this.keysListFormArray.push(dataKeyFormGroup); |
|||
} |
|||
|
|||
deleteKey($event: Event, index: number): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.keysListFormArray.removeAt(index); |
|||
this.keysListFormArray.markAsDirty(); |
|||
} |
|||
|
|||
cancel() { |
|||
this.popover?.hide(); |
|||
} |
|||
|
|||
applyKeysData() { |
|||
let keys = this.keysListFormArray.value; |
|||
if (this.keysType === MappingKeysType.CUSTOM) { |
|||
keys = {}; |
|||
for (let key of this.keysListFormArray.value) { |
|||
keys[key.key] = key.value; |
|||
} |
|||
} |
|||
this.keysDataApplied.emit(keys); |
|||
} |
|||
|
|||
private prepareKeysFormArray(keys: Array<MappingDataKey> | {[key: string]: any}): UntypedFormArray { |
|||
const keysControlGroups: Array<AbstractControl> = []; |
|||
if (keys) { |
|||
if (this.keysType === MappingKeysType.CUSTOM) { |
|||
keys = Object.keys(keys).map(key => { |
|||
return {key, value: keys[key], type: ''}; |
|||
}); |
|||
} |
|||
keys.forEach((keyData) => { |
|||
const { key, value, type } = keyData; |
|||
const dataKeyFormGroup = this.fb.group({ |
|||
key: [key, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
value: [value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
type: [type, []] |
|||
}); |
|||
keysControlGroups.push(dataKeyFormGroup); |
|||
}); |
|||
} |
|||
return this.fb.array(keysControlGroups); |
|||
} |
|||
|
|||
valueTitle(value: any): string { |
|||
if (isDefinedAndNotNull(value)) { |
|||
if (typeof value === 'object') { |
|||
return JSON.stringify(value); |
|||
} |
|||
return value; |
|||
} |
|||
return ''; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,123 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-mapping-table tb-absolute-fill"> |
|||
<div fxFlex fxLayout="column" class="tb-mapping-table-content"> |
|||
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode"> |
|||
<div class="mat-toolbar-tools"> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container"> |
|||
<span class="tb-mapping-table-title">{{mappingTypeTranslationsMap.get(mappingType) | translate}}</span> |
|||
</div> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
(click)="manageMapping($event)" |
|||
matTooltip="{{ 'action.add' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>add</mat-icon> |
|||
</button> |
|||
<button mat-icon-button |
|||
(click)="enterFilterMode()" |
|||
matTooltip="{{ 'action.search' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>search</mat-icon> |
|||
</button> |
|||
</div> |
|||
</mat-toolbar> |
|||
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="textSearchMode"> |
|||
<div class="mat-toolbar-tools"> |
|||
<button mat-icon-button |
|||
matTooltip="{{ 'action.search' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>search</mat-icon> |
|||
</button> |
|||
<mat-form-field fxFlex> |
|||
<mat-label> </mat-label> |
|||
<input #searchInput matInput |
|||
[formControl]="textSearch" |
|||
placeholder="{{ 'common.enter-search' | translate }}"/> |
|||
</mat-form-field> |
|||
<button mat-icon-button (click)="exitFilterMode()" |
|||
matTooltip="{{ 'action.close' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>close</mat-icon> |
|||
</button> |
|||
</div> |
|||
</mat-toolbar> |
|||
<div class="table-container"> |
|||
<table mat-table [dataSource]="dataSource"> |
|||
<ng-container [matColumnDef]="column.def" *ngFor="let column of mappingColumns; let i = index"> |
|||
<mat-header-cell *matHeaderCellDef class="table-value-column" [class.request-column]="mappingType === mappingTypeEnum.REQUESTS"> |
|||
{{ column.title | translate }} |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let mapping" class="table-value-column" [class.request-column]="mappingType === mappingTypeEnum.REQUESTS"> |
|||
{{ mapping[column.def] }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="actions" stickyEnd> |
|||
<mat-header-cell *matHeaderCellDef |
|||
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px', textAlign: 'center'}"> |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let mapping; let i = index" |
|||
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}"> |
|||
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> |
|||
<button mat-icon-button |
|||
(click)="manageMapping($event, i)"> |
|||
<tb-icon>edit</tb-icon> |
|||
</button> |
|||
<button mat-icon-button |
|||
(click)="deleteMapping($event, i)"> |
|||
<tb-icon>delete</tb-icon> |
|||
</button> |
|||
</div> |
|||
<div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end"> |
|||
<button mat-icon-button |
|||
(click)="$event.stopPropagation()" |
|||
[matMenuTriggerFor]="cellActionsMenu"> |
|||
<mat-icon class="material-icons">more_vert</mat-icon> |
|||
</button> |
|||
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> |
|||
<button mat-icon-button |
|||
(click)="manageMapping($event, i)"> |
|||
<tb-icon>edit</tb-icon> |
|||
</button> |
|||
<button mat-icon-button |
|||
(click)="deleteMapping($event, i)"> |
|||
<tb-icon>delete</tb-icon> |
|||
</button> |
|||
</mat-menu> |
|||
</div> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> |
|||
<mat-row *matRowDef="let mapping; columns: displayedColumns;"></mat-row> |
|||
</table> |
|||
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center" |
|||
class="mat-headline-5 tb-absolute-fill tb-add-new"> |
|||
<button mat-button class="connector" |
|||
(click)="manageMapping($event)"> |
|||
<mat-icon class="tb-mat-96">add</mat-icon> |
|||
<span>{{ 'gateway.add-mapping' | translate }}</span> |
|||
</button> |
|||
</section> |
|||
</div> |
|||
<span [fxShow]="textSearchMode && (dataSource.isEmpty() | async)" |
|||
fxLayoutAlign="center center" |
|||
class="no-data-found" translate> |
|||
widget.no-data-found |
|||
</span> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,101 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
@import '../scss/constants'; |
|||
|
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: block; |
|||
.tb-mapping-table { |
|||
.tb-mapping-table-content { |
|||
width: 100%; |
|||
height: 100%; |
|||
background: #fff; |
|||
overflow: hidden; |
|||
|
|||
&.tb-outlined-border { |
|||
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%); |
|||
border: solid 1px #e0e0e0; |
|||
border-radius: 4px; |
|||
} |
|||
|
|||
.mat-toolbar-tools{ |
|||
min-height: auto; |
|||
} |
|||
|
|||
.title-container{ |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.tb-mapping-table-title { |
|||
padding-right: 20px; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
.table-container { |
|||
overflow: auto; |
|||
.mat-mdc-table { |
|||
table-layout: fixed; |
|||
min-width: 450px; |
|||
|
|||
.table-value-column { |
|||
padding: 0 12px; |
|||
width: 23%; |
|||
|
|||
&.request-column { |
|||
width: 38%; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.ellipsis { |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.no-data-found { |
|||
height: calc(100% - 120px); |
|||
} |
|||
|
|||
@media #{$mat-xs} { |
|||
.mat-toolbar { |
|||
height: auto; |
|||
min-height: 100px; |
|||
|
|||
.tb-mapping-table-title{ |
|||
padding-bottom: 5px; |
|||
width: 100%; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
mat-cell.tb-value-cell { |
|||
cursor: pointer; |
|||
.mat-icon { |
|||
height: 24px; |
|||
width: 24px; |
|||
font-size: 24px; |
|||
color: #757575 |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,339 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
AfterViewInit, |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
ElementRef, |
|||
forwardRef, |
|||
Input, |
|||
NgZone, |
|||
OnDestroy, |
|||
OnInit, |
|||
ViewChild, |
|||
ViewContainerRef |
|||
} from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { BehaviorSubject, Observable, Subject } from 'rxjs'; |
|||
import { debounceTime, distinctUntilChanged, map, takeUntil } from 'rxjs/operators'; |
|||
import { Overlay } from '@angular/cdk/overlay'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR, UntypedFormArray } from '@angular/forms'; |
|||
import { |
|||
ConvertorTypeTranslationsMap, |
|||
MappingInfo, |
|||
MappingType, |
|||
MappingTypeTranslationsMap, |
|||
RequestType, |
|||
RequestTypesTranslationsMap |
|||
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
|||
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; |
|||
import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component'; |
|||
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-mapping-table', |
|||
templateUrl: './mapping-table.component.html', |
|||
styleUrls: ['./mapping-table.component.scss'], |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => MappingTableComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class MappingTableComponent extends PageComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { |
|||
|
|||
mappingTypeTranslationsMap = MappingTypeTranslationsMap; |
|||
mappingTypeEnum = MappingType; |
|||
displayedColumns = []; |
|||
mappingColumns = []; |
|||
textSearchMode = false; |
|||
dataSource: MappingDatasource; |
|||
hidePageSize = false; |
|||
|
|||
activeValue = false; |
|||
dirtyValue = false; |
|||
|
|||
viewsInited = false; |
|||
|
|||
mappingTypeValue: MappingType; |
|||
|
|||
get mappingType(): MappingType { |
|||
return this.mappingTypeValue; |
|||
} |
|||
|
|||
@Input() |
|||
set mappingType(value: MappingType) { |
|||
if (this.mappingTypeValue !== value) { |
|||
this.mappingTypeValue = value; |
|||
} |
|||
} |
|||
|
|||
@ViewChild('searchInput') searchInputField: ElementRef; |
|||
|
|||
mappingFormGroup: UntypedFormArray; |
|||
textSearch = this.fb.control('', {nonNullable: true}); |
|||
|
|||
private destroy$ = new Subject<void>(); |
|||
private propagateChange = (v: any) => {}; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
public translate: TranslateService, |
|||
public dialog: MatDialog, |
|||
private overlay: Overlay, |
|||
private viewContainerRef: ViewContainerRef, |
|||
private dialogService: DialogService, |
|||
private entityService: EntityService, |
|||
private utils: UtilsService, |
|||
private zone: NgZone, |
|||
private cd: ChangeDetectorRef, |
|||
private elementRef: ElementRef, |
|||
private fb: FormBuilder) { |
|||
super(store); |
|||
this.mappingFormGroup = this.fb.array([]); |
|||
this.dirtyValue = !this.activeValue; |
|||
this.dataSource = new MappingDatasource(); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
if (this.mappingType === MappingType.DATA) { |
|||
this.mappingColumns.push( |
|||
{def: 'topicFilter', title: 'gateway.topic-filter'}, |
|||
{def: 'QoS', title: 'gateway.mqtt-qos'}, |
|||
{def: 'converter', title: 'gateway.payload-type'} |
|||
) |
|||
} else { |
|||
this.mappingColumns.push( |
|||
{def: 'type', title: 'gateway.type'}, |
|||
{def: 'details', title: 'gateway.details'} |
|||
); |
|||
} |
|||
this.displayedColumns.push(...this.mappingColumns.map(column => column.def), 'actions'); |
|||
this.mappingFormGroup.valueChanges.pipe( |
|||
takeUntil(this.destroy$) |
|||
).subscribe((value) => { |
|||
this.updateTableData(value); |
|||
this.updateView(value); |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
} |
|||
|
|||
ngAfterViewInit() { |
|||
this.textSearch.valueChanges.pipe( |
|||
debounceTime(150), |
|||
distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()), |
|||
takeUntil(this.destroy$) |
|||
).subscribe((text) => { |
|||
const searchText = text.trim(); |
|||
this.updateTableData(this.mappingFormGroup.value, searchText.trim()) |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void {} |
|||
|
|||
writeValue(config: any) { |
|||
if (isUndefinedOrNull(config)) { |
|||
config = this.mappingType === MappingType.REQUESTS ? {} : []; |
|||
} |
|||
let mappingConfigs = config; |
|||
if (this.mappingType === MappingType.REQUESTS) { |
|||
mappingConfigs = []; |
|||
|
|||
Object.keys(config).forEach((configKey) => { |
|||
for (let mapping of config[configKey]) { |
|||
mappingConfigs.push({ |
|||
requestType: configKey, |
|||
requestValue: mapping |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
this.mappingFormGroup.clear({emitEvent: false}); |
|||
for (let mapping of mappingConfigs) { |
|||
this.mappingFormGroup.push(this.fb.group(mapping), {emitEvent: false}); |
|||
} |
|||
this.updateTableData(mappingConfigs); |
|||
} |
|||
|
|||
updateView(mappingConfigs: Array<{[key: string]: any}>) { |
|||
let config; |
|||
if (this.mappingType === MappingType.REQUESTS) { |
|||
config = {}; |
|||
for (let mappingConfig of mappingConfigs) { |
|||
if (config[mappingConfig.requestType]) { |
|||
config[mappingConfig.requestType].push(mappingConfig.requestValue); |
|||
} else { |
|||
config[mappingConfig.requestType] = [mappingConfig.requestValue]; |
|||
} |
|||
} |
|||
} else { |
|||
config = mappingConfigs; |
|||
} |
|||
|
|||
this.propagateChange(config); |
|||
} |
|||
|
|||
enterFilterMode() { |
|||
this.textSearchMode = true; |
|||
setTimeout(() => { |
|||
this.searchInputField.nativeElement.focus(); |
|||
this.searchInputField.nativeElement.setSelectionRange(0, 0); |
|||
}, 10); |
|||
} |
|||
|
|||
exitFilterMode() { |
|||
this.updateTableData(this.mappingFormGroup.value); |
|||
this.textSearchMode = false; |
|||
this.textSearch.reset(); |
|||
} |
|||
|
|||
manageMapping($event: Event, index?: number) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const value = isDefinedAndNotNull(index) ? this.mappingFormGroup.at(index).value : {}; |
|||
this.dialog.open<MappingDialogComponent, MappingInfo, boolean>(MappingDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
mappingType: this.mappingType, |
|||
value, |
|||
buttonTitle: isUndefinedOrNull(index) ? 'action.add' : 'action.apply' |
|||
} |
|||
}).afterClosed().subscribe( |
|||
(res) => { |
|||
if (res) { |
|||
if (isDefinedAndNotNull(index)) { |
|||
this.mappingFormGroup.at(index).patchValue(res); |
|||
} else { |
|||
this.mappingFormGroup.push(this.fb.group(res)); |
|||
} |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
updateTableData(value: Array<{[key: string]: any}>, textSearch?: string): void { |
|||
let tableValue = value; |
|||
if (this.mappingType === MappingType.DATA) { |
|||
tableValue = tableValue.map((value) => { |
|||
return { |
|||
topicFilter: value.topicFilter, |
|||
QoS: value.subscriptionQos, |
|||
converter: this.translate.instant(ConvertorTypeTranslationsMap.get(value.converter.type)) |
|||
}; |
|||
}); |
|||
} else { |
|||
tableValue = tableValue.map((value) => { |
|||
let details; |
|||
if (value.requestType === RequestType.ATTRIBUTE_UPDATE) { |
|||
details = value.requestValue.attributeFilter; |
|||
} else if (value.requestType === RequestType.SERVER_SIDE_RPC) { |
|||
details = value.requestValue.methodFilter; |
|||
} else { |
|||
details = value.requestValue.topicFilter; |
|||
} |
|||
return { |
|||
type: this.translate.instant(RequestTypesTranslationsMap.get(value.requestType)), |
|||
details |
|||
}; |
|||
}); |
|||
} |
|||
if (textSearch) { |
|||
tableValue = tableValue.filter(value => |
|||
Object.values(value).some(val => |
|||
val.toString().toLowerCase().includes(textSearch.toLowerCase()) |
|||
) |
|||
); |
|||
} |
|||
this.dataSource.loadMappings(tableValue); |
|||
} |
|||
|
|||
deleteMapping($event: Event, index: number) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.dialogService.confirm( |
|||
this.translate.instant('gateway.delete-mapping-title'), |
|||
'', |
|||
this.translate.instant('action.no'), |
|||
this.translate.instant('action.yes'), |
|||
true |
|||
).subscribe((result) => { |
|||
if (result) { |
|||
this.mappingFormGroup.removeAt(index); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
} |
|||
|
|||
export class MappingDatasource implements DataSource<{[key: string]: any}> { |
|||
|
|||
private mappingSubject = new BehaviorSubject<Array<{[key: string]: any}>>([]); |
|||
|
|||
private allMappings: Observable<Array<{[key: string]: any}>>; |
|||
|
|||
constructor() {} |
|||
|
|||
connect(collectionViewer: CollectionViewer): Observable<Array<{[key: string]: any}>> { |
|||
return this.mappingSubject.asObservable(); |
|||
} |
|||
|
|||
disconnect(collectionViewer: CollectionViewer): void { |
|||
this.mappingSubject.complete(); |
|||
} |
|||
|
|||
loadMappings(mappings: Array<{[key: string]: any}>, pageLink?: PageLink, reload: boolean = false): void { |
|||
if (reload) { |
|||
this.allMappings = null; |
|||
} |
|||
this.mappingSubject.next(mappings); |
|||
} |
|||
|
|||
isEmpty(): Observable<boolean> { |
|||
return this.mappingSubject.pipe( |
|||
map((mappings) => !mappings.length) |
|||
); |
|||
} |
|||
|
|||
total(): Observable<number> { |
|||
return this.mappingSubject.pipe( |
|||
map((mappings) => mappings.length) |
|||
); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,100 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div [formGroup]="connectorForm" class="add-connector"> |
|||
<mat-toolbar color="primary"> |
|||
<h2>{{ "gateway.add-connector" | translate}}</h2> |
|||
<span fxFlex></span> |
|||
<div [tb-help]="helpLinkId()"></div> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<div mat-dialog-content> |
|||
<div class="tb-form-panel no-border no-padding" fxLayout="column"> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.type</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="type"> |
|||
<mat-option *ngFor="let type of gatewayConnectorDefaultTypesTranslatesMap | keyvalue" [value]="type.key"> |
|||
{{ type.value }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.name</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="(connectorForm.get('name').hasError('duplicateName') ? |
|||
'gateway.connector-duplicate-name' :'gateway.name-required') | translate" |
|||
*ngIf="(connectorForm.get('name').hasError('required') && connectorForm.get('name').touched) |
|||
|| connectorForm.get('name').hasError('duplicateName')" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.remote-logging-level</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="logLevel"> |
|||
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{ logLevel }}</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="useDefaults"> |
|||
<mat-label tb-hint-tooltip-icon="{{ 'gateway.fill-connector-defaults-hint' | translate }}"> |
|||
{{ 'gateway.fill-connector-defaults' | translate }} |
|||
</mat-label> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div *ngIf="connectorForm.get('type').value === connectorType.MQTT" class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="sendDataOnlyOnChange"> |
|||
<mat-label tb-hint-tooltip-icon="{{ 'gateway.send-change-data-hint' | translate }}"> |
|||
{{ 'gateway.send-change-data' | translate }} |
|||
</mat-label> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
cdkFocusInitial |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
(click)="add()" |
|||
[disabled]="connectorForm.invalid || !connectorForm.dirty"> |
|||
{{ 'action.add' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
:host { |
|||
.add-connector { |
|||
min-width: 400px; |
|||
width: 500px; |
|||
} |
|||
} |
|||
@ -0,0 +1,122 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, Inject, OnDestroy } from '@angular/core'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { FormBuilder, UntypedFormControl, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms'; |
|||
import { BaseData, HasId } from '@shared/models/base-data'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Router } from '@angular/router'; |
|||
import { |
|||
AddConnectorConfigData, |
|||
ConnectorType, |
|||
CreatedConnectorConfigData, |
|||
GatewayConnectorDefaultTypesTranslatesMap, |
|||
GatewayLogLevel, |
|||
getDefaultConfig, |
|||
noLeadTrailSpacesRegex |
|||
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
|||
import { Subject } from 'rxjs'; |
|||
import { ResourcesService } from '@core/services/resources.service'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-add-connector-dialog', |
|||
templateUrl: './add-connector-dialog.component.html', |
|||
styleUrls: ['./add-connector-dialog.component.scss'], |
|||
providers: [], |
|||
}) |
|||
export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDialogComponent, BaseData<HasId>> implements OnDestroy { |
|||
|
|||
connectorForm: UntypedFormGroup; |
|||
|
|||
connectorType = ConnectorType; |
|||
|
|||
gatewayConnectorDefaultTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap; |
|||
gatewayLogLevel = Object.values(GatewayLogLevel); |
|||
|
|||
submitted = false; |
|||
|
|||
private destroy$ = new Subject<void>(); |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: AddConnectorConfigData, |
|||
public dialogRef: MatDialogRef<AddConnectorDialogComponent, CreatedConnectorConfigData>, |
|||
private fb: FormBuilder, |
|||
private resourcesService: ResourcesService) { |
|||
super(store, router, dialogRef); |
|||
this.connectorForm = this.fb.group({ |
|||
type: [ConnectorType.MQTT, []], |
|||
name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
logLevel: [GatewayLogLevel.INFO, []], |
|||
useDefaults: [true, []], |
|||
sendDataOnlyOnChange: [false, []], |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
super.ngOnDestroy(); |
|||
} |
|||
|
|||
helpLinkId(): string { |
|||
return 'https://thingsboard.io/docs/iot-gateway/configuration/'; |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
add(): void { |
|||
this.submitted = true; |
|||
const value = this.connectorForm.getRawValue(); |
|||
if (value.useDefaults) { |
|||
getDefaultConfig(this.resourcesService, value.type).subscribe((defaultConfig) => { |
|||
value.configurationJson = defaultConfig; |
|||
if (this.connectorForm.valid) { |
|||
this.dialogRef.close(value); |
|||
} |
|||
}); |
|||
} |
|||
if (this.connectorForm.valid) { |
|||
this.dialogRef.close(value); |
|||
} |
|||
} |
|||
|
|||
private uniqNameRequired(): ValidatorFn { |
|||
return (c: UntypedFormControl) => { |
|||
const newName = c.value.trim().toLowerCase(); |
|||
const found = this.data.dataSourceData.find((connectorAttr) => { |
|||
const connectorData = connectorAttr.value; |
|||
return connectorData.name.toLowerCase() === newName; |
|||
}); |
|||
if (found) { |
|||
if (c.hasError('required')) { |
|||
return c.getError('required'); |
|||
} |
|||
return { |
|||
duplicateName: { |
|||
valid: false |
|||
} |
|||
}; |
|||
} |
|||
return null; |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,634 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div [formGroup]="mappingForm" class="key-mapping"> |
|||
<mat-toolbar color="primary"> |
|||
<h2>{{ MappingTypeTranslationsMap.get(this.data?.mappingType) | translate}}</h2> |
|||
<span fxFlex></span> |
|||
<div [tb-help]="helpLinkId()"></div> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<div mat-dialog-content> |
|||
<div class="tb-form-panel no-border no-padding" fxLayout="column"> |
|||
<div class="tb-form-hint tb-primary-fill"> |
|||
{{ MappingHintTranslationsMap.get(this.data?.mappingType) | translate }} |
|||
</div> |
|||
<ng-container [ngSwitch]="data.mappingType"> |
|||
<ng-template [ngSwitchCase]="MappingType.DATA"> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.topic-filter</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="topicFilter" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.topic-required') | translate" |
|||
*ngIf="mappingForm.get('topicFilter').hasError('required') && |
|||
mappingForm.get('topicFilter').touched;" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<div matSuffix |
|||
class="see-example" |
|||
[tb-help-popup]="'widget/lib/gateway/topic-filter_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}" translate> |
|||
gateway.mqtt-qos |
|||
</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="subscriptionQos"> |
|||
<mat-option *ngFor="let type of qualityTypes" [value]="type"> |
|||
{{ QualityTranslationsMap.get(type) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<ng-container formGroupName="converter"> |
|||
<div class="tb-form-row space-between tb-flex"> |
|||
<div class="fixed-title-width" translate>gateway.payload-type</div> |
|||
<tb-toggle-select formControlName="type" appearance="fill"> |
|||
<tb-toggle-option *ngFor="let type of convertorTypes" [value]="type"> |
|||
{{ ConvertorTypeTranslationsMap.get(type) | translate }} |
|||
</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
<div class="tb-form-panel stroked"> |
|||
<div class="tb-form-panel-title" translate>gateway.data-conversion</div> |
|||
<div class="tb-form-hint tb-primary-fill"> |
|||
{{ DataConversionTranslationsMap.get(converterType) | translate }} |
|||
</div> |
|||
<ng-container [formGroupName]="converterType" [ngSwitch]="converterType"> |
|||
<ng-template [ngSwitchCase]="ConvertorTypeEnum.JSON"> |
|||
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.FULL" required="true"> |
|||
</tb-device-info-table> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="ConvertorTypeEnum.BYTES"> |
|||
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.FULL" |
|||
[sourceTypes]="[sourceTypesEnum.MSG, sourceTypesEnum.CONST]" required="true"> |
|||
</tb-device-info-table> |
|||
</ng-template> |
|||
<div class="tb-form-panel no-border no-padding" |
|||
*ngIf="converterType === ConvertorTypeEnum.BYTES || converterType === ConvertorTypeEnum.JSON"> |
|||
<div class="tb-form-row space-between tb-flex"> |
|||
<div class="fixed-title-width" translate>gateway.attributes</div> |
|||
<div class="tb-flex ellipsis-chips-container"> |
|||
<mat-chip-listbox [tb-ellipsis-chip-list]="converterAttributes" class="tb-flex"> |
|||
<mat-chip *ngFor="let attribute of converterAttributes"> |
|||
{{ attribute }} |
|||
</mat-chip> |
|||
<mat-chip class="mat-mdc-chip ellipsis-chip"> |
|||
<label class="ellipsis-text"></label> |
|||
</mat-chip> |
|||
</mat-chip-listbox> |
|||
<button type="button" |
|||
mat-icon-button |
|||
color="primary" |
|||
#attributesButton |
|||
(click)="manageKeys($event, attributesButton, MappingKeysType.ATTRIBUTES)"> |
|||
<tb-icon matButtonIcon>edit</tb-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row space-between tb-flex"> |
|||
<div class="fixed-title-width" translate>gateway.timeseries</div> |
|||
<div class="tb-flex ellipsis-chips-container"> |
|||
<mat-chip-listbox class="tb-flex" [tb-ellipsis-chip-list]="converterTelemetry"> |
|||
<mat-chip *ngFor="let telemetry of converterTelemetry"> |
|||
{{ telemetry }} |
|||
</mat-chip> |
|||
<mat-chip class="mat-mdc-chip ellipsis-chip"> |
|||
<label class="ellipsis-text"></label> |
|||
</mat-chip> |
|||
</mat-chip-listbox> |
|||
<button type="button" |
|||
mat-icon-button |
|||
color="primary" |
|||
#telemetryButton |
|||
(click)="manageKeys($event, telemetryButton, MappingKeysType.TIMESERIES)"> |
|||
<tb-icon matButtonIcon>edit</tb-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-panel no-border no-padding" *ngIf="converterType === ConvertorTypeEnum.CUSTOM"> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" |
|||
tb-hint-tooltip-icon="{{ 'gateway.extension-hint' | translate }}" translate> |
|||
gateway.extension |
|||
</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="extension" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.extension-required') | translate" |
|||
*ngIf="mappingForm.get('converter.custom.extension').hasError('required') && |
|||
mappingForm.get('converter.custom.extension').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row space-between same-padding tb-flex column"> |
|||
<div class="tb-form-panel-title" translate>gateway.extension-configuration</div> |
|||
<div class="tb-form-hint tb-primary-fill">{{ 'gateway.extension-configuration-hint' | translate }}</div> |
|||
<div class="tb-form-row space-between tb-flex"> |
|||
<div class="fixed-title-width" translate>gateway.keys</div> |
|||
<div class="tb-flex ellipsis-chips-container"> |
|||
<mat-chip-listbox [tb-ellipsis-chip-list]="customKeys" class="tb-flex"> |
|||
<mat-chip *ngFor="let telemetry of customKeys"> |
|||
{{ telemetry }} |
|||
</mat-chip> |
|||
<mat-chip class="mat-mdc-chip ellipsis-chip"> |
|||
<label class="ellipsis-text"></label> |
|||
</mat-chip> |
|||
</mat-chip-listbox> |
|||
<button type="button" |
|||
mat-icon-button |
|||
color="primary" |
|||
#keysButton |
|||
(click)="manageKeys($event, keysButton, MappingKeysType.CUSTOM)"> |
|||
<tb-icon matButtonIcon>edit</tb-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
</div> |
|||
</ng-container> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="MappingType.REQUESTS"> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" translate>gateway.request-type</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="requestType"> |
|||
<mat-option *ngFor="let type of requestTypes" [value]="type"> |
|||
{{ RequestTypesTranslationsMap.get(type) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<ng-container formGroupName="requestValue"> |
|||
<ng-container [formGroup]="mappingForm.get('requestValue').get(requestMappingType)" [ngSwitch]="requestMappingType"> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center" |
|||
*ngIf="requestMappingType === RequestTypeEnum.ATTRIBUTE_REQUEST || |
|||
requestMappingType === RequestTypeEnum.CONNECT_REQUEST || |
|||
requestMappingType === RequestTypeEnum.DISCONNECT_REQUEST"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.topic-filter</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" [formControl]="mappingForm.get('requestValue').get(requestMappingType).get('topicFilter')" |
|||
placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.topic-required') | translate" |
|||
*ngIf="mappingForm.get('requestValue').get(requestMappingType).get('topicFilter').hasError('required') && |
|||
mappingForm.get('requestValue').get(requestMappingType).get('topicFilter').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<div matSuffix |
|||
class="see-example" |
|||
[tb-help-popup]="'widget/lib/gateway/topic-filter_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<ng-template [ngSwitchCase]="RequestTypeEnum.CONNECT_REQUEST"> |
|||
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.FULL" required="true"> |
|||
</tb-device-info-table> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="RequestTypeEnum.DISCONNECT_REQUEST"> |
|||
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.PARTIAL" required="true"> |
|||
</tb-device-info-table> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_REQUEST"> |
|||
<div class="tb-form-panel stroked"> |
|||
<div class="tb-form-panel-title tb-required" translate>gateway.from-device-request-settings</div> |
|||
<div class="tb-form-hint tb-primary-fill" translate> |
|||
gateway.from-device-request-settings-hint |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center" formGroupName="deviceInfo"> |
|||
<div class="fixed-title-width tb-flex no-flex align-center" translate> |
|||
<div class="tb-required" translate>gateway.device-info.device-name-expression</div> |
|||
</div> |
|||
<div class="tb-flex"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="deviceNameExpressionSource"> |
|||
<mat-option *ngFor="let type of sourceTypes" [value]="type"> |
|||
{{ SourceTypeTranslationsMap.get(type) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="deviceNameExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.device-info.device-name-expression-required') | translate" |
|||
*ngIf="(mappingForm.get('requestValue.attributeRequests.deviceInfo.deviceNameExpression').hasError('required') && |
|||
mappingForm.get('requestValue.attributeRequests.deviceInfo.deviceNameExpression').touched)" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<div matSuffix |
|||
class="see-example" |
|||
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.attribute-name-expression</div> |
|||
<div class="tb-flex"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="attributeNameExpressionSource"> |
|||
<mat-option *ngFor="let type of sourceTypes" [value]="type"> |
|||
{{ SourceTypeTranslationsMap.get(type) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="attributeNameExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.attribute-name-expression-required') | translate" |
|||
*ngIf="mappingForm.get('requestValue.attributeRequests.attributeNameExpression').hasError('required') && |
|||
mappingForm.get('requestValue.attributeRequests.attributeNameExpression').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<div matSuffix |
|||
class="see-example" |
|||
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-panel stroked"> |
|||
<div class="tb-form-panel-title tb-required" translate>gateway.to-device-response-settings</div> |
|||
<div class="tb-form-hint tb-primary-fill" translate> |
|||
gateway.to-device-response-settings-hint |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.response-value-expression</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.response-value-expression-required') | translate" |
|||
*ngIf="mappingForm.get('requestValue.attributeRequests.valueExpression').hasError('required') && |
|||
mappingForm.get('requestValue.attributeRequests.valueExpression').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<div matSuffix |
|||
class="see-example" |
|||
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.response-topic-expression</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="topicExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.response-topic-expression-required') | translate" |
|||
*ngIf="mappingForm.get('requestValue.attributeRequests.topicExpression').hasError('required') && |
|||
mappingForm.get('requestValue.attributeRequests.topicExpression').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<div matSuffix |
|||
class="see-example" |
|||
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="retain"> |
|||
<mat-label tb-hint-tooltip-icon="{{ 'gateway.retain-hint' | translate }}"> |
|||
{{ 'gateway.retain' | translate }} |
|||
</mat-label> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_UPDATE"> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" |
|||
tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}" translate> |
|||
gateway.device-name-filter |
|||
</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="deviceNameFilter" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.device-name-filter-required') | translate" |
|||
*ngIf="mappingForm.get('requestValue.attributeUpdates.deviceNameFilter').hasError('required') && |
|||
mappingForm.get('requestValue.attributeUpdates.deviceNameFilter').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.attribute-filter-hint' | translate }}" translate> |
|||
gateway.attribute-filter |
|||
</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="attributeFilter" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.attribute-filter-required') | translate" |
|||
*ngIf="mappingForm.get('requestValue.attributeUpdates.attributeFilter').hasError('required') && |
|||
mappingForm.get('requestValue.attributeUpdates.attributeFilter').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.response-value-expression</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.response-value-expression-required') | translate" |
|||
*ngIf="mappingForm.get('requestValue.attributeUpdates.valueExpression').hasError('required') && |
|||
mappingForm.get('requestValue.attributeUpdates.valueExpression').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<div matSuffix |
|||
class="see-example" |
|||
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.response-topic-expression</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="topicExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.response-topic-expression-required') | translate" |
|||
*ngIf="mappingForm.get('requestValue.attributeUpdates.topicExpression').hasError('required') && |
|||
mappingForm.get('requestValue.attributeUpdates.topicExpression').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<div matSuffix |
|||
class="see-example" |
|||
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="retain"> |
|||
<mat-label tb-hint-tooltip-icon="{{ 'gateway.retain-hint' | translate }}"> |
|||
{{ 'gateway.retain' | translate }} |
|||
</mat-label> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="RequestTypeEnum.SERVER_SIDE_RPC"> |
|||
<div class="tb-flex row center align-center no-gap fill-width"> |
|||
<tb-toggle-select formControlName="type" appearance="fill"> |
|||
<tb-toggle-option [value]="ServerSideRPCType.TWO_WAY"> |
|||
{{ 'gateway.with-response' | translate }} |
|||
</tb-toggle-option> |
|||
<tb-toggle-option [value]="ServerSideRPCType.ONE_WAY"> |
|||
{{ 'gateway.without-response' | translate }} |
|||
</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}" translate> |
|||
gateway.device-name-filter |
|||
</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="deviceNameFilter" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.device-name-filter-required') | translate" |
|||
*ngIf="mappingForm.get('requestValue.serverSideRpc.deviceNameFilter').hasError('required') && |
|||
mappingForm.get('requestValue.serverSideRpc.deviceNameFilter').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.method-filter-hint' | translate }}" translate> |
|||
gateway.method-filter |
|||
</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="methodFilter" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.method-filter-required') | translate" |
|||
*ngIf="mappingForm.get('requestValue.serverSideRpc.methodFilter').hasError('required') && |
|||
mappingForm.get('requestValue.serverSideRpc.methodFilter').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.request-topic-expression</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="requestTopicExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.request-topic-expression-required') | translate" |
|||
*ngIf="mappingForm.get('requestValue.serverSideRpc.requestTopicExpression').hasError('required') && |
|||
mappingForm.get('requestValue.serverSideRpc.requestTopicExpression').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<div matSuffix |
|||
class="see-example" |
|||
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.value-expression</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.value-expression-required') | translate" |
|||
*ngIf="mappingForm.get('requestValue.serverSideRpc.valueExpression').hasError('required') && |
|||
mappingForm.get('requestValue.serverSideRpc.valueExpression').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<div matSuffix |
|||
class="see-example" |
|||
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<ng-container *ngIf="mappingForm.get('requestValue.serverSideRpc.type').value === ServerSideRPCType.TWO_WAY"> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.response-topic-expression</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" formControlName="responseTopicExpression" placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="('gateway.response-topic-expression-required') | translate" |
|||
*ngIf="mappingForm.get('requestValue.serverSideRpc.responseTopicExpression').hasError('required') && |
|||
mappingForm.get('requestValue.serverSideRpc.responseTopicExpression').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
<div matSuffix |
|||
class="see-example" |
|||
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}" translate> |
|||
gateway.response-topic-Qos |
|||
</div> |
|||
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="responseTopicQoS"> |
|||
<mat-option *ngFor="let type of qualityTypes" [value]="type"> |
|||
{{ QualityTranslationsMap.get(type) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
|||
<div class="fixed-title-width tb-required" translate>gateway.response-timeout</div> |
|||
<div class="tb-flex no-gap"> |
|||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput name="value" type="number" min="1" formControlName="responseTimeout" |
|||
placeholder="{{ 'gateway.set' | translate }}"/> |
|||
<mat-icon matSuffix |
|||
matTooltipPosition="above" |
|||
matTooltipClass="tb-error-tooltip" |
|||
[matTooltip]="responseTimeoutErrorTooltip" |
|||
*ngIf="(mappingForm.get('requestValue.serverSideRpc.responseTimeout').hasError('required') || |
|||
mappingForm.get('requestValue.serverSideRpc.responseTimeout').hasError('min')) && |
|||
mappingForm.get('requestValue.serverSideRpc.responseTimeout').touched" |
|||
class="tb-error"> |
|||
warning |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
</ng-template> |
|||
</ng-container> |
|||
</ng-container> |
|||
</ng-template> |
|||
</ng-container> |
|||
</div> |
|||
</div> |
|||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
cdkFocusInitial |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
(click)="add()" |
|||
[disabled]="mappingForm.invalid || !mappingForm.dirty || !keysPopupClosed"> |
|||
{{ this.data.buttonTitle | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,76 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
:host { |
|||
display: grid; |
|||
height: 100%; |
|||
|
|||
.key-mapping { |
|||
max-width: 900px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
|
|||
.mat-toolbar { |
|||
min-height: 64px; |
|||
} |
|||
|
|||
tb-toggle-select { |
|||
padding: 4px 0; |
|||
} |
|||
} |
|||
|
|||
.mat-mdc-dialog-content { |
|||
max-height: 670px; |
|||
height: 670px; |
|||
} |
|||
|
|||
.ellipsis-chips-container { |
|||
max-width: 70%; |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.key-mapping { |
|||
.mat-mdc-chip-listbox { |
|||
.mdc-evolution-chip-set__chips { |
|||
justify-content: flex-end; |
|||
align-items: center; |
|||
} |
|||
} |
|||
} |
|||
.tb-form-row { |
|||
.fixed-title-width { |
|||
min-width: 40px; |
|||
width: 35%; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
.mat-mdc-form-field { |
|||
width: 0; |
|||
} |
|||
} |
|||
|
|||
.see-example { |
|||
width: 32px; |
|||
height: 32px; |
|||
margin: 4px; |
|||
} |
|||
|
|||
.mat-mdc-form-field-icon-suffix { |
|||
display: flex; |
|||
} |
|||
} |
|||
@ -0,0 +1,360 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, Inject, OnDestroy, Renderer2, ViewContainerRef } from '@angular/core'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { FormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
|||
import { BaseData, HasId } from '@shared/models/base-data'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Router } from '@angular/router'; |
|||
import { |
|||
ConvertorType, |
|||
ConvertorTypeTranslationsMap, |
|||
DataConversionTranslationsMap, |
|||
DeviceInfoType, |
|||
MappingHintTranslationsMap, |
|||
MappingInfo, |
|||
MappingKeysAddKeyTranslationsMap, |
|||
MappingKeysDeleteKeyTranslationsMap, |
|||
MappingKeysNoKeysTextTranslationsMap, |
|||
MappingKeysPanelTitleTranslationsMap, |
|||
MappingKeysType, |
|||
MappingType, |
|||
MappingTypeTranslationsMap, |
|||
noLeadTrailSpacesRegex, |
|||
QualityTypes, |
|||
QualityTypeTranslationsMap, |
|||
RequestType, |
|||
RequestTypesTranslationsMap, |
|||
ServerSideRPCType, |
|||
SourceTypes, |
|||
SourceTypeTranslationsMap |
|||
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
|||
import { Subject } from 'rxjs'; |
|||
import { startWith, takeUntil } from 'rxjs/operators'; |
|||
import { MatButton } from '@angular/material/button'; |
|||
import { TbPopoverService } from '@shared/components/popover.service'; |
|||
import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-mapping-dialog', |
|||
templateUrl: './mapping-dialog.component.html', |
|||
styleUrls: ['./mapping-dialog.component.scss'], |
|||
providers: [], |
|||
}) |
|||
export class MappingDialogComponent extends DialogComponent<MappingDialogComponent, BaseData<HasId>> implements OnDestroy { |
|||
|
|||
mappingForm: UntypedFormGroup; |
|||
|
|||
MappingType = MappingType; |
|||
|
|||
qualityTypes = QualityTypes; |
|||
QualityTranslationsMap = QualityTypeTranslationsMap; |
|||
|
|||
convertorTypes = Object.values(ConvertorType); |
|||
ConvertorTypeEnum = ConvertorType; |
|||
ConvertorTypeTranslationsMap = ConvertorTypeTranslationsMap; |
|||
|
|||
sourceTypes = Object.values(SourceTypes); |
|||
sourceTypesEnum = SourceTypes; |
|||
SourceTypeTranslationsMap = SourceTypeTranslationsMap; |
|||
|
|||
requestTypes = Object.values(RequestType); |
|||
RequestTypeEnum = RequestType; |
|||
RequestTypesTranslationsMap = RequestTypesTranslationsMap; |
|||
|
|||
DeviceInfoType = DeviceInfoType; |
|||
|
|||
ServerSideRPCType = ServerSideRPCType; |
|||
|
|||
MappingKeysType = MappingKeysType; |
|||
|
|||
MappingHintTranslationsMap = MappingHintTranslationsMap; |
|||
|
|||
MappingTypeTranslationsMap = MappingTypeTranslationsMap; |
|||
|
|||
DataConversionTranslationsMap = DataConversionTranslationsMap; |
|||
|
|||
hiddenAttributesCount = 0; |
|||
|
|||
keysPopupClosed = true; |
|||
|
|||
submitted = false; |
|||
|
|||
private destroy$ = new Subject<void>(); |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: MappingInfo, |
|||
public dialogRef: MatDialogRef<MappingDialogComponent, {[key: string]: any}>, |
|||
private fb: FormBuilder, |
|||
private popoverService: TbPopoverService, |
|||
private renderer: Renderer2, |
|||
private viewContainerRef: ViewContainerRef, |
|||
private translate: TranslateService) { |
|||
super(store, router, dialogRef); |
|||
|
|||
this.createMappingForm(); |
|||
} |
|||
|
|||
get converterAttributes(): Array<string> { |
|||
if (this.converterType) { |
|||
return this.mappingForm.get('converter').get(this.converterType).value.attributes.map(value => value.key); |
|||
} |
|||
} |
|||
|
|||
get converterTelemetry(): Array<string> { |
|||
if (this.converterType) { |
|||
return this.mappingForm.get('converter').get(this.converterType).value.timeseries.map(value => value.key); |
|||
} |
|||
} |
|||
|
|||
get converterType(): ConvertorType { |
|||
return this.mappingForm.get('converter').get('type').value; |
|||
} |
|||
|
|||
get customKeys(): Array<string> { |
|||
return Object.keys(this.mappingForm.get('converter').get('custom').value.extensionConfig); |
|||
} |
|||
|
|||
get requestMappingType(): RequestType { |
|||
return this.mappingForm.get('requestType').value; |
|||
} |
|||
|
|||
get responseTimeoutErrorTooltip(): string { |
|||
const control = this.mappingForm.get('requestValue.serverSideRpc.responseTimeout'); |
|||
if (control.hasError('required')) { |
|||
return this.translate.instant('gateway.response-timeout-required'); |
|||
} else if (control.hasError('min')) { |
|||
return this.translate.instant('gateway.response-timeout-limits-error', {min: 1}); |
|||
} |
|||
return ''; |
|||
} |
|||
|
|||
|
|||
ngOnDestroy(): void { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
super.ngOnDestroy(); |
|||
} |
|||
|
|||
private createMappingForm(): void { |
|||
this.mappingForm = this.fb.group({}); |
|||
if (this.data.mappingType === MappingType.DATA) { |
|||
this.mappingForm.addControl('topicFilter', |
|||
this.fb.control('', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)])); |
|||
this.mappingForm.addControl('subscriptionQos', this.fb.control(0)); |
|||
this.mappingForm.addControl('converter', this.fb.group({ |
|||
type: [ConvertorType.JSON, []], |
|||
json: this.fb.group({ |
|||
deviceInfo: [{}, []], |
|||
attributes: [[], []], |
|||
timeseries: [[], []] |
|||
}), |
|||
bytes: this.fb.group({ |
|||
deviceInfo: [{}, []], |
|||
attributes: [[], []], |
|||
timeseries: [[], []] |
|||
}), |
|||
custom: this.fb.group({ |
|||
extension: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
extensionConfig: [{}, []] |
|||
}), |
|||
})); |
|||
this.mappingForm.patchValue(this.prepareFormValueData()); |
|||
this.mappingForm.get('converter.type').valueChanges.pipe( |
|||
startWith(this.mappingForm.get('converter.type').value), |
|||
takeUntil(this.destroy$) |
|||
).subscribe((value) => { |
|||
const converterGroup = this.mappingForm.get('converter'); |
|||
converterGroup.get('json').disable({emitEvent: false}); |
|||
converterGroup.get('bytes').disable({emitEvent: false}); |
|||
converterGroup.get('custom').disable({emitEvent: false}); |
|||
converterGroup.get(value).enable({emitEvent: false}); |
|||
}) |
|||
} |
|||
|
|||
if (this.data.mappingType === MappingType.REQUESTS) { |
|||
this.mappingForm.addControl('requestType', this.fb.control(RequestType.CONNECT_REQUEST, [])); |
|||
this.mappingForm.addControl('requestValue', this.fb.group({ |
|||
connectRequests: this.fb.group({ |
|||
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
deviceInfo: [{}, []] |
|||
}), |
|||
disconnectRequests: this.fb.group({ |
|||
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
deviceInfo: [{}, []] |
|||
}), |
|||
attributeRequests: this.fb.group({ |
|||
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
deviceInfo: this.fb.group({ |
|||
deviceNameExpressionSource: [SourceTypes.MSG, []], |
|||
deviceNameExpression: ['', [Validators.required]], |
|||
}), |
|||
attributeNameExpressionSource: [SourceTypes.MSG, []], |
|||
attributeNameExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
retain: [false, []] |
|||
}), |
|||
attributeUpdates: this.fb.group({ |
|||
deviceNameFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
attributeFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
retain: [true, []] |
|||
}), |
|||
serverSideRpc: this.fb.group({ |
|||
type: [ServerSideRPCType.TWO_WAY, []], |
|||
deviceNameFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
methodFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
requestTopicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
responseTopicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
|||
responseTopicQoS: [0, []], |
|||
responseTimeout: [10000, [Validators.required, Validators.min(1)]], |
|||
}) |
|||
})); |
|||
this.mappingForm.get('requestType').valueChanges.pipe( |
|||
startWith(this.mappingForm.get('requestType').value), |
|||
takeUntil(this.destroy$) |
|||
).subscribe((value) => { |
|||
const requestValueGroup = this.mappingForm.get('requestValue'); |
|||
requestValueGroup.get('connectRequests').disable({emitEvent: false}); |
|||
requestValueGroup.get('disconnectRequests').disable({emitEvent: false}); |
|||
requestValueGroup.get('attributeRequests').disable({emitEvent: false}); |
|||
requestValueGroup.get('attributeUpdates').disable({emitEvent: false}); |
|||
requestValueGroup.get('serverSideRpc').disable({emitEvent: false}); |
|||
requestValueGroup.get(value).enable(); |
|||
}); |
|||
this.mappingForm.get('requestValue.serverSideRpc.type').valueChanges.pipe( |
|||
takeUntil(this.destroy$) |
|||
).subscribe((value) => { |
|||
const requestValueGroup = this.mappingForm.get('requestValue.serverSideRpc'); |
|||
if (value === ServerSideRPCType.ONE_WAY) { |
|||
requestValueGroup.get('responseTopicExpression').disable({emitEvent: false}); |
|||
requestValueGroup.get('responseTopicQoS').disable({emitEvent: false}); |
|||
requestValueGroup.get('responseTimeout').disable({emitEvent: false}); |
|||
} else { |
|||
requestValueGroup.get('responseTopicExpression').enable({emitEvent: false}); |
|||
requestValueGroup.get('responseTopicQoS').enable({emitEvent: false}); |
|||
requestValueGroup.get('responseTimeout').enable({emitEvent: false}); |
|||
} |
|||
}); |
|||
this.mappingForm.patchValue(this.prepareFormValueData()); |
|||
} |
|||
} |
|||
|
|||
helpLinkId(): string { |
|||
return 'https://thingsboard.io/docs/iot-gateway/config/mqtt/#section-mapping'; |
|||
} |
|||
|
|||
cancel(): void { |
|||
if (this.keysPopupClosed) { |
|||
this.dialogRef.close(null); |
|||
} |
|||
} |
|||
|
|||
add(): void { |
|||
this.submitted = true; |
|||
if (this.mappingForm.valid) { |
|||
this.dialogRef.close(this.prepareMappingData()); |
|||
} |
|||
} |
|||
|
|||
manageKeys($event: Event, matButton: MatButton, keysType: MappingKeysType) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const trigger = matButton._elementRef.nativeElement; |
|||
if (this.popoverService.hasPopover(trigger)) { |
|||
this.popoverService.hidePopover(trigger); |
|||
} else { |
|||
const keysControl = this.mappingForm.get('converter').get(this.converterType).get(keysType); |
|||
const ctx: any = { |
|||
keys: keysControl.value, |
|||
keysType: keysType, |
|||
rawData: this.mappingForm.get('converter.type').value === ConvertorType.BYTES, |
|||
panelTitle: MappingKeysPanelTitleTranslationsMap.get(keysType), |
|||
addKeyTitle: MappingKeysAddKeyTranslationsMap.get(keysType), |
|||
deleteKeyTitle: MappingKeysDeleteKeyTranslationsMap.get(keysType), |
|||
noKeysText: MappingKeysNoKeysTextTranslationsMap.get(keysType) |
|||
}; |
|||
this.keysPopupClosed = false; |
|||
const dataKeysPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, |
|||
this.viewContainerRef, MappingDataKeysPanelComponent, 'leftBottom', false, null, |
|||
ctx, |
|||
{}, |
|||
{}, {}, true); |
|||
dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover; |
|||
dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.subscribe((keysData) => { |
|||
dataKeysPanelPopover.hide(); |
|||
keysControl.patchValue(keysData); |
|||
keysControl.markAsDirty(); |
|||
}); |
|||
dataKeysPanelPopover.tbHideStart.subscribe(() => { |
|||
this.keysPopupClosed = true; |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private prepareMappingData(): {[key: string]: any} { |
|||
const formValue = this.mappingForm.value; |
|||
if (this.data.mappingType === MappingType.DATA) { |
|||
const { converter, topicFilter, subscriptionQos } = formValue; |
|||
return { |
|||
topicFilter, |
|||
subscriptionQos, |
|||
converter: { |
|||
type: converter.type, |
|||
...converter[converter.type] |
|||
} |
|||
}; |
|||
} else { |
|||
return { |
|||
requestType: formValue.requestType, |
|||
requestValue: formValue.requestValue[formValue.requestType] |
|||
}; |
|||
} |
|||
} |
|||
|
|||
private prepareFormValueData(): {[key: string]: any} { |
|||
if (this.data.value && Object.keys(this.data.value).length) { |
|||
if (this.data.mappingType === MappingType.DATA) { |
|||
const { converter, topicFilter, subscriptionQos } = this.data.value; |
|||
return { |
|||
topicFilter, |
|||
subscriptionQos, |
|||
converter: { |
|||
type: converter.type, |
|||
[converter.type]: { ...converter } |
|||
} |
|||
}; |
|||
} else { |
|||
return { |
|||
requestType: this.data.value.requestType, |
|||
requestValue: { |
|||
[this.data.value.requestType]: this.data.value.requestValue |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
return this.data.value; |
|||
} |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
### Expressions |
|||
#### JSON Path: |
|||
|
|||
The expression field is used to extract data from the MQTT message. There are various available options for different parts of the messages: |
|||
|
|||
- The JSONPath format can be used to extract data from the message body. |
|||
|
|||
- The regular expression format can be used to extract data from the topic where the message will arrive. |
|||
|
|||
- Slices can only be used in the expression fields of bytes converters. |
|||
|
|||
JSONPath expressions specify the items within a JSON structure (which could be an object, array, or nested combination of both) that you want to access. These expressions can select elements from JSON data on specific criteria. Here's a basic overview of how JSONPath expressions are structured: |
|||
|
|||
- `$`: The root element of the JSON document; |
|||
|
|||
- `.`: Child operator used to select child elements. For example, $.store.book ; |
|||
|
|||
- `[]`: Child operator used to select child elements. $['store']['book'] accesses the book array within a store object; |
|||
|
|||
##### Examples: |
|||
|
|||
For example, if we want to extract the device name from the following message, we can use the expression below: |
|||
|
|||
MQTT message: |
|||
|
|||
``` |
|||
{ |
|||
"sensorModelInfo": { |
|||
"sensorName": "AM-123", |
|||
"sensorType": "myDeviceType" |
|||
}, |
|||
"data": { |
|||
"temp": 12.2, |
|||
"hum": 56, |
|||
"status": "ok" |
|||
} |
|||
} |
|||
{:copy-code} |
|||
``` |
|||
|
|||
Expression: |
|||
|
|||
`${sensorModelInfo.sensorName}` |
|||
|
|||
Converted data: |
|||
|
|||
`AM-123` |
|||
|
|||
If we want to extract all data from the message above, we can use the following expression: |
|||
|
|||
`${data}` |
|||
|
|||
Converted data: |
|||
|
|||
`{"temp": 12.2, "hum": 56, "status": "ok"}` |
|||
|
|||
Or if we want to extract specific data (for example “temperature”), you can use the following expression: |
|||
|
|||
`${data.temp}` |
|||
|
|||
And as a converted data we will get: |
|||
|
|||
`12.2` |
|||
|
|||
<br/> |
|||
|
|||
#### Regular expressions for topic: |
|||
|
|||
Device name or device profile can be parsed from the MQTT topic using regular expression. A regular expression, often abbreviated as regex or regexp, is a sequence of characters that forms a search pattern, primarily used for string matching and manipulation. |
|||
|
|||
##### Regular expression for topic examples: |
|||
|
|||
| Topic | Regular expression | Output data | Description | |
|||
|:---------------------------|----------------------------------|--------------------------|--------------------------------------| |
|||
| /devices/AM123/mytype/data | /devices/([^/]+)/mytype/data | AM123 | Getting device name from topic | |
|||
| /devices/AM123/mytype/data | /devices/[A-Z0-9]+/([^/]+)/data | mytype | Getting device profile from topic | |
|||
|
|||
<br/> |
|||
|
|||
#### Bytes converter: |
|||
|
|||
For bytes converter, expression fields can use slices format only. A slice specifies how to slice a sequence, determining the start point, and the endpoint. Here's a basic overview of slice components: |
|||
|
|||
- `start`: The starting index of the slice. It is included in the slice. If omitted, slicing starts at the beginning of the sequence. Indexing starts at 0, so the first element of the sequence is at index 0. |
|||
|
|||
- `stop`: The ending index of the slice. It is excluded from the slice, meaning the slice will end just before this index. If omitted, slicing goes through the end of the sequence. |
|||
|
|||
##### Bytes parsing examples: |
|||
|
|||
|
|||
| Message body | Slice | Output data | Description | |
|||
|:-----------------------|-----------------|--------------------------|------------------------------| |
|||
| AM123,mytype,12.2,45 | [:5] | AM123 | Extracting device name | |
|||
| AM123,mytype,12.2,45 | [:] | AM123,mytype,12.2,45 | Extracting all data | |
|||
| AM123,mytype,12.2,45 | [18:] | 45 | Extracting humidity value | |
|||
| AM123,mytype,12.2,45 | [13:17] | 12.2 | Extracting temperature value | |
|||
@ -0,0 +1,15 @@ |
|||
### Topic filter |
|||
|
|||
The `topicFilter` supports special symbols: `#` and `+`, allowing to subscribe to multiple topics. |
|||
|
|||
Also, the MQTT connector supports shared subscriptions. To create a shared subscription you need to add `$share/` |
|||
as a prefix for the topic filter and shared subscription group name. For example to subscribe to the my-shared-topic |
|||
in group **my-group-name** you can set the topic filter to “$share/**my-group-name**/my-shared-topic”. |
|||
|
|||
|
|||
#### Examples: |
|||
|
|||
| Topic | Topic filter | Payload | Description | |
|||
|:-----------------------|----------------------------------|---------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------| |
|||
| sensor/data | sensor/data | `{"serialNumber": "SN-001", "sensorType": "Thermometer", "sensorModel": "T1000", "temp": 42, "hum": 58}` | Device Name is part of the payload, to extract it you need to use JSON Path expression | |
|||
| sensor/SN-001/data | sensor/+/data | `{"sensorType": "Thermometer", "sensorModel": "T1000", "temp": 42, "hum": 58}` | Device Name is part of the topic, to extract it you need to use regular expression. | |
|||
@ -0,0 +1,58 @@ |
|||
{ |
|||
"general": { |
|||
"objectName": "TB_gateway", |
|||
"address": "0.0.0.0:47808", |
|||
"objectIdentifier": 599, |
|||
"maxApduLengthAccepted": 1476, |
|||
"segmentationSupported": "segmentedBoth", |
|||
"vendorIdentifier": 15 |
|||
}, |
|||
"devices": [ |
|||
{ |
|||
"deviceName": "BACnet Device ${objectName}", |
|||
"deviceType": "default", |
|||
"address": "192.168.2.110:47808", |
|||
"pollPeriod": 10000, |
|||
"attributes": [ |
|||
{ |
|||
"key": "temperature", |
|||
"type": "string", |
|||
"objectId": "analogOutput:1", |
|||
"propertyId": "presentValue" |
|||
} |
|||
], |
|||
"timeseries": [ |
|||
{ |
|||
"key": "state", |
|||
"type": "bool", |
|||
"objectId": "binaryValue:1", |
|||
"propertyId": "presentValue" |
|||
} |
|||
], |
|||
"attributeUpdates": [ |
|||
{ |
|||
"key": "brightness", |
|||
"requestType": "writeProperty", |
|||
"objectId": "analogOutput:1", |
|||
"propertyId": "presentValue" |
|||
} |
|||
], |
|||
"serverSideRpc": [ |
|||
{ |
|||
"method": "set_state", |
|||
"requestType": "writeProperty", |
|||
"requestTimeout": 10000, |
|||
"objectId": "binaryOutput:1", |
|||
"propertyId": "presentValue" |
|||
}, |
|||
{ |
|||
"method": "get_state", |
|||
"requestType": "readProperty", |
|||
"requestTimeout": 10000, |
|||
"objectId": "binaryOutput:1", |
|||
"propertyId": "presentValue" |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
{ |
|||
"passiveScanMode": true, |
|||
"showMap": false, |
|||
"scanner": { |
|||
"timeout": 10000, |
|||
"deviceName": "Device name" |
|||
}, |
|||
"devices": [ |
|||
{ |
|||
"name": "Temperature and humidity sensor", |
|||
"MACAddress": "4C:65:A8:DF:85:C0", |
|||
"pollPeriod": 500000, |
|||
"showMap": false, |
|||
"timeout": 10000, |
|||
"connectRetry": 5, |
|||
"connectRetryInSeconds": 0, |
|||
"waitAfterConnectRetries": 10, |
|||
"telemetry": [ |
|||
{ |
|||
"key": "temperature", |
|||
"method": "notify", |
|||
"characteristicUUID": "226CAA55-6476-4566-7562-66734470666D", |
|||
"valueExpression": "[2]" |
|||
}, |
|||
{ |
|||
"key": "humidity", |
|||
"method": "notify", |
|||
"characteristicUUID": "226CAA55-6476-4566-7562-66734470666D", |
|||
"valueExpression": "[0]" |
|||
} |
|||
], |
|||
"attributes": [ |
|||
{ |
|||
"key": "name", |
|||
"method": "read", |
|||
"characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB", |
|||
"valueExpression": "[0:2]cm [2:]A" |
|||
}, |
|||
{ |
|||
"key": "values", |
|||
"method": "read", |
|||
"characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB", |
|||
"valueExpression": "All values: [:]" |
|||
} |
|||
], |
|||
"attributeUpdates": [ |
|||
{ |
|||
"attributeOnThingsBoard": "sharedName", |
|||
"characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB" |
|||
} |
|||
], |
|||
"serverSideRpc": [ |
|||
{ |
|||
"methodRPC": "rpcMethod1", |
|||
"withResponse": true, |
|||
"characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB", |
|||
"methodProcessing": "read" |
|||
}, |
|||
{ |
|||
"methodRPC": "rpcMethod2", |
|||
"withResponse": true, |
|||
"characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB", |
|||
"methodProcessing": "write" |
|||
}, |
|||
{ |
|||
"methodRPC": "rpcMethod3", |
|||
"withResponse": true, |
|||
"methodProcessing": "scan" |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
{ |
|||
"interface": "socketcan", |
|||
"channel": "vcan0", |
|||
"backend": { |
|||
"fd": true |
|||
}, |
|||
"reconnectPeriod": 5, |
|||
"devices": [ |
|||
{ |
|||
"name": "Car", |
|||
"sendDataOnlyOnChange": false, |
|||
"enableUnknownRpc": true, |
|||
"strictEval": false, |
|||
"attributes": [ |
|||
{ |
|||
"key": "isDriverDoorOpened", |
|||
"nodeId": 41, |
|||
"command": "2:2:big:8717", |
|||
"value": "4:1:int", |
|||
"expression": "bool(value & 0b00000100)", |
|||
"polling": { |
|||
"type": "once", |
|||
"dataInHex": "AB CD AB CD" |
|||
} |
|||
} |
|||
], |
|||
"timeseries": [ |
|||
{ |
|||
"key": "rpm", |
|||
"nodeId": 1918, |
|||
"isExtendedId": true, |
|||
"command": "2:2:big:48059", |
|||
"value": "4:2:big:int", |
|||
"expression": "value / 4", |
|||
"polling": { |
|||
"type": "always", |
|||
"period": 5, |
|||
"dataInHex": "aaaa bbbb aaaa bbbb" |
|||
} |
|||
}, |
|||
{ |
|||
"key": "milliage", |
|||
"nodeId": 1918, |
|||
"isExtendedId": true, |
|||
"value": "4:2:little:int", |
|||
"expression": "value * 10", |
|||
"polling": { |
|||
"type": "always", |
|||
"period": 30, |
|||
"dataInHex": "aa bb cc dd ee ff aa bb" |
|||
} |
|||
} |
|||
], |
|||
"attributeUpdates": [ |
|||
{ |
|||
"attributeOnThingsBoard": "softwareVersion", |
|||
"nodeId": 64, |
|||
"isExtendedId": true, |
|||
"dataLength": 4, |
|||
"dataExpression": "value + 5", |
|||
"dataByteorder": "little" |
|||
} |
|||
], |
|||
"serverSideRpc": [ |
|||
{ |
|||
"method": "sendSameData", |
|||
"nodeId": 4, |
|||
"isExtendedId": true, |
|||
"isFd": true, |
|||
"bitrateSwitch": true, |
|||
"dataInHex": "aa bb cc dd ee ff aa bb aa bb cc d ee ff" |
|||
}, |
|||
{ |
|||
"method": "setLightLevel", |
|||
"nodeId": 5, |
|||
"dataLength": 2, |
|||
"dataByteorder": "little", |
|||
"dataBefore": "00AA" |
|||
}, |
|||
{ |
|||
"method": "setSpeed", |
|||
"nodeId": 16, |
|||
"dataAfter": "0102", |
|||
"dataExpression": "userSpeed if maxAllowedSpeed > userSpeed else maxAllowedSpeed" |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
{ |
|||
"host": "0.0.0.0", |
|||
"port": 21, |
|||
"TLSSupport": false, |
|||
"security": { |
|||
"type": "basic", |
|||
"username": "admin", |
|||
"password": "admin" |
|||
}, |
|||
"paths": [ |
|||
{ |
|||
"devicePatternName": "asd", |
|||
"devicePatternType": "Device", |
|||
"delimiter": ",", |
|||
"path": "fol/*_hello*.txt", |
|||
"readMode": "FULL", |
|||
"maxFileSize": 5, |
|||
"pollPeriod": 500, |
|||
"txtFileDataView": "SLICED", |
|||
"withSortingFiles": true, |
|||
"attributes": [ |
|||
{ |
|||
"key": "temp", |
|||
"value": "[1:]" |
|||
}, |
|||
{ |
|||
"key": "tmp", |
|||
"value": "[0:1]" |
|||
} |
|||
], |
|||
"timeseries": [ |
|||
{ |
|||
"type": "int", |
|||
"key": "[0:1]", |
|||
"value": "[0:1]" |
|||
}, |
|||
{ |
|||
"type": "int", |
|||
"key": "temp", |
|||
"value": "[1:]" |
|||
} |
|||
] |
|||
} |
|||
], |
|||
"attributeUpdates": [ |
|||
{ |
|||
"path": "fol/hello.json", |
|||
"deviceNameFilter": ".*", |
|||
"writingMode": "WRITE", |
|||
"valueExpression": "{'${attributeKey}':'${attributeValue}'}" |
|||
} |
|||
], |
|||
"serverSideRpc": [ |
|||
{ |
|||
"deviceNameFilter": ".*", |
|||
"methodFilter": "read", |
|||
"valueExpression": "${params}" |
|||
}, |
|||
{ |
|||
"deviceNameFilter": ".*", |
|||
"methodFilter": "write", |
|||
"valueExpression": "${params}" |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,248 @@ |
|||
{ |
|||
"master": { |
|||
"slaves": [ |
|||
{ |
|||
"host": "127.0.0.1", |
|||
"port": 5021, |
|||
"type": "tcp", |
|||
"method": "socket", |
|||
"timeout": 35, |
|||
"byteOrder": "LITTLE", |
|||
"wordOrder": "LITTLE", |
|||
"retries": true, |
|||
"retryOnEmpty": true, |
|||
"retryOnInvalid": true, |
|||
"pollPeriod": 5000, |
|||
"unitId": 1, |
|||
"deviceName": "Temp Sensor", |
|||
"sendDataOnlyOnChange": true, |
|||
"connectAttemptTimeMs": 5000, |
|||
"connectAttemptCount": 5, |
|||
"waitAfterFailedAttemptsMs": 300000, |
|||
"attributes": [ |
|||
{ |
|||
"tag": "string_read", |
|||
"type": "string", |
|||
"functionCode": 4, |
|||
"objectsCount": 4, |
|||
"address": 1 |
|||
}, |
|||
{ |
|||
"tag": "bits_read", |
|||
"type": "bits", |
|||
"functionCode": 4, |
|||
"objectsCount": 1, |
|||
"address": 5 |
|||
}, |
|||
{ |
|||
"tag": "8int_read", |
|||
"type": "8int", |
|||
"functionCode": 4, |
|||
"objectsCount": 1, |
|||
"address": 6 |
|||
}, |
|||
{ |
|||
"tag": "16int_read", |
|||
"type": "16int", |
|||
"functionCode": 4, |
|||
"objectsCount": 1, |
|||
"address": 7 |
|||
}, |
|||
{ |
|||
"tag": "32int_read_divider", |
|||
"type": "32int", |
|||
"functionCode": 4, |
|||
"objectsCount": 2, |
|||
"address": 8, |
|||
"divider": 10 |
|||
}, |
|||
{ |
|||
"tag": "8int_read_multiplier", |
|||
"type": "8int", |
|||
"functionCode": 4, |
|||
"objectsCount": 1, |
|||
"address": 10, |
|||
"multiplier": 10 |
|||
}, |
|||
{ |
|||
"tag": "32int_read", |
|||
"type": "32int", |
|||
"functionCode": 4, |
|||
"objectsCount": 2, |
|||
"address": 11 |
|||
}, |
|||
{ |
|||
"tag": "64int_read", |
|||
"type": "64int", |
|||
"functionCode": 4, |
|||
"objectsCount": 4, |
|||
"address": 13 |
|||
} |
|||
], |
|||
"timeseries": [ |
|||
{ |
|||
"tag": "8uint_read", |
|||
"type": "8uint", |
|||
"functionCode": 4, |
|||
"objectsCount": 1, |
|||
"address": 17 |
|||
}, |
|||
{ |
|||
"tag": "16uint_read", |
|||
"type": "16uint", |
|||
"functionCode": 4, |
|||
"objectsCount": 2, |
|||
"address": 18 |
|||
}, |
|||
{ |
|||
"tag": "32uint_read", |
|||
"type": "32uint", |
|||
"functionCode": 4, |
|||
"objectsCount": 4, |
|||
"address": 20 |
|||
}, |
|||
{ |
|||
"tag": "64uint_read", |
|||
"type": "64uint", |
|||
"functionCode": 4, |
|||
"objectsCount": 1, |
|||
"address": 24 |
|||
}, |
|||
{ |
|||
"tag": "16float_read", |
|||
"type": "16float", |
|||
"functionCode": 4, |
|||
"objectsCount": 1, |
|||
"address": 25 |
|||
}, |
|||
{ |
|||
"tag": "32float_read", |
|||
"type": "32float", |
|||
"functionCode": 4, |
|||
"objectsCount": 2, |
|||
"address": 26 |
|||
}, |
|||
{ |
|||
"tag": "64float_read", |
|||
"type": "64float", |
|||
"functionCode": 4, |
|||
"objectsCount": 4, |
|||
"address": 28 |
|||
} |
|||
], |
|||
"attributeUpdates": [ |
|||
{ |
|||
"tag": "shared_attribute_write", |
|||
"type": "32int", |
|||
"functionCode": 6, |
|||
"objectsCount": 2, |
|||
"address": 29 |
|||
} |
|||
], |
|||
"rpc": [ |
|||
{ |
|||
"tag": "setValue", |
|||
"type": "bits", |
|||
"functionCode": 5, |
|||
"objectsCount": 1, |
|||
"address": 31 |
|||
}, |
|||
{ |
|||
"tag": "getValue", |
|||
"type": "bits", |
|||
"functionCode": 1, |
|||
"objectsCount": 1, |
|||
"address": 31 |
|||
}, |
|||
{ |
|||
"tag": "setCPUFanSpeed", |
|||
"type": "32int", |
|||
"functionCode": 16, |
|||
"objectsCount": 2, |
|||
"address": 33 |
|||
}, |
|||
{ |
|||
"tag": "getCPULoad", |
|||
"type": "32int", |
|||
"functionCode": 4, |
|||
"objectsCount": 2, |
|||
"address": 35 |
|||
} |
|||
] |
|||
} |
|||
] |
|||
}, |
|||
"slave": { |
|||
"type": "tcp", |
|||
"host": "127.0.0.1", |
|||
"port": 5026, |
|||
"method": "socket", |
|||
"deviceName": "Modbus Slave Example", |
|||
"deviceType": "default", |
|||
"pollPeriod": 5000, |
|||
"sendDataToThingsBoard": false, |
|||
"byteOrder": "LITTLE", |
|||
"wordOrder": "LITTLE", |
|||
"unitId": 0, |
|||
"values": { |
|||
"holding_registers": [ |
|||
{ |
|||
"attributes": [ |
|||
{ |
|||
"address": 1, |
|||
"type": "string", |
|||
"tag": "sm", |
|||
"objectsCount": 1, |
|||
"value": "ON" |
|||
} |
|||
], |
|||
"timeseries": [ |
|||
{ |
|||
"address": 2, |
|||
"type": "int", |
|||
"tag": "smm", |
|||
"objectsCount": 1, |
|||
"value": "12334" |
|||
} |
|||
], |
|||
"attributeUpdates": [ |
|||
{ |
|||
"tag": "shared_attribute_write", |
|||
"type": "32int", |
|||
"functionCode": 6, |
|||
"objectsCount": 2, |
|||
"address": 29, |
|||
"value": 1243 |
|||
} |
|||
], |
|||
"rpc": [ |
|||
{ |
|||
"tag": "setValue", |
|||
"type": "bits", |
|||
"functionCode": 5, |
|||
"objectsCount": 1, |
|||
"address": 31, |
|||
"value": 22 |
|||
} |
|||
] |
|||
} |
|||
], |
|||
"coils_initializer": [ |
|||
{ |
|||
"attributes": [ |
|||
{ |
|||
"address": 5, |
|||
"type": "string", |
|||
"tag": "sm", |
|||
"objectsCount": 1, |
|||
"value": "12" |
|||
} |
|||
], |
|||
"timeseries": [], |
|||
"attributeUpdates": [], |
|||
"rpc": [] |
|||
} |
|||
] |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,218 @@ |
|||
{ |
|||
"broker": { |
|||
"name": "Default Local Broker", |
|||
"host": "127.0.0.1", |
|||
"port": 1883, |
|||
"clientId": "ThingsBoard_gateway", |
|||
"version": 5, |
|||
"maxMessageNumberPerWorker": 10, |
|||
"maxNumberOfWorkers": 100, |
|||
"sendDataOnlyOnChange": false, |
|||
"security": { |
|||
"type": "anonymous" |
|||
} |
|||
}, |
|||
"dataMapping": [ |
|||
{ |
|||
"topicFilter": "sensor/data", |
|||
"subscriptionQos": 1, |
|||
"converter": { |
|||
"type": "json", |
|||
"deviceInfo": { |
|||
"deviceNameExpressionSource": "message", |
|||
"deviceNameExpression": "${serialNumber}", |
|||
"deviceProfileExpressionSource": "message", |
|||
"deviceProfileExpression": "${sensorType}" |
|||
}, |
|||
"sendDataOnlyOnChange": false, |
|||
"timeout": 60000, |
|||
"attributes": [ |
|||
{ |
|||
"type": "string", |
|||
"key": "model", |
|||
"value": "${sensorModel}" |
|||
}, |
|||
{ |
|||
"type": "string", |
|||
"key": "${sensorModel}", |
|||
"value": "on" |
|||
} |
|||
], |
|||
"timeseries": [ |
|||
{ |
|||
"type": "string", |
|||
"key": "temperature", |
|||
"value": "${temp}" |
|||
}, |
|||
{ |
|||
"type": "double", |
|||
"key": "humidity", |
|||
"value": "${hum}" |
|||
}, |
|||
{ |
|||
"type": "string", |
|||
"key": "combine", |
|||
"value": "${hum}:${temp}" |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
{ |
|||
"topicFilter": "sensor/+/data", |
|||
"subscriptionQos": 1, |
|||
"converter": { |
|||
"type": "json", |
|||
"deviceInfo": { |
|||
"deviceNameExpressionSource": "topic", |
|||
"deviceNameExpression": "(?<=sensor\/)(.*?)(?=\/data)", |
|||
"deviceProfileExpressionSource": "constant", |
|||
"deviceProfileExpression": "Thermometer" |
|||
}, |
|||
"sendDataOnlyOnChange": false, |
|||
"timeout": 60000, |
|||
"attributes": [ |
|||
{ |
|||
"type": "string", |
|||
"key": "model", |
|||
"value": "${sensorModel}" |
|||
} |
|||
], |
|||
"timeseries": [ |
|||
{ |
|||
"type": "double", |
|||
"key": "temperature", |
|||
"value": "${temp}" |
|||
}, |
|||
{ |
|||
"type": "string", |
|||
"key": "humidity", |
|||
"value": "${hum}" |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
{ |
|||
"topicFilter": "sensor/raw_data", |
|||
"subscriptionQos": 1, |
|||
"converter": { |
|||
"type": "bytes", |
|||
"deviceInfo": { |
|||
"deviceNameExpressionSource": "message", |
|||
"deviceNameExpression": "[0:4]", |
|||
"deviceProfileExpressionSource": "constant", |
|||
"deviceProfileExpression": "default" |
|||
}, |
|||
"sendDataOnlyOnChange": false, |
|||
"timeout": 60000, |
|||
"attributes": [ |
|||
{ |
|||
"type": "raw", |
|||
"key": "rawData", |
|||
"value": "[:]" |
|||
} |
|||
], |
|||
"timeseries": [ |
|||
{ |
|||
"type": "raw", |
|||
"key": "temp", |
|||
"value": "[4:]" |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
{ |
|||
"topicFilter": "custom/sensors/+", |
|||
"subscriptionQos": 1, |
|||
"converter": { |
|||
"type": "custom", |
|||
"extension": "CustomMqttUplinkConverter", |
|||
"cached": true, |
|||
"extensionConfig": { |
|||
"temperature": 2, |
|||
"humidity": 2, |
|||
"batteryLevel": 1 |
|||
} |
|||
} |
|||
} |
|||
], |
|||
"requestsMapping": { |
|||
"connectRequests": [ |
|||
{ |
|||
"topicFilter": "sensor/connect", |
|||
"deviceInfo": { |
|||
"deviceNameExpressionSource": "message", |
|||
"deviceNameExpression": "${serialNumber}", |
|||
"deviceProfileExpressionSource": "constant", |
|||
"deviceProfileExpression": "Thermometer" |
|||
} |
|||
}, |
|||
{ |
|||
"topicFilter": "sensor/+/connect", |
|||
"deviceInfo": { |
|||
"deviceNameExpressionSource": "topic", |
|||
"deviceNameExpression": "(?<=sensor\/)(.*?)(?=\/connect)", |
|||
"deviceProfileExpressionSource": "constant", |
|||
"deviceProfileExpression": "Thermometer" |
|||
} |
|||
} |
|||
], |
|||
"disconnectRequests": [ |
|||
{ |
|||
"topicFilter": "sensor/disconnect", |
|||
"deviceInfo": { |
|||
"deviceNameExpressionSource": "message", |
|||
"deviceNameExpression": "${serialNumber}" |
|||
} |
|||
}, |
|||
{ |
|||
"topicFilter": "sensor/+/disconnect", |
|||
"deviceInfo": { |
|||
"deviceNameExpressionSource": "topic", |
|||
"deviceNameExpression": "(?<=sensor\/)(.*?)(?=\/connect)" |
|||
} |
|||
} |
|||
], |
|||
"attributeRequests": [ |
|||
{ |
|||
"retain": false, |
|||
"topicFilter": "v1/devices/me/attributes/request", |
|||
"deviceInfo": { |
|||
"deviceNameExpressionSource": "message", |
|||
"deviceNameExpression": "${serialNumber}" |
|||
}, |
|||
"attributeNameExpressionSource": "message", |
|||
"attributeNameExpression": "${versionAttribute}, ${pduAttribute}", |
|||
"topicExpression": "devices/${deviceName}/attrs", |
|||
"valueExpression": "${attributeKey}: ${attributeValue}" |
|||
} |
|||
], |
|||
"attributeUpdates": [ |
|||
{ |
|||
"retain": true, |
|||
"deviceNameFilter": ".*", |
|||
"attributeFilter": "firmwareVersion", |
|||
"topicExpression": "sensor/${deviceName}/${attributeKey}", |
|||
"valueExpression": "{\"${attributeKey}\":\"${attributeValue}\"}" |
|||
} |
|||
], |
|||
"serverSideRpc": [ |
|||
{ |
|||
"type": "twoWay", |
|||
"deviceNameFilter": ".*", |
|||
"methodFilter": "echo", |
|||
"requestTopicExpression": "sensor/${deviceName}/request/${methodName}/${requestId}", |
|||
"responseTopicExpression": "sensor/${deviceName}/response/${methodName}/${requestId}", |
|||
"responseTopicQoS": 1, |
|||
"responseTimeout": 10000, |
|||
"valueExpression": "${params}" |
|||
}, |
|||
{ |
|||
"type": "oneWay", |
|||
"deviceNameFilter": ".*", |
|||
"methodFilter": "no-reply", |
|||
"requestTopicExpression": "sensor/${deviceName}/request/${methodName}/${requestId}", |
|||
"valueExpression": "${params}" |
|||
} |
|||
] |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
{ |
|||
"centralSystem": { |
|||
"name": "Central System", |
|||
"host": "127.0.0.1", |
|||
"port": 9000, |
|||
"connection": { |
|||
"type": "insecure" |
|||
}, |
|||
"security": [ |
|||
{ |
|||
"type": "token", |
|||
"tokens": [ |
|||
"Bearer ACCESS_TOKEN" |
|||
] |
|||
}, |
|||
{ |
|||
"type": "basic", |
|||
"credentials": [ |
|||
{ |
|||
"username": "admin", |
|||
"password": "admin" |
|||
} |
|||
] |
|||
} |
|||
] |
|||
}, |
|||
"chargePoints": [ |
|||
{ |
|||
"idRegexpPattern": "bidon/hello/CP_1", |
|||
"deviceNameExpression": "${Vendor} ${Model}", |
|||
"deviceTypeExpression": "default", |
|||
"attributes": [ |
|||
{ |
|||
"messageTypeFilter": "MeterValues,", |
|||
"key": "temp1", |
|||
"value": "${meter_value[:].sampled_value[:].value}" |
|||
}, |
|||
{ |
|||
"messageTypeFilter": "MeterValues,", |
|||
"key": "vendorId", |
|||
"value": "${connector_id}" |
|||
} |
|||
], |
|||
"timeseries": [ |
|||
{ |
|||
"messageTypeFilter": "DataTransfer,", |
|||
"key": "temp", |
|||
"value": "${data.temp}" |
|||
} |
|||
], |
|||
"attributeUpdates": [ |
|||
{ |
|||
"attributeOnThingsBoard": "shared", |
|||
"valueExpression": "{\"${attributeKey}\":\"${attributeValue}\"}" |
|||
} |
|||
], |
|||
"serverSideRpc": [ |
|||
{ |
|||
"methodRPC": "rpc1", |
|||
"withResponse": true, |
|||
"valueExpression": "${params}" |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
{ |
|||
"connection": { |
|||
"str": "Driver={PostgreSQL};Server=localhost;Port=5432;Database=thingsboard;Uid=postgres;Pwd=postgres;", |
|||
"attributes": { |
|||
"autocommit": true, |
|||
"timeout": 0 |
|||
}, |
|||
"encoding": "utf-8", |
|||
"decoding": { |
|||
"char": "utf-8", |
|||
"wchar": "utf-8", |
|||
"metadata": "utf-16le" |
|||
}, |
|||
"reconnect": true, |
|||
"reconnectPeriod": 60 |
|||
}, |
|||
"pyodbc": { |
|||
"pooling": false |
|||
}, |
|||
"polling": { |
|||
"query": "SELECT bool_v, str_v, dbl_v, long_v, entity_id, ts FROM ts_kv WHERE ts > ? ORDER BY ts ASC LIMIT 10", |
|||
"period": 10, |
|||
"iterator": { |
|||
"column": "ts", |
|||
"query": "SELECT MIN(ts) - 1 FROM ts_kv", |
|||
"persistent": false |
|||
} |
|||
}, |
|||
"mapping": { |
|||
"device": { |
|||
"type": "postgres", |
|||
"name": "'ODBC ' + entity_id" |
|||
}, |
|||
"sendDataOnlyOnChange": false, |
|||
"attributes": "*", |
|||
"timeseries": [ |
|||
{ |
|||
"name": "value", |
|||
"value": "[i for i in [str_v, long_v, dbl_v,bool_v] if i is not None][0]" |
|||
} |
|||
] |
|||
}, |
|||
"serverSideRpc": { |
|||
"enableUnknownRpc": false, |
|||
"overrideRpcConfig": true, |
|||
"methods": [ |
|||
"procedureOne", |
|||
{ |
|||
"name": "procedureTwo", |
|||
"args": [ |
|||
"One", |
|||
2, |
|||
3.0 |
|||
] |
|||
} |
|||
] |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
{ |
|||
"server": { |
|||
"name": "OPC-UA Default Server", |
|||
"url": "localhost:4840/freeopcua/server/", |
|||
"timeoutInMillis": 5000, |
|||
"scanPeriodInMillis": 5000, |
|||
"disableSubscriptions": false, |
|||
"subCheckPeriodInMillis": 100, |
|||
"showMap": false, |
|||
"security": "Basic128Rsa15", |
|||
"identity": { |
|||
"type": "anonymous" |
|||
}, |
|||
"mapping": [ |
|||
{ |
|||
"deviceNodePattern": "Root\\.Objects\\.Device1", |
|||
"deviceNamePattern": "Device ${Root\\.Objects\\.Device1\\.serialNumber}", |
|||
"attributes": [ |
|||
{ |
|||
"key": "temperature °C", |
|||
"path": "${ns=2;i=5}" |
|||
} |
|||
], |
|||
"timeseries": [ |
|||
{ |
|||
"key": "humidity", |
|||
"path": "${Root\\.Objects\\.Device1\\.TemperatureAndHumiditySensor\\.Humidity}" |
|||
}, |
|||
{ |
|||
"key": "batteryLevel", |
|||
"path": "${Battery\\.batteryLevel}" |
|||
} |
|||
], |
|||
"rpc_methods": [ |
|||
{ |
|||
"method": "multiply", |
|||
"arguments": [ |
|||
2, |
|||
4 |
|||
] |
|||
} |
|||
], |
|||
"attributes_updates": [ |
|||
{ |
|||
"attributeOnThingsBoard": "deviceName", |
|||
"attributeOnDevice": "Root\\.Objects\\.Device1\\.serialNumber" |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
{ |
|||
"server": { |
|||
"name": "OPC-UA Default Server", |
|||
"url": "localhost:4840/freeopcua/server/", |
|||
"timeoutInMillis": 5000, |
|||
"scanPeriodInMillis": 5000, |
|||
"disableSubscriptions": false, |
|||
"subCheckPeriodInMillis": 100, |
|||
"showMap": false, |
|||
"security": "Basic128Rsa15", |
|||
"identity": { |
|||
"type": "anonymous" |
|||
}, |
|||
"mapping": [ |
|||
{ |
|||
"deviceNodePattern": "Root\\.Objects\\.Device1", |
|||
"deviceNamePattern": "Device ${Root\\.Objects\\.Device1\\.serialNumber}", |
|||
"attributes": [ |
|||
{ |
|||
"key": "temperature °C", |
|||
"path": "${ns=2;i=5}" |
|||
} |
|||
], |
|||
"timeseries": [ |
|||
{ |
|||
"key": "humidity", |
|||
"path": "${Root\\.Objects\\.Device1\\.TemperatureAndHumiditySensor\\.Humidity}" |
|||
}, |
|||
{ |
|||
"key": "batteryLevel", |
|||
"path": "${Battery\\.batteryLevel}" |
|||
} |
|||
], |
|||
"rpc_methods": [ |
|||
{ |
|||
"method": "multiply", |
|||
"arguments": [ |
|||
2, |
|||
4 |
|||
] |
|||
} |
|||
], |
|||
"attributes_updates": [ |
|||
{ |
|||
"attributeOnThingsBoard": "deviceName", |
|||
"attributeOnDevice": "Root\\.Objects\\.Device1\\.serialNumber" |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue