/***************************************************************************** * Open MCT, Copyright (c) 2014-2024, 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. *****************************************************************************/ import { EventEmitter } from 'eventemitter3'; import { v4 as uuid } from 'uuid'; import Condition from './Condition.js'; import { getLatestTimestamp } from './utils/time.js'; export default class ConditionManager extends EventEmitter { #latestDataTable = new Map(); constructor(conditionSetDomainObject, openmct) { super(); this.openmct = openmct; this.conditionSetDomainObject = conditionSetDomainObject; this.timeSystems = this.openmct.time.getAllTimeSystems(); this.composition = this.openmct.composition.get(conditionSetDomainObject); this.composition.on('add', this.subscribeToTelemetry, this); this.composition.on('remove', this.unsubscribeFromTelemetry, this); this.shouldEvaluateNewTelemetry = this.shouldEvaluateNewTelemetry.bind(this); this.compositionLoad = this.composition.load(); this.telemetryCollections = {}; this.telemetryObjects = {}; this.testData = { conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData, applied: false }; this.initialize(); } subscribeToTelemetry(telemetryObject) { const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); if (this.telemetryCollections[keyString]) { return; } const requestOptions = { size: 1, strategy: 'latest' }; this.telemetryCollections[keyString] = this.openmct.telemetry.requestCollection( telemetryObject, requestOptions ); const metadata = this.openmct.telemetry.getMetadata(telemetryObject); const telemetryMetaData = metadata ? metadata.valueMetadatas : []; this.telemetryObjects[keyString] = { ...telemetryObject, telemetryMetaData }; this.telemetryCollections[keyString].on( 'add', this.telemetryReceived.bind(this, telemetryObject) ); this.telemetryCollections[keyString].load(); this.updateConditionTelemetryObjects(); } unsubscribeFromTelemetry(endpointIdentifier) { const keyString = this.openmct.objects.makeKeyString(endpointIdentifier); if (!this.telemetryCollections[keyString]) { return; } this.telemetryCollections[keyString].destroy(); this.telemetryCollections[keyString] = null; this.telemetryObjects[keyString] = null; this.removeConditionTelemetryObjects(); //force re-computation of condition set result as we might be in a state where // there is no telemetry datum coming in for a while or at all. const latestTimestamp = getLatestTimestamp( {}, {}, this.timeSystems, this.openmct.time.getTimeSystem() ); this.updateConditionResults({ id: keyString }); this.updateCurrentCondition(latestTimestamp); if (Object.keys(this.telemetryObjects).length === 0) { // no telemetry objects this.emit('noTelemetryObjects'); } } initialize() { this.conditions = []; if (this.conditionSetDomainObject.configuration.conditionCollection.length) { this.conditionSetDomainObject.configuration.conditionCollection.forEach( (conditionConfiguration, index) => { this.initCondition(conditionConfiguration, index); } ); } if (Object.keys(this.telemetryObjects).length === 0) { // no telemetry objects this.emit('noTelemetryObjects'); } } updateConditionTelemetryObjects() { this.conditions.forEach((condition) => { condition.updateTelemetryObjects(); let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex( (item) => item.id === condition.id ); if (index > -1) { //Only assign the summary, don't mutate the domain object this.conditionSetDomainObject.configuration.conditionCollection[index].summary = this.updateConditionDescription(condition); } }); } removeConditionTelemetryObjects() { let conditionsChanged = false; this.conditionSetDomainObject.configuration.conditionCollection.forEach( (conditionConfiguration, conditionIndex) => { let conditionChanged = false; conditionConfiguration.configuration.criteria.forEach((criterion, index) => { const isAnyAllTelemetry = criterion.telemetry && (criterion.telemetry === 'any' || criterion.telemetry === 'all'); if (!isAnyAllTelemetry) { const found = Object.values(this.telemetryObjects).find((telemetryObject) => { return this.openmct.objects.areIdsEqual( telemetryObject.identifier, criterion.telemetry ); }); if (!found) { criterion.telemetry = ''; criterion.metadata = ''; criterion.input = []; criterion.operation = ''; conditionChanged = true; } } else { conditionChanged = true; } }); if (conditionChanged) { this.updateCondition(conditionConfiguration, conditionIndex); conditionsChanged = true; } } ); if (conditionsChanged) { this.persistConditions(); } } updateConditionDescription(condition) { condition.updateDescription(); return condition.summary; } updateCondition(conditionConfiguration) { let condition = this.findConditionById(conditionConfiguration.id); if (condition) { condition.update(conditionConfiguration); conditionConfiguration.summary = this.updateConditionDescription(condition); } let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex( (item) => item.id === conditionConfiguration.id ); if (index > -1) { this.conditionSetDomainObject.configuration.conditionCollection[index] = conditionConfiguration; this.persistConditions(); } } initCondition(conditionConfiguration, index) { let condition = new Condition(conditionConfiguration, this.openmct, this); conditionConfiguration.summary = this.updateConditionDescription(condition); if (index !== undefined) { this.conditions.splice(index + 1, 0, condition); } else { this.conditions.unshift(condition); } } createCondition(conditionConfiguration) { let conditionObj; if (conditionConfiguration) { conditionObj = { ...conditionConfiguration, id: uuid(), configuration: { ...conditionConfiguration.configuration, name: `Copy of ${conditionConfiguration.configuration.name}` } }; } else { conditionObj = { id: uuid(), configuration: { name: 'Unnamed Condition', output: 'false', trigger: 'all', criteria: [ { id: uuid(), telemetry: '', operation: '', input: [], metadata: '' } ] }, summary: '' }; } return conditionObj; } addCondition() { this.createAndSaveCondition(); } cloneCondition(conditionConfiguration, index) { let clonedConfig = JSON.parse(JSON.stringify(conditionConfiguration)); clonedConfig.configuration.criteria.forEach((criterion) => (criterion.id = uuid())); this.createAndSaveCondition(index, clonedConfig); } createAndSaveCondition(index, conditionConfiguration) { const newCondition = this.createCondition(conditionConfiguration); if (index !== undefined) { this.conditionSetDomainObject.configuration.conditionCollection.splice( index + 1, 0, newCondition ); } else { this.conditionSetDomainObject.configuration.conditionCollection.unshift(newCondition); } this.initCondition(newCondition, index); this.persistConditions(); } removeCondition(id) { let index = this.conditions.findIndex((item) => item.id === id); if (index > -1) { this.conditions[index].destroy(); this.conditions.splice(index, 1); } let conditionCollectionIndex = this.conditionSetDomainObject.configuration.conditionCollection.findIndex( (item) => item.id === id ); if (conditionCollectionIndex > -1) { this.conditionSetDomainObject.configuration.conditionCollection.splice( conditionCollectionIndex, 1 ); this.persistConditions(); } } findConditionById(id) { return this.conditions.find((condition) => condition.id === id); } reorderConditions(reorderPlan) { let oldConditions = Array.from(this.conditionSetDomainObject.configuration.conditionCollection); let newCollection = []; reorderPlan.forEach((reorderEvent) => { let item = oldConditions[reorderEvent.oldIndex]; newCollection.push(item); }); this.conditionSetDomainObject.configuration.conditionCollection = newCollection; this.persistConditions(); } 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]; for (let i = 0; i < conditionCollection.length - 1; i++) { if (conditionResults[conditionCollection[i].id]) { //first condition to be true wins currentCondition = conditionCollection[i]; break; } } return currentCondition; } async requestLADConditionSetOutput(options) { if (!this.conditions.length) { return []; } await this.compositionLoad; let latestTimestamp; let conditionResults = {}; let nextLegOptions = { ...options }; delete nextLegOptions.onPartialResponse; const results = await Promise.all( this.conditions.map((condition) => condition.requestLADConditionResult(nextLegOptions)) ); results.forEach((resultObj) => { const { id, data, data: { result } } = resultObj; if (this.findConditionById(id)) { conditionResults[id] = Boolean(result); } latestTimestamp = getLatestTimestamp( latestTimestamp, data, this.timeSystems, this.openmct.time.getTimeSystem() ); }); if (!Object.values(latestTimestamp).some((timeSystem) => timeSystem)) { return []; } const currentCondition = this.getCurrentConditionLAD(conditionResults); const currentOutput = { output: currentCondition.configuration.output, id: this.conditionSetDomainObject.identifier, conditionId: currentCondition.id, ...latestTimestamp }; return [currentOutput]; } isTelemetryUsed(endpoint) { const id = this.openmct.objects.makeKeyString(endpoint.identifier); for (let condition of this.conditions) { if (condition.isTelemetryUsed(id)) { return true; } } return false; } shouldEvaluateNewTelemetry(currentTimestamp) { return this.openmct.time.getBounds().end >= currentTimestamp; } telemetryReceived(endpoint, data) { if (!this.isTelemetryUsed(endpoint)) { return; } const datum = data[0]; const normalizedDatum = this.createNormalizedDatum(datum, endpoint); const timeSystemKey = this.openmct.time.getTimeSystem().key; const currentTimestamp = normalizedDatum[timeSystemKey]; const timestamp = {}; timestamp[timeSystemKey] = currentTimestamp; this.#latestDataTable.set(normalizedDatum.id, normalizedDatum); if (this.shouldEvaluateNewTelemetry(currentTimestamp)) { this.updateConditionResults(); this.updateCurrentCondition(timestamp); } } updateConditionResults() { //We want to stop when the first condition evaluates to true. this.conditions.some((condition) => { condition.updateResult(this.#latestDataTable); return condition.result === true; }); } updateCurrentCondition(timestamp) { const currentCondition = this.getCurrentCondition(); this.emit( 'conditionSetResultUpdated', Object.assign( { output: currentCondition.configuration.output, id: this.conditionSetDomainObject.identifier, conditionId: currentCondition.id }, timestamp ) ); } getTestData(metadatum) { let data = undefined; if (this.testData.applied) { const found = this.testData.conditionTestInputs.find( (testInput) => testInput.metadata === metadatum.source ); if (found) { data = found.value; } } return data; } createNormalizedDatum(telemetryDatum, endpoint) { const id = this.openmct.objects.makeKeyString(endpoint.identifier); const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas; const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => { const testValue = this.getTestData(metadatum); const formatter = this.openmct.telemetry.getValueFormatter(metadatum); datum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]); return datum; }, {}); normalizedDatum.id = id; return normalizedDatum; } updateTestData(testData) { if (!_.isEqual(testData, this.testData)) { this.testData = testData; this.openmct.objects.mutate( this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs ); } } persistConditions() { this.openmct.objects.mutate( this.conditionSetDomainObject, 'configuration.conditionCollection', this.conditionSetDomainObject.configuration.conditionCollection ); } destroy() { this.composition.off('add', this.subscribeToTelemetry, this); this.composition.off('remove', this.unsubscribeFromTelemetry, this); Object.values(this.telemetryCollections).forEach((telemetryCollection) => telemetryCollection.destroy() ); this.conditions.forEach((condition) => { condition.destroy(); }); } }