12 changed files with 1623 additions and 62 deletions
@ -0,0 +1,670 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models'; |
||||
|
import { AggregationType, SubscriptionTimewindow, YEAR } from '@shared/models/time/time.models'; |
||||
|
import { SubscriptionDataKey } from '@core/api/datasource-subcription'; |
||||
|
import { |
||||
|
EntityData, |
||||
|
EntityDataPageLink, |
||||
|
EntityFilter, |
||||
|
EntityKey, |
||||
|
EntityKeyType, |
||||
|
KeyFilter, |
||||
|
TsValue |
||||
|
} from '@shared/models/query/query.models'; |
||||
|
import { |
||||
|
DataKeyType, |
||||
|
EntityDataCmd, |
||||
|
SubscriptionData, |
||||
|
SubscriptionDataHolder, |
||||
|
TelemetryService, |
||||
|
TelemetrySubscriber |
||||
|
} from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { EntityDataListener } from '@core/api/entity-data.service'; |
||||
|
import { deepClone, isDefinedAndNotNull, isObject, objectHashCode } from '@core/utils'; |
||||
|
import { PageData } from '@shared/models/page/page-data'; |
||||
|
import { DataAggregator } from '@core/api/data-aggregator'; |
||||
|
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
||||
|
import { EntityType } from '@shared/models/entity-type.models'; |
||||
|
import Timeout = NodeJS.Timeout; |
||||
|
|
||||
|
export interface EntityDataSubscriptionOptions { |
||||
|
datasourceType: DatasourceType; |
||||
|
dataKeys: Array<SubscriptionDataKey>; |
||||
|
type: widgetType; |
||||
|
entityFilter?: EntityFilter; |
||||
|
pageLink?: EntityDataPageLink; |
||||
|
keyFilters?: Array<KeyFilter>; |
||||
|
subscriptionTimewindow?: SubscriptionTimewindow; |
||||
|
} |
||||
|
|
||||
|
declare type DataKeyFunction = (time: number, prevValue: any) => any; |
||||
|
declare type DataKeyPostFunction = (time: number, value: any, prevValue: any, timePrev: number, prevOrigValue: any) => any; |
||||
|
declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void; |
||||
|
|
||||
|
export class EntityDataSubscription { |
||||
|
|
||||
|
private listeners: Array<EntityDataListener> = []; |
||||
|
private datasourceType: DatasourceType = this.entityDataSubscriptionOptions.datasourceType; |
||||
|
|
||||
|
private history = this.entityDataSubscriptionOptions.subscriptionTimewindow && |
||||
|
isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); |
||||
|
|
||||
|
private realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && |
||||
|
isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs); |
||||
|
|
||||
|
private subscriber: TelemetrySubscriber; |
||||
|
|
||||
|
private attrFields: Array<EntityKey>; |
||||
|
private tsFields: Array<EntityKey>; |
||||
|
private latestValues: Array<EntityKey>; |
||||
|
|
||||
|
private pageData: PageData<EntityData>; |
||||
|
private subsTw: SubscriptionTimewindow; |
||||
|
private dataAggregators: Array<DataAggregator>; |
||||
|
private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {} |
||||
|
private datasourceData: {[index: number]: {[key: string]: DataSetHolder}}; |
||||
|
private datasourceOrigData: {[index: number]: {[key: string]: DataSetHolder}}; |
||||
|
private entityIdToDataIndex: {[id: string]: number}; |
||||
|
|
||||
|
private frequency: number; |
||||
|
private tickScheduledTime = 0; |
||||
|
private tickElapsed = 0; |
||||
|
private timer: Timeout; |
||||
|
|
||||
|
constructor(private entityDataSubscriptionOptions: EntityDataSubscriptionOptions, |
||||
|
private telemetryService: TelemetryService, |
||||
|
private utils: UtilsService) { |
||||
|
this.initializeSubscription(); |
||||
|
} |
||||
|
|
||||
|
private initializeSubscription() { |
||||
|
for (let i = 0; i < this.entityDataSubscriptionOptions.dataKeys.length; i++) { |
||||
|
const dataKey = deepClone(this.entityDataSubscriptionOptions.dataKeys[i]); |
||||
|
dataKey.index = i; |
||||
|
if (this.datasourceType === DatasourceType.function) { |
||||
|
if (!dataKey.func) { |
||||
|
dataKey.func = new Function('time', 'prevValue', dataKey.funcBody) as DataKeyFunction; |
||||
|
} |
||||
|
} else { |
||||
|
if (dataKey.postFuncBody && !dataKey.postFunc) { |
||||
|
dataKey.postFunc = new Function('time', 'value', 'prevValue', 'timePrev', 'prevOrigValue', |
||||
|
dataKey.postFuncBody) as DataKeyPostFunction; |
||||
|
} |
||||
|
} |
||||
|
let key: string; |
||||
|
if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { |
||||
|
if (this.datasourceType === DatasourceType.function) { |
||||
|
key = `${dataKey.name}_${dataKey.index}_${dataKey.type}`; |
||||
|
} else { |
||||
|
key = `${dataKey.name}_${dataKey.type}`; |
||||
|
} |
||||
|
let dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>; |
||||
|
if (!dataKeysList) { |
||||
|
dataKeysList = []; |
||||
|
this.dataKeys[key] = dataKeysList; |
||||
|
} |
||||
|
dataKeysList.push(dataKey); |
||||
|
} else { |
||||
|
key = String(objectHashCode(dataKey)); |
||||
|
this.dataKeys[key] = dataKey; |
||||
|
} |
||||
|
dataKey.key = key; |
||||
|
} |
||||
|
if (this.datasourceType === DatasourceType.function) { |
||||
|
this.frequency = 1000; |
||||
|
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { |
||||
|
this.frequency = Math.min(this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.interval, 5000); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public addListener(listener: EntityDataListener) { |
||||
|
this.listeners.push(listener); |
||||
|
if (this.history) { |
||||
|
this.start(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public hasListeners(): boolean { |
||||
|
return this.listeners.length > 0; |
||||
|
} |
||||
|
|
||||
|
public removeListener(listener: EntityDataListener) { |
||||
|
this.listeners.splice(this.listeners.indexOf(listener), 1); |
||||
|
} |
||||
|
|
||||
|
public syncListener(listener: EntityDataListener) { |
||||
|
if (this.pageData) { |
||||
|
let key: string; |
||||
|
let dataKey: SubscriptionDataKey; |
||||
|
const data: Array<Array<DataSetHolder>> = []; |
||||
|
for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { |
||||
|
data[dataIndex] = []; |
||||
|
for (key of Object.keys(this.dataKeys)) { |
||||
|
if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { |
||||
|
const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>; |
||||
|
for (let i = 0; i < dataKeysList.length; i++) { |
||||
|
dataKey = dataKeysList[i]; |
||||
|
const datasourceKey = `${key}_${i}`; |
||||
|
data[dataIndex][dataKey.index] = this.datasourceData[dataIndex][datasourceKey]; |
||||
|
} |
||||
|
} else { |
||||
|
dataKey = this.dataKeys[key] as SubscriptionDataKey; |
||||
|
data[dataIndex][dataKey.index] = this.datasourceData[dataIndex][key]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
listener.dataLoaded(this.pageData, data, listener.configDatasourceIndex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public unsubscribe() { |
||||
|
if (this.timer) { |
||||
|
clearTimeout(this.timer); |
||||
|
this.timer = null; |
||||
|
} |
||||
|
if (this.datasourceType === DatasourceType.entity) { |
||||
|
if (this.subscriber) { |
||||
|
this.subscriber.unsubscribe(); |
||||
|
this.subscriber = null; |
||||
|
} |
||||
|
} |
||||
|
if (this.dataAggregators) { |
||||
|
this.dataAggregators.forEach((aggregator) => { |
||||
|
aggregator.destroy(); |
||||
|
}) |
||||
|
this.dataAggregators = null; |
||||
|
} |
||||
|
this.pageData = null; |
||||
|
} |
||||
|
|
||||
|
public start() { |
||||
|
if (this.history && !this.hasListeners()) { |
||||
|
return; |
||||
|
} |
||||
|
this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; |
||||
|
if (this.datasourceType === DatasourceType.entity) { |
||||
|
const entityFields: Array<EntityKey> = |
||||
|
this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.entityField).map( |
||||
|
dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name }) |
||||
|
); |
||||
|
if (!entityFields.find(key => key.key === 'name')) { |
||||
|
entityFields.push({ |
||||
|
type: EntityKeyType.ENTITY_FIELD, |
||||
|
key: 'name' |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
this.attrFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.attribute).map( |
||||
|
dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name }) |
||||
|
); |
||||
|
|
||||
|
this.tsFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.timeseries).map( |
||||
|
dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) |
||||
|
); |
||||
|
|
||||
|
this.latestValues = this.attrFields.concat(this.tsFields); |
||||
|
|
||||
|
this.subscriber = new TelemetrySubscriber(this.telemetryService); |
||||
|
const command = new EntityDataCmd(); |
||||
|
|
||||
|
command.query = { |
||||
|
entityFilter: this.entityDataSubscriptionOptions.entityFilter, |
||||
|
pageLink: this.entityDataSubscriptionOptions.pageLink, |
||||
|
keyFilters: this.entityDataSubscriptionOptions.keyFilters, |
||||
|
entityFields, |
||||
|
latestValues: this.latestValues |
||||
|
}; |
||||
|
|
||||
|
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { |
||||
|
if (this.tsFields.length > 0) { |
||||
|
if (this.history) { |
||||
|
command.historyCmd = { |
||||
|
keys: this.tsFields.map(key => key.key), |
||||
|
startTs: this.subsTw.fixedWindow.startTimeMs, |
||||
|
endTs: this.subsTw.fixedWindow.endTimeMs, |
||||
|
interval: this.subsTw.aggregation.interval, |
||||
|
limit: this.subsTw.aggregation.limit, |
||||
|
agg: this.subsTw.aggregation.type |
||||
|
}; |
||||
|
if (this.subsTw.aggregation.stateData) { |
||||
|
command.historyCmd.startTs -= YEAR; |
||||
|
} |
||||
|
} else { |
||||
|
command.tsCmd = { |
||||
|
keys: this.tsFields.map(key => key.key), |
||||
|
startTs: this.subsTw.startTs, |
||||
|
timeWindow: this.subsTw.aggregation.timeWindow, |
||||
|
interval: this.subsTw.aggregation.interval, |
||||
|
limit: this.subsTw.aggregation.limit, |
||||
|
agg: this.subsTw.aggregation.type |
||||
|
} |
||||
|
if (this.subsTw.aggregation.stateData) { |
||||
|
command.historyCmd = { |
||||
|
keys: this.tsFields.map(key => key.key), |
||||
|
startTs: this.subsTw.startTs - YEAR, |
||||
|
endTs: this.subsTw.startTs, |
||||
|
interval: this.subsTw.aggregation.interval, |
||||
|
limit: this.subsTw.aggregation.limit, |
||||
|
agg: this.subsTw.aggregation.type |
||||
|
}; |
||||
|
} |
||||
|
this.subscriber.reconnect$.subscribe(() => { |
||||
|
let newSubsTw: SubscriptionTimewindow = null; |
||||
|
this.listeners.forEach((listener) => { |
||||
|
if (!newSubsTw) { |
||||
|
newSubsTw = listener.updateRealtimeSubscription(); |
||||
|
} else { |
||||
|
listener.setRealtimeSubscription(newSubsTw); |
||||
|
} |
||||
|
}); |
||||
|
this.subsTw = newSubsTw; |
||||
|
command.tsCmd.startTs = this.subsTw.startTs; |
||||
|
command.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; |
||||
|
command.tsCmd.interval = this.subsTw.aggregation.interval; |
||||
|
command.tsCmd.limit = this.subsTw.aggregation.limit; |
||||
|
command.tsCmd.agg = this.subsTw.aggregation.type; |
||||
|
if (this.subsTw.aggregation.stateData) { |
||||
|
command.historyCmd.startTs = this.subsTw.startTs - YEAR; |
||||
|
command.historyCmd.endTs = this.subsTw.startTs; |
||||
|
command.historyCmd.interval = this.subsTw.aggregation.interval; |
||||
|
command.historyCmd.limit = this.subsTw.aggregation.limit; |
||||
|
command.historyCmd.agg = this.subsTw.aggregation.type; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { |
||||
|
if (this.latestValues.length > 0) { |
||||
|
command.latestCmd = { |
||||
|
keys: this.latestValues.map(key => key.key) |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
this.subscriber.subscriptionCommands.push(command); |
||||
|
|
||||
|
this.subscriber.entityData$.subscribe( |
||||
|
(entityDataUpdate) => { |
||||
|
if (entityDataUpdate.data) { |
||||
|
this.onPageData(entityDataUpdate.data); |
||||
|
} else if (entityDataUpdate.update) { |
||||
|
this.onDataUpdate(entityDataUpdate.update); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
this.subscriber.subscribe(); |
||||
|
} else if (this.datasourceType === DatasourceType.function) { |
||||
|
const entityData: EntityData = { |
||||
|
entityId: { |
||||
|
id: NULL_UUID, |
||||
|
entityType: EntityType.DEVICE |
||||
|
}, |
||||
|
timeseries: {}, |
||||
|
latest: {} |
||||
|
}; |
||||
|
const name = DatasourceType.function; |
||||
|
entityData.latest[EntityKeyType.ENTITY_FIELD] = { |
||||
|
name: {ts: Date.now(), value: name} |
||||
|
}; |
||||
|
const pageData: PageData<EntityData> = { |
||||
|
data: [entityData], |
||||
|
hasNext: false, |
||||
|
totalElements: 1, |
||||
|
totalPages: 1 |
||||
|
}; |
||||
|
this.onPageData(pageData); |
||||
|
this.tickScheduledTime = this.utils.currentPerfTime(); |
||||
|
if (this.history) { |
||||
|
this.onTick(true); |
||||
|
} else { |
||||
|
this.timer = setTimeout(this.onTick.bind(this, true), 0); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private onPageData(pageData: PageData<EntityData>) { |
||||
|
if (this.dataAggregators) { |
||||
|
this.dataAggregators.forEach((aggregator) => { |
||||
|
aggregator.destroy(); |
||||
|
}) |
||||
|
this.dataAggregators = null; |
||||
|
} |
||||
|
this.datasourceData = []; |
||||
|
this.dataAggregators = []; |
||||
|
this.entityIdToDataIndex = {}; |
||||
|
let tsKeyNames; |
||||
|
if (this.datasourceType === DatasourceType.function) { |
||||
|
tsKeyNames = []; |
||||
|
for (const key of Object.keys(this.dataKeys)) { |
||||
|
const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>; |
||||
|
dataKeysList.forEach((subscriptionDataKey) => { |
||||
|
tsKeyNames.push(`${subscriptionDataKey.name}_${subscriptionDataKey.index}`); |
||||
|
}); |
||||
|
} |
||||
|
} else { |
||||
|
tsKeyNames = this.tsFields.map(field => field.key); |
||||
|
} |
||||
|
for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) { |
||||
|
const entityData = pageData.data[dataIndex]; |
||||
|
this.entityIdToDataIndex[entityData.entityId.id] = dataIndex; |
||||
|
this.datasourceData[dataIndex] = {}; |
||||
|
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { |
||||
|
if (this.datasourceType === DatasourceType.function) { |
||||
|
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames, |
||||
|
DataKeyType.function, dataIndex, this.notifyListeners.bind(this)); |
||||
|
} else if (!this.history && tsKeyNames.length) { |
||||
|
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames, |
||||
|
DataKeyType.timeseries, dataIndex, this.notifyListeners.bind(this)); |
||||
|
} |
||||
|
} |
||||
|
for (const key of Object.keys(this.dataKeys)) { |
||||
|
const dataKey = this.dataKeys[key]; |
||||
|
if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { |
||||
|
const dataKeysList = dataKey as Array<SubscriptionDataKey>; |
||||
|
for (let index = 0; index < dataKeysList.length; index++) { |
||||
|
this.datasourceData[dataIndex][key + '_' + index] = { |
||||
|
data: [] |
||||
|
}; |
||||
|
} |
||||
|
} else { |
||||
|
this.datasourceData[dataIndex][key] = { |
||||
|
data: [] |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
this.datasourceOrigData = deepClone(this.datasourceData); |
||||
|
|
||||
|
const data: Array<Array<DataSetHolder>> = []; |
||||
|
for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) { |
||||
|
const entityData = pageData.data[dataIndex]; |
||||
|
this.processEntityData(entityData, dataIndex, false, |
||||
|
(data1, dataIndex1, dataKeyIndex) => { |
||||
|
if (!data[dataIndex1]) { |
||||
|
data[dataIndex1] = []; |
||||
|
} |
||||
|
data[dataIndex1][dataKeyIndex] = data1; |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
this.listeners.forEach((listener) => { |
||||
|
listener.dataLoaded(pageData, data, |
||||
|
listener.configDatasourceIndex); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private onDataUpdate(update: Array<EntityData>) { |
||||
|
for (const entityData of update) { |
||||
|
const dataIndex = this.entityIdToDataIndex[entityData.entityId.id]; |
||||
|
this.processEntityData(entityData, dataIndex, true, this.notifyListeners.bind(this)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private notifyListeners(data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) { |
||||
|
this.listeners.forEach((listener) => { |
||||
|
listener.dataUpdated(data, |
||||
|
listener.configDatasourceIndex, |
||||
|
dataIndex, dataKeyIndex, detectChanges); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private processEntityData(entityData: EntityData, dataIndex: number, aggregate: boolean, |
||||
|
dataUpdatedCb: DataUpdatedCb) { |
||||
|
if (this.entityDataSubscriptionOptions.type === widgetType.latest && entityData.latest) { |
||||
|
for (const type of Object.keys(entityData.latest)) { |
||||
|
const subscriptionData = this.toSubscriptionData(entityData.latest[type], false); |
||||
|
this.onData(subscriptionData, type, dataIndex, true, dataUpdatedCb); |
||||
|
} |
||||
|
} |
||||
|
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && entityData.timeseries) { |
||||
|
const subscriptionData = this.toSubscriptionData(entityData.timeseries, true); |
||||
|
if (aggregate) { |
||||
|
this.dataAggregators[dataIndex].onData({data: subscriptionData}, false, false, true); |
||||
|
} else { |
||||
|
this.onData(subscriptionData, DataKeyType.timeseries, dataIndex, true, dataUpdatedCb); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private onData(sourceData: SubscriptionData, type: string, dataIndex: number, detectChanges: boolean, |
||||
|
dataUpdatedCb: DataUpdatedCb) { |
||||
|
for (const keyName of Object.keys(sourceData)) { |
||||
|
const keyData = sourceData[keyName]; |
||||
|
const key = `${keyName}_${type}`; |
||||
|
const dataKeyList = this.dataKeys[key] as Array<SubscriptionDataKey>; |
||||
|
for (let keyIndex = 0; dataKeyList && keyIndex < dataKeyList.length; keyIndex++) { |
||||
|
const datasourceKey = `${key}_${keyIndex}`; |
||||
|
if (this.datasourceData[dataIndex][datasourceKey].data) { |
||||
|
const dataKey = dataKeyList[keyIndex]; |
||||
|
const data: DataSet = []; |
||||
|
let prevSeries: [number, any]; |
||||
|
let prevOrigSeries: [number, any]; |
||||
|
let datasourceKeyData: DataSet; |
||||
|
let datasourceOrigKeyData: DataSet; |
||||
|
let update = false; |
||||
|
if (this.realtime) { |
||||
|
datasourceKeyData = []; |
||||
|
datasourceOrigKeyData = []; |
||||
|
} else { |
||||
|
datasourceKeyData = this.datasourceData[dataIndex][datasourceKey].data; |
||||
|
datasourceOrigKeyData = this.datasourceOrigData[dataIndex][datasourceKey].data; |
||||
|
} |
||||
|
if (datasourceKeyData.length > 0) { |
||||
|
prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; |
||||
|
prevOrigSeries = datasourceOrigKeyData[datasourceOrigKeyData.length - 1]; |
||||
|
} else { |
||||
|
prevSeries = [0, 0]; |
||||
|
prevOrigSeries = [0, 0]; |
||||
|
} |
||||
|
this.datasourceOrigData[dataIndex][datasourceKey].data = []; |
||||
|
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { |
||||
|
keyData.forEach((keySeries) => { |
||||
|
let series = keySeries; |
||||
|
const time = series[0]; |
||||
|
this.datasourceOrigData[dataIndex][datasourceKey].data.push(series); |
||||
|
let value = this.convertValue(series[1]); |
||||
|
if (dataKey.postFunc) { |
||||
|
value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); |
||||
|
} |
||||
|
prevOrigSeries = series; |
||||
|
series = [time, value]; |
||||
|
data.push(series); |
||||
|
prevSeries = series; |
||||
|
}); |
||||
|
update = true; |
||||
|
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { |
||||
|
if (keyData.length > 0) { |
||||
|
let series = keyData[0]; |
||||
|
const time = series[0]; |
||||
|
this.datasourceOrigData[dataIndex][datasourceKey].data.push(series); |
||||
|
let value = this.convertValue(series[1]); |
||||
|
if (dataKey.postFunc) { |
||||
|
value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); |
||||
|
} |
||||
|
series = [time, value]; |
||||
|
data.push(series); |
||||
|
} |
||||
|
update = true; |
||||
|
} |
||||
|
if (update) { |
||||
|
this.datasourceData[datasourceKey].data = data; |
||||
|
dataUpdatedCb(this.datasourceData[dataIndex][datasourceKey], dataIndex, dataKey.index, detectChanges); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private isNumeric(val: any): boolean { |
||||
|
return (val - parseFloat( val ) + 1) >= 0; |
||||
|
} |
||||
|
|
||||
|
private convertValue(val: string): any { |
||||
|
if (val && this.isNumeric(val)) { |
||||
|
return Number(val); |
||||
|
} else { |
||||
|
return val; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private toSubscriptionData(sourceData: {[key: string]: TsValue | TsValue[]}, isTs: boolean): SubscriptionData { |
||||
|
const subsData: SubscriptionData = {}; |
||||
|
for (const keyName of Object.keys(sourceData)) { |
||||
|
const values = sourceData[keyName]; |
||||
|
const dataSet: [number, any][] = []; |
||||
|
if (isTs) { |
||||
|
(values as TsValue[]).forEach((keySeries) => { |
||||
|
dataSet.push([keySeries.ts, keySeries.value]); |
||||
|
}); |
||||
|
} else { |
||||
|
const tsValue = values as TsValue; |
||||
|
dataSet.push([tsValue.ts, tsValue.value]); |
||||
|
} |
||||
|
subsData[keyName] = dataSet; |
||||
|
} |
||||
|
return subsData; |
||||
|
} |
||||
|
|
||||
|
private createRealtimeDataAggregator(subsTw: SubscriptionTimewindow, |
||||
|
tsKeyNames: Array<string>, |
||||
|
dataKeyType: DataKeyType, |
||||
|
dataIndex: number, |
||||
|
dataUpdatedCb: DataUpdatedCb): DataAggregator { |
||||
|
return new DataAggregator( |
||||
|
(data, detectChanges) => { |
||||
|
this.onData(data, dataKeyType, dataIndex, detectChanges, dataUpdatedCb); |
||||
|
}, |
||||
|
tsKeyNames, |
||||
|
subsTw.startTs, |
||||
|
subsTw.aggregation.limit, |
||||
|
subsTw.aggregation.type, |
||||
|
subsTw.aggregation.timeWindow, |
||||
|
subsTw.aggregation.interval, |
||||
|
subsTw.aggregation.stateData, |
||||
|
this.utils |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private generateSeries(dataKey: SubscriptionDataKey, index: number, startTime: number, endTime: number): [number, any][] { |
||||
|
const data: [number, any][] = []; |
||||
|
let prevSeries: [number, any]; |
||||
|
const datasourceDataKey = `${dataKey.key}_${index}`; |
||||
|
const datasourceKeyData = this.datasourceData[0][datasourceDataKey].data; |
||||
|
if (datasourceKeyData.length > 0) { |
||||
|
prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; |
||||
|
} else { |
||||
|
prevSeries = [0, 0]; |
||||
|
} |
||||
|
for (let time = startTime; time <= endTime && (this.timer || this.history); time += this.frequency) { |
||||
|
const value = dataKey.func(time, prevSeries[1]); |
||||
|
const series: [number, any] = [time, value]; |
||||
|
data.push(series); |
||||
|
prevSeries = series; |
||||
|
} |
||||
|
if (data.length > 0) { |
||||
|
dataKey.lastUpdateTime = data[data.length - 1][0]; |
||||
|
} |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
private generateLatest(dataKey: SubscriptionDataKey, detectChanges: boolean) { |
||||
|
let prevSeries: [number, any]; |
||||
|
const datasourceKeyData = this.datasourceData[0][dataKey.key].data; |
||||
|
if (datasourceKeyData.length > 0) { |
||||
|
prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; |
||||
|
} else { |
||||
|
prevSeries = [0, 0]; |
||||
|
} |
||||
|
const time = Date.now(); |
||||
|
const value = dataKey.func(time, prevSeries[1]); |
||||
|
const series: [number, any] = [time, value]; |
||||
|
this.datasourceData[0][dataKey.key].data = [series]; |
||||
|
this.listeners.forEach( |
||||
|
(listener) => { |
||||
|
listener.dataUpdated(this.datasourceData[0][dataKey.key], |
||||
|
listener.configDatasourceIndex, |
||||
|
0, |
||||
|
dataKey.index, detectChanges); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private onTick(detectChanges: boolean) { |
||||
|
const now = this.utils.currentPerfTime(); |
||||
|
this.tickElapsed += now - this.tickScheduledTime; |
||||
|
this.tickScheduledTime = now; |
||||
|
|
||||
|
if (this.timer) { |
||||
|
clearTimeout(this.timer); |
||||
|
} |
||||
|
let key: string; |
||||
|
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { |
||||
|
let startTime: number; |
||||
|
let endTime: number; |
||||
|
let delta: number; |
||||
|
const generatedData: SubscriptionDataHolder = { |
||||
|
data: {} |
||||
|
}; |
||||
|
if (!this.history) { |
||||
|
delta = Math.floor(this.tickElapsed / this.frequency); |
||||
|
} |
||||
|
const deltaElapsed = this.history ? this.frequency : delta * this.frequency; |
||||
|
this.tickElapsed = this.tickElapsed - deltaElapsed; |
||||
|
for (key of Object.keys(this.dataKeys)) { |
||||
|
const dataKeyList = this.dataKeys[key] as Array<SubscriptionDataKey>; |
||||
|
for (let index = 0; index < dataKeyList.length && (this.timer || this.history); index ++) { |
||||
|
const dataKey = dataKeyList[index]; |
||||
|
if (!startTime) { |
||||
|
if (this.realtime) { |
||||
|
if (dataKey.lastUpdateTime) { |
||||
|
startTime = dataKey.lastUpdateTime + this.frequency; |
||||
|
endTime = dataKey.lastUpdateTime + deltaElapsed; |
||||
|
} else { |
||||
|
startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs; |
||||
|
endTime = startTime + this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs + this.frequency; |
||||
|
if (this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type === AggregationType.NONE) { |
||||
|
const time = endTime - this.frequency * this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.limit; |
||||
|
startTime = Math.max(time, startTime); |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs; |
||||
|
endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs; |
||||
|
} |
||||
|
} |
||||
|
generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, index, startTime, endTime); |
||||
|
} |
||||
|
} |
||||
|
if (this.dataAggregators && this.dataAggregators.length) { |
||||
|
this.dataAggregators[0].onData(generatedData, true, this.history, detectChanges); |
||||
|
} |
||||
|
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { |
||||
|
for (key of Object.keys(this.dataKeys)) { |
||||
|
this.generateLatest(this.dataKeys[key] as SubscriptionDataKey, detectChanges); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!this.history) { |
||||
|
this.timer = setTimeout(this.onTick.bind(this, true), this.frequency); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,108 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { DataSetHolder, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models'; |
||||
|
import { SubscriptionTimewindow } from '@shared/models/time/time.models'; |
||||
|
import { EntityData, EntityDataPageLink, EntityFilter, KeyFilter } from '@shared/models/query/query.models'; |
||||
|
import { PageData } from '@shared/models/page/page-data'; |
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { SubscriptionDataKey } from '@core/api/datasource-subcription'; |
||||
|
import { deepClone, objectHashCode } from '@core/utils'; |
||||
|
import { EntityDataSubscription, EntityDataSubscriptionOptions } from '@core/api/entity-data-subscription'; |
||||
|
|
||||
|
export interface EntityDataListener { |
||||
|
subscriptionType: widgetType; |
||||
|
subscriptionTimewindow: SubscriptionTimewindow; |
||||
|
configDatasource: Datasource; |
||||
|
configDatasourceIndex: number; |
||||
|
dataLoaded: (pageData: PageData<EntityData>, data: Array<Array<DataSetHolder>>, datasourceIndex: number) => void; |
||||
|
dataUpdated: (data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void; |
||||
|
updateRealtimeSubscription: () => SubscriptionTimewindow; |
||||
|
setRealtimeSubscription: (subscriptionTimewindow: SubscriptionTimewindow) => void; |
||||
|
entityDataSubscriptionKey?: number; |
||||
|
} |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root' |
||||
|
}) |
||||
|
export class EntityDataService { |
||||
|
|
||||
|
private subscriptions: {[entityDataSubscriptionKey: string]: EntityDataSubscription} = {}; |
||||
|
|
||||
|
constructor(private telemetryService: TelemetryWebsocketService, |
||||
|
private utils: UtilsService) {} |
||||
|
|
||||
|
public subscribeToEntityData(listener: EntityDataListener) { |
||||
|
const datasource = listener.configDatasource; |
||||
|
if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !datasource.pageLink)) { |
||||
|
return; |
||||
|
} |
||||
|
const subscriptionDataKeys: Array<SubscriptionDataKey> = []; |
||||
|
datasource.dataKeys.forEach((dataKey) => { |
||||
|
const subscriptionDataKey: SubscriptionDataKey = { |
||||
|
name: dataKey.name, |
||||
|
type: dataKey.type, |
||||
|
funcBody: dataKey.funcBody, |
||||
|
postFuncBody: dataKey.postFuncBody |
||||
|
}; |
||||
|
subscriptionDataKeys.push(subscriptionDataKey); |
||||
|
}); |
||||
|
|
||||
|
const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = { |
||||
|
datasourceType: datasource.type, |
||||
|
dataKeys: subscriptionDataKeys, |
||||
|
type: listener.subscriptionType |
||||
|
}; |
||||
|
|
||||
|
if (listener.subscriptionType === widgetType.timeseries) { |
||||
|
entityDataSubscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); |
||||
|
} |
||||
|
if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { |
||||
|
entityDataSubscriptionOptions.entityFilter = datasource.entityFilter; |
||||
|
entityDataSubscriptionOptions.pageLink = datasource.pageLink; |
||||
|
entityDataSubscriptionOptions.keyFilters = datasource.keyFilters; |
||||
|
} |
||||
|
listener.entityDataSubscriptionKey = objectHashCode(entityDataSubscriptionOptions); |
||||
|
let subscription: EntityDataSubscription; |
||||
|
if (this.subscriptions[listener.entityDataSubscriptionKey]) { |
||||
|
subscription = this.subscriptions[listener.entityDataSubscriptionKey]; |
||||
|
subscription.syncListener(listener); |
||||
|
} else { |
||||
|
subscription = new EntityDataSubscription(entityDataSubscriptionOptions, |
||||
|
this.telemetryService, this.utils); |
||||
|
this.subscriptions[listener.entityDataSubscriptionKey] = subscription; |
||||
|
subscription.start(); |
||||
|
} |
||||
|
subscription.addListener(listener); |
||||
|
} |
||||
|
|
||||
|
public unsubscribeFromDatasource(listener: EntityDataListener) { |
||||
|
if (listener.entityDataSubscriptionKey) { |
||||
|
const subscription = this.subscriptions[listener.entityDataSubscriptionKey]; |
||||
|
if (subscription) { |
||||
|
subscription.removeListener(listener); |
||||
|
if (!subscription.hasListeners()) { |
||||
|
subscription.unsubscribe(); |
||||
|
delete this.subscriptions[listener.entityDataSubscriptionKey]; |
||||
|
} |
||||
|
} |
||||
|
listener.entityDataSubscriptionKey = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,157 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { AliasFilterType, EntityFilters } from '@shared/models/alias.models'; |
||||
|
import { EntityId } from '@shared/models/id/entity-id'; |
||||
|
|
||||
|
export enum EntityKeyType { |
||||
|
ATTRIBUTE = 'ATTRIBUTE', |
||||
|
CLIENT_ATTRIBUTE = 'CLIENT_ATTRIBUTE', |
||||
|
SHARED_ATTRIBUTE = 'SHARED_ATTRIBUTE', |
||||
|
SERVER_ATTRIBUTE = 'SERVER_ATTRIBUTE', |
||||
|
TIME_SERIES = 'TIME_SERIES', |
||||
|
ENTITY_FIELD = 'ENTITY_FIELD' |
||||
|
} |
||||
|
|
||||
|
export interface EntityKey { |
||||
|
type: EntityKeyType; |
||||
|
key: string; |
||||
|
} |
||||
|
|
||||
|
export enum FilterPredicateType { |
||||
|
STRING = 'STRING', |
||||
|
NUMERIC = 'NUMERIC', |
||||
|
BOOLEAN = 'BOOLEAN', |
||||
|
COMPLEX = 'COMPLEX' |
||||
|
} |
||||
|
|
||||
|
export enum StringOperation { |
||||
|
EQUAL = 'EQUAL', |
||||
|
NOT_EQUAL = 'NOT_EQUAL', |
||||
|
STARTS_WITH = 'STARTS_WITH', |
||||
|
ENDS_WITH = 'ENDS_WITH', |
||||
|
CONTAINS = 'CONTAINS', |
||||
|
NOT_CONTAIN = 'NOT_CONTAIN' |
||||
|
} |
||||
|
|
||||
|
export enum NumericOperation { |
||||
|
EQUAL = 'EQUAL', |
||||
|
NOT_EQUAL = 'NOT_EQUAL', |
||||
|
GREATER = 'GREATER', |
||||
|
LESS = 'LESS', |
||||
|
GREATER_OR_EQUAL = 'GREATER_OR_EQUAL', |
||||
|
LESS_OR_EQUAL = 'LESS_OR_EQUAL' |
||||
|
} |
||||
|
|
||||
|
export enum BooleanOperation { |
||||
|
EQUAL = 'EQUAL', |
||||
|
NOT_EQUAL = 'NOT_EQUAL' |
||||
|
} |
||||
|
|
||||
|
export enum ComplexOperation { |
||||
|
AND = 'AND', |
||||
|
OR = 'OR' |
||||
|
} |
||||
|
|
||||
|
export interface StringFilterPredicate { |
||||
|
operation: StringOperation; |
||||
|
value: string; |
||||
|
ignoreCase: boolean; |
||||
|
} |
||||
|
|
||||
|
export interface NumericFilterPredicate { |
||||
|
operation: NumericOperation; |
||||
|
value: number; |
||||
|
} |
||||
|
|
||||
|
export interface BooleanFilterPredicate { |
||||
|
operation: BooleanOperation; |
||||
|
value: boolean; |
||||
|
} |
||||
|
|
||||
|
export interface ComplexFilterPredicate { |
||||
|
operation: ComplexOperation; |
||||
|
predicates: Array<KeyFilterPredicate>; |
||||
|
} |
||||
|
|
||||
|
export type KeyFilterPredicates = StringFilterPredicate & |
||||
|
NumericFilterPredicate & |
||||
|
BooleanFilterPredicate & |
||||
|
ComplexFilterPredicate; |
||||
|
|
||||
|
export interface KeyFilterPredicate extends KeyFilterPredicates { |
||||
|
type?: FilterPredicateType; |
||||
|
} |
||||
|
|
||||
|
export interface KeyFilter { |
||||
|
key: EntityKey; |
||||
|
predicate: KeyFilterPredicate; |
||||
|
} |
||||
|
|
||||
|
export interface EntityFilter extends EntityFilters { |
||||
|
type?: AliasFilterType; |
||||
|
} |
||||
|
|
||||
|
export enum Direction { |
||||
|
ASC = 'ASC', |
||||
|
DESC = 'DESC' |
||||
|
} |
||||
|
|
||||
|
export interface EntityDataSortOrder { |
||||
|
key: EntityKey; |
||||
|
direction: Direction; |
||||
|
} |
||||
|
|
||||
|
export interface EntityDataPageLink { |
||||
|
pageSize: number; |
||||
|
page: number; |
||||
|
textSearch?: string; |
||||
|
sortOrder?: EntityDataSortOrder; |
||||
|
} |
||||
|
|
||||
|
export const defaultEntityDataPageLink: EntityDataPageLink = { |
||||
|
pageSize: 1024, |
||||
|
page: 0, |
||||
|
sortOrder: { |
||||
|
key: { |
||||
|
type: EntityKeyType.ENTITY_FIELD, |
||||
|
key: 'createdTime' |
||||
|
}, |
||||
|
direction: Direction.DESC |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export interface EntityCountQuery { |
||||
|
entityFilter: EntityFilter; |
||||
|
} |
||||
|
|
||||
|
export interface EntityDataQuery extends EntityCountQuery { |
||||
|
pageLink: EntityDataPageLink; |
||||
|
entityFields?: Array<EntityKey>; |
||||
|
latestValues?: Array<EntityKey>; |
||||
|
keyFilters?: Array<KeyFilter>; |
||||
|
} |
||||
|
|
||||
|
export interface TsValue { |
||||
|
ts: number; |
||||
|
value: string; |
||||
|
} |
||||
|
|
||||
|
export interface EntityData { |
||||
|
entityId: EntityId; |
||||
|
latest: {[entityKeyType: string]: {[key: string]: TsValue}}; |
||||
|
timeseries: {[key: string]: Array<TsValue>}; |
||||
|
} |
||||
Loading…
Reference in new issue