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 | 17 | import { AliasInfo, IAliasController, StateControllerHolder, StateEntityInfo } from '@core/api/widget-api.models'; |
18 | 18 | import { forkJoin, Observable, of, ReplaySubject, Subject } from 'rxjs'; |
19 | 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 | 21 | import { EntityService } from '@core/http/entity.service'; |
22 | 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 | 24 | import { EntityInfo } from '@shared/models/entity.models'; |
25 | 25 | import { map } from 'rxjs/operators'; |
26 | +import { defaultEntityDataPageLink } from '@shared/models/query/query.models'; | |
26 | 27 | |
27 | 28 | export class AliasController implements IAliasController { |
28 | 29 | |
... | ... | @@ -176,6 +177,92 @@ export class AliasController implements IAliasController { |
176 | 177 | datasource.aliasName = aliasInfo.alias; |
177 | 178 | if (aliasInfo.resolveMultiple && !isSingle) { |
178 | 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 | 266 | const resolvedEntities = aliasInfo.resolvedEntities; |
180 | 267 | if (resolvedEntities && resolvedEntities.length) { |
181 | 268 | const datasources: Array<Datasource> = []; |
... | ... | @@ -242,7 +329,7 @@ export class AliasController implements IAliasController { |
242 | 329 | } else { |
243 | 330 | return of([datasource]); |
244 | 331 | } |
245 | - } | |
332 | + } */ | |
246 | 333 | |
247 | 334 | resolveAlarmSource(alarmSource: Datasource): Observable<Datasource> { |
248 | 335 | return this.resolveDatasource(alarmSource, true).pipe( |
... | ... | @@ -279,6 +366,48 @@ export class AliasController implements IAliasController { |
279 | 366 | arrayOfDatasources.forEach((datasourcesArray) => { |
280 | 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 | 411 | result.sort((d1, d2) => { |
283 | 412 | const i1 = d1.generated ? 1 : 0; |
284 | 413 | const i2 = d2.generated ? 1 : 0; |
... | ... | @@ -317,9 +446,9 @@ export class AliasController implements IAliasController { |
317 | 446 | return result; |
318 | 447 | }) |
319 | 448 | ); |
320 | - } | |
449 | + }*/ | |
321 | 450 | |
322 | - private updateDatasourceKeyLabels(datasource: Datasource) { | |
451 | + /* private updateDatasourceKeyLabels(datasource: Datasource) { | |
323 | 452 | datasource.dataKeys.forEach((dataKey) => { |
324 | 453 | this.updateDataKeyLabel(dataKey, datasource); |
325 | 454 | }); |
... | ... | @@ -330,7 +459,7 @@ export class AliasController implements IAliasController { |
330 | 459 | dataKey.pattern = deepClone(dataKey.label); |
331 | 460 | } |
332 | 461 | dataKey.label = createLabelFromDatasource(datasource, dataKey.pattern); |
333 | - } | |
462 | + }*/ | |
334 | 463 | |
335 | 464 | getInstantAliasInfo(aliasId: string): AliasInfo { |
336 | 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 | 41 | import { EntityInfo } from '@app/shared/models/entity.models'; |
42 | 42 | import { IDashboardComponent } from '@home/models/dashboard-component.models'; |
43 | 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 | 48 | export interface TimewindowFunctions { |
46 | 49 | onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; |
... | ... | @@ -76,6 +79,7 @@ export interface WidgetActionsApi { |
76 | 79 | export interface AliasInfo { |
77 | 80 | alias?: string; |
78 | 81 | stateEntity?: boolean; |
82 | + entityFilter?: EntityFilter; | |
79 | 83 | currentEntity?: EntityInfo; |
80 | 84 | selectedId?: string; |
81 | 85 | resolvedEntities?: Array<EntityInfo>; |
... | ... | @@ -169,7 +173,8 @@ export class WidgetSubscriptionContext { |
169 | 173 | timeService: TimeService; |
170 | 174 | deviceService: DeviceService; |
171 | 175 | alarmService: AlarmService; |
172 | - datasourceService: DatasourceService; | |
176 | + // datasourceService: DatasourceService; | |
177 | + entityDataService: EntityDataService; | |
173 | 178 | utils: UtilsService; |
174 | 179 | raf: RafService; |
175 | 180 | widgetUtils: IWidgetUtils; |
... | ... | @@ -197,6 +202,8 @@ export interface WidgetSubscriptionOptions { |
197 | 202 | alarmsMaxCountLoad?: number; |
198 | 203 | alarmsFetchSize?: number; |
199 | 204 | datasources?: Array<Datasource>; |
205 | + keyFilters?: Array<KeyFilter>; | |
206 | + pageLink?: EntityDataPageLink; | |
200 | 207 | targetDeviceAliasIds?: Array<string>; |
201 | 208 | targetDeviceIds?: Array<string>; |
202 | 209 | useDashboardTimewindow?: boolean; |
... | ... | @@ -230,6 +237,9 @@ export interface IWidgetSubscription { |
230 | 237 | useDashboardTimewindow: boolean; |
231 | 238 | |
232 | 239 | legendData: LegendData; |
240 | + | |
241 | + datasourcePages?: PageData<Datasource>[]; | |
242 | + dataPages?: PageData<Array<DatasourceData>>[]; | |
233 | 243 | datasources?: Array<Datasource>; |
234 | 244 | data?: Array<DatasourceData>; |
235 | 245 | hiddenData?: Array<{data: DataSet}>; | ... | ... |
... | ... | @@ -47,13 +47,16 @@ import { Observable, ReplaySubject, Subject, throwError } from 'rxjs'; |
47 | 47 | import { CancelAnimationFrame } from '@core/services/raf.service'; |
48 | 48 | import { EntityType } from '@shared/models/entity-type.models'; |
49 | 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 | 51 | import { AlarmSourceListener } from '@core/http/alarm.service'; |
52 | 52 | import { DatasourceListener } from '@core/api/datasource.service'; |
53 | 53 | import { EntityId } from '@app/shared/models/id/entity-id'; |
54 | 54 | import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
55 | 55 | import { entityFields } from '@shared/models/entity.models'; |
56 | 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 | 61 | const moment = moment_; |
59 | 62 | |
... | ... | @@ -70,9 +73,14 @@ export class WidgetSubscription implements IWidgetSubscription { |
70 | 73 | subscriptionTimewindow: SubscriptionTimewindow; |
71 | 74 | useDashboardTimewindow: boolean; |
72 | 75 | |
76 | + datasourcePages: PageData<Datasource>[]; | |
77 | + dataPages: PageData<Array<DatasourceData>>[]; | |
78 | + entityDataListeners: Array<EntityDataListener>; | |
79 | + configuredDatasources: Array<Datasource>; | |
80 | + | |
73 | 81 | data: Array<DatasourceData>; |
74 | 82 | datasources: Array<Datasource>; |
75 | - datasourceListeners: Array<DatasourceListener>; | |
83 | + // datasourceListeners: Array<DatasourceListener>; | |
76 | 84 | hiddenData: Array<DataSetHolder>; |
77 | 85 | legendData: LegendData; |
78 | 86 | legendConfig: LegendConfig; |
... | ... | @@ -197,8 +205,13 @@ export class WidgetSubscription implements IWidgetSubscription { |
197 | 205 | this.callbacks.legendDataUpdated = this.callbacks.legendDataUpdated || (() => {}); |
198 | 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 | 215 | this.data = []; |
203 | 216 | this.hiddenData = []; |
204 | 217 | this.originalTimewindow = null; |
... | ... | @@ -336,6 +349,35 @@ export class WidgetSubscription implements IWidgetSubscription { |
336 | 349 | this.loadStDiff().subscribe(() => { |
337 | 350 | if (!this.ctx.aliasController) { |
338 | 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 | 381 | this.configureData(); |
340 | 382 | initDataSubscriptionSubject.next(); |
341 | 383 | initDataSubscriptionSubject.complete(); |
... | ... | @@ -358,9 +400,9 @@ export class WidgetSubscription implements IWidgetSubscription { |
358 | 400 | } |
359 | 401 | }); |
360 | 402 | return initDataSubscriptionSubject.asObservable(); |
361 | - } | |
403 | + } */ | |
362 | 404 | |
363 | - private configureData() { | |
405 | + /* private configureData() { | |
364 | 406 | const additionalDatasources: Datasource[] = []; |
365 | 407 | let dataIndex = 0; |
366 | 408 | let additionalKeysNumber = 0; |
... | ... | @@ -448,9 +490,19 @@ export class WidgetSubscription implements IWidgetSubscription { |
448 | 490 | if (this.displayLegend) { |
449 | 491 | this.legendData.keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label)); |
450 | 492 | } |
451 | - } | |
493 | + } */ | |
452 | 494 | |
453 | 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 | 506 | for (let i = 0; i < this.data.length; i++) { |
455 | 507 | this.data[i].data = []; |
456 | 508 | this.hiddenData[i].data = []; |
... | ... | @@ -463,7 +515,7 @@ export class WidgetSubscription implements IWidgetSubscription { |
463 | 515 | } |
464 | 516 | } |
465 | 517 | this.onDataUpdated(); |
466 | - } | |
518 | + }*/ | |
467 | 519 | |
468 | 520 | getFirstEntityInfo(): SubscriptionEntityInfo { |
469 | 521 | let entityId: EntityId; |
... | ... | @@ -756,6 +808,87 @@ export class WidgetSubscription implements IWidgetSubscription { |
756 | 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 | 892 | let index = 0; |
760 | 893 | let forceUpdate = !this.datasources.length; |
761 | 894 | this.datasources.forEach((datasource) => { |
... | ... | @@ -814,7 +947,7 @@ export class WidgetSubscription implements IWidgetSubscription { |
814 | 947 | this.onDataUpdated(); |
815 | 948 | } |
816 | 949 | } |
817 | - } | |
950 | + } */ | |
818 | 951 | |
819 | 952 | private alarmsSubscribe() { |
820 | 953 | this.notifyDataLoading(); |
... | ... | @@ -855,6 +988,20 @@ export class WidgetSubscription implements IWidgetSubscription { |
855 | 988 | if (this.type === widgetType.alarm) { |
856 | 989 | this.alarmsUnsubscribe(); |
857 | 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 | 1005 | this.datasourceListeners.forEach((listener) => { |
859 | 1006 | this.ctx.datasourceService.unsubscribeFromDatasource(listener); |
860 | 1007 | }); |
... | ... | @@ -862,7 +1009,7 @@ export class WidgetSubscription implements IWidgetSubscription { |
862 | 1009 | this.resetData(); |
863 | 1010 | } |
864 | 1011 | } |
865 | - } | |
1012 | + } */ | |
866 | 1013 | |
867 | 1014 | private alarmsUnsubscribe() { |
868 | 1015 | if (this.alarmSourceListener) { |
... | ... | @@ -970,7 +1117,180 @@ export class WidgetSubscription implements IWidgetSubscription { |
970 | 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 | 1294 | for (let x = 0; x < this.datasourceListeners.length; x++) { |
975 | 1295 | this.datasources[x].dataReceived = this.datasources[x].dataReceived === true; |
976 | 1296 | if (this.datasourceListeners[x].datasourceIndex === datasourceIndex && sourceData.data.length > 0) { |
... | ... | @@ -1010,7 +1330,7 @@ export class WidgetSubscription implements IWidgetSubscription { |
1010 | 1330 | } |
1011 | 1331 | this.onDataUpdated(detectChanges); |
1012 | 1332 | } |
1013 | - } | |
1333 | + } */ | |
1014 | 1334 | |
1015 | 1335 | private alarmsUpdated(alarms: Array<AlarmInfo>) { |
1016 | 1336 | this.notifyDataLoaded(); | ... | ... |
... | ... | @@ -52,7 +52,7 @@ import { |
52 | 52 | EntitySearchQuery |
53 | 53 | } from '@shared/models/relation.models'; |
54 | 54 | import { EntityRelationService } from '@core/http/entity-relation.service'; |
55 | -import { isDefined } from '@core/utils'; | |
55 | +import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils'; | |
56 | 56 | import { Asset, AssetSearchQuery } from '@shared/models/asset.models'; |
57 | 57 | import { Device, DeviceCredentialsType, DeviceSearchQuery } from '@shared/models/device.models'; |
58 | 58 | import { EntityViewSearchQuery } from '@shared/models/entity-view.models'; |
... | ... | @@ -604,10 +604,11 @@ export class EntityService { |
604 | 604 | |
605 | 605 | public resolveAlias(entityAlias: EntityAlias, stateParams: StateParams): Observable<AliasInfo> { |
606 | 606 | const filter = entityAlias.filter; |
607 | - return this.resolveAliasFilter(filter, stateParams, -1, false).pipe( | |
607 | + return this.resolveAliasFilter(filter, stateParams).pipe( | |
608 | 608 | map((result) => { |
609 | 609 | const aliasInfo: AliasInfo = { |
610 | 610 | alias: entityAlias.alias, |
611 | + entityFilter: result.entityFilter, | |
611 | 612 | stateEntity: result.stateEntity, |
612 | 613 | entityParamName: result.entityParamName, |
613 | 614 | resolveMultiple: filter.resolveMultiple |
... | ... | @@ -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 | 644 | const result: EntityAliasFilterResult = { |
628 | 645 | entities: [], |
646 | + entityFilter: null, | |
629 | 647 | stateEntity: false |
630 | 648 | }; |
631 | 649 | if (filter.stateEntityParamName && filter.stateEntityParamName.length) { |
... | ... | @@ -636,14 +654,21 @@ export class EntityService { |
636 | 654 | switch (filter.type) { |
637 | 655 | case AliasFilterType.singleEntity: |
638 | 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 | 663 | map((entity) => { |
641 | 664 | result.entities = this.entitiesToEntitiesInfo([entity]); |
642 | 665 | return result; |
643 | 666 | } |
644 | - )); | |
667 | + ));*/ | |
645 | 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 | 672 | map((entities) => { |
648 | 673 | if (entities && entities.length || !failOnEmpty) { |
649 | 674 | result.entities = this.entitiesToEntitiesInfo(entities); |
... | ... | @@ -652,9 +677,11 @@ export class EntityService { |
652 | 677 | throw new Error(); |
653 | 678 | } |
654 | 679 | } |
655 | - )); | |
680 | + ));*/ | |
656 | 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 | 685 | '', {ignoreLoading: true, ignoreErrors: true}).pipe( |
659 | 686 | map((entities) => { |
660 | 687 | if (entities && entities.length || !failOnEmpty) { |
... | ... | @@ -665,11 +692,17 @@ export class EntityService { |
665 | 692 | } |
666 | 693 | } |
667 | 694 | ) |
668 | - ); | |
695 | + );*/ | |
669 | 696 | case AliasFilterType.stateEntity: |
670 | 697 | result.stateEntity = true; |
671 | 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 | 706 | map((entity) => { |
674 | 707 | result.entities = this.entitiesToEntitiesInfo([entity]); |
675 | 708 | return result; |
... | ... | @@ -677,9 +710,11 @@ export class EntityService { |
677 | 710 | )); |
678 | 711 | } else { |
679 | 712 | return of(result); |
680 | - } | |
713 | + }*/ | |
681 | 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 | 718 | filter.assetType, {ignoreLoading: true, ignoreErrors: true}).pipe( |
684 | 719 | map((entities) => { |
685 | 720 | if (entities && entities.length || !failOnEmpty) { |
... | ... | @@ -690,9 +725,11 @@ export class EntityService { |
690 | 725 | } |
691 | 726 | } |
692 | 727 | ) |
693 | - ); | |
728 | + );*/ | |
694 | 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 | 733 | filter.deviceType, {ignoreLoading: true, ignoreErrors: true}).pipe( |
697 | 734 | map((entities) => { |
698 | 735 | if (entities && entities.length || !failOnEmpty) { |
... | ... | @@ -703,9 +740,11 @@ export class EntityService { |
703 | 740 | } |
704 | 741 | } |
705 | 742 | ) |
706 | - ); | |
743 | + );*/ | |
707 | 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 | 748 | filter.entityViewType, {ignoreLoading: true, ignoreErrors: true}).pipe( |
710 | 749 | map((entities) => { |
711 | 750 | if (entities && entities.length || !failOnEmpty) { |
... | ... | @@ -716,7 +755,7 @@ export class EntityService { |
716 | 755 | } |
717 | 756 | } |
718 | 757 | ) |
719 | - ); | |
758 | + );*/ | |
720 | 759 | case AliasFilterType.relationsQuery: |
721 | 760 | result.stateEntity = filter.rootStateEntity; |
722 | 761 | let rootEntityType; |
... | ... | @@ -730,7 +769,10 @@ export class EntityService { |
730 | 769 | } |
731 | 770 | if (rootEntityType && rootEntityId) { |
732 | 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 | 776 | parameters: { |
735 | 777 | rootId: relationQueryRootEntityId.id, |
736 | 778 | rootType: relationQueryRootEntityId.entityType as EntityType, |
... | ... | @@ -757,7 +799,7 @@ export class EntityService { |
757 | 799 | return throwError(null); |
758 | 800 | } |
759 | 801 | }) |
760 | - ); | |
802 | + );*/ | |
761 | 803 | } else { |
762 | 804 | return of(result); |
763 | 805 | } |
... | ... | @@ -774,7 +816,10 @@ export class EntityService { |
774 | 816 | } |
775 | 817 | if (rootEntityType && rootEntityId) { |
776 | 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 | 823 | parameters: { |
779 | 824 | rootId: searchQueryRootEntityId.id, |
780 | 825 | rootType: searchQueryRootEntityId.entityType as EntityType, |
... | ... | @@ -811,7 +856,7 @@ export class EntityService { |
811 | 856 | throw Error(); |
812 | 857 | } |
813 | 858 | }) |
814 | - ); | |
859 | + );*/ | |
815 | 860 | } else { |
816 | 861 | return of(result); |
817 | 862 | } |
... | ... | @@ -819,17 +864,18 @@ export class EntityService { |
819 | 864 | } |
820 | 865 | |
821 | 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 | 868 | map((result) => { |
824 | 869 | if (result.stateEntity) { |
825 | 870 | return true; |
826 | 871 | } else { |
827 | - const entities = result.entities; | |
872 | + return isDefinedAndNotNull(result.entityFilter); | |
873 | + /*const entities = result.entities; | |
828 | 874 | if (entities && entities.length) { |
829 | 875 | return true; |
830 | 876 | } else { |
831 | 877 | return false; |
832 | - } | |
878 | + }*/ | |
833 | 879 | } |
834 | 880 | }), |
835 | 881 | catchError(err => of(false)) | ... | ... |
... | ... | @@ -16,8 +16,8 @@ |
16 | 16 | |
17 | 17 | import { Inject, Injectable, NgZone } from '@angular/core'; |
18 | 18 | import { |
19 | - AttributesSubscriptionCmd, | |
20 | - GetHistoryCmd, | |
19 | + AttributesSubscriptionCmd, EntityDataCmd, EntityDataUnsubscribeCmd, EntityDataUpdate, | |
20 | + GetHistoryCmd, isEntityDataUpdateMsg, | |
21 | 21 | SubscriptionCmd, |
22 | 22 | SubscriptionUpdate, |
23 | 23 | SubscriptionUpdateMsg, |
... | ... | @@ -25,7 +25,7 @@ import { |
25 | 25 | TelemetryPluginCmdsWrapper, |
26 | 26 | TelemetryService, |
27 | 27 | TelemetrySubscriber, |
28 | - TimeseriesSubscriptionCmd | |
28 | + TimeseriesSubscriptionCmd, WebsocketDataMsg | |
29 | 29 | } from '@app/shared/models/telemetry/telemetry.models'; |
30 | 30 | import { select, Store } from '@ngrx/store'; |
31 | 31 | import { AppState } from '@core/core.state'; |
... | ... | @@ -63,7 +63,7 @@ export class TelemetryWebsocketService implements TelemetryService { |
63 | 63 | cmdsWrapper = new TelemetryPluginCmdsWrapper(); |
64 | 64 | telemetryUri: string; |
65 | 65 | |
66 | - dataStream: WebSocketSubject<TelemetryPluginCmdsWrapper | SubscriptionUpdateMsg>; | |
66 | + dataStream: WebSocketSubject<TelemetryPluginCmdsWrapper | WebsocketDataMsg>; | |
67 | 67 | |
68 | 68 | constructor(private store: Store<AppState>, |
69 | 69 | private authService: AuthService, |
... | ... | @@ -105,6 +105,8 @@ export class TelemetryWebsocketService implements TelemetryService { |
105 | 105 | } |
106 | 106 | } else if (subscriptionCommand instanceof GetHistoryCmd) { |
107 | 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 | 125 | } else { |
124 | 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 | 133 | const cmdId = subscriptionCommand.cmdId; |
128 | 134 | if (cmdId) { |
... | ... | @@ -223,7 +229,7 @@ export class TelemetryWebsocketService implements TelemetryService { |
223 | 229 | |
224 | 230 | this.dataStream.subscribe((message) => { |
225 | 231 | this.ngZone.runOutsideAngular(() => { |
226 | - this.onMessage(message as SubscriptionUpdateMsg); | |
232 | + this.onMessage(message as WebsocketDataMsg); | |
227 | 233 | }); |
228 | 234 | }, |
229 | 235 | (error) => { |
... | ... | @@ -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 | 262 | if (message.errorCode) { |
257 | 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 | 278 | this.checkToClose(); | ... | ... |
... | ... | @@ -92,6 +92,7 @@ import { WidgetSubscription } from '@core/api/widget-subscription'; |
92 | 92 | import { EntityService } from '@core/http/entity.service'; |
93 | 93 | import { ServicesMap } from '@home/models/services.map'; |
94 | 94 | import { ResizeObserver } from '@juggle/resize-observer'; |
95 | +import { EntityDataService } from '@core/api/entity-data.service'; | |
95 | 96 | |
96 | 97 | @Component({ |
97 | 98 | selector: 'tb-widget', |
... | ... | @@ -161,7 +162,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
161 | 162 | private entityService: EntityService, |
162 | 163 | private alarmService: AlarmService, |
163 | 164 | private dashboardService: DashboardService, |
164 | - private datasourceService: DatasourceService, | |
165 | + // private datasourceService: DatasourceService, | |
166 | + private entityDataService: EntityDataService, | |
165 | 167 | private utils: UtilsService, |
166 | 168 | private raf: RafService, |
167 | 169 | private ngZone: NgZone, |
... | ... | @@ -292,7 +294,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
292 | 294 | this.subscriptionContext.timeService = this.timeService; |
293 | 295 | this.subscriptionContext.deviceService = this.deviceService; |
294 | 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 | 299 | this.subscriptionContext.utils = this.utils; |
297 | 300 | this.subscriptionContext.raf = this.raf; |
298 | 301 | this.subscriptionContext.widgetUtils = this.widgetContext.utils; | ... | ... |
... | ... | @@ -18,6 +18,7 @@ import { EntityType } from '@shared/models/entity-type.models'; |
18 | 18 | import { EntityId } from '@shared/models/id/entity-id'; |
19 | 19 | import { EntitySearchDirection, EntityTypeFilter } from '@shared/models/relation.models'; |
20 | 20 | import { EntityInfo } from './entity.models'; |
21 | +import { EntityFilter } from '@shared/models/query/query.models'; | |
21 | 22 | |
22 | 23 | export enum AliasFilterType { |
23 | 24 | singleEntity = 'singleEntity', |
... | ... | @@ -157,5 +158,6 @@ export interface EntityAliases { |
157 | 158 | export interface EntityAliasFilterResult { |
158 | 159 | entities: Array<EntityInfo>; |
159 | 160 | stateEntity: boolean; |
161 | + entityFilter: EntityFilter; | |
160 | 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 | 21 | import { EntityId } from '@shared/models/id/entity-id'; |
22 | 22 | import { map } from 'rxjs/operators'; |
23 | 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 | 27 | export enum DataKeyType { |
26 | 28 | timeseries = 'timeseries', |
... | ... | @@ -79,8 +81,11 @@ export interface AttributeData { |
79 | 81 | value: any; |
80 | 82 | } |
81 | 83 | |
82 | -export interface TelemetryPluginCmd { | |
84 | +export interface WebsocketCmd { | |
83 | 85 | cmdId: number; |
86 | +} | |
87 | + | |
88 | +export interface TelemetryPluginCmd extends WebsocketCmd { | |
84 | 89 | keys: string; |
85 | 90 | } |
86 | 91 | |
... | ... | @@ -124,27 +129,69 @@ export class GetHistoryCmd implements TelemetryPluginCmd { |
124 | 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 | 166 | export class TelemetryPluginCmdsWrapper { |
128 | 167 | attrSubCmds: Array<AttributesSubscriptionCmd>; |
129 | 168 | tsSubCmds: Array<TimeseriesSubscriptionCmd>; |
130 | 169 | historyCmds: Array<GetHistoryCmd>; |
170 | + entityDataCmds: Array<EntityDataCmd>; | |
171 | + entityDataUnsubscribeCmds: Array<EntityDataUnsubscribeCmd>; | |
131 | 172 | |
132 | 173 | constructor() { |
133 | 174 | this.attrSubCmds = []; |
134 | 175 | this.tsSubCmds = []; |
135 | 176 | this.historyCmds = []; |
177 | + this.entityDataCmds = []; | |
178 | + this.entityDataUnsubscribeCmds = []; | |
136 | 179 | } |
137 | 180 | |
138 | 181 | public hasCommands(): boolean { |
139 | 182 | return this.tsSubCmds.length > 0 || |
140 | 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 | 189 | public clear() { |
145 | 190 | this.attrSubCmds.length = 0; |
146 | 191 | this.tsSubCmds.length = 0; |
147 | 192 | this.historyCmds.length = 0; |
193 | + this.entityDataCmds.length = 0; | |
194 | + this.entityDataUnsubscribeCmds.length = 0; | |
148 | 195 | } |
149 | 196 | |
150 | 197 | public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper { |
... | ... | @@ -155,10 +202,14 @@ export class TelemetryPluginCmdsWrapper { |
155 | 202 | preparedWrapper.historyCmds = this.popCmds(this.historyCmds, leftCount); |
156 | 203 | leftCount -= preparedWrapper.historyCmds.length; |
157 | 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 | 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 | 213 | const toPublish = Math.min(cmds.length, leftCount); |
163 | 214 | if (toPublish > 0) { |
164 | 215 | return cmds.splice(0, toPublish); |
... | ... | @@ -182,6 +233,20 @@ export interface SubscriptionUpdateMsg extends SubscriptionDataHolder { |
182 | 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 | 250 | export class SubscriptionUpdate implements SubscriptionUpdateMsg { |
186 | 251 | subscriptionId: number; |
187 | 252 | errorCode: number; |
... | ... | @@ -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 | 315 | export interface TelemetryService { |
235 | 316 | subscribe(subscriber: TelemetrySubscriber); |
236 | 317 | unsubscribe(subscriber: TelemetrySubscriber); |
... | ... | @@ -239,13 +320,15 @@ export interface TelemetryService { |
239 | 320 | export class TelemetrySubscriber { |
240 | 321 | |
241 | 322 | private dataSubject = new ReplaySubject<SubscriptionUpdate>(1); |
323 | + private entityDataSubject = new ReplaySubject<EntityDataUpdate>(1); | |
242 | 324 | private reconnectSubject = new Subject(); |
243 | 325 | |
244 | 326 | private zone: NgZone; |
245 | 327 | |
246 | - public subscriptionCommands: Array<TelemetryPluginCmd>; | |
328 | + public subscriptionCommands: Array<WebsocketCmd>; | |
247 | 329 | |
248 | 330 | public data$ = this.dataSubject.asObservable(); |
331 | + public entityData$ = this.entityDataSubject.asObservable(); | |
249 | 332 | public reconnect$ = this.reconnectSubject.asObservable(); |
250 | 333 | |
251 | 334 | public static createEntityAttributesSubscription(telemetryService: TelemetryService, |
... | ... | @@ -284,6 +367,7 @@ export class TelemetrySubscriber { |
284 | 367 | |
285 | 368 | public complete() { |
286 | 369 | this.dataSubject.complete(); |
370 | + this.entityDataSubject.complete(); | |
287 | 371 | this.reconnectSubject.complete(); |
288 | 372 | } |
289 | 373 | |
... | ... | @@ -292,8 +376,9 @@ export class TelemetrySubscriber { |
292 | 376 | let keys: string[]; |
293 | 377 | const cmd = this.subscriptionCommands.find((command) => command.cmdId === cmdId); |
294 | 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 | 384 | message.prepareData(keys); |
... | ... | @@ -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 | 408 | public onReconnected() { |
312 | 409 | this.reconnectSubject.next(); |
313 | 410 | } | ... | ... |
... | ... | @@ -23,6 +23,7 @@ import { AlarmSearchStatus } from '@shared/models/alarm.models'; |
23 | 23 | import { DataKeyType } from './telemetry/telemetry.models'; |
24 | 24 | import { EntityId } from '@shared/models/id/entity-id'; |
25 | 25 | import * as moment_ from 'moment'; |
26 | +import { EntityDataPageLink, EntityFilter, KeyFilter } from '@shared/models/query/query.models'; | |
26 | 27 | |
27 | 28 | export enum widgetType { |
28 | 29 | timeseries = 'timeseries', |
... | ... | @@ -263,6 +264,10 @@ export interface Datasource { |
263 | 264 | entityDescription?: string; |
264 | 265 | generated?: boolean; |
265 | 266 | isAdditional?: boolean; |
267 | + pageLink?: EntityDataPageLink; | |
268 | + keyFilters?: Array<KeyFilter>; | |
269 | + entityFilter?: EntityFilter; | |
270 | + dataKeyStartIndex?: number; | |
266 | 271 | [key: string]: any; |
267 | 272 | } |
268 | 273 | ... | ... |