mirror of
https://github.com/nasa/openmct.git
synced 2024-12-30 01:48:51 +00:00
Ensures correct results are returned for conditions and criteria for a given telemetry datapoint (#2904)
* Ensures that results for a specific datapoint are evaluated atomically. * Remove generating timestamp for telemetry data * Get results directly instead of using events * Refactor all/any telemetry criterion to use new evaluator * Use current timesystem to compare latest * AllTelemetryCriterion extends TelemetryCriterion Co-authored-by: David Tsay <david.e.tsay@nasa.gov> Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com> Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
parent
11f2c35bb2
commit
26838635b6
@ -23,8 +23,7 @@
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import uuid from 'uuid';
|
||||
import TelemetryCriterion from "./criterion/TelemetryCriterion";
|
||||
import { TRIGGER } from "./utils/constants";
|
||||
import {computeCondition, computeConditionByLimit} from "./utils/evaluator";
|
||||
import { evaluateResults } from './utils/evaluator';
|
||||
import { getLatestTimestamp } from './utils/time';
|
||||
import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
|
||||
|
||||
@ -57,28 +56,40 @@ export default class ConditionClass extends EventEmitter {
|
||||
this.conditionManager = conditionManager;
|
||||
this.id = conditionConfiguration.id;
|
||||
this.criteria = [];
|
||||
this.criteriaResults = {};
|
||||
this.result = undefined;
|
||||
this.latestTimestamp = {};
|
||||
this.timeSystems = this.openmct.time.getAllTimeSystems();
|
||||
if (conditionConfiguration.configuration.criteria) {
|
||||
this.createCriteria(conditionConfiguration.configuration.criteria);
|
||||
}
|
||||
this.trigger = conditionConfiguration.configuration.trigger;
|
||||
this.conditionManager.on('broadcastTelemetry', this.handleBroadcastTelemetry, this);
|
||||
}
|
||||
|
||||
handleBroadcastTelemetry(datum) {
|
||||
getResult(datum) {
|
||||
if (!datum || !datum.id) {
|
||||
console.log('no data received');
|
||||
return;
|
||||
}
|
||||
this.criteria.forEach(criterion => {
|
||||
if (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any')) {
|
||||
criterion.handleSubscription(datum, this.conditionManager.telemetryObjects);
|
||||
} else {
|
||||
criterion.emit(`subscription:${datum.id}`, datum);
|
||||
if (!this.isTelemetryUsed(datum.id)) {
|
||||
return;
|
||||
}
|
||||
this.criteria.forEach(criterion => {
|
||||
if (this.isAnyOrAllTelemetry(criterion)) {
|
||||
criterion.getResult(datum, this.conditionManager.telemetryObjects);
|
||||
} else {
|
||||
criterion.getResult(datum);
|
||||
}
|
||||
});
|
||||
|
||||
this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger);
|
||||
}
|
||||
|
||||
isAnyOrAllTelemetry(criterion) {
|
||||
return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any'));
|
||||
}
|
||||
|
||||
isTelemetryUsed(id) {
|
||||
return this.criteria.some(criterion => {
|
||||
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id;
|
||||
});
|
||||
}
|
||||
|
||||
@ -90,7 +101,6 @@ export default class ConditionClass extends EventEmitter {
|
||||
updateTrigger(trigger) {
|
||||
if (this.trigger !== trigger) {
|
||||
this.trigger = trigger;
|
||||
this.handleConditionUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,7 +144,6 @@ export default class ConditionClass extends EventEmitter {
|
||||
criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct);
|
||||
}
|
||||
criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
criterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
|
||||
if (!this.criteria) {
|
||||
this.criteria = [];
|
||||
}
|
||||
@ -163,20 +172,11 @@ export default class ConditionClass extends EventEmitter {
|
||||
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
|
||||
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
|
||||
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
newCriterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
|
||||
|
||||
let criterion = found.item;
|
||||
criterion.unsubscribe();
|
||||
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
criterion.off('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
|
||||
this.criteria.splice(found.index, 1, newCriterion);
|
||||
delete this.criteriaResults[criterion.id];
|
||||
}
|
||||
}
|
||||
|
||||
removeCriterion(id) {
|
||||
if (this.destroyCriterion(id)) {
|
||||
this.handleConditionUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,7 +189,6 @@ export default class ConditionClass extends EventEmitter {
|
||||
});
|
||||
criterion.destroy();
|
||||
this.criteria.splice(found.index, 1);
|
||||
delete this.criteriaResults[criterion.id];
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -200,27 +199,9 @@ export default class ConditionClass extends EventEmitter {
|
||||
let found = this.findCriterion(criterion.id);
|
||||
if (found) {
|
||||
this.criteria[found.index] = criterion.data;
|
||||
// TODO nothing is listening to this
|
||||
this.emitEvent('conditionUpdated', {
|
||||
trigger: this.trigger,
|
||||
criteria: this.criteria
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateCriteriaResults(eventData) {
|
||||
const id = eventData.id;
|
||||
|
||||
if (this.findCriterion(id)) {
|
||||
this.criteriaResults[id] = !!eventData.data.result;
|
||||
}
|
||||
}
|
||||
|
||||
handleCriterionResult(eventData) {
|
||||
this.updateCriteriaResults(eventData);
|
||||
this.handleConditionUpdated(eventData.data);
|
||||
}
|
||||
|
||||
requestLADConditionResult() {
|
||||
let latestTimestamp;
|
||||
let criteriaResults = {};
|
||||
@ -237,28 +218,21 @@ export default class ConditionClass extends EventEmitter {
|
||||
latestTimestamp = getLatestTimestamp(
|
||||
latestTimestamp,
|
||||
data,
|
||||
this.timeSystems
|
||||
this.timeSystems,
|
||||
this.openmct.time.timeSystem()
|
||||
);
|
||||
});
|
||||
return {
|
||||
id: this.id,
|
||||
data: Object.assign({}, latestTimestamp, { result: this.evaluate(criteriaResults) })
|
||||
}
|
||||
data: Object.assign(
|
||||
{},
|
||||
latestTimestamp,
|
||||
{ result: evaluateResults(Object.values(criteriaResults), this.trigger) }
|
||||
)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getTelemetrySubscriptions() {
|
||||
return this.criteria.map(criterion => criterion.telemetryObjectIdAsString);
|
||||
}
|
||||
|
||||
handleConditionUpdated(datum) {
|
||||
// trigger an updated event so that consumers can react accordingly
|
||||
this.result = this.evaluate(this.criteriaResults);
|
||||
this.emitEvent('conditionResultUpdated',
|
||||
Object.assign({}, datum, { result: this.result })
|
||||
);
|
||||
}
|
||||
|
||||
getCriteria() {
|
||||
return this.criteria;
|
||||
}
|
||||
@ -272,28 +246,7 @@ export default class ConditionClass extends EventEmitter {
|
||||
return success;
|
||||
}
|
||||
|
||||
evaluate(results) {
|
||||
if (this.trigger && this.trigger === TRIGGER.XOR) {
|
||||
return computeConditionByLimit(results, 1);
|
||||
} else if (this.trigger && this.trigger === TRIGGER.NOT) {
|
||||
return computeConditionByLimit(results, 0);
|
||||
} else {
|
||||
return computeCondition(results, this.trigger === TRIGGER.ALL);
|
||||
}
|
||||
}
|
||||
|
||||
emitEvent(eventName, data) {
|
||||
this.emit(eventName, {
|
||||
id: this.id,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.conditionManager.off('broadcastTelemetry', this.handleBroadcastTelemetry, this);
|
||||
if (typeof this.stopObservingForChanges === 'function') {
|
||||
this.stopObservingForChanges();
|
||||
}
|
||||
this.destroyCriteria();
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ export default class ConditionManager extends EventEmitter {
|
||||
super();
|
||||
this.openmct = openmct;
|
||||
this.conditionSetDomainObject = conditionSetDomainObject;
|
||||
this.timeAPI = this.openmct.time;
|
||||
this.timeSystems = this.openmct.time.getAllTimeSystems();
|
||||
this.composition = this.openmct.composition.get(conditionSetDomainObject);
|
||||
this.composition.on('add', this.subscribeToTelemetry, this);
|
||||
@ -56,8 +55,9 @@ export default class ConditionManager extends EventEmitter {
|
||||
this.telemetryObjects[id] = Object.assign({}, endpoint, {telemetryMetaData: this.openmct.telemetry.getMetadata(endpoint).valueMetadatas});
|
||||
this.subscriptions[id] = this.openmct.telemetry.subscribe(
|
||||
endpoint,
|
||||
this.broadcastTelemetry.bind(this, id)
|
||||
this.telemetryReceived.bind(this, id)
|
||||
);
|
||||
// TODO check if this is needed
|
||||
this.updateConditionTelemetry();
|
||||
}
|
||||
|
||||
@ -74,7 +74,6 @@ export default class ConditionManager extends EventEmitter {
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.conditionResults = {};
|
||||
this.conditionClassCollection = [];
|
||||
if (this.conditionSetDomainObject.configuration.conditionCollection.length) {
|
||||
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
|
||||
@ -96,7 +95,6 @@ export default class ConditionManager extends EventEmitter {
|
||||
|
||||
initCondition(conditionConfiguration, index) {
|
||||
let condition = new Condition(conditionConfiguration, this.openmct, this);
|
||||
condition.on('conditionResultUpdated', this.handleConditionResult.bind(this));
|
||||
if (index !== undefined) {
|
||||
this.conditionClassCollection.splice(index + 1, 0, condition);
|
||||
} else {
|
||||
@ -160,13 +158,10 @@ export default class ConditionManager extends EventEmitter {
|
||||
|
||||
removeCondition(index) {
|
||||
let condition = this.conditionClassCollection[index];
|
||||
condition.destroyCriteria();
|
||||
condition.off('conditionResultUpdated', this.handleConditionResult.bind(this));
|
||||
condition.destroy();
|
||||
this.conditionClassCollection.splice(index, 1);
|
||||
this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1);
|
||||
delete this.conditionResults[condition.id];
|
||||
this.persistConditions();
|
||||
this.handleConditionResult();
|
||||
}
|
||||
|
||||
findConditionById(id) {
|
||||
@ -184,7 +179,23 @@ export default class ConditionManager extends EventEmitter {
|
||||
this.persistConditions();
|
||||
}
|
||||
|
||||
getCurrentCondition(conditionResults) {
|
||||
getCurrentCondition() {
|
||||
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
|
||||
let currentCondition = conditionCollection[conditionCollection.length-1];
|
||||
|
||||
for (let i = 0; i < conditionCollection.length - 1; i++) {
|
||||
const condition = this.findConditionById(conditionCollection[i].id)
|
||||
if (condition.result) {
|
||||
//first condition to be true wins
|
||||
currentCondition = conditionCollection[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return currentCondition;
|
||||
}
|
||||
|
||||
getCurrentConditionLAD(conditionResults) {
|
||||
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
|
||||
let currentCondition = conditionCollection[conditionCollection.length-1];
|
||||
|
||||
@ -198,36 +209,6 @@ export default class ConditionManager extends EventEmitter {
|
||||
return currentCondition;
|
||||
}
|
||||
|
||||
updateConditionResults(resultObj) {
|
||||
if (!resultObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = resultObj.id;
|
||||
|
||||
if (this.findConditionById(id)) {
|
||||
this.conditionResults[id] = resultObj.data.result;
|
||||
}
|
||||
}
|
||||
|
||||
handleConditionResult(resultObj) {
|
||||
this.updateConditionResults(resultObj);
|
||||
const currentCondition = this.getCurrentCondition(this.conditionResults);
|
||||
const timestamp = JSON.parse(JSON.stringify(resultObj.data))
|
||||
delete timestamp.result
|
||||
|
||||
this.emit('conditionSetResultUpdated',
|
||||
Object.assign(
|
||||
{
|
||||
output: currentCondition.configuration.output,
|
||||
id: this.conditionSetDomainObject.identifier,
|
||||
conditionId: currentCondition.id
|
||||
},
|
||||
timestamp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
requestLADConditionSetOutput() {
|
||||
if (!this.conditionClassCollection.length) {
|
||||
return Promise.resolve([]);
|
||||
@ -249,10 +230,11 @@ export default class ConditionManager extends EventEmitter {
|
||||
latestTimestamp = getLatestTimestamp(
|
||||
latestTimestamp,
|
||||
data,
|
||||
this.timeSystems
|
||||
this.timeSystems,
|
||||
this.openmct.time.timeSystem()
|
||||
);
|
||||
});
|
||||
const currentCondition = this.getCurrentCondition(conditionResults);
|
||||
const currentCondition = this.getCurrentConditionLAD(conditionResults);
|
||||
|
||||
return Object.assign(
|
||||
{
|
||||
@ -266,8 +248,28 @@ export default class ConditionManager extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
broadcastTelemetry(id, datum) {
|
||||
this.emit(`broadcastTelemetry`, Object.assign({}, this.createNormalizedDatum(datum, id), {id: id}));
|
||||
telemetryReceived(id, datum) {
|
||||
const normalizedDatum = this.createNormalizedDatum(datum, id);
|
||||
const timeSystemKey = this.openmct.time.timeSystem().key;
|
||||
let timestamp = {};
|
||||
timestamp[timeSystemKey] = normalizedDatum[timeSystemKey];
|
||||
|
||||
this.conditionClassCollection.forEach(condition => {
|
||||
condition.getResult(normalizedDatum);
|
||||
});
|
||||
|
||||
const currentCondition = this.getCurrentCondition();
|
||||
|
||||
this.emit('conditionSetResultUpdated',
|
||||
Object.assign(
|
||||
{
|
||||
output: currentCondition.configuration.output,
|
||||
id: this.conditionSetDomainObject.identifier,
|
||||
conditionId: currentCondition.id
|
||||
},
|
||||
timestamp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
getTestData(metadatum) {
|
||||
@ -282,12 +284,16 @@ export default class ConditionManager extends EventEmitter {
|
||||
}
|
||||
|
||||
createNormalizedDatum(telemetryDatum, id) {
|
||||
return Object.values(this.telemetryObjects[id].telemetryMetaData).reduce((normalizedDatum, metadatum) => {
|
||||
const normalizedDatum = Object.values(this.telemetryObjects[id].telemetryMetaData).reduce((datum, metadatum) => {
|
||||
const testValue = this.getTestData(metadatum);
|
||||
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
|
||||
normalizedDatum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]);
|
||||
return normalizedDatum;
|
||||
datum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]);
|
||||
return datum;
|
||||
}, {});
|
||||
|
||||
normalizedDatum.id = id;
|
||||
|
||||
return normalizedDatum;
|
||||
}
|
||||
|
||||
updateTestData(testData) {
|
||||
@ -310,7 +316,6 @@ export default class ConditionManager extends EventEmitter {
|
||||
}
|
||||
|
||||
this.conditionClassCollection.forEach((condition) => {
|
||||
condition.off('conditionResultUpdated', this.handleConditionResult);
|
||||
condition.destroy();
|
||||
})
|
||||
}
|
||||
|
@ -121,7 +121,7 @@ describe('ConditionManager', () => {
|
||||
conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
|
||||
|
||||
conditionMgr.on('conditionSetResultUpdated', mockListener);
|
||||
conditionMgr.on('broadcastTelemetry', mockListener);
|
||||
conditionMgr.on('telemetryReceived', mockListener);
|
||||
});
|
||||
|
||||
it('creates a conditionCollection with a default condition', function () {
|
||||
|
@ -25,12 +25,11 @@ import {TRIGGER} from "./utils/constants";
|
||||
import TelemetryCriterion from "./criterion/TelemetryCriterion";
|
||||
|
||||
let openmct = {},
|
||||
mockListener,
|
||||
testConditionDefinition,
|
||||
testTelemetryObject,
|
||||
conditionObj,
|
||||
conditionManager,
|
||||
mockBroadcastTelemetry,
|
||||
mockTelemetryReceived,
|
||||
mockTimeSystems;
|
||||
|
||||
describe("The condition", function () {
|
||||
@ -39,10 +38,9 @@ describe("The condition", function () {
|
||||
conditionManager = jasmine.createSpyObj('conditionManager',
|
||||
['on']
|
||||
);
|
||||
mockBroadcastTelemetry = jasmine.createSpy('listener');
|
||||
conditionManager.on('broadcastTelemetry', mockBroadcastTelemetry);
|
||||
mockTelemetryReceived = jasmine.createSpy('listener');
|
||||
conditionManager.on('telemetryReceived', mockTelemetryReceived);
|
||||
|
||||
mockListener = jasmine.createSpy('listener');
|
||||
testTelemetryObject = {
|
||||
identifier:{ namespace: "", key: "test-object"},
|
||||
type: "test-object",
|
||||
@ -104,8 +102,6 @@ describe("The condition", function () {
|
||||
openmct,
|
||||
conditionManager
|
||||
);
|
||||
|
||||
conditionObj.on('conditionUpdated', mockListener);
|
||||
});
|
||||
|
||||
it("generates criteria with the correct properties", function () {
|
||||
|
@ -20,11 +20,10 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import {OPERATIONS} from '../utils/operations';
|
||||
import {computeCondition} from "@/plugins/condition/utils/evaluator";
|
||||
import TelemetryCriterion from './TelemetryCriterion';
|
||||
import { evaluateResults } from "../utils/evaluator";
|
||||
|
||||
export default class TelemetryCriterion extends EventEmitter {
|
||||
export default class AllTelemetryCriterion extends TelemetryCriterion {
|
||||
|
||||
/**
|
||||
* Subscribes/Unsubscribes to telemetry and emits the result
|
||||
@ -34,23 +33,20 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
* @param openmct
|
||||
*/
|
||||
constructor(telemetryDomainObjectDefinition, openmct) {
|
||||
super();
|
||||
super(telemetryDomainObjectDefinition, openmct);
|
||||
}
|
||||
|
||||
this.openmct = openmct;
|
||||
this.objectAPI = this.openmct.objects;
|
||||
this.telemetryAPI = this.openmct.telemetry;
|
||||
this.timeAPI = this.openmct.time;
|
||||
this.id = telemetryDomainObjectDefinition.id;
|
||||
this.telemetry = telemetryDomainObjectDefinition.telemetry;
|
||||
this.operation = telemetryDomainObjectDefinition.operation;
|
||||
this.telemetryObjects = Object.assign({}, telemetryDomainObjectDefinition.telemetryObjects);
|
||||
this.input = telemetryDomainObjectDefinition.input;
|
||||
this.metadata = telemetryDomainObjectDefinition.metadata;
|
||||
initialize() {
|
||||
this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };
|
||||
this.telemetryDataCache = {};
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation;
|
||||
}
|
||||
|
||||
updateTelemetry(telemetryObjects) {
|
||||
this.telemetryObjects = Object.assign({}, telemetryObjects);
|
||||
this.telemetryObjects = { ...telemetryObjects };
|
||||
}
|
||||
|
||||
formatData(data, telemetryObjects) {
|
||||
@ -68,60 +64,32 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
});
|
||||
|
||||
const datum = {
|
||||
result: computeCondition(this.telemetryDataCache, this.telemetry === 'all')
|
||||
result: evaluateResults(Object.values(this.telemetryDataCache), this.telemetry)
|
||||
};
|
||||
|
||||
if (data) {
|
||||
// TODO check back to see if we should format times here
|
||||
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
|
||||
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
|
||||
datum[timeSystem.key] = data[timeSystem.key]
|
||||
});
|
||||
}
|
||||
return datum;
|
||||
}
|
||||
|
||||
handleSubscription(data, telemetryObjects) {
|
||||
if(this.isValid()) {
|
||||
this.emitEvent('criterionResultUpdated', this.formatData(data, telemetryObjects));
|
||||
} else {
|
||||
this.emitEvent('criterionResultUpdated', this.formatData({}, telemetryObjects));
|
||||
}
|
||||
getResult(data, telemetryObjects) {
|
||||
const validatedData = this.isValid() ? data : {};
|
||||
|
||||
if (validatedData) {
|
||||
this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
|
||||
}
|
||||
|
||||
findOperation(operation) {
|
||||
for (let i=0; i < OPERATIONS.length; i++) {
|
||||
if (operation === OPERATIONS[i].name) {
|
||||
return OPERATIONS[i].operation;
|
||||
Object.values(telemetryObjects).forEach(telemetryObject => {
|
||||
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
if (this.telemetryDataCache[id] === undefined) {
|
||||
this.telemetryDataCache[id] = false;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
computeResult(data) {
|
||||
let result = false;
|
||||
if (data) {
|
||||
let comparator = this.findOperation(this.operation);
|
||||
let params = [];
|
||||
params.push(data[this.metadata]);
|
||||
if (this.input instanceof Array && this.input.length) {
|
||||
this.input.forEach(input => params.push(input));
|
||||
}
|
||||
if (typeof comparator === 'function') {
|
||||
result = comparator(params);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
emitEvent(eventName, data) {
|
||||
this.emit(eventName, {
|
||||
id: this.id,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation;
|
||||
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
|
||||
}
|
||||
|
||||
requestLAD(options) {
|
||||
@ -139,7 +107,7 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
|
||||
let keys = Object.keys(Object.assign({}, options.telemetryObjects));
|
||||
const telemetryRequests = keys
|
||||
.map(key => this.telemetryAPI.request(
|
||||
.map(key => this.openmct.telemetry.request(
|
||||
options.telemetryObjects[key],
|
||||
options
|
||||
));
|
||||
@ -163,10 +131,7 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.emitEvent('criterionRemoved');
|
||||
delete this.telemetryObjects;
|
||||
delete this.telemetryDataCache;
|
||||
delete this.telemetryObjectIdAsString;
|
||||
delete this.telemetryObject;
|
||||
}
|
||||
}
|
||||
|
@ -36,20 +36,27 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.objectAPI = this.openmct.objects;
|
||||
this.telemetryAPI = this.openmct.telemetry;
|
||||
this.timeAPI = this.openmct.time;
|
||||
this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition;
|
||||
this.id = telemetryDomainObjectDefinition.id;
|
||||
this.telemetry = telemetryDomainObjectDefinition.telemetry;
|
||||
this.operation = telemetryDomainObjectDefinition.operation;
|
||||
this.input = telemetryDomainObjectDefinition.input;
|
||||
this.metadata = telemetryDomainObjectDefinition.metadata;
|
||||
this.telemetryObject = telemetryDomainObjectDefinition.telemetryObject;
|
||||
this.telemetryObjectIdAsString = this.objectAPI.makeKeyString(telemetryDomainObjectDefinition.telemetry);
|
||||
this.on(`subscription:${this.telemetryObjectIdAsString}`, this.handleSubscription);
|
||||
this.result = undefined;
|
||||
|
||||
this.initialize();
|
||||
this.emitEvent('criterionUpdated', this);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.telemetryObject = this.telemetryDomainObjectDefinition.telemetryObject;
|
||||
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return this.telemetryObject && this.metadata && this.operation;
|
||||
}
|
||||
|
||||
updateTelemetry(telemetryObjects) {
|
||||
this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];
|
||||
}
|
||||
@ -60,20 +67,44 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
};
|
||||
|
||||
if (data) {
|
||||
// TODO check back to see if we should format times here
|
||||
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
|
||||
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
|
||||
datum[timeSystem.key] = data[timeSystem.key]
|
||||
});
|
||||
}
|
||||
return datum;
|
||||
}
|
||||
|
||||
handleSubscription(data) {
|
||||
if(this.isValid()) {
|
||||
this.emitEvent('criterionResultUpdated', this.formatData(data));
|
||||
} else {
|
||||
this.emitEvent('criterionResultUpdated', this.formatData({}));
|
||||
getResult(data) {
|
||||
const validatedData = this.isValid() ? data : {};
|
||||
this.result = this.computeResult(validatedData);
|
||||
}
|
||||
|
||||
requestLAD(options) {
|
||||
options = Object.assign({},
|
||||
options,
|
||||
{
|
||||
strategy: 'latest',
|
||||
size: 1
|
||||
}
|
||||
);
|
||||
|
||||
if (!this.isValid()) {
|
||||
return {
|
||||
id: this.id,
|
||||
data: this.formatData({})
|
||||
};
|
||||
}
|
||||
|
||||
return this.openmct.telemetry.request(
|
||||
this.telemetryObject,
|
||||
options
|
||||
).then(results => {
|
||||
const latestDatum = results.length ? results[results.length - 1] : {};
|
||||
return {
|
||||
id: this.id,
|
||||
data: this.formatData(latestDatum)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
findOperation(operation) {
|
||||
@ -95,7 +126,7 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
this.input.forEach(input => params.push(input));
|
||||
}
|
||||
if (typeof comparator === 'function') {
|
||||
result = comparator(params);
|
||||
result = !!comparator(params);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@ -108,42 +139,9 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return this.telemetryObject && this.metadata && this.operation;
|
||||
}
|
||||
|
||||
requestLAD(options) {
|
||||
options = Object.assign({},
|
||||
options,
|
||||
{
|
||||
strategy: 'latest',
|
||||
size: 1
|
||||
}
|
||||
);
|
||||
|
||||
if (!this.isValid()) {
|
||||
return {
|
||||
id: this.id,
|
||||
data: this.formatData({})
|
||||
};
|
||||
}
|
||||
|
||||
return this.telemetryAPI.request(
|
||||
this.telemetryObject,
|
||||
options
|
||||
).then(results => {
|
||||
const latestDatum = results.length ? results[results.length - 1] : {};
|
||||
return {
|
||||
id: this.id,
|
||||
data: this.formatData(latestDatum)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.off(`subscription:${this.telemetryObjectIdAsString}`, this.handleSubscription);
|
||||
this.emitEvent('criterionRemoved');
|
||||
delete this.telemetryObjectIdAsString;
|
||||
delete this.telemetryObject;
|
||||
delete this.telemetryObjectIdAsString;
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ describe("The telemetry criterion", function () {
|
||||
key: "testSource",
|
||||
source: "value",
|
||||
name: "Test",
|
||||
format: "enum"
|
||||
format: "string"
|
||||
}]
|
||||
}
|
||||
};
|
||||
@ -80,8 +80,9 @@ describe("The telemetry criterion", function () {
|
||||
testCriterionDefinition = {
|
||||
id: 'test-criterion-id',
|
||||
telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier),
|
||||
operation: 'lessThan',
|
||||
metadata: 'sin',
|
||||
operation: 'textContains',
|
||||
metadata: 'value',
|
||||
input: ['Hell'],
|
||||
telemetryObject: testTelemetryObject
|
||||
};
|
||||
|
||||
@ -100,12 +101,21 @@ describe("The telemetry criterion", function () {
|
||||
expect(telemetryCriterion.telemetryObjectIdAsString).toEqual(testTelemetryObject.identifier.key);
|
||||
});
|
||||
|
||||
it("updates and emits event on new data from telemetry providers", function () {
|
||||
spyOn(telemetryCriterion, 'emitEvent').and.callThrough();
|
||||
telemetryCriterion.handleSubscription({
|
||||
it("returns a result on new data from relevant telemetry providers", function () {
|
||||
telemetryCriterion.getResult({
|
||||
value: 'Hello',
|
||||
utc: 'Hi'
|
||||
utc: 'Hi',
|
||||
id: testTelemetryObject.identifier.key
|
||||
});
|
||||
expect(telemetryCriterion.emitEvent).toHaveBeenCalled();
|
||||
expect(telemetryCriterion.result).toBeTrue();
|
||||
});
|
||||
|
||||
// it("does not return a result on new data from irrelavant telemetry providers", function () {
|
||||
// telemetryCriterion.getResult({
|
||||
// value: 'Hello',
|
||||
// utc: 'Hi',
|
||||
// id: '1234'
|
||||
// });
|
||||
// expect(telemetryCriterion.result).toBeFalse();
|
||||
// });
|
||||
});
|
||||
|
@ -19,36 +19,50 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import { TRIGGER } from "./constants";
|
||||
|
||||
export const computeCondition = (resultMap, allMustBeTrue) => {
|
||||
let result = false;
|
||||
for (let key in resultMap) {
|
||||
if (resultMap.hasOwnProperty(key)) {
|
||||
result = resultMap[key];
|
||||
if (allMustBeTrue && !result) {
|
||||
//If we want all conditions to be true, then even one negative result should break.
|
||||
break;
|
||||
} else if (!allMustBeTrue && result) {
|
||||
//If we want at least one condition to be true, then even one positive result should break.
|
||||
break;
|
||||
export const evaluateResults = (results, trigger) => {
|
||||
if (trigger && trigger === TRIGGER.XOR) {
|
||||
return matchExact(results, 1);
|
||||
} else if (trigger && trigger === TRIGGER.NOT) {
|
||||
return matchExact(results, 0);
|
||||
} else if (trigger && trigger === TRIGGER.ALL) {
|
||||
return matchAll(results);
|
||||
} else {
|
||||
return matchAny(results);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
//Returns true only if limit number of results are satisfied
|
||||
export const computeConditionByLimit = (resultMap, limit) => {
|
||||
let trueCount = 0;
|
||||
for (let key in resultMap) {
|
||||
if (resultMap.hasOwnProperty(key)) {
|
||||
if (resultMap[key]) {
|
||||
trueCount++;
|
||||
}
|
||||
if (trueCount > limit) {
|
||||
break;
|
||||
function matchAll(results) {
|
||||
for (const result of results) {
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function matchAny(results) {
|
||||
for (const result of results) {
|
||||
if (result) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function matchExact(results, target) {
|
||||
let matches = 0;
|
||||
for (const result of results) {
|
||||
if (result) {
|
||||
matches++;
|
||||
}
|
||||
if (matches > target) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return matches === target;
|
||||
}
|
||||
return trueCount === limit;
|
||||
};
|
||||
|
@ -20,47 +20,185 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { computeConditionByLimit } from "./evaluator";
|
||||
import { evaluateResults } from './evaluator';
|
||||
import { TRIGGER } from './constants';
|
||||
|
||||
describe('evaluate results based on trigger', function () {
|
||||
describe('evaluate results', () => {
|
||||
// const allTrue = [true, true, true, true, true];
|
||||
// const oneTrue = [false, false, false, false, true];
|
||||
// const multipleTrue = [false, true, false, true, false];
|
||||
// const noneTrue = [false, false, false, false, false];
|
||||
// const allTrueWithUndefined = [true, true, true, undefined, true];
|
||||
// const oneTrueWithUndefined = [undefined, undefined, undefined, undefined, true];
|
||||
// const multipleTrueWithUndefined = [true, undefined, true, undefined, true];
|
||||
// const allUndefined = [undefined, undefined, undefined, undefined, undefined];
|
||||
// const singleTrue = [true];
|
||||
// const singleFalse = [false];
|
||||
// const singleUndefined = [undefined];
|
||||
// const empty = [];
|
||||
|
||||
it('should evaluate to true if trigger is NOT', () => {
|
||||
const results = {
|
||||
result: false,
|
||||
result1: false,
|
||||
result2: false
|
||||
};
|
||||
const result = computeConditionByLimit(results, 0);
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
const tests = [
|
||||
{
|
||||
name: 'allTrue',
|
||||
values: [true, true, true, true, true],
|
||||
any: true,
|
||||
all: true,
|
||||
not: false,
|
||||
xor: false
|
||||
}, {
|
||||
name: 'oneTrue',
|
||||
values: [false, false, false, false, true],
|
||||
any: true,
|
||||
all: false,
|
||||
not: false,
|
||||
xor: true
|
||||
}, {
|
||||
name: 'multipleTrue',
|
||||
values: [false, true, false, true, false],
|
||||
any: true,
|
||||
all: false,
|
||||
not: false,
|
||||
xor: false
|
||||
}, {
|
||||
name: 'noneTrue',
|
||||
values: [false, false, false, false, false],
|
||||
any: false,
|
||||
all: false,
|
||||
not: true,
|
||||
xor: false
|
||||
}, {
|
||||
name: 'allTrueWithUndefined',
|
||||
values: [true, true, true, undefined, true],
|
||||
any: true,
|
||||
all: false,
|
||||
not: false,
|
||||
xor: false
|
||||
}, {
|
||||
name: 'oneTrueWithUndefined',
|
||||
values: [undefined, undefined, undefined, undefined, true],
|
||||
any: true,
|
||||
all: false,
|
||||
not: false,
|
||||
xor: true
|
||||
}, {
|
||||
name: 'multipleTrueWithUndefined',
|
||||
values: [true, undefined, true, undefined, true],
|
||||
any: true,
|
||||
all: false,
|
||||
not: false,
|
||||
xor: false
|
||||
}, {
|
||||
name: 'allUndefined',
|
||||
values: [undefined, undefined, undefined, undefined, undefined],
|
||||
any: false,
|
||||
all: false,
|
||||
not: true,
|
||||
xor: false
|
||||
}, {
|
||||
name: 'singleTrue',
|
||||
values: [true],
|
||||
any: true,
|
||||
all: true,
|
||||
not: false,
|
||||
xor: true
|
||||
}, {
|
||||
name: 'singleFalse',
|
||||
values: [false],
|
||||
any: false,
|
||||
all: false,
|
||||
not: true,
|
||||
xor: false
|
||||
}, {
|
||||
name: 'singleUndefined',
|
||||
values: [undefined],
|
||||
any: false,
|
||||
all: false,
|
||||
not: true,
|
||||
xor: false
|
||||
}
|
||||
// , {
|
||||
// name: 'empty',
|
||||
// values: [],
|
||||
// any: false,
|
||||
// all: false,
|
||||
// not: true,
|
||||
// xor: false
|
||||
// }
|
||||
];
|
||||
|
||||
it('should evaluate to false if trigger is NOT', () => {
|
||||
const results = {
|
||||
result: true,
|
||||
result1: false,
|
||||
result2: false
|
||||
};
|
||||
const result = computeConditionByLimit(results, 0);
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
|
||||
it('should evaluate to true if trigger is XOR', () => {
|
||||
const results = {
|
||||
result: false,
|
||||
result1: true,
|
||||
result2: false
|
||||
};
|
||||
const result = computeConditionByLimit(results, 1);
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
|
||||
it('should evaluate to false if trigger is XOR', () => {
|
||||
const results = {
|
||||
result: false,
|
||||
result1: true,
|
||||
result2: true
|
||||
};
|
||||
const result = computeConditionByLimit(results, 1);
|
||||
expect(result).toBeFalse();
|
||||
describe(`based on trigger ${TRIGGER.ANY}`, () => {
|
||||
it('should evaluate to expected result', () => {
|
||||
tests.forEach(test => {
|
||||
const result = evaluateResults(test.values, TRIGGER.ANY);
|
||||
expect(result).toEqual(test[TRIGGER.ANY])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`based on trigger ${TRIGGER.ALL}`, () => {
|
||||
it('should evaluate to expected result', () => {
|
||||
tests.forEach(test => {
|
||||
const result = evaluateResults(test.values, TRIGGER.ALL);
|
||||
expect(result).toEqual(test[TRIGGER.ALL])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`based on trigger ${TRIGGER.NOT}`, () => {
|
||||
it('should evaluate to expected result', () => {
|
||||
tests.forEach(test => {
|
||||
const result = evaluateResults(test.values, TRIGGER.NOT);
|
||||
expect(result).toEqual(test[TRIGGER.NOT])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`based on trigger ${TRIGGER.XOR}`, () => {
|
||||
it('should evaluate to expected result', () => {
|
||||
tests.forEach(test => {
|
||||
const result = evaluateResults(test.values, TRIGGER.XOR);
|
||||
expect(result).toEqual(test[TRIGGER.XOR])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// it('should evaluate to true if trigger is NOT', () => {
|
||||
// const results = {
|
||||
// result: false,
|
||||
// result1: false,
|
||||
// result2: false
|
||||
// };
|
||||
// const result = computeConditionByLimit(results, 0);
|
||||
// expect(result).toBeTrue();
|
||||
// });
|
||||
|
||||
// it('should evaluate to false if trigger is NOT', () => {
|
||||
// const results = {
|
||||
// result: true,
|
||||
// result1: false,
|
||||
// result2: false
|
||||
// };
|
||||
// const result = computeConditionByLimit(results, 0);
|
||||
// expect(result).toBeFalse();
|
||||
// });
|
||||
|
||||
// it('should evaluate to true if trigger is XOR', () => {
|
||||
// const results = {
|
||||
// result: false,
|
||||
// result1: true,
|
||||
// result2: false
|
||||
// };
|
||||
// const result = computeConditionByLimit(results, 1);
|
||||
// expect(result).toBeTrue();
|
||||
// });
|
||||
|
||||
// it('should evaluate to false if trigger is XOR', () => {
|
||||
// const results = {
|
||||
// result: false,
|
||||
// result1: true,
|
||||
// result2: true
|
||||
// };
|
||||
// const result = computeConditionByLimit(results, 1);
|
||||
// expect(result).toBeFalse();
|
||||
// });
|
||||
});
|
||||
|
@ -20,15 +20,33 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
export const getLatestTimestamp = (current, compare, timeSystems) => {
|
||||
const timestamp = Object.assign({}, current);
|
||||
export const getLatestTimestamp = (
|
||||
currentTimestamp,
|
||||
compareTimestamp,
|
||||
timeSystems,
|
||||
currentTimeSystem
|
||||
) => {
|
||||
let latest = { ...currentTimestamp };
|
||||
const compare = { ...compareTimestamp };
|
||||
const key = currentTimeSystem.key;
|
||||
|
||||
if (!latest || !latest[key]) {
|
||||
latest = updateLatestTimeStamp(compare, timeSystems)
|
||||
}
|
||||
|
||||
if (compare[key] > latest[key]) {
|
||||
latest = updateLatestTimeStamp(compare, timeSystems)
|
||||
}
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
||||
function updateLatestTimeStamp(timestamp, timeSystems) {
|
||||
let latest = {};
|
||||
|
||||
timeSystems.forEach(timeSystem => {
|
||||
if (!timestamp[timeSystem.key]
|
||||
|| compare[timeSystem.key] > timestamp[timeSystem.key]
|
||||
) {
|
||||
timestamp[timeSystem.key] = compare[timeSystem.key];
|
||||
}
|
||||
latest[timeSystem.key] = timestamp[timeSystem.key];
|
||||
});
|
||||
return timestamp;
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user