Commit 53bc87150c5dd62bc143259181decdc971e4e658
1 parent
5aa055d7
UI: Entity data query - initial implementation
Showing
12 changed files
with
1623 additions
and
62 deletions
@@ -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 |