Merge pull request #2871 from nasa/dave/conditionals-own-results

[Conditionals] Request and subscription results are mutually exclusive
This commit is contained in:
David Tsay 2020-04-08 14:21:31 -07:00 committed by GitHub
commit 5d74882646
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 105 additions and 67 deletions

View File

@ -25,6 +25,7 @@ import uuid from 'uuid';
import TelemetryCriterion from "./criterion/TelemetryCriterion"; import TelemetryCriterion from "./criterion/TelemetryCriterion";
import { TRIGGER } from "./utils/constants"; import { TRIGGER } from "./utils/constants";
import {computeCondition, computeConditionByLimit} from "./utils/evaluator"; import {computeCondition, computeConditionByLimit} from "./utils/evaluator";
import { getLatestTimestamp } from './utils/time';
import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion"; import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
/* /*
@ -59,7 +60,7 @@ export default class ConditionClass extends EventEmitter {
this.criteriaResults = {}; this.criteriaResults = {};
this.result = undefined; this.result = undefined;
this.latestTimestamp = {}; this.latestTimestamp = {};
this.timeSystems = this.openmct.time.getAllTimeSystems();
if (conditionConfiguration.configuration.criteria) { if (conditionConfiguration.configuration.criteria) {
this.createCriteria(conditionConfiguration.configuration.criteria); this.createCriteria(conditionConfiguration.configuration.criteria);
} }
@ -169,11 +170,9 @@ export default class ConditionClass extends EventEmitter {
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('criterionResultUpdated', (obj) => this.handleCriterionResult(obj)); criterion.off('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
this.criteria.splice(found.index, 1, newCriterion); this.criteria.splice(found.index, 1, newCriterion);
if (this.criteriaResults[criterion.id] !== undefined) {
delete this.criteriaResults[criterion.id]; delete this.criteriaResults[criterion.id];
} }
} }
}
removeCriterion(id) { removeCriterion(id) {
if (this.destroyCriterion(id)) { if (this.destroyCriterion(id)) {
@ -185,15 +184,13 @@ export default class ConditionClass extends EventEmitter {
let found = this.findCriterion(id); let found = this.findCriterion(id);
if (found) { if (found) {
let criterion = found.item; let criterion = found.item;
criterion.destroy(); criterion.off('criterionUpdated', (obj) => {
// TODO this is passing the wrong args this.handleCriterionUpdated(obj);
criterion.off('criterionUpdated', (result) => {
this.handleCriterionUpdated(id, result);
}); });
criterion.destroy();
this.criteria.splice(found.index, 1); this.criteria.splice(found.index, 1);
if (this.criteriaResults[criterion.id] !== undefined) {
delete this.criteriaResults[criterion.id]; delete this.criteriaResults[criterion.id];
}
return true; return true;
} }
return false; return false;
@ -215,7 +212,6 @@ export default class ConditionClass extends EventEmitter {
const id = eventData.id; const id = eventData.id;
if (this.findCriterion(id)) { if (this.findCriterion(id)) {
// The !! here is important to convert undefined to false otherwise the criteriaResults won't get deleted when the criteria is destroyed
this.criteriaResults[id] = !!eventData.data.result; this.criteriaResults[id] = !!eventData.data.result;
} }
} }
@ -226,20 +222,27 @@ export default class ConditionClass extends EventEmitter {
} }
requestLADConditionResult() { requestLADConditionResult() {
const criteriaResults = this.criteria let latestTimestamp;
let criteriaResults = {};
const criteriaRequests = this.criteria
.map(criterion => criterion.requestLAD({telemetryObjects: this.conditionManager.telemetryObjects})); .map(criterion => criterion.requestLAD({telemetryObjects: this.conditionManager.telemetryObjects}));
return Promise.all(criteriaResults) return Promise.all(criteriaRequests)
.then(results => { .then(results => {
results.forEach(result => { results.forEach(resultObj => {
this.updateCriteriaResults(result); const { id, data, data: { result } } = resultObj;
this.latestTimestamp = this.getLatestTimestamp(this.latestTimestamp, result.data) if (this.findCriterion(id)) {
criteriaResults[id] = !!result;
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems
);
}); });
this.evaluate();
return { return {
id: this.id, id: this.id,
data: Object.assign({}, this.latestTimestamp, { result: this.result }) data: Object.assign({}, latestTimestamp, { result: this.evaluate(criteriaResults) })
} }
}); });
} }
@ -250,7 +253,7 @@ export default class ConditionClass extends EventEmitter {
handleConditionUpdated(datum) { handleConditionUpdated(datum) {
// trigger an updated event so that consumers can react accordingly // trigger an updated event so that consumers can react accordingly
this.evaluate(); this.result = this.evaluate(this.criteriaResults);
this.emitEvent('conditionResultUpdated', this.emitEvent('conditionResultUpdated',
Object.assign({}, datum, { result: this.result }) Object.assign({}, datum, { result: this.result })
); );
@ -269,29 +272,16 @@ export default class ConditionClass extends EventEmitter {
return success; return success;
} }
evaluate() { evaluate(results) {
if (this.trigger && this.trigger === TRIGGER.XOR) { if (this.trigger && this.trigger === TRIGGER.XOR) {
this.result = computeConditionByLimit(this.criteriaResults, 1); return computeConditionByLimit(results, 1);
} else if (this.trigger && this.trigger === TRIGGER.NOT) { } else if (this.trigger && this.trigger === TRIGGER.NOT) {
this.result = computeConditionByLimit(this.criteriaResults, 0); return computeConditionByLimit(results, 0);
} else { } else {
this.result = computeCondition(this.criteriaResults, this.trigger === TRIGGER.ALL); return computeCondition(results, this.trigger === TRIGGER.ALL);
} }
} }
getLatestTimestamp(current, compare) {
const timestamp = Object.assign({}, current);
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
if (!timestamp[timeSystem.key]
|| compare[timeSystem.key] > timestamp[timeSystem.key]
) {
timestamp[timeSystem.key] = compare[timeSystem.key];
}
});
return timestamp;
}
emitEvent(eventName, data) { emitEvent(eventName, data) {
this.emit(eventName, { this.emit(eventName, {
id: this.id, id: this.id,

View File

@ -21,6 +21,7 @@
*****************************************************************************/ *****************************************************************************/
import Condition from "./Condition"; import Condition from "./Condition";
import { getLatestTimestamp } from './utils/time';
import uuid from "uuid"; import uuid from "uuid";
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
@ -30,7 +31,7 @@ export default class ConditionManager extends EventEmitter {
this.openmct = openmct; this.openmct = openmct;
this.conditionSetDomainObject = conditionSetDomainObject; this.conditionSetDomainObject = conditionSetDomainObject;
this.timeAPI = this.openmct.time; this.timeAPI = this.openmct.time;
this.latestTimestamp = {}; this.timeSystems = this.openmct.time.getAllTimeSystems();
this.composition = this.openmct.composition.get(conditionSetDomainObject); this.composition = this.openmct.composition.get(conditionSetDomainObject);
this.composition.on('add', this.subscribeToTelemetry, this); this.composition.on('add', this.subscribeToTelemetry, this);
this.composition.on('remove', this.unsubscribeFromTelemetry, this); this.composition.on('remove', this.unsubscribeFromTelemetry, this);
@ -163,9 +164,7 @@ export default class ConditionManager extends EventEmitter {
condition.off('conditionResultUpdated', this.handleConditionResult.bind(this)); condition.off('conditionResultUpdated', this.handleConditionResult.bind(this));
this.conditionClassCollection.splice(index, 1); this.conditionClassCollection.splice(index, 1);
this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1); this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1);
if (this.conditionResults[condition.id] !== undefined) {
delete this.conditionResults[condition.id]; delete this.conditionResults[condition.id];
}
this.persistConditions(); this.persistConditions();
this.handleConditionResult(); this.handleConditionResult();
} }
@ -174,7 +173,6 @@ export default class ConditionManager extends EventEmitter {
return this.conditionClassCollection.find(conditionClass => conditionClass.id === id); return this.conditionClassCollection.find(conditionClass => conditionClass.id === id);
} }
//this.$set(this.conditionClassCollection, reorderEvent.newIndex, oldConditions[reorderEvent.oldIndex]);
reorderConditions(reorderPlan) { reorderConditions(reorderPlan) {
let oldConditions = Array.from(this.conditionSetDomainObject.configuration.conditionCollection); let oldConditions = Array.from(this.conditionSetDomainObject.configuration.conditionCollection);
let newCollection = []; let newCollection = [];
@ -186,12 +184,12 @@ export default class ConditionManager extends EventEmitter {
this.persistConditions(); this.persistConditions();
} }
getCurrentCondition() { getCurrentCondition(conditionResults) {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection; const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
let currentCondition = conditionCollection[conditionCollection.length-1]; let currentCondition = conditionCollection[conditionCollection.length-1];
for (let i = 0; i < conditionCollection.length - 1; i++) { for (let i = 0; i < conditionCollection.length - 1; i++) {
if (this.conditionResults[conditionCollection[i].id]) { if (conditionResults[conditionCollection[i].id]) {
//first condition to be true wins //first condition to be true wins
currentCondition = conditionCollection[i]; currentCondition = conditionCollection[i];
break; break;
@ -210,14 +208,14 @@ export default class ConditionManager extends EventEmitter {
if (this.findConditionById(id)) { if (this.findConditionById(id)) {
this.conditionResults[id] = resultObj.data.result; this.conditionResults[id] = resultObj.data.result;
} }
this.updateTimestamp(resultObj.data);
} }
handleConditionResult(resultObj) { handleConditionResult(resultObj) {
// update conditions results and then calculate the current condition
this.updateConditionResults(resultObj); this.updateConditionResults(resultObj);
const currentCondition = this.getCurrentCondition(); const currentCondition = this.getCurrentCondition(this.conditionResults);
const timestamp = JSON.parse(JSON.stringify(resultObj.data))
delete timestamp.result
this.emit('conditionSetResultUpdated', this.emit('conditionSetResultUpdated',
Object.assign( Object.assign(
{ {
@ -225,34 +223,36 @@ export default class ConditionManager extends EventEmitter {
id: this.conditionSetDomainObject.identifier, id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id conditionId: currentCondition.id
}, },
this.latestTimestamp timestamp
) )
) )
} }
updateTimestamp(timestamp) {
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
if (!this.latestTimestamp[timeSystem.key]
|| timestamp[timeSystem.key] > this.latestTimestamp[timeSystem.key]
) {
this.latestTimestamp[timeSystem.key] = timestamp[timeSystem.key];
}
});
}
requestLADConditionSetOutput() { requestLADConditionSetOutput() {
if (!this.conditionClassCollection.length) { if (!this.conditionClassCollection.length) {
return Promise.resolve([]); return Promise.resolve([]);
} }
return this.compositionLoad.then(() => { return this.compositionLoad.then(() => {
const ladConditionResults = this.conditionClassCollection let latestTimestamp;
let conditionResults = {};
const conditionRequests = this.conditionClassCollection
.map(condition => condition.requestLADConditionResult()); .map(condition => condition.requestLADConditionResult());
return Promise.all(ladConditionResults) return Promise.all(conditionRequests)
.then((results) => { .then((results) => {
results.forEach(resultObj => { this.updateConditionResults(resultObj); }); results.forEach(resultObj => {
const currentCondition = this.getCurrentCondition(); const { id, data, data: { result } } = resultObj;
if (this.findConditionById(id)) {
conditionResults[id] = !!result;
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems
);
});
const currentCondition = this.getCurrentCondition(conditionResults);
return Object.assign( return Object.assign(
{ {
@ -260,7 +260,7 @@ export default class ConditionManager extends EventEmitter {
id: this.conditionSetDomainObject.identifier, id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id conditionId: currentCondition.id
}, },
this.latestTimestamp latestTimestamp
); );
}); });
}); });
@ -303,7 +303,7 @@ export default class ConditionManager extends EventEmitter {
this.composition.off('add', this.subscribeToTelemetry, this); this.composition.off('add', this.subscribeToTelemetry, this);
this.composition.off('remove', this.unsubscribeFromTelemetry, this); this.composition.off('remove', this.unsubscribeFromTelemetry, this);
Object.values(this.subscriptions).forEach(unsubscribe => unsubscribe()); Object.values(this.subscriptions).forEach(unsubscribe => unsubscribe());
this.subscriptions = undefined; delete this.subscriptions;
if(this.stopObservingForChanges) { if(this.stopObservingForChanges) {
this.stopObservingForChanges(); this.stopObservingForChanges();

View File

@ -49,6 +49,7 @@ describe('ConditionManager', () => {
}; };
let mockComposition; let mockComposition;
let loader; let loader;
let mockTimeSystems;
function mockAngularComponents() { function mockAngularComponents() {
let mockInjector = jasmine.createSpyObj('$injector', ['get']); let mockInjector = jasmine.createSpyObj('$injector', ['get']);
@ -111,6 +112,12 @@ describe('ConditionManager', () => {
openmct.objects.observe.and.returnValue(function () {}); openmct.objects.observe.and.returnValue(function () {});
openmct.objects.mutate.and.returnValue(function () {}); openmct.objects.mutate.and.returnValue(function () {});
mockTimeSystems = {
key: 'utc'
};
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']);
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener); conditionMgr.on('conditionSetResultUpdated', mockListener);

View File

@ -30,7 +30,8 @@ let openmct = {},
testTelemetryObject, testTelemetryObject,
conditionObj, conditionObj,
conditionManager, conditionManager,
mockBroadcastTelemetry; mockBroadcastTelemetry,
mockTimeSystems;
describe("The condition", function () { describe("The condition", function () {
@ -74,6 +75,12 @@ describe("The condition", function () {
openmct.telemetry.subscribe.and.returnValue(function () {}); openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values); openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values);
mockTimeSystems = {
key: 'utc'
};
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']);
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
testConditionDefinition = { testConditionDefinition = {
id: '123-456', id: '123-456',
configuration: { configuration: {

View File

@ -0,0 +1,34 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is 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.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export const getLatestTimestamp = (current, compare, timeSystems) => {
const timestamp = Object.assign({}, current);
timeSystems.forEach(timeSystem => {
if (!timestamp[timeSystem.key]
|| compare[timeSystem.key] > timestamp[timeSystem.key]
) {
timestamp[timeSystem.key] = compare[timeSystem.key];
}
});
return timestamp;
}