Commit 53bc87150c5dd62bc143259181decdc971e4e658

Authored by Igor Kulikov
1 parent 5aa055d7

UI: Entity data query - initial implementation

@@ -17,12 +17,13 @@ @@ -17,12 +17,13 @@
17 import { AliasInfo, IAliasController, StateControllerHolder, StateEntityInfo } from '@core/api/widget-api.models'; 17 import { AliasInfo, IAliasController, StateControllerHolder, StateEntityInfo } from '@core/api/widget-api.models';
18 import { forkJoin, Observable, of, ReplaySubject, Subject } from 'rxjs'; 18 import { forkJoin, Observable, of, ReplaySubject, Subject } from 'rxjs';
19 import { DataKey, Datasource, DatasourceType } from '@app/shared/models/widget.models'; 19 import { DataKey, Datasource, DatasourceType } from '@app/shared/models/widget.models';
20 -import { deepClone, isEqual, createLabelFromDatasource } from '@core/utils'; 20 +import { createLabelFromDatasource, deepClone, isEqual } from '@core/utils';
21 import { EntityService } from '@core/http/entity.service'; 21 import { EntityService } from '@core/http/entity.service';
22 import { UtilsService } from '@core/services/utils.service'; 22 import { UtilsService } from '@core/services/utils.service';
23 -import { EntityAliases } from '@shared/models/alias.models'; 23 +import { AliasFilterType, EntityAliases } from '@shared/models/alias.models';
24 import { EntityInfo } from '@shared/models/entity.models'; 24 import { EntityInfo } from '@shared/models/entity.models';
25 import { map } from 'rxjs/operators'; 25 import { map } from 'rxjs/operators';
  26 +import { defaultEntityDataPageLink } from '@shared/models/query/query.models';
26 27
27 export class AliasController implements IAliasController { 28 export class AliasController implements IAliasController {
28 29
@@ -176,6 +177,92 @@ export class AliasController implements IAliasController { @@ -176,6 +177,92 @@ export class AliasController implements IAliasController {
176 datasource.aliasName = aliasInfo.alias; 177 datasource.aliasName = aliasInfo.alias;
177 if (aliasInfo.resolveMultiple && !isSingle) { 178 if (aliasInfo.resolveMultiple && !isSingle) {
178 let newDatasource: Datasource; 179 let newDatasource: Datasource;
  180 + // const resolvedEntities = aliasInfo.resolvedEntities;
  181 + if (aliasInfo.entityFilter) {
  182 + newDatasource = deepClone(datasource);
  183 + newDatasource.entityFilter = aliasInfo.entityFilter;
  184 + /*const datasources: Array<Datasource> = [];
  185 + for (let i = 0; i < resolvedEntities.length; i++) {
  186 + const resolvedEntity = resolvedEntities[i];
  187 + newDatasource = deepClone(datasource);
  188 + if (resolvedEntity.origEntity) {
  189 + newDatasource.entity = deepClone(resolvedEntity.origEntity);
  190 + } else {
  191 + newDatasource.entity = {};
  192 + }
  193 + newDatasource.entityId = resolvedEntity.id;
  194 + newDatasource.entityType = resolvedEntity.entityType;
  195 + newDatasource.entityName = resolvedEntity.name;
  196 + newDatasource.entityLabel = resolvedEntity.label;
  197 + newDatasource.entityDescription = resolvedEntity.entityDescription;
  198 + newDatasource.name = resolvedEntity.name;
  199 + newDatasource.generated = i > 0 ? true : false;
  200 + datasources.push(newDatasource);
  201 + }
  202 + return datasources;*/
  203 + return [newDatasource];
  204 + } else {
  205 + if (aliasInfo.stateEntity) {
  206 + newDatasource = deepClone(datasource);
  207 + newDatasource.unresolvedStateEntity = true;
  208 + return [newDatasource];
  209 + } else {
  210 + return [];
  211 + // throw new Error('Unable to resolve datasource.');
  212 + }
  213 + }
  214 + } else {
  215 + const entity = aliasInfo.currentEntity;
  216 + if (entity) {
  217 + if (entity.origEntity) {
  218 + datasource.entity = deepClone(entity.origEntity);
  219 + } else {
  220 + datasource.entity = {};
  221 + }
  222 + datasource.entityId = entity.id;
  223 + datasource.entityType = entity.entityType;
  224 + datasource.entityName = entity.name;
  225 + datasource.entityLabel = entity.label;
  226 + datasource.name = entity.name;
  227 + datasource.entityDescription = entity.entityDescription;
  228 + datasource.entityFilter = {
  229 + type: AliasFilterType.singleEntity,
  230 + singleEntity: {
  231 + id: entity.id,
  232 + entityType: entity.entityType
  233 + }
  234 + };
  235 + return [datasource];
  236 + } else {
  237 + if (aliasInfo.stateEntity) {
  238 + datasource.unresolvedStateEntity = true;
  239 + return [datasource];
  240 + } else {
  241 + return [];
  242 + // throw new Error('Unable to resolve datasource.');
  243 + }
  244 + }
  245 + }
  246 + })
  247 + );
  248 + } else {
  249 + datasource.aliasName = datasource.entityName;
  250 + datasource.name = datasource.entityName;
  251 + return of([datasource]);
  252 + }
  253 + } else {
  254 + return of([datasource]);
  255 + }
  256 + }
  257 +
  258 + /* private resolveDatasourceOld(datasource: Datasource, isSingle?: boolean): Observable<Array<Datasource>> {
  259 + if (datasource.type === DatasourceType.entity) {
  260 + if (datasource.entityAliasId) {
  261 + return this.getAliasInfo(datasource.entityAliasId).pipe(
  262 + map((aliasInfo) => {
  263 + datasource.aliasName = aliasInfo.alias;
  264 + if (aliasInfo.resolveMultiple && !isSingle) {
  265 + let newDatasource: Datasource;
179 const resolvedEntities = aliasInfo.resolvedEntities; 266 const resolvedEntities = aliasInfo.resolvedEntities;
180 if (resolvedEntities && resolvedEntities.length) { 267 if (resolvedEntities && resolvedEntities.length) {
181 const datasources: Array<Datasource> = []; 268 const datasources: Array<Datasource> = [];
@@ -242,7 +329,7 @@ export class AliasController implements IAliasController { @@ -242,7 +329,7 @@ export class AliasController implements IAliasController {
242 } else { 329 } else {
243 return of([datasource]); 330 return of([datasource]);
244 } 331 }
245 - } 332 + } */
246 333
247 resolveAlarmSource(alarmSource: Datasource): Observable<Datasource> { 334 resolveAlarmSource(alarmSource: Datasource): Observable<Datasource> {
248 return this.resolveDatasource(alarmSource, true).pipe( 335 return this.resolveDatasource(alarmSource, true).pipe(
@@ -279,6 +366,48 @@ export class AliasController implements IAliasController { @@ -279,6 +366,48 @@ export class AliasController implements IAliasController {
279 arrayOfDatasources.forEach((datasourcesArray) => { 366 arrayOfDatasources.forEach((datasourcesArray) => {
280 result.push(...datasourcesArray); 367 result.push(...datasourcesArray);
281 }); 368 });
  369 + let functionIndex = 0;
  370 + result.forEach((datasource) => {
  371 + if (datasource.type === DatasourceType.function) {
  372 + let name: string;
  373 + if (datasource.name && datasource.name.length) {
  374 + name = datasource.name;
  375 + } else {
  376 + functionIndex++;
  377 + name = DatasourceType.function;
  378 + if (functionIndex > 1) {
  379 + name += ' ' + functionIndex;
  380 + }
  381 + }
  382 + datasource.name = name;
  383 + datasource.aliasName = name;
  384 + datasource.entityName = name;
  385 + } else if (datasource.unresolvedStateEntity) {
  386 + datasource.name = 'Unresolved';
  387 + datasource.entityName = 'Unresolved';
  388 + } else if (datasource.type === DatasourceType.entity) {
  389 + if (!datasource.pageLink) {
  390 + datasource.pageLink = deepClone(defaultEntityDataPageLink);
  391 + }
  392 + }
  393 + });
  394 + return result;
  395 + })
  396 + );
  397 + }
  398 +
  399 + /*resolveDatasourcesOld(datasources: Array<Datasource>): Observable<Array<Datasource>> {
  400 + const newDatasources = deepClone(datasources);
  401 + const observables = new Array<Observable<Array<Datasource>>>();
  402 + newDatasources.forEach((datasource) => {
  403 + observables.push(this.resolveDatasource(datasource));
  404 + });
  405 + return forkJoin(observables).pipe(
  406 + map((arrayOfDatasources) => {
  407 + const result = new Array<Datasource>();
  408 + arrayOfDatasources.forEach((datasourcesArray) => {
  409 + result.push(...datasourcesArray);
  410 + });
282 result.sort((d1, d2) => { 411 result.sort((d1, d2) => {
283 const i1 = d1.generated ? 1 : 0; 412 const i1 = d1.generated ? 1 : 0;
284 const i2 = d2.generated ? 1 : 0; 413 const i2 = d2.generated ? 1 : 0;
@@ -317,9 +446,9 @@ export class AliasController implements IAliasController { @@ -317,9 +446,9 @@ export class AliasController implements IAliasController {
317 return result; 446 return result;
318 }) 447 })
319 ); 448 );
320 - } 449 + }*/
321 450
322 - private updateDatasourceKeyLabels(datasource: Datasource) { 451 + /* private updateDatasourceKeyLabels(datasource: Datasource) {
323 datasource.dataKeys.forEach((dataKey) => { 452 datasource.dataKeys.forEach((dataKey) => {
324 this.updateDataKeyLabel(dataKey, datasource); 453 this.updateDataKeyLabel(dataKey, datasource);
325 }); 454 });
@@ -330,7 +459,7 @@ export class AliasController implements IAliasController { @@ -330,7 +459,7 @@ export class AliasController implements IAliasController {
330 dataKey.pattern = deepClone(dataKey.label); 459 dataKey.pattern = deepClone(dataKey.label);
331 } 460 }
332 dataKey.label = createLabelFromDatasource(datasource, dataKey.pattern); 461 dataKey.label = createLabelFromDatasource(datasource, dataKey.pattern);
333 - } 462 + }*/
334 463
335 getInstantAliasInfo(aliasId: string): AliasInfo { 464 getInstantAliasInfo(aliasId: string): AliasInfo {
336 return this.resolvedAliases[aliasId]; 465 return this.resolvedAliases[aliasId];
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models';
  18 +import { AggregationType, SubscriptionTimewindow, YEAR } from '@shared/models/time/time.models';
  19 +import { SubscriptionDataKey } from '@core/api/datasource-subcription';
  20 +import {
  21 + EntityData,
  22 + EntityDataPageLink,
  23 + EntityFilter,
  24 + EntityKey,
  25 + EntityKeyType,
  26 + KeyFilter,
  27 + TsValue
  28 +} from '@shared/models/query/query.models';
  29 +import {
  30 + DataKeyType,
  31 + EntityDataCmd,
  32 + SubscriptionData,
  33 + SubscriptionDataHolder,
  34 + TelemetryService,
  35 + TelemetrySubscriber
  36 +} from '@shared/models/telemetry/telemetry.models';
  37 +import { UtilsService } from '@core/services/utils.service';
  38 +import { EntityDataListener } from '@core/api/entity-data.service';
  39 +import { deepClone, isDefinedAndNotNull, isObject, objectHashCode } from '@core/utils';
  40 +import { PageData } from '@shared/models/page/page-data';
  41 +import { DataAggregator } from '@core/api/data-aggregator';
  42 +import { NULL_UUID } from '@shared/models/id/has-uuid';
  43 +import { EntityType } from '@shared/models/entity-type.models';
  44 +import Timeout = NodeJS.Timeout;
  45 +
  46 +export interface EntityDataSubscriptionOptions {
  47 + datasourceType: DatasourceType;
  48 + dataKeys: Array<SubscriptionDataKey>;
  49 + type: widgetType;
  50 + entityFilter?: EntityFilter;
  51 + pageLink?: EntityDataPageLink;
  52 + keyFilters?: Array<KeyFilter>;
  53 + subscriptionTimewindow?: SubscriptionTimewindow;
  54 +}
  55 +
  56 +declare type DataKeyFunction = (time: number, prevValue: any) => any;
  57 +declare type DataKeyPostFunction = (time: number, value: any, prevValue: any, timePrev: number, prevOrigValue: any) => any;
  58 +declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void;
  59 +
  60 +export class EntityDataSubscription {
  61 +
  62 + private listeners: Array<EntityDataListener> = [];
  63 + private datasourceType: DatasourceType = this.entityDataSubscriptionOptions.datasourceType;
  64 +
  65 + private history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
  66 + isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
  67 +
  68 + private realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
  69 + isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs);
  70 +
  71 + private subscriber: TelemetrySubscriber;
  72 +
  73 + private attrFields: Array<EntityKey>;
  74 + private tsFields: Array<EntityKey>;
  75 + private latestValues: Array<EntityKey>;
  76 +
  77 + private pageData: PageData<EntityData>;
  78 + private subsTw: SubscriptionTimewindow;
  79 + private dataAggregators: Array<DataAggregator>;
  80 + private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {}
  81 + private datasourceData: {[index: number]: {[key: string]: DataSetHolder}};
  82 + private datasourceOrigData: {[index: number]: {[key: string]: DataSetHolder}};
  83 + private entityIdToDataIndex: {[id: string]: number};
  84 +
  85 + private frequency: number;
  86 + private tickScheduledTime = 0;
  87 + private tickElapsed = 0;
  88 + private timer: Timeout;
  89 +
  90 + constructor(private entityDataSubscriptionOptions: EntityDataSubscriptionOptions,
  91 + private telemetryService: TelemetryService,
  92 + private utils: UtilsService) {
  93 + this.initializeSubscription();
  94 + }
  95 +
  96 + private initializeSubscription() {
  97 + for (let i = 0; i < this.entityDataSubscriptionOptions.dataKeys.length; i++) {
  98 + const dataKey = deepClone(this.entityDataSubscriptionOptions.dataKeys[i]);
  99 + dataKey.index = i;
  100 + if (this.datasourceType === DatasourceType.function) {
  101 + if (!dataKey.func) {
  102 + dataKey.func = new Function('time', 'prevValue', dataKey.funcBody) as DataKeyFunction;
  103 + }
  104 + } else {
  105 + if (dataKey.postFuncBody && !dataKey.postFunc) {
  106 + dataKey.postFunc = new Function('time', 'value', 'prevValue', 'timePrev', 'prevOrigValue',
  107 + dataKey.postFuncBody) as DataKeyPostFunction;
  108 + }
  109 + }
  110 + let key: string;
  111 + if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  112 + if (this.datasourceType === DatasourceType.function) {
  113 + key = `${dataKey.name}_${dataKey.index}_${dataKey.type}`;
  114 + } else {
  115 + key = `${dataKey.name}_${dataKey.type}`;
  116 + }
  117 + let dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
  118 + if (!dataKeysList) {
  119 + dataKeysList = [];
  120 + this.dataKeys[key] = dataKeysList;
  121 + }
  122 + dataKeysList.push(dataKey);
  123 + } else {
  124 + key = String(objectHashCode(dataKey));
  125 + this.dataKeys[key] = dataKey;
  126 + }
  127 + dataKey.key = key;
  128 + }
  129 + if (this.datasourceType === DatasourceType.function) {
  130 + this.frequency = 1000;
  131 + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  132 + this.frequency = Math.min(this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.interval, 5000);
  133 + }
  134 + }
  135 + }
  136 +
  137 + public addListener(listener: EntityDataListener) {
  138 + this.listeners.push(listener);
  139 + if (this.history) {
  140 + this.start();
  141 + }
  142 + }
  143 +
  144 + public hasListeners(): boolean {
  145 + return this.listeners.length > 0;
  146 + }
  147 +
  148 + public removeListener(listener: EntityDataListener) {
  149 + this.listeners.splice(this.listeners.indexOf(listener), 1);
  150 + }
  151 +
  152 + public syncListener(listener: EntityDataListener) {
  153 + if (this.pageData) {
  154 + let key: string;
  155 + let dataKey: SubscriptionDataKey;
  156 + const data: Array<Array<DataSetHolder>> = [];
  157 + for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
  158 + data[dataIndex] = [];
  159 + for (key of Object.keys(this.dataKeys)) {
  160 + if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  161 + const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
  162 + for (let i = 0; i < dataKeysList.length; i++) {
  163 + dataKey = dataKeysList[i];
  164 + const datasourceKey = `${key}_${i}`;
  165 + data[dataIndex][dataKey.index] = this.datasourceData[dataIndex][datasourceKey];
  166 + }
  167 + } else {
  168 + dataKey = this.dataKeys[key] as SubscriptionDataKey;
  169 + data[dataIndex][dataKey.index] = this.datasourceData[dataIndex][key];
  170 + }
  171 + }
  172 + }
  173 + listener.dataLoaded(this.pageData, data, listener.configDatasourceIndex);
  174 + }
  175 + }
  176 +
  177 + public unsubscribe() {
  178 + if (this.timer) {
  179 + clearTimeout(this.timer);
  180 + this.timer = null;
  181 + }
  182 + if (this.datasourceType === DatasourceType.entity) {
  183 + if (this.subscriber) {
  184 + this.subscriber.unsubscribe();
  185 + this.subscriber = null;
  186 + }
  187 + }
  188 + if (this.dataAggregators) {
  189 + this.dataAggregators.forEach((aggregator) => {
  190 + aggregator.destroy();
  191 + })
  192 + this.dataAggregators = null;
  193 + }
  194 + this.pageData = null;
  195 + }
  196 +
  197 + public start() {
  198 + if (this.history && !this.hasListeners()) {
  199 + return;
  200 + }
  201 + this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
  202 + if (this.datasourceType === DatasourceType.entity) {
  203 + const entityFields: Array<EntityKey> =
  204 + this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.entityField).map(
  205 + dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name })
  206 + );
  207 + if (!entityFields.find(key => key.key === 'name')) {
  208 + entityFields.push({
  209 + type: EntityKeyType.ENTITY_FIELD,
  210 + key: 'name'
  211 + });
  212 + }
  213 +
  214 + this.attrFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.attribute).map(
  215 + dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name })
  216 + );
  217 +
  218 + this.tsFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.timeseries).map(
  219 + dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name })
  220 + );
  221 +
  222 + this.latestValues = this.attrFields.concat(this.tsFields);
  223 +
  224 + this.subscriber = new TelemetrySubscriber(this.telemetryService);
  225 + const command = new EntityDataCmd();
  226 +
  227 + command.query = {
  228 + entityFilter: this.entityDataSubscriptionOptions.entityFilter,
  229 + pageLink: this.entityDataSubscriptionOptions.pageLink,
  230 + keyFilters: this.entityDataSubscriptionOptions.keyFilters,
  231 + entityFields,
  232 + latestValues: this.latestValues
  233 + };
  234 +
  235 + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  236 + if (this.tsFields.length > 0) {
  237 + if (this.history) {
  238 + command.historyCmd = {
  239 + keys: this.tsFields.map(key => key.key),
  240 + startTs: this.subsTw.fixedWindow.startTimeMs,
  241 + endTs: this.subsTw.fixedWindow.endTimeMs,
  242 + interval: this.subsTw.aggregation.interval,
  243 + limit: this.subsTw.aggregation.limit,
  244 + agg: this.subsTw.aggregation.type
  245 + };
  246 + if (this.subsTw.aggregation.stateData) {
  247 + command.historyCmd.startTs -= YEAR;
  248 + }
  249 + } else {
  250 + command.tsCmd = {
  251 + keys: this.tsFields.map(key => key.key),
  252 + startTs: this.subsTw.startTs,
  253 + timeWindow: this.subsTw.aggregation.timeWindow,
  254 + interval: this.subsTw.aggregation.interval,
  255 + limit: this.subsTw.aggregation.limit,
  256 + agg: this.subsTw.aggregation.type
  257 + }
  258 + if (this.subsTw.aggregation.stateData) {
  259 + command.historyCmd = {
  260 + keys: this.tsFields.map(key => key.key),
  261 + startTs: this.subsTw.startTs - YEAR,
  262 + endTs: this.subsTw.startTs,
  263 + interval: this.subsTw.aggregation.interval,
  264 + limit: this.subsTw.aggregation.limit,
  265 + agg: this.subsTw.aggregation.type
  266 + };
  267 + }
  268 + this.subscriber.reconnect$.subscribe(() => {
  269 + let newSubsTw: SubscriptionTimewindow = null;
  270 + this.listeners.forEach((listener) => {
  271 + if (!newSubsTw) {
  272 + newSubsTw = listener.updateRealtimeSubscription();
  273 + } else {
  274 + listener.setRealtimeSubscription(newSubsTw);
  275 + }
  276 + });
  277 + this.subsTw = newSubsTw;
  278 + command.tsCmd.startTs = this.subsTw.startTs;
  279 + command.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow;
  280 + command.tsCmd.interval = this.subsTw.aggregation.interval;
  281 + command.tsCmd.limit = this.subsTw.aggregation.limit;
  282 + command.tsCmd.agg = this.subsTw.aggregation.type;
  283 + if (this.subsTw.aggregation.stateData) {
  284 + command.historyCmd.startTs = this.subsTw.startTs - YEAR;
  285 + command.historyCmd.endTs = this.subsTw.startTs;
  286 + command.historyCmd.interval = this.subsTw.aggregation.interval;
  287 + command.historyCmd.limit = this.subsTw.aggregation.limit;
  288 + command.historyCmd.agg = this.subsTw.aggregation.type;
  289 + }
  290 + });
  291 + }
  292 + }
  293 + } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
  294 + if (this.latestValues.length > 0) {
  295 + command.latestCmd = {
  296 + keys: this.latestValues.map(key => key.key)
  297 + };
  298 + }
  299 + }
  300 + this.subscriber.subscriptionCommands.push(command);
  301 +
  302 + this.subscriber.entityData$.subscribe(
  303 + (entityDataUpdate) => {
  304 + if (entityDataUpdate.data) {
  305 + this.onPageData(entityDataUpdate.data);
  306 + } else if (entityDataUpdate.update) {
  307 + this.onDataUpdate(entityDataUpdate.update);
  308 + }
  309 + }
  310 + );
  311 +
  312 + this.subscriber.subscribe();
  313 + } else if (this.datasourceType === DatasourceType.function) {
  314 + const entityData: EntityData = {
  315 + entityId: {
  316 + id: NULL_UUID,
  317 + entityType: EntityType.DEVICE
  318 + },
  319 + timeseries: {},
  320 + latest: {}
  321 + };
  322 + const name = DatasourceType.function;
  323 + entityData.latest[EntityKeyType.ENTITY_FIELD] = {
  324 + name: {ts: Date.now(), value: name}
  325 + };
  326 + const pageData: PageData<EntityData> = {
  327 + data: [entityData],
  328 + hasNext: false,
  329 + totalElements: 1,
  330 + totalPages: 1
  331 + };
  332 + this.onPageData(pageData);
  333 + this.tickScheduledTime = this.utils.currentPerfTime();
  334 + if (this.history) {
  335 + this.onTick(true);
  336 + } else {
  337 + this.timer = setTimeout(this.onTick.bind(this, true), 0);
  338 + }
  339 + }
  340 + }
  341 +
  342 + private onPageData(pageData: PageData<EntityData>) {
  343 + if (this.dataAggregators) {
  344 + this.dataAggregators.forEach((aggregator) => {
  345 + aggregator.destroy();
  346 + })
  347 + this.dataAggregators = null;
  348 + }
  349 + this.datasourceData = [];
  350 + this.dataAggregators = [];
  351 + this.entityIdToDataIndex = {};
  352 + let tsKeyNames;
  353 + if (this.datasourceType === DatasourceType.function) {
  354 + tsKeyNames = [];
  355 + for (const key of Object.keys(this.dataKeys)) {
  356 + const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
  357 + dataKeysList.forEach((subscriptionDataKey) => {
  358 + tsKeyNames.push(`${subscriptionDataKey.name}_${subscriptionDataKey.index}`);
  359 + });
  360 + }
  361 + } else {
  362 + tsKeyNames = this.tsFields.map(field => field.key);
  363 + }
  364 + for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) {
  365 + const entityData = pageData.data[dataIndex];
  366 + this.entityIdToDataIndex[entityData.entityId.id] = dataIndex;
  367 + this.datasourceData[dataIndex] = {};
  368 + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  369 + if (this.datasourceType === DatasourceType.function) {
  370 + this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames,
  371 + DataKeyType.function, dataIndex, this.notifyListeners.bind(this));
  372 + } else if (!this.history && tsKeyNames.length) {
  373 + this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames,
  374 + DataKeyType.timeseries, dataIndex, this.notifyListeners.bind(this));
  375 + }
  376 + }
  377 + for (const key of Object.keys(this.dataKeys)) {
  378 + const dataKey = this.dataKeys[key];
  379 + if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  380 + const dataKeysList = dataKey as Array<SubscriptionDataKey>;
  381 + for (let index = 0; index < dataKeysList.length; index++) {
  382 + this.datasourceData[dataIndex][key + '_' + index] = {
  383 + data: []
  384 + };
  385 + }
  386 + } else {
  387 + this.datasourceData[dataIndex][key] = {
  388 + data: []
  389 + };
  390 + }
  391 + }
  392 + }
  393 + this.datasourceOrigData = deepClone(this.datasourceData);
  394 +
  395 + const data: Array<Array<DataSetHolder>> = [];
  396 + for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) {
  397 + const entityData = pageData.data[dataIndex];
  398 + this.processEntityData(entityData, dataIndex, false,
  399 + (data1, dataIndex1, dataKeyIndex) => {
  400 + if (!data[dataIndex1]) {
  401 + data[dataIndex1] = [];
  402 + }
  403 + data[dataIndex1][dataKeyIndex] = data1;
  404 + }
  405 + );
  406 + }
  407 +
  408 + this.listeners.forEach((listener) => {
  409 + listener.dataLoaded(pageData, data,
  410 + listener.configDatasourceIndex);
  411 + });
  412 + }
  413 +
  414 + private onDataUpdate(update: Array<EntityData>) {
  415 + for (const entityData of update) {
  416 + const dataIndex = this.entityIdToDataIndex[entityData.entityId.id];
  417 + this.processEntityData(entityData, dataIndex, true, this.notifyListeners.bind(this));
  418 + }
  419 + }
  420 +
  421 + private notifyListeners(data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) {
  422 + this.listeners.forEach((listener) => {
  423 + listener.dataUpdated(data,
  424 + listener.configDatasourceIndex,
  425 + dataIndex, dataKeyIndex, detectChanges);
  426 + });
  427 + }
  428 +
  429 + private processEntityData(entityData: EntityData, dataIndex: number, aggregate: boolean,
  430 + dataUpdatedCb: DataUpdatedCb) {
  431 + if (this.entityDataSubscriptionOptions.type === widgetType.latest && entityData.latest) {
  432 + for (const type of Object.keys(entityData.latest)) {
  433 + const subscriptionData = this.toSubscriptionData(entityData.latest[type], false);
  434 + this.onData(subscriptionData, type, dataIndex, true, dataUpdatedCb);
  435 + }
  436 + }
  437 + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && entityData.timeseries) {
  438 + const subscriptionData = this.toSubscriptionData(entityData.timeseries, true);
  439 + if (aggregate) {
  440 + this.dataAggregators[dataIndex].onData({data: subscriptionData}, false, false, true);
  441 + } else {
  442 + this.onData(subscriptionData, DataKeyType.timeseries, dataIndex, true, dataUpdatedCb);
  443 + }
  444 + }
  445 + }
  446 +
  447 + private onData(sourceData: SubscriptionData, type: string, dataIndex: number, detectChanges: boolean,
  448 + dataUpdatedCb: DataUpdatedCb) {
  449 + for (const keyName of Object.keys(sourceData)) {
  450 + const keyData = sourceData[keyName];
  451 + const key = `${keyName}_${type}`;
  452 + const dataKeyList = this.dataKeys[key] as Array<SubscriptionDataKey>;
  453 + for (let keyIndex = 0; dataKeyList && keyIndex < dataKeyList.length; keyIndex++) {
  454 + const datasourceKey = `${key}_${keyIndex}`;
  455 + if (this.datasourceData[dataIndex][datasourceKey].data) {
  456 + const dataKey = dataKeyList[keyIndex];
  457 + const data: DataSet = [];
  458 + let prevSeries: [number, any];
  459 + let prevOrigSeries: [number, any];
  460 + let datasourceKeyData: DataSet;
  461 + let datasourceOrigKeyData: DataSet;
  462 + let update = false;
  463 + if (this.realtime) {
  464 + datasourceKeyData = [];
  465 + datasourceOrigKeyData = [];
  466 + } else {
  467 + datasourceKeyData = this.datasourceData[dataIndex][datasourceKey].data;
  468 + datasourceOrigKeyData = this.datasourceOrigData[dataIndex][datasourceKey].data;
  469 + }
  470 + if (datasourceKeyData.length > 0) {
  471 + prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
  472 + prevOrigSeries = datasourceOrigKeyData[datasourceOrigKeyData.length - 1];
  473 + } else {
  474 + prevSeries = [0, 0];
  475 + prevOrigSeries = [0, 0];
  476 + }
  477 + this.datasourceOrigData[dataIndex][datasourceKey].data = [];
  478 + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  479 + keyData.forEach((keySeries) => {
  480 + let series = keySeries;
  481 + const time = series[0];
  482 + this.datasourceOrigData[dataIndex][datasourceKey].data.push(series);
  483 + let value = this.convertValue(series[1]);
  484 + if (dataKey.postFunc) {
  485 + value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]);
  486 + }
  487 + prevOrigSeries = series;
  488 + series = [time, value];
  489 + data.push(series);
  490 + prevSeries = series;
  491 + });
  492 + update = true;
  493 + } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
  494 + if (keyData.length > 0) {
  495 + let series = keyData[0];
  496 + const time = series[0];
  497 + this.datasourceOrigData[dataIndex][datasourceKey].data.push(series);
  498 + let value = this.convertValue(series[1]);
  499 + if (dataKey.postFunc) {
  500 + value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]);
  501 + }
  502 + series = [time, value];
  503 + data.push(series);
  504 + }
  505 + update = true;
  506 + }
  507 + if (update) {
  508 + this.datasourceData[datasourceKey].data = data;
  509 + dataUpdatedCb(this.datasourceData[dataIndex][datasourceKey], dataIndex, dataKey.index, detectChanges);
  510 + }
  511 + }
  512 + }
  513 + }
  514 + }
  515 +
  516 + private isNumeric(val: any): boolean {
  517 + return (val - parseFloat( val ) + 1) >= 0;
  518 + }
  519 +
  520 + private convertValue(val: string): any {
  521 + if (val && this.isNumeric(val)) {
  522 + return Number(val);
  523 + } else {
  524 + return val;
  525 + }
  526 + }
  527 +
  528 + private toSubscriptionData(sourceData: {[key: string]: TsValue | TsValue[]}, isTs: boolean): SubscriptionData {
  529 + const subsData: SubscriptionData = {};
  530 + for (const keyName of Object.keys(sourceData)) {
  531 + const values = sourceData[keyName];
  532 + const dataSet: [number, any][] = [];
  533 + if (isTs) {
  534 + (values as TsValue[]).forEach((keySeries) => {
  535 + dataSet.push([keySeries.ts, keySeries.value]);
  536 + });
  537 + } else {
  538 + const tsValue = values as TsValue;
  539 + dataSet.push([tsValue.ts, tsValue.value]);
  540 + }
  541 + subsData[keyName] = dataSet;
  542 + }
  543 + return subsData;
  544 + }
  545 +
  546 + private createRealtimeDataAggregator(subsTw: SubscriptionTimewindow,
  547 + tsKeyNames: Array<string>,
  548 + dataKeyType: DataKeyType,
  549 + dataIndex: number,
  550 + dataUpdatedCb: DataUpdatedCb): DataAggregator {
  551 + return new DataAggregator(
  552 + (data, detectChanges) => {
  553 + this.onData(data, dataKeyType, dataIndex, detectChanges, dataUpdatedCb);
  554 + },
  555 + tsKeyNames,
  556 + subsTw.startTs,
  557 + subsTw.aggregation.limit,
  558 + subsTw.aggregation.type,
  559 + subsTw.aggregation.timeWindow,
  560 + subsTw.aggregation.interval,
  561 + subsTw.aggregation.stateData,
  562 + this.utils
  563 + );
  564 + }
  565 +
  566 + private generateSeries(dataKey: SubscriptionDataKey, index: number, startTime: number, endTime: number): [number, any][] {
  567 + const data: [number, any][] = [];
  568 + let prevSeries: [number, any];
  569 + const datasourceDataKey = `${dataKey.key}_${index}`;
  570 + const datasourceKeyData = this.datasourceData[0][datasourceDataKey].data;
  571 + if (datasourceKeyData.length > 0) {
  572 + prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
  573 + } else {
  574 + prevSeries = [0, 0];
  575 + }
  576 + for (let time = startTime; time <= endTime && (this.timer || this.history); time += this.frequency) {
  577 + const value = dataKey.func(time, prevSeries[1]);
  578 + const series: [number, any] = [time, value];
  579 + data.push(series);
  580 + prevSeries = series;
  581 + }
  582 + if (data.length > 0) {
  583 + dataKey.lastUpdateTime = data[data.length - 1][0];
  584 + }
  585 + return data;
  586 + }
  587 +
  588 + private generateLatest(dataKey: SubscriptionDataKey, detectChanges: boolean) {
  589 + let prevSeries: [number, any];
  590 + const datasourceKeyData = this.datasourceData[0][dataKey.key].data;
  591 + if (datasourceKeyData.length > 0) {
  592 + prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
  593 + } else {
  594 + prevSeries = [0, 0];
  595 + }
  596 + const time = Date.now();
  597 + const value = dataKey.func(time, prevSeries[1]);
  598 + const series: [number, any] = [time, value];
  599 + this.datasourceData[0][dataKey.key].data = [series];
  600 + this.listeners.forEach(
  601 + (listener) => {
  602 + listener.dataUpdated(this.datasourceData[0][dataKey.key],
  603 + listener.configDatasourceIndex,
  604 + 0,
  605 + dataKey.index, detectChanges);
  606 + }
  607 + );
  608 + }
  609 +
  610 + private onTick(detectChanges: boolean) {
  611 + const now = this.utils.currentPerfTime();
  612 + this.tickElapsed += now - this.tickScheduledTime;
  613 + this.tickScheduledTime = now;
  614 +
  615 + if (this.timer) {
  616 + clearTimeout(this.timer);
  617 + }
  618 + let key: string;
  619 + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  620 + let startTime: number;
  621 + let endTime: number;
  622 + let delta: number;
  623 + const generatedData: SubscriptionDataHolder = {
  624 + data: {}
  625 + };
  626 + if (!this.history) {
  627 + delta = Math.floor(this.tickElapsed / this.frequency);
  628 + }
  629 + const deltaElapsed = this.history ? this.frequency : delta * this.frequency;
  630 + this.tickElapsed = this.tickElapsed - deltaElapsed;
  631 + for (key of Object.keys(this.dataKeys)) {
  632 + const dataKeyList = this.dataKeys[key] as Array<SubscriptionDataKey>;
  633 + for (let index = 0; index < dataKeyList.length && (this.timer || this.history); index ++) {
  634 + const dataKey = dataKeyList[index];
  635 + if (!startTime) {
  636 + if (this.realtime) {
  637 + if (dataKey.lastUpdateTime) {
  638 + startTime = dataKey.lastUpdateTime + this.frequency;
  639 + endTime = dataKey.lastUpdateTime + deltaElapsed;
  640 + } else {
  641 + startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs;
  642 + endTime = startTime + this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs + this.frequency;
  643 + if (this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type === AggregationType.NONE) {
  644 + const time = endTime - this.frequency * this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.limit;
  645 + startTime = Math.max(time, startTime);
  646 + }
  647 + }
  648 + } else {
  649 + startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs;
  650 + endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs;
  651 + }
  652 + }
  653 + generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, index, startTime, endTime);
  654 + }
  655 + }
  656 + if (this.dataAggregators && this.dataAggregators.length) {
  657 + this.dataAggregators[0].onData(generatedData, true, this.history, detectChanges);
  658 + }
  659 + } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
  660 + for (key of Object.keys(this.dataKeys)) {
  661 + this.generateLatest(this.dataKeys[key] as SubscriptionDataKey, detectChanges);
  662 + }
  663 + }
  664 +
  665 + if (!this.history) {
  666 + this.timer = setTimeout(this.onTick.bind(this, true), this.frequency);
  667 + }
  668 + }
  669 +
  670 +}
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { DataSetHolder, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models';
  18 +import { SubscriptionTimewindow } from '@shared/models/time/time.models';
  19 +import { EntityData, EntityDataPageLink, EntityFilter, KeyFilter } from '@shared/models/query/query.models';
  20 +import { PageData } from '@shared/models/page/page-data';
  21 +import { Injectable } from '@angular/core';
  22 +import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
  23 +import { UtilsService } from '@core/services/utils.service';
  24 +import { SubscriptionDataKey } from '@core/api/datasource-subcription';
  25 +import { deepClone, objectHashCode } from '@core/utils';
  26 +import { EntityDataSubscription, EntityDataSubscriptionOptions } from '@core/api/entity-data-subscription';
  27 +
  28 +export interface EntityDataListener {
  29 + subscriptionType: widgetType;
  30 + subscriptionTimewindow: SubscriptionTimewindow;
  31 + configDatasource: Datasource;
  32 + configDatasourceIndex: number;
  33 + dataLoaded: (pageData: PageData<EntityData>, data: Array<Array<DataSetHolder>>, datasourceIndex: number) => void;
  34 + dataUpdated: (data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void;
  35 + updateRealtimeSubscription: () => SubscriptionTimewindow;
  36 + setRealtimeSubscription: (subscriptionTimewindow: SubscriptionTimewindow) => void;
  37 + entityDataSubscriptionKey?: number;
  38 +}
  39 +
  40 +@Injectable({
  41 + providedIn: 'root'
  42 +})
  43 +export class EntityDataService {
  44 +
  45 + private subscriptions: {[entityDataSubscriptionKey: string]: EntityDataSubscription} = {};
  46 +
  47 + constructor(private telemetryService: TelemetryWebsocketService,
  48 + private utils: UtilsService) {}
  49 +
  50 + public subscribeToEntityData(listener: EntityDataListener) {
  51 + const datasource = listener.configDatasource;
  52 + if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !datasource.pageLink)) {
  53 + return;
  54 + }
  55 + const subscriptionDataKeys: Array<SubscriptionDataKey> = [];
  56 + datasource.dataKeys.forEach((dataKey) => {
  57 + const subscriptionDataKey: SubscriptionDataKey = {
  58 + name: dataKey.name,
  59 + type: dataKey.type,
  60 + funcBody: dataKey.funcBody,
  61 + postFuncBody: dataKey.postFuncBody
  62 + };
  63 + subscriptionDataKeys.push(subscriptionDataKey);
  64 + });
  65 +
  66 + const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = {
  67 + datasourceType: datasource.type,
  68 + dataKeys: subscriptionDataKeys,
  69 + type: listener.subscriptionType
  70 + };
  71 +
  72 + if (listener.subscriptionType === widgetType.timeseries) {
  73 + entityDataSubscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
  74 + }
  75 + if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) {
  76 + entityDataSubscriptionOptions.entityFilter = datasource.entityFilter;
  77 + entityDataSubscriptionOptions.pageLink = datasource.pageLink;
  78 + entityDataSubscriptionOptions.keyFilters = datasource.keyFilters;
  79 + }
  80 + listener.entityDataSubscriptionKey = objectHashCode(entityDataSubscriptionOptions);
  81 + let subscription: EntityDataSubscription;
  82 + if (this.subscriptions[listener.entityDataSubscriptionKey]) {
  83 + subscription = this.subscriptions[listener.entityDataSubscriptionKey];
  84 + subscription.syncListener(listener);
  85 + } else {
  86 + subscription = new EntityDataSubscription(entityDataSubscriptionOptions,
  87 + this.telemetryService, this.utils);
  88 + this.subscriptions[listener.entityDataSubscriptionKey] = subscription;
  89 + subscription.start();
  90 + }
  91 + subscription.addListener(listener);
  92 + }
  93 +
  94 + public unsubscribeFromDatasource(listener: EntityDataListener) {
  95 + if (listener.entityDataSubscriptionKey) {
  96 + const subscription = this.subscriptions[listener.entityDataSubscriptionKey];
  97 + if (subscription) {
  98 + subscription.removeListener(listener);
  99 + if (!subscription.hasListeners()) {
  100 + subscription.unsubscribe();
  101 + delete this.subscriptions[listener.entityDataSubscriptionKey];
  102 + }
  103 + }
  104 + listener.entityDataSubscriptionKey = null;
  105 + }
  106 + }
  107 +
  108 +}
@@ -41,6 +41,9 @@ import { EntityAliases } from '@shared/models/alias.models'; @@ -41,6 +41,9 @@ import { EntityAliases } from '@shared/models/alias.models';
41 import { EntityInfo } from '@app/shared/models/entity.models'; 41 import { EntityInfo } from '@app/shared/models/entity.models';
42 import { IDashboardComponent } from '@home/models/dashboard-component.models'; 42 import { IDashboardComponent } from '@home/models/dashboard-component.models';
43 import * as moment_ from 'moment'; 43 import * as moment_ from 'moment';
  44 +import { EntityDataPageLink, EntityFilter, KeyFilter } from '@shared/models/query/query.models';
  45 +import { EntityDataService } from '@core/api/entity-data.service';
  46 +import { PageData } from '@shared/models/page/page-data';
44 47
45 export interface TimewindowFunctions { 48 export interface TimewindowFunctions {
46 onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; 49 onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void;
@@ -76,6 +79,7 @@ export interface WidgetActionsApi { @@ -76,6 +79,7 @@ export interface WidgetActionsApi {
76 export interface AliasInfo { 79 export interface AliasInfo {
77 alias?: string; 80 alias?: string;
78 stateEntity?: boolean; 81 stateEntity?: boolean;
  82 + entityFilter?: EntityFilter;
79 currentEntity?: EntityInfo; 83 currentEntity?: EntityInfo;
80 selectedId?: string; 84 selectedId?: string;
81 resolvedEntities?: Array<EntityInfo>; 85 resolvedEntities?: Array<EntityInfo>;
@@ -169,7 +173,8 @@ export class WidgetSubscriptionContext { @@ -169,7 +173,8 @@ export class WidgetSubscriptionContext {
169 timeService: TimeService; 173 timeService: TimeService;
170 deviceService: DeviceService; 174 deviceService: DeviceService;
171 alarmService: AlarmService; 175 alarmService: AlarmService;
172 - datasourceService: DatasourceService; 176 + // datasourceService: DatasourceService;
  177 + entityDataService: EntityDataService;
173 utils: UtilsService; 178 utils: UtilsService;
174 raf: RafService; 179 raf: RafService;
175 widgetUtils: IWidgetUtils; 180 widgetUtils: IWidgetUtils;
@@ -197,6 +202,8 @@ export interface WidgetSubscriptionOptions { @@ -197,6 +202,8 @@ export interface WidgetSubscriptionOptions {
197 alarmsMaxCountLoad?: number; 202 alarmsMaxCountLoad?: number;
198 alarmsFetchSize?: number; 203 alarmsFetchSize?: number;
199 datasources?: Array<Datasource>; 204 datasources?: Array<Datasource>;
  205 + keyFilters?: Array<KeyFilter>;
  206 + pageLink?: EntityDataPageLink;
200 targetDeviceAliasIds?: Array<string>; 207 targetDeviceAliasIds?: Array<string>;
201 targetDeviceIds?: Array<string>; 208 targetDeviceIds?: Array<string>;
202 useDashboardTimewindow?: boolean; 209 useDashboardTimewindow?: boolean;
@@ -230,6 +237,9 @@ export interface IWidgetSubscription { @@ -230,6 +237,9 @@ export interface IWidgetSubscription {
230 useDashboardTimewindow: boolean; 237 useDashboardTimewindow: boolean;
231 238
232 legendData: LegendData; 239 legendData: LegendData;
  240 +
  241 + datasourcePages?: PageData<Datasource>[];
  242 + dataPages?: PageData<Array<DatasourceData>>[];
233 datasources?: Array<Datasource>; 243 datasources?: Array<Datasource>;
234 data?: Array<DatasourceData>; 244 data?: Array<DatasourceData>;
235 hiddenData?: Array<{data: DataSet}>; 245 hiddenData?: Array<{data: DataSet}>;
@@ -47,13 +47,16 @@ import { Observable, ReplaySubject, Subject, throwError } from 'rxjs'; @@ -47,13 +47,16 @@ import { Observable, ReplaySubject, Subject, throwError } from 'rxjs';
47 import { CancelAnimationFrame } from '@core/services/raf.service'; 47 import { CancelAnimationFrame } from '@core/services/raf.service';
48 import { EntityType } from '@shared/models/entity-type.models'; 48 import { EntityType } from '@shared/models/entity-type.models';
49 import { AlarmInfo, AlarmSearchStatus } from '@shared/models/alarm.models'; 49 import { AlarmInfo, AlarmSearchStatus } from '@shared/models/alarm.models';
50 -import { deepClone, isDefined, isEqual } from '@core/utils'; 50 +import { createLabelFromDatasource, deepClone, isDefined, isEqual } from '@core/utils';
51 import { AlarmSourceListener } from '@core/http/alarm.service'; 51 import { AlarmSourceListener } from '@core/http/alarm.service';
52 import { DatasourceListener } from '@core/api/datasource.service'; 52 import { DatasourceListener } from '@core/api/datasource.service';
53 import { EntityId } from '@app/shared/models/id/entity-id'; 53 import { EntityId } from '@app/shared/models/id/entity-id';
54 import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; 54 import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
55 import { entityFields } from '@shared/models/entity.models'; 55 import { entityFields } from '@shared/models/entity.models';
56 import * as moment_ from 'moment'; 56 import * as moment_ from 'moment';
  57 +import { PageData } from '@shared/models/page/page-data';
  58 +import { EntityDataListener } from '@core/api/entity-data.service';
  59 +import { EntityData, EntityDataPageLink, EntityKeyType } from '@shared/models/query/query.models';
57 60
58 const moment = moment_; 61 const moment = moment_;
59 62
@@ -70,9 +73,14 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -70,9 +73,14 @@ export class WidgetSubscription implements IWidgetSubscription {
70 subscriptionTimewindow: SubscriptionTimewindow; 73 subscriptionTimewindow: SubscriptionTimewindow;
71 useDashboardTimewindow: boolean; 74 useDashboardTimewindow: boolean;
72 75
  76 + datasourcePages: PageData<Datasource>[];
  77 + dataPages: PageData<Array<DatasourceData>>[];
  78 + entityDataListeners: Array<EntityDataListener>;
  79 + configuredDatasources: Array<Datasource>;
  80 +
73 data: Array<DatasourceData>; 81 data: Array<DatasourceData>;
74 datasources: Array<Datasource>; 82 datasources: Array<Datasource>;
75 - datasourceListeners: Array<DatasourceListener>; 83 + // datasourceListeners: Array<DatasourceListener>;
76 hiddenData: Array<DataSetHolder>; 84 hiddenData: Array<DataSetHolder>;
77 legendData: LegendData; 85 legendData: LegendData;
78 legendConfig: LegendConfig; 86 legendConfig: LegendConfig;
@@ -197,8 +205,13 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -197,8 +205,13 @@ export class WidgetSubscription implements IWidgetSubscription {
197 this.callbacks.legendDataUpdated = this.callbacks.legendDataUpdated || (() => {}); 205 this.callbacks.legendDataUpdated = this.callbacks.legendDataUpdated || (() => {});
198 this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || (() => {}); 206 this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || (() => {});
199 207
200 - this.datasources = this.ctx.utils.validateDatasources(options.datasources);  
201 - this.datasourceListeners = []; 208 + // this.datasources = this.ctx.utils.validateDatasources(options.datasources);
  209 + this.configuredDatasources = this.ctx.utils.validateDatasources(options.datasources);
  210 + this.entityDataListeners = [];
  211 + // this.datasourceListeners = [];
  212 + this.datasourcePages = [];
  213 + this.datasources = [];
  214 + this.dataPages = [];
202 this.data = []; 215 this.data = [];
203 this.hiddenData = []; 216 this.hiddenData = [];
204 this.originalTimewindow = null; 217 this.originalTimewindow = null;
@@ -336,6 +349,35 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -336,6 +349,35 @@ export class WidgetSubscription implements IWidgetSubscription {
336 this.loadStDiff().subscribe(() => { 349 this.loadStDiff().subscribe(() => {
337 if (!this.ctx.aliasController) { 350 if (!this.ctx.aliasController) {
338 this.hasResolvedData = true; 351 this.hasResolvedData = true;
  352 + // this.configureData();
  353 + initDataSubscriptionSubject.next();
  354 + initDataSubscriptionSubject.complete();
  355 + } else {
  356 + this.ctx.aliasController.resolveDatasources(this.configuredDatasources).subscribe(
  357 + (datasources) => {
  358 + this.configuredDatasources = datasources;
  359 + if (datasources && datasources.length) {
  360 + this.hasResolvedData = true;
  361 + }
  362 + // this.configureData();
  363 + initDataSubscriptionSubject.next();
  364 + initDataSubscriptionSubject.complete();
  365 + },
  366 + (err) => {
  367 + this.notifyDataLoaded();
  368 + initDataSubscriptionSubject.error(err);
  369 + }
  370 + );
  371 + }
  372 + });
  373 + return initDataSubscriptionSubject.asObservable();
  374 + }
  375 +
  376 +/* private initDataSubscriptionOld(): Observable<any> {
  377 + const initDataSubscriptionSubject = new ReplaySubject(1);
  378 + this.loadStDiff().subscribe(() => {
  379 + if (!this.ctx.aliasController) {
  380 + this.hasResolvedData = true;
339 this.configureData(); 381 this.configureData();
340 initDataSubscriptionSubject.next(); 382 initDataSubscriptionSubject.next();
341 initDataSubscriptionSubject.complete(); 383 initDataSubscriptionSubject.complete();
@@ -358,9 +400,9 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -358,9 +400,9 @@ export class WidgetSubscription implements IWidgetSubscription {
358 } 400 }
359 }); 401 });
360 return initDataSubscriptionSubject.asObservable(); 402 return initDataSubscriptionSubject.asObservable();
361 - } 403 + } */
362 404
363 - private configureData() { 405 + /* private configureData() {
364 const additionalDatasources: Datasource[] = []; 406 const additionalDatasources: Datasource[] = [];
365 let dataIndex = 0; 407 let dataIndex = 0;
366 let additionalKeysNumber = 0; 408 let additionalKeysNumber = 0;
@@ -448,9 +490,19 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -448,9 +490,19 @@ export class WidgetSubscription implements IWidgetSubscription {
448 if (this.displayLegend) { 490 if (this.displayLegend) {
449 this.legendData.keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label)); 491 this.legendData.keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label));
450 } 492 }
451 - } 493 + } */
452 494
453 private resetData() { 495 private resetData() {
  496 + this.data = [];
  497 + this.hiddenData = [];
  498 + if (this.displayLegend) {
  499 + this.legendData.keys = [];
  500 + this.legendData.data = [];
  501 + }
  502 + this.onDataUpdated();
  503 + }
  504 +
  505 +/* private resetDataOld() {
454 for (let i = 0; i < this.data.length; i++) { 506 for (let i = 0; i < this.data.length; i++) {
455 this.data[i].data = []; 507 this.data[i].data = [];
456 this.hiddenData[i].data = []; 508 this.hiddenData[i].data = [];
@@ -463,7 +515,7 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -463,7 +515,7 @@ export class WidgetSubscription implements IWidgetSubscription {
463 } 515 }
464 } 516 }
465 this.onDataUpdated(); 517 this.onDataUpdated();
466 - } 518 + }*/
467 519
468 getFirstEntityInfo(): SubscriptionEntityInfo { 520 getFirstEntityInfo(): SubscriptionEntityInfo {
469 let entityId: EntityId; 521 let entityId: EntityId;
@@ -756,6 +808,87 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -756,6 +808,87 @@ export class WidgetSubscription implements IWidgetSubscription {
756 this.onDataUpdated(); 808 this.onDataUpdated();
757 } 809 }
758 } 810 }
  811 + // let index = 0;
  812 + const forceUpdate = !this.datasources.length;
  813 + this.configuredDatasources.forEach((datasource, index) => {
  814 + const listener: EntityDataListener = {
  815 + subscriptionType: this.type,
  816 + subscriptionTimewindow: this.subscriptionTimewindow,
  817 + configDatasource: datasource,
  818 + configDatasourceIndex: index,
  819 + dataLoaded: this.dataLoaded.bind(this),
  820 + dataUpdated: this.dataUpdated.bind(this),
  821 + updateRealtimeSubscription: () => {
  822 + this.subscriptionTimewindow = this.updateRealtimeSubscription();
  823 + return this.subscriptionTimewindow;
  824 + },
  825 + setRealtimeSubscription: (subscriptionTimewindow) => {
  826 + this.updateRealtimeSubscription(deepClone(subscriptionTimewindow));
  827 + }
  828 + };
  829 +
  830 + /*if (this.comparisonEnabled && datasource.isAdditional) {
  831 + listener.subscriptionTimewindow = this.timewindowForComparison;
  832 + listener.updateRealtimeSubscription = () => {
  833 + this.subscriptionTimewindow = this.updateSubscriptionForComparison();
  834 + return this.subscriptionTimewindow;
  835 + };
  836 + listener.setRealtimeSubscription = () => {
  837 + this.updateSubscriptionForComparison();
  838 + };
  839 + }*/
  840 +
  841 +/* let entityFieldKey = false;
  842 +
  843 + for (let a = 0; a < datasource.dataKeys.length; a++) {
  844 + if (datasource.dataKeys[a].type !== DataKeyType.entityField) {
  845 + this.data[index + a].data = [];
  846 + } else {
  847 + entityFieldKey = true;
  848 + }
  849 + }
  850 + index += datasource.dataKeys.length;*/
  851 +
  852 + this.entityDataListeners.push(listener);
  853 + // this.datasourceListeners.push(listener);
  854 +
  855 + // if (datasource.dataKeys.length) {
  856 + // this.ctx.datasourceService.subscribeToDatasource(listener);
  857 + // }
  858 +
  859 + this.ctx.entityDataService.subscribeToEntityData(listener);
  860 +
  861 + /* if (datasource.unresolvedStateEntity || entityFieldKey ||
  862 + !datasource.dataKeys.length ||
  863 + (datasource.type === DatasourceType.entity && !datasource.entityId)
  864 + ) {
  865 + forceUpdate = true;
  866 + }*/
  867 + });
  868 + if (forceUpdate) {
  869 + this.notifyDataLoaded();
  870 + this.onDataUpdated();
  871 + }
  872 + }
  873 + }
  874 +
  875 + /* private doSubscribeOld() {
  876 + if (this.type === widgetType.rpc) {
  877 + return;
  878 + }
  879 + if (this.type === widgetType.alarm) {
  880 + this.alarmsSubscribe();
  881 + } else {
  882 + this.notifyDataLoading();
  883 + if (this.type === widgetType.timeseries && this.timeWindowConfig) {
  884 + this.updateRealtimeSubscription();
  885 + if (this.comparisonEnabled) {
  886 + this.updateSubscriptionForComparison();
  887 + }
  888 + if (this.subscriptionTimewindow.fixedWindow) {
  889 + this.onDataUpdated();
  890 + }
  891 + }
759 let index = 0; 892 let index = 0;
760 let forceUpdate = !this.datasources.length; 893 let forceUpdate = !this.datasources.length;
761 this.datasources.forEach((datasource) => { 894 this.datasources.forEach((datasource) => {
@@ -814,7 +947,7 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -814,7 +947,7 @@ export class WidgetSubscription implements IWidgetSubscription {
814 this.onDataUpdated(); 947 this.onDataUpdated();
815 } 948 }
816 } 949 }
817 - } 950 + } */
818 951
819 private alarmsSubscribe() { 952 private alarmsSubscribe() {
820 this.notifyDataLoading(); 953 this.notifyDataLoading();
@@ -855,6 +988,20 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -855,6 +988,20 @@ export class WidgetSubscription implements IWidgetSubscription {
855 if (this.type === widgetType.alarm) { 988 if (this.type === widgetType.alarm) {
856 this.alarmsUnsubscribe(); 989 this.alarmsUnsubscribe();
857 } else { 990 } else {
  991 + this.entityDataListeners.forEach((listener) => {
  992 + this.ctx.entityDataService.unsubscribeFromDatasource(listener);
  993 + });
  994 + this.entityDataListeners.length = 0;
  995 + this.resetData();
  996 + }
  997 + }
  998 + }
  999 +
  1000 +/* unsubscribeOld() {
  1001 + if (this.type !== widgetType.rpc) {
  1002 + if (this.type === widgetType.alarm) {
  1003 + this.alarmsUnsubscribe();
  1004 + } else {
858 this.datasourceListeners.forEach((listener) => { 1005 this.datasourceListeners.forEach((listener) => {
859 this.ctx.datasourceService.unsubscribeFromDatasource(listener); 1006 this.ctx.datasourceService.unsubscribeFromDatasource(listener);
860 }); 1007 });
@@ -862,7 +1009,7 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -862,7 +1009,7 @@ export class WidgetSubscription implements IWidgetSubscription {
862 this.resetData(); 1009 this.resetData();
863 } 1010 }
864 } 1011 }
865 - } 1012 + } */
866 1013
867 private alarmsUnsubscribe() { 1014 private alarmsUnsubscribe() {
868 if (this.alarmSourceListener) { 1015 if (this.alarmSourceListener) {
@@ -970,7 +1117,180 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -970,7 +1117,180 @@ export class WidgetSubscription implements IWidgetSubscription {
970 return this.timewindowForComparison; 1117 return this.timewindowForComparison;
971 } 1118 }
972 1119
973 - private dataUpdated(sourceData: DataSetHolder, datasourceIndex: number, dataKeyIndex: number, detectChanges: boolean) { 1120 + private dataLoaded(pageData: PageData<EntityData>, data: Array<Array<DataSetHolder>>, datasourceIndex: number) {
  1121 + const datasource = this.configuredDatasources[datasourceIndex];
  1122 + const datasources = pageData.data.map((entityData, index) =>
  1123 + this.entityDataToDatasource(datasource, entityData, index)
  1124 + );
  1125 + const datasourcesPage: PageData<Datasource> = {
  1126 + data: datasources,
  1127 + hasNext: pageData.hasNext,
  1128 + totalElements: pageData.totalElements,
  1129 + totalPages: pageData.totalPages
  1130 + };
  1131 + this.datasourcePages[datasourceIndex] = datasourcesPage;
  1132 + const datasourceData = datasources.map((datasourceElement, index) =>
  1133 + this.entityDataToDatasourceData(datasourceElement, data[index])
  1134 + );
  1135 + const datasourceDataPage: PageData<Array<DatasourceData>> = {
  1136 + data: datasourceData,
  1137 + hasNext: pageData.hasNext,
  1138 + totalElements: pageData.totalElements,
  1139 + totalPages: pageData.totalPages
  1140 + };
  1141 + this.dataPages[datasourceIndex] = datasourceDataPage;
  1142 + this.configureLoadedData();
  1143 + this.notifyDataLoaded();
  1144 + }
  1145 +
  1146 + private configureLoadedData() {
  1147 + this.datasources.length = 0;
  1148 + this.data.length = 0;
  1149 + this.hiddenData.length = 0;
  1150 + if (this.displayLegend) {
  1151 + this.legendData.keys.length = 0;
  1152 + this.legendData.data.length = 0;
  1153 + }
  1154 +
  1155 + let dataKeyIndex = 0;
  1156 + this.configuredDatasources.forEach((configuredDatasource, datasourceIndex) => {
  1157 + configuredDatasource.dataKeyStartIndex = dataKeyIndex;
  1158 + const datasourcesPage = this.datasourcePages[datasourceIndex];
  1159 + const datasourceDataPage = this.dataPages[datasourceIndex];
  1160 + if (datasourcesPage) {
  1161 + datasourcesPage.data.forEach((datasource, currentDatasourceIndex) => {
  1162 + datasource.dataKeys.forEach((dataKey, currentDataKeyIndex) => {
  1163 + const datasourceData = datasourceDataPage.data[currentDatasourceIndex][currentDataKeyIndex];
  1164 + this.data.push(datasourceData);
  1165 + this.hiddenData.push({data: []});
  1166 + if (this.displayLegend) {
  1167 + const legendKey: LegendKey = {
  1168 + dataKey,
  1169 + dataIndex: dataKeyIndex
  1170 + };
  1171 + this.legendData.keys.push(legendKey);
  1172 + const legendKeyData: LegendKeyData = {
  1173 + min: null,
  1174 + max: null,
  1175 + avg: null,
  1176 + total: null,
  1177 + hidden: false
  1178 + };
  1179 + this.legendData.data.push(legendKeyData);
  1180 + }
  1181 + dataKeyIndex++;
  1182 + });
  1183 + this.datasources.push(datasource);
  1184 + });
  1185 + }
  1186 + }
  1187 + );
  1188 + let index = 0;
  1189 + this.datasources.forEach((datasource) => {
  1190 + datasource.dataKeys.forEach((dataKey) => {
  1191 + if (datasource.generated) {
  1192 + dataKey._hash = Math.random();
  1193 + dataKey.color = this.ctx.utils.getMaterialColor(index);
  1194 + }
  1195 + index++;
  1196 + });
  1197 + });
  1198 + if (this.displayLegend) {
  1199 + this.legendData.keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label));
  1200 + }
  1201 + if (this.caulculateLegendData) {
  1202 + this.data.forEach((dataSetHolder, keyIndex) => {
  1203 + this.updateLegend(keyIndex, dataSetHolder.data, false);
  1204 + });
  1205 + this.callbacks.legendDataUpdated(this, true);
  1206 + }
  1207 + this.onDataUpdated(true);
  1208 + }
  1209 +
  1210 + private entityDataToDatasourceData(datasource: Datasource, data: Array<DataSetHolder>): Array<DatasourceData> {
  1211 + return datasource.dataKeys.map((dataKey, keyIndex) => {
  1212 + dataKey.hidden = dataKey.settings.hideDataByDefault ? true : false;
  1213 + dataKey.inLegend = dataKey.settings.removeFromLegend ? false : true;
  1214 + dataKey.pattern = dataKey.label;
  1215 + dataKey.label = createLabelFromDatasource(datasource, dataKey.pattern);
  1216 + const datasourceData: DatasourceData = {
  1217 + datasource,
  1218 + dataKey,
  1219 + data: []
  1220 + };
  1221 + return datasourceData;
  1222 + });
  1223 + }
  1224 +
  1225 + private entityDataToDatasource(configDatasource: Datasource, entityData: EntityData, index: number): Datasource {
  1226 + const newDatasource = deepClone(configDatasource);
  1227 + newDatasource.dataReceived = true;
  1228 + newDatasource.entity = {};
  1229 + newDatasource.entityId = entityData.entityId.id;
  1230 + newDatasource.entityType = entityData.entityId.entityType as EntityType;
  1231 + if (configDatasource.type === DatasourceType.entity) {
  1232 + let name;
  1233 + let label;
  1234 + if (entityData.latest && entityData.latest[EntityKeyType.ENTITY_FIELD]) {
  1235 + const fields = entityData.latest[EntityKeyType.ENTITY_FIELD];
  1236 + if (fields.name) {
  1237 + name = fields.name.value;
  1238 + }
  1239 + if (fields.label) {
  1240 + label = fields.label.value;
  1241 + }
  1242 + }
  1243 + name = name || 'TODO';
  1244 + label = label || 'TODO';
  1245 + newDatasource.name = name;
  1246 + newDatasource.entityName = name;
  1247 + newDatasource.entityLabel = label;
  1248 + newDatasource.entityDescription = 'TODO';
  1249 + }
  1250 + newDatasource.generated = index > 0 ? true : false;
  1251 + return newDatasource;
  1252 + }
  1253 +
  1254 + private dataUpdated(data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) {
  1255 + const configuredDatasource = this.configuredDatasources[datasourceIndex];
  1256 + const startIndex = configuredDatasource.dataKeyStartIndex;
  1257 + const dataKeysCount = configuredDatasource.dataKeys.length;
  1258 + const index = startIndex + dataIndex*dataKeysCount + dataKeyIndex;
  1259 + let update = true;
  1260 + let currentData: DataSetHolder;
  1261 + if (this.displayLegend && this.legendData.keys[index].dataKey.hidden) {
  1262 + currentData = this.hiddenData[index];
  1263 + } else {
  1264 + currentData = this.data[index];
  1265 + }
  1266 + if (this.type === widgetType.latest) {
  1267 + const prevData = currentData.data;
  1268 + if (!data.data.length) {
  1269 + update = false;
  1270 + } else if (prevData && prevData[0] && prevData[0].length > 1 && data.data.length > 0) {
  1271 + const prevTs = prevData[0][0];
  1272 + const prevValue = prevData[0][1];
  1273 + if (prevTs === data.data[0][0] && prevValue === data.data[0][1]) {
  1274 + update = false;
  1275 + }
  1276 + }
  1277 + }
  1278 + if (update) {
  1279 + if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) {
  1280 + this.updateTimewindow();
  1281 + if (this.timewindowForComparison && this.timewindowForComparison.realtimeWindowMs) {
  1282 + this.updateComparisonTimewindow();
  1283 + }
  1284 + }
  1285 + currentData.data = data.data;
  1286 + if (this.caulculateLegendData) {
  1287 + this.updateLegend(index, data.data, detectChanges);
  1288 + }
  1289 + this.onDataUpdated(detectChanges);
  1290 + }
  1291 + }
  1292 +
  1293 +/* private dataUpdatedOld(sourceData: DataSetHolder, datasourceIndex: number, dataKeyIndex: number, detectChanges: boolean) {
974 for (let x = 0; x < this.datasourceListeners.length; x++) { 1294 for (let x = 0; x < this.datasourceListeners.length; x++) {
975 this.datasources[x].dataReceived = this.datasources[x].dataReceived === true; 1295 this.datasources[x].dataReceived = this.datasources[x].dataReceived === true;
976 if (this.datasourceListeners[x].datasourceIndex === datasourceIndex && sourceData.data.length > 0) { 1296 if (this.datasourceListeners[x].datasourceIndex === datasourceIndex && sourceData.data.length > 0) {
@@ -1010,7 +1330,7 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -1010,7 +1330,7 @@ export class WidgetSubscription implements IWidgetSubscription {
1010 } 1330 }
1011 this.onDataUpdated(detectChanges); 1331 this.onDataUpdated(detectChanges);
1012 } 1332 }
1013 - } 1333 + } */
1014 1334
1015 private alarmsUpdated(alarms: Array<AlarmInfo>) { 1335 private alarmsUpdated(alarms: Array<AlarmInfo>) {
1016 this.notifyDataLoaded(); 1336 this.notifyDataLoaded();
@@ -52,7 +52,7 @@ import { @@ -52,7 +52,7 @@ import {
52 EntitySearchQuery 52 EntitySearchQuery
53 } from '@shared/models/relation.models'; 53 } from '@shared/models/relation.models';
54 import { EntityRelationService } from '@core/http/entity-relation.service'; 54 import { EntityRelationService } from '@core/http/entity-relation.service';
55 -import { isDefined } from '@core/utils'; 55 +import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils';
56 import { Asset, AssetSearchQuery } from '@shared/models/asset.models'; 56 import { Asset, AssetSearchQuery } from '@shared/models/asset.models';
57 import { Device, DeviceCredentialsType, DeviceSearchQuery } from '@shared/models/device.models'; 57 import { Device, DeviceCredentialsType, DeviceSearchQuery } from '@shared/models/device.models';
58 import { EntityViewSearchQuery } from '@shared/models/entity-view.models'; 58 import { EntityViewSearchQuery } from '@shared/models/entity-view.models';
@@ -604,10 +604,11 @@ export class EntityService { @@ -604,10 +604,11 @@ export class EntityService {
604 604
605 public resolveAlias(entityAlias: EntityAlias, stateParams: StateParams): Observable<AliasInfo> { 605 public resolveAlias(entityAlias: EntityAlias, stateParams: StateParams): Observable<AliasInfo> {
606 const filter = entityAlias.filter; 606 const filter = entityAlias.filter;
607 - return this.resolveAliasFilter(filter, stateParams, -1, false).pipe( 607 + return this.resolveAliasFilter(filter, stateParams).pipe(
608 map((result) => { 608 map((result) => {
609 const aliasInfo: AliasInfo = { 609 const aliasInfo: AliasInfo = {
610 alias: entityAlias.alias, 610 alias: entityAlias.alias,
  611 + entityFilter: result.entityFilter,
611 stateEntity: result.stateEntity, 612 stateEntity: result.stateEntity,
612 entityParamName: result.entityParamName, 613 entityParamName: result.entityParamName,
613 resolveMultiple: filter.resolveMultiple 614 resolveMultiple: filter.resolveMultiple
@@ -621,11 +622,28 @@ export class EntityService { @@ -621,11 +622,28 @@ export class EntityService {
621 }) 622 })
622 ); 623 );
623 } 624 }
  625 +/*
  626 + public resolveEntityFilter(filter: EntityAliasFilter, stateParams: StateParams): EntityFilter {
  627 + const stateEntityInfo = this.getStateEntityInfo(filter, stateParams);
  628 + let result: EntityFilter = filter;
  629 + const stateEntityId = stateEntityInfo.entityId;
  630 + if (filter.type === AliasFilterType.stateEntity) {
  631 + result = {
  632 + singleEntity: stateEntityId,
  633 + type: AliasFilterType.singleEntity
  634 + };
  635 + } else if (filter.rootStateEntity) {
  636 + let rootEntityType;
  637 + let rootEntityId;
  638 +
  639 + }
  640 + return result;
  641 + }*/
624 642
625 - public resolveAliasFilter(filter: EntityAliasFilter, stateParams: StateParams,  
626 - maxItems: number, failOnEmpty: boolean): Observable<EntityAliasFilterResult> { 643 + public resolveAliasFilter(filter: EntityAliasFilter, stateParams: StateParams): Observable<EntityAliasFilterResult> {
627 const result: EntityAliasFilterResult = { 644 const result: EntityAliasFilterResult = {
628 entities: [], 645 entities: [],
  646 + entityFilter: null,
629 stateEntity: false 647 stateEntity: false
630 }; 648 };
631 if (filter.stateEntityParamName && filter.stateEntityParamName.length) { 649 if (filter.stateEntityParamName && filter.stateEntityParamName.length) {
@@ -636,14 +654,21 @@ export class EntityService { @@ -636,14 +654,21 @@ export class EntityService {
636 switch (filter.type) { 654 switch (filter.type) {
637 case AliasFilterType.singleEntity: 655 case AliasFilterType.singleEntity:
638 const aliasEntityId = this.resolveAliasEntityId(filter.singleEntity.entityType, filter.singleEntity.id); 656 const aliasEntityId = this.resolveAliasEntityId(filter.singleEntity.entityType, filter.singleEntity.id);
639 - return this.getEntity(aliasEntityId.entityType as EntityType, aliasEntityId.id, {ignoreLoading: true, ignoreErrors: true}).pipe( 657 + result.entityFilter = {
  658 + type: AliasFilterType.singleEntity,
  659 + singleEntity: aliasEntityId
  660 + };
  661 + return of(result);
  662 + /*return this.getEntity(aliasEntityId.entityType as EntityType, aliasEntityId.id, {ignoreLoading: true, ignoreErrors: true}).pipe(
640 map((entity) => { 663 map((entity) => {
641 result.entities = this.entitiesToEntitiesInfo([entity]); 664 result.entities = this.entitiesToEntitiesInfo([entity]);
642 return result; 665 return result;
643 } 666 }
644 - )); 667 + ));*/
645 case AliasFilterType.entityList: 668 case AliasFilterType.entityList:
646 - return this.getEntities(filter.entityType, filter.entityList, {ignoreLoading: true, ignoreErrors: true}).pipe( 669 + result.entityFilter = deepClone(filter);
  670 + return of(result);
  671 + /*return this.getEntities(filter.entityType, filter.entityList, {ignoreLoading: true, ignoreErrors: true}).pipe(
647 map((entities) => { 672 map((entities) => {
648 if (entities && entities.length || !failOnEmpty) { 673 if (entities && entities.length || !failOnEmpty) {
649 result.entities = this.entitiesToEntitiesInfo(entities); 674 result.entities = this.entitiesToEntitiesInfo(entities);
@@ -652,9 +677,11 @@ export class EntityService { @@ -652,9 +677,11 @@ export class EntityService {
652 throw new Error(); 677 throw new Error();
653 } 678 }
654 } 679 }
655 - )); 680 + ));*/
656 case AliasFilterType.entityName: 681 case AliasFilterType.entityName:
657 - return this.getEntitiesByNameFilter(filter.entityType, filter.entityNameFilter, maxItems, 682 + result.entityFilter = deepClone(filter);
  683 + return of(result);
  684 + /*return this.getEntitiesByNameFilter(filter.entityType, filter.entityNameFilter, maxItems,
658 '', {ignoreLoading: true, ignoreErrors: true}).pipe( 685 '', {ignoreLoading: true, ignoreErrors: true}).pipe(
659 map((entities) => { 686 map((entities) => {
660 if (entities && entities.length || !failOnEmpty) { 687 if (entities && entities.length || !failOnEmpty) {
@@ -665,11 +692,17 @@ export class EntityService { @@ -665,11 +692,17 @@ export class EntityService {
665 } 692 }
666 } 693 }
667 ) 694 )
668 - ); 695 + );*/
669 case AliasFilterType.stateEntity: 696 case AliasFilterType.stateEntity:
670 result.stateEntity = true; 697 result.stateEntity = true;
671 if (stateEntityId) { 698 if (stateEntityId) {
672 - return this.getEntity(stateEntityId.entityType as EntityType, stateEntityId.id, {ignoreLoading: true, ignoreErrors: true}).pipe( 699 + result.entityFilter = {
  700 + type: AliasFilterType.singleEntity,
  701 + singleEntity: stateEntityId
  702 + };
  703 + }
  704 + return of(result);
  705 + /*return this.getEntity(stateEntityId.entityType as EntityType, stateEntityId.id, {ignoreLoading: true, ignoreErrors: true}).pipe(
673 map((entity) => { 706 map((entity) => {
674 result.entities = this.entitiesToEntitiesInfo([entity]); 707 result.entities = this.entitiesToEntitiesInfo([entity]);
675 return result; 708 return result;
@@ -677,9 +710,11 @@ export class EntityService { @@ -677,9 +710,11 @@ export class EntityService {
677 )); 710 ));
678 } else { 711 } else {
679 return of(result); 712 return of(result);
680 - } 713 + }*/
681 case AliasFilterType.assetType: 714 case AliasFilterType.assetType:
682 - return this.getEntitiesByNameFilter(EntityType.ASSET, filter.assetNameFilter, maxItems, 715 + result.entityFilter = deepClone(filter);
  716 + return of(result);
  717 + /*return this.getEntitiesByNameFilter(EntityType.ASSET, filter.assetNameFilter, maxItems,
683 filter.assetType, {ignoreLoading: true, ignoreErrors: true}).pipe( 718 filter.assetType, {ignoreLoading: true, ignoreErrors: true}).pipe(
684 map((entities) => { 719 map((entities) => {
685 if (entities && entities.length || !failOnEmpty) { 720 if (entities && entities.length || !failOnEmpty) {
@@ -690,9 +725,11 @@ export class EntityService { @@ -690,9 +725,11 @@ export class EntityService {
690 } 725 }
691 } 726 }
692 ) 727 )
693 - ); 728 + );*/
694 case AliasFilterType.deviceType: 729 case AliasFilterType.deviceType:
695 - return this.getEntitiesByNameFilter(EntityType.DEVICE, filter.deviceNameFilter, maxItems, 730 + result.entityFilter = deepClone(filter);
  731 + return of(result);
  732 + /*return this.getEntitiesByNameFilter(EntityType.DEVICE, filter.deviceNameFilter, maxItems,
696 filter.deviceType, {ignoreLoading: true, ignoreErrors: true}).pipe( 733 filter.deviceType, {ignoreLoading: true, ignoreErrors: true}).pipe(
697 map((entities) => { 734 map((entities) => {
698 if (entities && entities.length || !failOnEmpty) { 735 if (entities && entities.length || !failOnEmpty) {
@@ -703,9 +740,11 @@ export class EntityService { @@ -703,9 +740,11 @@ export class EntityService {
703 } 740 }
704 } 741 }
705 ) 742 )
706 - ); 743 + );*/
707 case AliasFilterType.entityViewType: 744 case AliasFilterType.entityViewType:
708 - return this.getEntitiesByNameFilter(EntityType.ENTITY_VIEW, filter.entityViewNameFilter, maxItems, 745 + result.entityFilter = deepClone(filter);
  746 + return of(result);
  747 + /*return this.getEntitiesByNameFilter(EntityType.ENTITY_VIEW, filter.entityViewNameFilter, maxItems,
709 filter.entityViewType, {ignoreLoading: true, ignoreErrors: true}).pipe( 748 filter.entityViewType, {ignoreLoading: true, ignoreErrors: true}).pipe(
710 map((entities) => { 749 map((entities) => {
711 if (entities && entities.length || !failOnEmpty) { 750 if (entities && entities.length || !failOnEmpty) {
@@ -716,7 +755,7 @@ export class EntityService { @@ -716,7 +755,7 @@ export class EntityService {
716 } 755 }
717 } 756 }
718 ) 757 )
719 - ); 758 + );*/
720 case AliasFilterType.relationsQuery: 759 case AliasFilterType.relationsQuery:
721 result.stateEntity = filter.rootStateEntity; 760 result.stateEntity = filter.rootStateEntity;
722 let rootEntityType; 761 let rootEntityType;
@@ -730,7 +769,10 @@ export class EntityService { @@ -730,7 +769,10 @@ export class EntityService {
730 } 769 }
731 if (rootEntityType && rootEntityId) { 770 if (rootEntityType && rootEntityId) {
732 const relationQueryRootEntityId = this.resolveAliasEntityId(rootEntityType, rootEntityId); 771 const relationQueryRootEntityId = this.resolveAliasEntityId(rootEntityType, rootEntityId);
733 - const searchQuery: EntityRelationsQuery = { 772 + result.entityFilter = deepClone(filter);
  773 + result.entityFilter.rootEntity = relationQueryRootEntityId;
  774 + return of(result);
  775 + /*const searchQuery: EntityRelationsQuery = {
734 parameters: { 776 parameters: {
735 rootId: relationQueryRootEntityId.id, 777 rootId: relationQueryRootEntityId.id,
736 rootType: relationQueryRootEntityId.entityType as EntityType, 778 rootType: relationQueryRootEntityId.entityType as EntityType,
@@ -757,7 +799,7 @@ export class EntityService { @@ -757,7 +799,7 @@ export class EntityService {
757 return throwError(null); 799 return throwError(null);
758 } 800 }
759 }) 801 })
760 - ); 802 + );*/
761 } else { 803 } else {
762 return of(result); 804 return of(result);
763 } 805 }
@@ -774,7 +816,10 @@ export class EntityService { @@ -774,7 +816,10 @@ export class EntityService {
774 } 816 }
775 if (rootEntityType && rootEntityId) { 817 if (rootEntityType && rootEntityId) {
776 const searchQueryRootEntityId = this.resolveAliasEntityId(rootEntityType, rootEntityId); 818 const searchQueryRootEntityId = this.resolveAliasEntityId(rootEntityType, rootEntityId);
777 - const searchQuery: EntitySearchQuery = { 819 + result.entityFilter = deepClone(filter);
  820 + result.entityFilter.rootEntity = searchQueryRootEntityId;
  821 + return of(result);
  822 + /* const searchQuery: EntitySearchQuery = {
778 parameters: { 823 parameters: {
779 rootId: searchQueryRootEntityId.id, 824 rootId: searchQueryRootEntityId.id,
780 rootType: searchQueryRootEntityId.entityType as EntityType, 825 rootType: searchQueryRootEntityId.entityType as EntityType,
@@ -811,7 +856,7 @@ export class EntityService { @@ -811,7 +856,7 @@ export class EntityService {
811 throw Error(); 856 throw Error();
812 } 857 }
813 }) 858 })
814 - ); 859 + );*/
815 } else { 860 } else {
816 return of(result); 861 return of(result);
817 } 862 }
@@ -819,17 +864,18 @@ export class EntityService { @@ -819,17 +864,18 @@ export class EntityService {
819 } 864 }
820 865
821 public checkEntityAlias(entityAlias: EntityAlias): Observable<boolean> { 866 public checkEntityAlias(entityAlias: EntityAlias): Observable<boolean> {
822 - return this.resolveAliasFilter(entityAlias.filter, null, 1, true).pipe( 867 + return this.resolveAliasFilter(entityAlias.filter, null).pipe(
823 map((result) => { 868 map((result) => {
824 if (result.stateEntity) { 869 if (result.stateEntity) {
825 return true; 870 return true;
826 } else { 871 } else {
827 - const entities = result.entities; 872 + return isDefinedAndNotNull(result.entityFilter);
  873 + /*const entities = result.entities;
828 if (entities && entities.length) { 874 if (entities && entities.length) {
829 return true; 875 return true;
830 } else { 876 } else {
831 return false; 877 return false;
832 - } 878 + }*/
833 } 879 }
834 }), 880 }),
835 catchError(err => of(false)) 881 catchError(err => of(false))
@@ -16,8 +16,8 @@ @@ -16,8 +16,8 @@
16 16
17 import { Inject, Injectable, NgZone } from '@angular/core'; 17 import { Inject, Injectable, NgZone } from '@angular/core';
18 import { 18 import {
19 - AttributesSubscriptionCmd,  
20 - GetHistoryCmd, 19 + AttributesSubscriptionCmd, EntityDataCmd, EntityDataUnsubscribeCmd, EntityDataUpdate,
  20 + GetHistoryCmd, isEntityDataUpdateMsg,
21 SubscriptionCmd, 21 SubscriptionCmd,
22 SubscriptionUpdate, 22 SubscriptionUpdate,
23 SubscriptionUpdateMsg, 23 SubscriptionUpdateMsg,
@@ -25,7 +25,7 @@ import { @@ -25,7 +25,7 @@ import {
25 TelemetryPluginCmdsWrapper, 25 TelemetryPluginCmdsWrapper,
26 TelemetryService, 26 TelemetryService,
27 TelemetrySubscriber, 27 TelemetrySubscriber,
28 - TimeseriesSubscriptionCmd 28 + TimeseriesSubscriptionCmd, WebsocketDataMsg
29 } from '@app/shared/models/telemetry/telemetry.models'; 29 } from '@app/shared/models/telemetry/telemetry.models';
30 import { select, Store } from '@ngrx/store'; 30 import { select, Store } from '@ngrx/store';
31 import { AppState } from '@core/core.state'; 31 import { AppState } from '@core/core.state';
@@ -63,7 +63,7 @@ export class TelemetryWebsocketService implements TelemetryService { @@ -63,7 +63,7 @@ export class TelemetryWebsocketService implements TelemetryService {
63 cmdsWrapper = new TelemetryPluginCmdsWrapper(); 63 cmdsWrapper = new TelemetryPluginCmdsWrapper();
64 telemetryUri: string; 64 telemetryUri: string;
65 65
66 - dataStream: WebSocketSubject<TelemetryPluginCmdsWrapper | SubscriptionUpdateMsg>; 66 + dataStream: WebSocketSubject<TelemetryPluginCmdsWrapper | WebsocketDataMsg>;
67 67
68 constructor(private store: Store<AppState>, 68 constructor(private store: Store<AppState>,
69 private authService: AuthService, 69 private authService: AuthService,
@@ -105,6 +105,8 @@ export class TelemetryWebsocketService implements TelemetryService { @@ -105,6 +105,8 @@ export class TelemetryWebsocketService implements TelemetryService {
105 } 105 }
106 } else if (subscriptionCommand instanceof GetHistoryCmd) { 106 } else if (subscriptionCommand instanceof GetHistoryCmd) {
107 this.cmdsWrapper.historyCmds.push(subscriptionCommand); 107 this.cmdsWrapper.historyCmds.push(subscriptionCommand);
  108 + } else if (subscriptionCommand instanceof EntityDataCmd) {
  109 + this.cmdsWrapper.entityDataCmds.push(subscriptionCommand);
108 } 110 }
109 } 111 }
110 ); 112 );
@@ -123,6 +125,10 @@ export class TelemetryWebsocketService implements TelemetryService { @@ -123,6 +125,10 @@ export class TelemetryWebsocketService implements TelemetryService {
123 } else { 125 } else {
124 this.cmdsWrapper.attrSubCmds.push(subscriptionCommand as AttributesSubscriptionCmd); 126 this.cmdsWrapper.attrSubCmds.push(subscriptionCommand as AttributesSubscriptionCmd);
125 } 127 }
  128 + } else if (subscriptionCommand instanceof EntityDataCmd) {
  129 + const entityDataUnsubscribeCmd = new EntityDataUnsubscribeCmd();
  130 + entityDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
  131 + this.cmdsWrapper.entityDataUnsubscribeCmds.push(entityDataUnsubscribeCmd);
126 } 132 }
127 const cmdId = subscriptionCommand.cmdId; 133 const cmdId = subscriptionCommand.cmdId;
128 if (cmdId) { 134 if (cmdId) {
@@ -223,7 +229,7 @@ export class TelemetryWebsocketService implements TelemetryService { @@ -223,7 +229,7 @@ export class TelemetryWebsocketService implements TelemetryService {
223 229
224 this.dataStream.subscribe((message) => { 230 this.dataStream.subscribe((message) => {
225 this.ngZone.runOutsideAngular(() => { 231 this.ngZone.runOutsideAngular(() => {
226 - this.onMessage(message as SubscriptionUpdateMsg); 232 + this.onMessage(message as WebsocketDataMsg);
227 }); 233 });
228 }, 234 },
229 (error) => { 235 (error) => {
@@ -252,13 +258,21 @@ export class TelemetryWebsocketService implements TelemetryService { @@ -252,13 +258,21 @@ export class TelemetryWebsocketService implements TelemetryService {
252 } 258 }
253 } 259 }
254 260
255 - private onMessage(message: SubscriptionUpdateMsg) { 261 + private onMessage(message: WebsocketDataMsg) {
256 if (message.errorCode) { 262 if (message.errorCode) {
257 this.showWsError(message.errorCode, message.errorMsg); 263 this.showWsError(message.errorCode, message.errorMsg);
258 - } else if (message.subscriptionId) {  
259 - const subscriber = this.subscribersMap.get(message.subscriptionId);  
260 - if (subscriber) {  
261 - subscriber.onData(new SubscriptionUpdate(message)); 264 + } else {
  265 + let subscriber: TelemetrySubscriber;
  266 + if (isEntityDataUpdateMsg(message)) {
  267 + subscriber = this.subscribersMap.get(message.cmdId);
  268 + if (subscriber) {
  269 + subscriber.onEntityData(new EntityDataUpdate(message));
  270 + }
  271 + } else if (message.subscriptionId) {
  272 + subscriber = this.subscribersMap.get(message.subscriptionId);
  273 + if (subscriber) {
  274 + subscriber.onData(new SubscriptionUpdate(message));
  275 + }
262 } 276 }
263 } 277 }
264 this.checkToClose(); 278 this.checkToClose();
@@ -92,6 +92,7 @@ import { WidgetSubscription } from '@core/api/widget-subscription'; @@ -92,6 +92,7 @@ import { WidgetSubscription } from '@core/api/widget-subscription';
92 import { EntityService } from '@core/http/entity.service'; 92 import { EntityService } from '@core/http/entity.service';
93 import { ServicesMap } from '@home/models/services.map'; 93 import { ServicesMap } from '@home/models/services.map';
94 import { ResizeObserver } from '@juggle/resize-observer'; 94 import { ResizeObserver } from '@juggle/resize-observer';
  95 +import { EntityDataService } from '@core/api/entity-data.service';
95 96
96 @Component({ 97 @Component({
97 selector: 'tb-widget', 98 selector: 'tb-widget',
@@ -161,7 +162,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -161,7 +162,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
161 private entityService: EntityService, 162 private entityService: EntityService,
162 private alarmService: AlarmService, 163 private alarmService: AlarmService,
163 private dashboardService: DashboardService, 164 private dashboardService: DashboardService,
164 - private datasourceService: DatasourceService, 165 + // private datasourceService: DatasourceService,
  166 + private entityDataService: EntityDataService,
165 private utils: UtilsService, 167 private utils: UtilsService,
166 private raf: RafService, 168 private raf: RafService,
167 private ngZone: NgZone, 169 private ngZone: NgZone,
@@ -292,7 +294,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -292,7 +294,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
292 this.subscriptionContext.timeService = this.timeService; 294 this.subscriptionContext.timeService = this.timeService;
293 this.subscriptionContext.deviceService = this.deviceService; 295 this.subscriptionContext.deviceService = this.deviceService;
294 this.subscriptionContext.alarmService = this.alarmService; 296 this.subscriptionContext.alarmService = this.alarmService;
295 - this.subscriptionContext.datasourceService = this.datasourceService; 297 + // this.subscriptionContext.datasourceService = this.datasourceService;
  298 + this.subscriptionContext.entityDataService = this.entityDataService;
296 this.subscriptionContext.utils = this.utils; 299 this.subscriptionContext.utils = this.utils;
297 this.subscriptionContext.raf = this.raf; 300 this.subscriptionContext.raf = this.raf;
298 this.subscriptionContext.widgetUtils = this.widgetContext.utils; 301 this.subscriptionContext.widgetUtils = this.widgetContext.utils;
@@ -18,6 +18,7 @@ import { EntityType } from '@shared/models/entity-type.models'; @@ -18,6 +18,7 @@ import { EntityType } from '@shared/models/entity-type.models';
18 import { EntityId } from '@shared/models/id/entity-id'; 18 import { EntityId } from '@shared/models/id/entity-id';
19 import { EntitySearchDirection, EntityTypeFilter } from '@shared/models/relation.models'; 19 import { EntitySearchDirection, EntityTypeFilter } from '@shared/models/relation.models';
20 import { EntityInfo } from './entity.models'; 20 import { EntityInfo } from './entity.models';
  21 +import { EntityFilter } from '@shared/models/query/query.models';
21 22
22 export enum AliasFilterType { 23 export enum AliasFilterType {
23 singleEntity = 'singleEntity', 24 singleEntity = 'singleEntity',
@@ -157,5 +158,6 @@ export interface EntityAliases { @@ -157,5 +158,6 @@ export interface EntityAliases {
157 export interface EntityAliasFilterResult { 158 export interface EntityAliasFilterResult {
158 entities: Array<EntityInfo>; 159 entities: Array<EntityInfo>;
159 stateEntity: boolean; 160 stateEntity: boolean;
  161 + entityFilter: EntityFilter;
160 entityParamName?: string; 162 entityParamName?: string;
161 } 163 }
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { AliasFilterType, EntityFilters } from '@shared/models/alias.models';
  18 +import { EntityId } from '@shared/models/id/entity-id';
  19 +
  20 +export enum EntityKeyType {
  21 + ATTRIBUTE = 'ATTRIBUTE',
  22 + CLIENT_ATTRIBUTE = 'CLIENT_ATTRIBUTE',
  23 + SHARED_ATTRIBUTE = 'SHARED_ATTRIBUTE',
  24 + SERVER_ATTRIBUTE = 'SERVER_ATTRIBUTE',
  25 + TIME_SERIES = 'TIME_SERIES',
  26 + ENTITY_FIELD = 'ENTITY_FIELD'
  27 +}
  28 +
  29 +export interface EntityKey {
  30 + type: EntityKeyType;
  31 + key: string;
  32 +}
  33 +
  34 +export enum FilterPredicateType {
  35 + STRING = 'STRING',
  36 + NUMERIC = 'NUMERIC',
  37 + BOOLEAN = 'BOOLEAN',
  38 + COMPLEX = 'COMPLEX'
  39 +}
  40 +
  41 +export enum StringOperation {
  42 + EQUAL = 'EQUAL',
  43 + NOT_EQUAL = 'NOT_EQUAL',
  44 + STARTS_WITH = 'STARTS_WITH',
  45 + ENDS_WITH = 'ENDS_WITH',
  46 + CONTAINS = 'CONTAINS',
  47 + NOT_CONTAIN = 'NOT_CONTAIN'
  48 +}
  49 +
  50 +export enum NumericOperation {
  51 + EQUAL = 'EQUAL',
  52 + NOT_EQUAL = 'NOT_EQUAL',
  53 + GREATER = 'GREATER',
  54 + LESS = 'LESS',
  55 + GREATER_OR_EQUAL = 'GREATER_OR_EQUAL',
  56 + LESS_OR_EQUAL = 'LESS_OR_EQUAL'
  57 +}
  58 +
  59 +export enum BooleanOperation {
  60 + EQUAL = 'EQUAL',
  61 + NOT_EQUAL = 'NOT_EQUAL'
  62 +}
  63 +
  64 +export enum ComplexOperation {
  65 + AND = 'AND',
  66 + OR = 'OR'
  67 +}
  68 +
  69 +export interface StringFilterPredicate {
  70 + operation: StringOperation;
  71 + value: string;
  72 + ignoreCase: boolean;
  73 +}
  74 +
  75 +export interface NumericFilterPredicate {
  76 + operation: NumericOperation;
  77 + value: number;
  78 +}
  79 +
  80 +export interface BooleanFilterPredicate {
  81 + operation: BooleanOperation;
  82 + value: boolean;
  83 +}
  84 +
  85 +export interface ComplexFilterPredicate {
  86 + operation: ComplexOperation;
  87 + predicates: Array<KeyFilterPredicate>;
  88 +}
  89 +
  90 +export type KeyFilterPredicates = StringFilterPredicate &
  91 + NumericFilterPredicate &
  92 + BooleanFilterPredicate &
  93 + ComplexFilterPredicate;
  94 +
  95 +export interface KeyFilterPredicate extends KeyFilterPredicates {
  96 + type?: FilterPredicateType;
  97 +}
  98 +
  99 +export interface KeyFilter {
  100 + key: EntityKey;
  101 + predicate: KeyFilterPredicate;
  102 +}
  103 +
  104 +export interface EntityFilter extends EntityFilters {
  105 + type?: AliasFilterType;
  106 +}
  107 +
  108 +export enum Direction {
  109 + ASC = 'ASC',
  110 + DESC = 'DESC'
  111 +}
  112 +
  113 +export interface EntityDataSortOrder {
  114 + key: EntityKey;
  115 + direction: Direction;
  116 +}
  117 +
  118 +export interface EntityDataPageLink {
  119 + pageSize: number;
  120 + page: number;
  121 + textSearch?: string;
  122 + sortOrder?: EntityDataSortOrder;
  123 +}
  124 +
  125 +export const defaultEntityDataPageLink: EntityDataPageLink = {
  126 + pageSize: 1024,
  127 + page: 0,
  128 + sortOrder: {
  129 + key: {
  130 + type: EntityKeyType.ENTITY_FIELD,
  131 + key: 'createdTime'
  132 + },
  133 + direction: Direction.DESC
  134 + }
  135 +}
  136 +
  137 +export interface EntityCountQuery {
  138 + entityFilter: EntityFilter;
  139 +}
  140 +
  141 +export interface EntityDataQuery extends EntityCountQuery {
  142 + pageLink: EntityDataPageLink;
  143 + entityFields?: Array<EntityKey>;
  144 + latestValues?: Array<EntityKey>;
  145 + keyFilters?: Array<KeyFilter>;
  146 +}
  147 +
  148 +export interface TsValue {
  149 + ts: number;
  150 + value: string;
  151 +}
  152 +
  153 +export interface EntityData {
  154 + entityId: EntityId;
  155 + latest: {[entityKeyType: string]: {[key: string]: TsValue}};
  156 + timeseries: {[key: string]: Array<TsValue>};
  157 +}
@@ -21,6 +21,8 @@ import { Observable, ReplaySubject, Subject } from 'rxjs'; @@ -21,6 +21,8 @@ import { Observable, ReplaySubject, Subject } from 'rxjs';
21 import { EntityId } from '@shared/models/id/entity-id'; 21 import { EntityId } from '@shared/models/id/entity-id';
22 import { map } from 'rxjs/operators'; 22 import { map } from 'rxjs/operators';
23 import { NgZone } from '@angular/core'; 23 import { NgZone } from '@angular/core';
  24 +import { EntityData, EntityDataQuery } from '@shared/models/query/query.models';
  25 +import { PageData } from '@shared/models/page/page-data';
24 26
25 export enum DataKeyType { 27 export enum DataKeyType {
26 timeseries = 'timeseries', 28 timeseries = 'timeseries',
@@ -79,8 +81,11 @@ export interface AttributeData { @@ -79,8 +81,11 @@ export interface AttributeData {
79 value: any; 81 value: any;
80 } 82 }
81 83
82 -export interface TelemetryPluginCmd { 84 +export interface WebsocketCmd {
83 cmdId: number; 85 cmdId: number;
  86 +}
  87 +
  88 +export interface TelemetryPluginCmd extends WebsocketCmd {
84 keys: string; 89 keys: string;
85 } 90 }
86 91
@@ -124,27 +129,69 @@ export class GetHistoryCmd implements TelemetryPluginCmd { @@ -124,27 +129,69 @@ export class GetHistoryCmd implements TelemetryPluginCmd {
124 agg: AggregationType; 129 agg: AggregationType;
125 } 130 }
126 131
  132 +export interface EntityHistoryCmd {
  133 + keys: Array<string>;
  134 + startTs: number;
  135 + endTs: number;
  136 + interval: number;
  137 + limit: number;
  138 + agg: AggregationType;
  139 +}
  140 +
  141 +export interface LatestValueCmd {
  142 + keys: Array<string>;
  143 +}
  144 +
  145 +export interface TimeSeriesCmd {
  146 + keys: Array<string>;
  147 + startTs: number;
  148 + timeWindow: number;
  149 + interval: number;
  150 + limit: number;
  151 + agg: AggregationType;
  152 +}
  153 +
  154 +export class EntityDataCmd implements WebsocketCmd {
  155 + cmdId: number;
  156 + query: EntityDataQuery;
  157 + historyCmd?: EntityHistoryCmd;
  158 + latestCmd?: LatestValueCmd;
  159 + tsCmd?: TimeSeriesCmd;
  160 +}
  161 +
  162 +export class EntityDataUnsubscribeCmd implements WebsocketCmd {
  163 + cmdId: number;
  164 +}
  165 +
127 export class TelemetryPluginCmdsWrapper { 166 export class TelemetryPluginCmdsWrapper {
128 attrSubCmds: Array<AttributesSubscriptionCmd>; 167 attrSubCmds: Array<AttributesSubscriptionCmd>;
129 tsSubCmds: Array<TimeseriesSubscriptionCmd>; 168 tsSubCmds: Array<TimeseriesSubscriptionCmd>;
130 historyCmds: Array<GetHistoryCmd>; 169 historyCmds: Array<GetHistoryCmd>;
  170 + entityDataCmds: Array<EntityDataCmd>;
  171 + entityDataUnsubscribeCmds: Array<EntityDataUnsubscribeCmd>;
131 172
132 constructor() { 173 constructor() {
133 this.attrSubCmds = []; 174 this.attrSubCmds = [];
134 this.tsSubCmds = []; 175 this.tsSubCmds = [];
135 this.historyCmds = []; 176 this.historyCmds = [];
  177 + this.entityDataCmds = [];
  178 + this.entityDataUnsubscribeCmds = [];
136 } 179 }
137 180
138 public hasCommands(): boolean { 181 public hasCommands(): boolean {
139 return this.tsSubCmds.length > 0 || 182 return this.tsSubCmds.length > 0 ||
140 this.historyCmds.length > 0 || 183 this.historyCmds.length > 0 ||
141 - this.attrSubCmds.length > 0; 184 + this.attrSubCmds.length > 0 ||
  185 + this.entityDataCmds.length > 0 ||
  186 + this.entityDataUnsubscribeCmds.length > 0;
142 } 187 }
143 188
144 public clear() { 189 public clear() {
145 this.attrSubCmds.length = 0; 190 this.attrSubCmds.length = 0;
146 this.tsSubCmds.length = 0; 191 this.tsSubCmds.length = 0;
147 this.historyCmds.length = 0; 192 this.historyCmds.length = 0;
  193 + this.entityDataCmds.length = 0;
  194 + this.entityDataUnsubscribeCmds.length = 0;
148 } 195 }
149 196
150 public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper { 197 public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper {
@@ -155,10 +202,14 @@ export class TelemetryPluginCmdsWrapper { @@ -155,10 +202,14 @@ export class TelemetryPluginCmdsWrapper {
155 preparedWrapper.historyCmds = this.popCmds(this.historyCmds, leftCount); 202 preparedWrapper.historyCmds = this.popCmds(this.historyCmds, leftCount);
156 leftCount -= preparedWrapper.historyCmds.length; 203 leftCount -= preparedWrapper.historyCmds.length;
157 preparedWrapper.attrSubCmds = this.popCmds(this.attrSubCmds, leftCount); 204 preparedWrapper.attrSubCmds = this.popCmds(this.attrSubCmds, leftCount);
  205 + leftCount -= preparedWrapper.attrSubCmds.length;
  206 + preparedWrapper.entityDataCmds = this.popCmds(this.entityDataCmds, leftCount);
  207 + leftCount -= preparedWrapper.entityDataCmds.length;
  208 + preparedWrapper.entityDataUnsubscribeCmds = this.popCmds(this.entityDataUnsubscribeCmds, leftCount);
158 return preparedWrapper; 209 return preparedWrapper;
159 } 210 }
160 211
161 - private popCmds<T extends TelemetryPluginCmd>(cmds: Array<T>, leftCount: number): Array<T> { 212 + private popCmds<T>(cmds: Array<T>, leftCount: number): Array<T> {
162 const toPublish = Math.min(cmds.length, leftCount); 213 const toPublish = Math.min(cmds.length, leftCount);
163 if (toPublish > 0) { 214 if (toPublish > 0) {
164 return cmds.splice(0, toPublish); 215 return cmds.splice(0, toPublish);
@@ -182,6 +233,20 @@ export interface SubscriptionUpdateMsg extends SubscriptionDataHolder { @@ -182,6 +233,20 @@ export interface SubscriptionUpdateMsg extends SubscriptionDataHolder {
182 errorMsg: string; 233 errorMsg: string;
183 } 234 }
184 235
  236 +export interface EntityDataUpdateMsg {
  237 + cmdId: number;
  238 + data?: PageData<EntityData>;
  239 + update?: Array<EntityData>;
  240 + errorCode: number;
  241 + errorMsg: string;
  242 +}
  243 +
  244 +export type WebsocketDataMsg = EntityDataUpdateMsg | SubscriptionUpdateMsg;
  245 +
  246 +export function isEntityDataUpdateMsg(message: WebsocketDataMsg): message is EntityDataUpdateMsg {
  247 + return (message as EntityDataUpdateMsg).cmdId !== undefined;
  248 +}
  249 +
185 export class SubscriptionUpdate implements SubscriptionUpdateMsg { 250 export class SubscriptionUpdate implements SubscriptionUpdateMsg {
186 subscriptionId: number; 251 subscriptionId: number;
187 errorCode: number; 252 errorCode: number;
@@ -231,6 +296,22 @@ export class SubscriptionUpdate implements SubscriptionUpdateMsg { @@ -231,6 +296,22 @@ export class SubscriptionUpdate implements SubscriptionUpdateMsg {
231 } 296 }
232 } 297 }
233 298
  299 +export class EntityDataUpdate implements EntityDataUpdateMsg {
  300 + cmdId: number;
  301 + errorCode: number;
  302 + errorMsg: string;
  303 + data?: PageData<EntityData>;
  304 + update?: Array<EntityData>;
  305 +
  306 + constructor(msg: EntityDataUpdateMsg) {
  307 + this.cmdId = msg.cmdId;
  308 + this.errorCode = msg.errorCode;
  309 + this.errorMsg = msg.errorMsg;
  310 + this.data = msg.data;
  311 + this.update = msg.update;
  312 + }
  313 +}
  314 +
234 export interface TelemetryService { 315 export interface TelemetryService {
235 subscribe(subscriber: TelemetrySubscriber); 316 subscribe(subscriber: TelemetrySubscriber);
236 unsubscribe(subscriber: TelemetrySubscriber); 317 unsubscribe(subscriber: TelemetrySubscriber);
@@ -239,13 +320,15 @@ export interface TelemetryService { @@ -239,13 +320,15 @@ export interface TelemetryService {
239 export class TelemetrySubscriber { 320 export class TelemetrySubscriber {
240 321
241 private dataSubject = new ReplaySubject<SubscriptionUpdate>(1); 322 private dataSubject = new ReplaySubject<SubscriptionUpdate>(1);
  323 + private entityDataSubject = new ReplaySubject<EntityDataUpdate>(1);
242 private reconnectSubject = new Subject(); 324 private reconnectSubject = new Subject();
243 325
244 private zone: NgZone; 326 private zone: NgZone;
245 327
246 - public subscriptionCommands: Array<TelemetryPluginCmd>; 328 + public subscriptionCommands: Array<WebsocketCmd>;
247 329
248 public data$ = this.dataSubject.asObservable(); 330 public data$ = this.dataSubject.asObservable();
  331 + public entityData$ = this.entityDataSubject.asObservable();
249 public reconnect$ = this.reconnectSubject.asObservable(); 332 public reconnect$ = this.reconnectSubject.asObservable();
250 333
251 public static createEntityAttributesSubscription(telemetryService: TelemetryService, 334 public static createEntityAttributesSubscription(telemetryService: TelemetryService,
@@ -284,6 +367,7 @@ export class TelemetrySubscriber { @@ -284,6 +367,7 @@ export class TelemetrySubscriber {
284 367
285 public complete() { 368 public complete() {
286 this.dataSubject.complete(); 369 this.dataSubject.complete();
  370 + this.entityDataSubject.complete();
287 this.reconnectSubject.complete(); 371 this.reconnectSubject.complete();
288 } 372 }
289 373
@@ -292,8 +376,9 @@ export class TelemetrySubscriber { @@ -292,8 +376,9 @@ export class TelemetrySubscriber {
292 let keys: string[]; 376 let keys: string[];
293 const cmd = this.subscriptionCommands.find((command) => command.cmdId === cmdId); 377 const cmd = this.subscriptionCommands.find((command) => command.cmdId === cmdId);
294 if (cmd) { 378 if (cmd) {
295 - if (cmd.keys && cmd.keys.length) {  
296 - keys = cmd.keys.split(','); 379 + const telemetryPluginCmd = cmd as TelemetryPluginCmd;
  380 + if (telemetryPluginCmd.keys && telemetryPluginCmd.keys.length) {
  381 + keys = telemetryPluginCmd.keys.split(',');
297 } 382 }
298 } 383 }
299 message.prepareData(keys); 384 message.prepareData(keys);
@@ -308,6 +393,18 @@ export class TelemetrySubscriber { @@ -308,6 +393,18 @@ export class TelemetrySubscriber {
308 } 393 }
309 } 394 }
310 395
  396 + public onEntityData(message: EntityDataUpdate) {
  397 + if (this.zone) {
  398 + this.zone.run(
  399 + () => {
  400 + this.entityDataSubject.next(message);
  401 + }
  402 + );
  403 + } else {
  404 + this.entityDataSubject.next(message);
  405 + }
  406 + }
  407 +
311 public onReconnected() { 408 public onReconnected() {
312 this.reconnectSubject.next(); 409 this.reconnectSubject.next();
313 } 410 }
@@ -23,6 +23,7 @@ import { AlarmSearchStatus } from '@shared/models/alarm.models'; @@ -23,6 +23,7 @@ import { AlarmSearchStatus } from '@shared/models/alarm.models';
23 import { DataKeyType } from './telemetry/telemetry.models'; 23 import { DataKeyType } from './telemetry/telemetry.models';
24 import { EntityId } from '@shared/models/id/entity-id'; 24 import { EntityId } from '@shared/models/id/entity-id';
25 import * as moment_ from 'moment'; 25 import * as moment_ from 'moment';
  26 +import { EntityDataPageLink, EntityFilter, KeyFilter } from '@shared/models/query/query.models';
26 27
27 export enum widgetType { 28 export enum widgetType {
28 timeseries = 'timeseries', 29 timeseries = 'timeseries',
@@ -263,6 +264,10 @@ export interface Datasource { @@ -263,6 +264,10 @@ export interface Datasource {
263 entityDescription?: string; 264 entityDescription?: string;
264 generated?: boolean; 265 generated?: boolean;
265 isAdditional?: boolean; 266 isAdditional?: boolean;
  267 + pageLink?: EntityDataPageLink;
  268 + keyFilters?: Array<KeyFilter>;
  269 + entityFilter?: EntityFilter;
  270 + dataKeyStartIndex?: number;
266 [key: string]: any; 271 [key: string]: any;
267 } 272 }
268 273