UNPKG

@mindconnect/mindconnect-nodejs

Version:

NodeJS Library for Siemens Insights Hub Connectivity - TypeScript SDK for Insights Hub and Industrial IoT - Command Line Interface - Insights Hub Development Proxy (Siemens Insights Hub was formerly known as MindSphere)

620 lines 29.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MindConnectAgent = void 0; const cross_fetch_1 = require("cross-fetch"); const debug = require("debug"); const path = require("path"); require("url-search-params-polyfill"); const agent_auth_1 = require("./agent-auth"); const mindconnect_template_1 = require("./mindconnect-template"); const mindconnect_validators_1 = require("./mindconnect-validators"); const sdk_1 = require("./sdk"); const multipart_uploader_1 = require("./sdk/common/multipart-uploader"); const utils_1 = require("./utils"); const _ = require("lodash"); const log = debug("mindconnect-agent"); /** * MindConnect Agent implements the V3 of the Mindsphere API. * * * The synchronous methods (IsOnBoarded, HasConfiguration, HasDataMapping...) are operating on agent state storage only. * * The asynchronous methods (GetDataSourceConfiguration, BulkPostData...) are calling MindSphere APIs. * * @see https://developer.siemens.com/industrial-iot-open-source/mindconnect-nodejs/agent-development/agent-state-storage.html * * @export * @class MindConnectAgent */ class MindConnectAgent extends agent_auth_1.AgentAuth { constructor() { super(...arguments); this._sdk = undefined; this.uploader = new multipart_uploader_1.MultipartUploader(this); } ClientId() { return this._configuration.content.clientId || "not defined yet"; } /** * * Check in the local storage if the agent is onboarded. * * * This is a local agent state storage setting only. MindSphere API is not called. * * @see https://developer.siemens.com/industrial-iot-open-source/mindconnect-nodejs/agent-development/agent-state-storage.html * * @returns {boolean} * @memberof MindConnectAgent */ IsOnBoarded() { return this._configuration.response ? true : false; } /** * Checks in the local storage if the agent has a data source configuration. * * * This is a local agent state storage setting only. MindSphere API is not called. * * Call await GetDataSourceConfiguration() if you want to check if there is configuration in the mindsphere. * * @see https://developer.siemens.com/industrial-iot-open-source/mindconnect-nodejs/agent-development/agent-state-storage.html * * @returns {boolean} * @memberof MindConnectAgent */ HasDataSourceConfiguration() { if (!this._configuration.dataSourceConfiguration) { return false; } else if (!this._configuration.dataSourceConfiguration.configurationId) { return false; } else { return true; } } /** * Checks in the local storage if the agent has configured mappings. * * * This is a local agent state storage setting only. MindSphere API is not called. * * Call await GetDataMappings() to check if the agent has configured mappings in the MindSphere. * * @see https://developer.siemens.com/industrial-iot-open-source/mindconnect-nodejs/agent-development/agent-state-storage.html * * @returns {boolean} * @memberof MindConnectAgent */ HasDataMappings() { return (this._configuration.mappings || []).length > 0; } /** * Stores the configuration in the mindsphere. * * By default the eTag parameter in the provided configuration is ignored, and the agent just updates the configuration every time the put method is stored * and automatically increases the eTag. * This is why its a good idea to check if the configuration was stored before the data was posted. If the ignoreEtag is set to false then the agent just uses * the eTag which was specified in the configuration. This might throw an "already stored" exception in the mindsphere. * * @param {DataSourceConfiguration} dataSourceConfiguration * @param {boolean} [ignoreEtag=true] * @returns {Promise<DataSourceConfiguration>} * @memberof MindConnectAgent */ PutDataSourceConfiguration(dataSourceConfiguration_1) { return __awaiter(this, arguments, void 0, function* (dataSourceConfiguration, ignoreEtag = true) { yield this.RenewToken(); this.checkConfiguration(); const eTag = this.calculateEtag(ignoreEtag, dataSourceConfiguration); const agentManangement = this.Sdk().GetAgentManagementClient(); try { const storedConfig = yield agentManangement.PutDataSourceConfiguration(this.ClientId(), dataSourceConfiguration, { ifMatch: eTag.toString(), }); this._configuration.dataSourceConfiguration = storedConfig; yield (0, utils_1.retry)(5, () => this.SaveConfig()); return storedConfig; } catch (err) { log(err); throw new Error(`Network error occured ${err.message}`); } }); } calculateEtag(ignoreEtag, dataSourceConfiguration) { let eTag = 0; if (this._configuration.dataSourceConfiguration && this._configuration.dataSourceConfiguration.eTag) { eTag = parseInt(this._configuration.dataSourceConfiguration.eTag); if (isNaN(eTag)) throw new Error("Invalid eTag in configuration!"); } if (!ignoreEtag) { if (!dataSourceConfiguration.eTag) throw new Error("There is no eTag in the provided configuration!"); eTag = parseInt(dataSourceConfiguration.eTag); if (isNaN(eTag)) throw new Error("Invalid eTag in provided configuration!"); } return eTag; } /** * Acquire DataSource Configuration and store it in the Agent Storage. * * @see https://developer.siemens.com/industrial-iot-open-source/mindconnect-nodejs/agent-development/agent-state-storage.html * * @returns {Promise<DataSourceConfiguration>} * * @memberOf MindConnectAgent */ GetDataSourceConfiguration() { return __awaiter(this, void 0, void 0, function* () { yield this.RenewToken(); if (!this._accessToken) throw new Error("The agent doesn't have a valid access token."); if (!this._configuration.content.clientId) throw new Error("No client id in the configuration of the agent."); const agentManagment = this.Sdk().GetAgentManagementClient(); try { const result = yield agentManagment.GetDataSourceConfiguration(this.ClientId()); this._configuration.dataSourceConfiguration = result; yield (0, utils_1.retry)(5, () => this.SaveConfig()); return result; } catch (err) { log(err); throw new Error(`Network error occured ${err.message}`); } }); } /** * Acquire the data mappings from the MindSphere and store them in the agent state storage. * * @see https://developer.siemens.com/industrial-iot-open-source/mindconnect-nodejs/agent-development/agent-state-storage.html * * @returns {Promise<Array<Mapping>>} * * @memberOf MindConnectAgent */ GetDataMappings() { return __awaiter(this, void 0, void 0, function* () { yield this.RenewToken(); this.checkConfiguration(); const mcapi = this.Sdk().GetMindConnectApiClient(); const agentFilter = JSON.stringify({ agentId: `${this._configuration.content.clientId}` }); try { const result = yield mcapi.GetDataPointMappings({ size: 2000, filter: agentFilter, }); this._configuration.mappings = result.content; yield (0, utils_1.retry)(5, () => this.SaveConfig()); return result.content; } catch (err) { log(err); throw new Error(`Network error occured ${err.message}`); } }); } checkConfiguration() { !this._accessToken && (0, utils_1.throwError)("The agent doesn't have a valid access token."); !this._configuration.content.clientId && (0, utils_1.throwError)("No client id in the configuration of the agent."); } /** * Store data mappings in the mindsphere and also in the local agent state storage. * * @see https://developer.siemens.com/industrial-iot-open-source/mindconnect-nodejs/agent-development/agent-state-storage.html * * @param {Mapping[]} mappings * @returns {Promise<boolean>} * * @memberOf MindConnectAgent */ PutDataMappings(mappings) { return __awaiter(this, void 0, void 0, function* () { yield this.RenewToken(); this.checkConfiguration(); const mcapi = this.Sdk().GetMindConnectApiClient(); for (const mapping of mappings) { log(`Storing mapping ${mapping}`); try { // we are ignoring the 409 so that method becomes retryable yield mcapi.PostDataPointMapping(mapping, { ignoreCodes: [409] }); } catch (err) { log(err); throw new Error(`Network error occured ${err.message}`); } let oldmappings = this._configuration.mappings || []; // there was a deprecation of old mappings in mindsphere, this isn't an array anymore. if (oldmappings.content) { oldmappings = []; } this._configuration.mappings = _.uniqWith([...oldmappings, ...mappings], (a, b) => { return (a.agentId === b.agentId && a.dataPointId === b.dataPointId && a.entityId === b.entityId && a.propertyName === b.propertyName && a.propertySetName === b.propertySetName); }); yield (0, utils_1.retry)(5, () => this.SaveConfig()); } return true; }); } /** * Deletes all mappings from the agent * * @memberOf MindConnectAgent */ DeleteAllMappings() { return __awaiter(this, void 0, void 0, function* () { const toDeleteMappings = yield this.GetDataMappings(); const mcapi = this.Sdk().GetMindConnectApiClient(); for (let index = 0; index < toDeleteMappings.length; index++) { const element = toDeleteMappings[index]; yield mcapi.DeleteDataMapping(element.id, { ignoreCodes: [404] }); } this._configuration.mappings = []; yield (0, utils_1.retry)(5, () => this.SaveConfig()); }); } /** * Posts the Events to the Exchange Endpoint * * @see: https://developer.mindsphere.io/apis/api-advanced-eventmanagement/index.html * * @param {*} events * @param {Date} [timeStamp=new Date()] * @param {boolean} [validateModel=true] * @returns {Promise<boolean>} * @memberof MindConnectAgent */ PostEvent(event_1) { return __awaiter(this, arguments, void 0, function* (event, timeStamp = new Date(), validateModel = true) { yield this.RenewToken(); if (!this._accessToken) throw new Error("The agent doesn't have a valid access token."); const eventManagement = this.Sdk().GetEventManagementClient(); const headers = Object.assign(Object.assign({}, this._apiHeaders), { Authorization: `Bearer ${this._accessToken.access_token}` }); // const url = `${this._configuration.content.baseUrl}/api/mindconnect/v3/exchange`; const url = `${this._configuration.content.baseUrl}/api/eventmanagement/v3/events`; log(`GetDataSourceConfiguration Headers ${JSON.stringify(headers)} Url ${url}`); if (!event.timestamp) { event.timestamp = timeStamp.toISOString(); } if (validateModel) { const validator = this.GetEventValidator(); const isValid = yield validator(event); if (!isValid) { throw new Error(`Data doesn't match the configuration! Errors: ${JSON.stringify(validator.errors)}`); } } yield eventManagement.PostEvent(event); return true; }); } /** * Post Data Point Values to the Exchange Endpoint * * @see: https://developer.mindsphere.io/howto/howto-upload-agent-data/index.html * * @param {DataPointValue[]} dataPoints * @param {Date} [timeStamp=new Date()] * @param {boolean} [validateModel=true] you can set this to false to speed up the things if your agent is working. * @returns {Promise<boolean>} * @memberof MindConnectAgent */ PostData(dataPoints_1) { return __awaiter(this, arguments, void 0, function* (dataPoints, timeStamp = new Date(), validateModel = true) { yield this.RenewToken(); if (!this._accessToken) throw new Error("The agent doesn't have a valid access token."); if (!this._configuration.content.clientId) throw new Error("No client id in the configuration of the agent."); if (!this._configuration.dataSourceConfiguration) throw new Error("No data source configuration for the agent."); if (!this._configuration.dataSourceConfiguration.configurationId) throw new Error("No data source configuration ID for the agent."); if (validateModel) { const validator = this.GetValidator(); const isValid = yield validator(dataPoints); if (!isValid) { throw new Error(`Data doesn't match the configuration! Errors: ${JSON.stringify(validator.errors)}`); } } const headers = Object.assign(Object.assign({}, this._multipartHeaders), { Authorization: `Bearer ${this._accessToken.access_token}` }); const url = `${this._configuration.content.baseUrl}/api/mindconnect/v3/exchange`; log(`GetDataSourceConfiguration Headers ${JSON.stringify(headers)} Url ${url}`); const dataMessage = (0, mindconnect_template_1.dataTemplate)(timeStamp, dataPoints, this._configuration.dataSourceConfiguration.configurationId); log(dataMessage); const result = yield this.SendMessage("POST", url, dataMessage, headers); return result; }); } /** * Post Bulk Data Point Values to the Exchange Endpoint. * * @param {TimeStampedDataPoint[]} timeStampedDataPoints * @param {boolean} [validateModel=true] * @returns {Promise<boolean>} * * @memberOf MindConnectAgent */ BulkPostData(timeStampedDataPoints_1) { return __awaiter(this, arguments, void 0, function* (timeStampedDataPoints, validateModel = true) { yield this.RenewToken(); if (!this._accessToken) throw new Error("The agent doesn't have a valid access token."); if (!this._configuration.content.clientId) throw new Error("No client id in the configuration of the agent."); if (!this._configuration.dataSourceConfiguration) throw new Error("No data source configuration for the agent."); if (!this._configuration.dataSourceConfiguration.configurationId) throw new Error("No data source configuration ID for the agent."); if (validateModel) { const validator = this.GetValidator(); for (let index = 0; index < timeStampedDataPoints.length; index++) { const element = timeStampedDataPoints[index]; const isValid = yield validator(element.values); if (!isValid) { throw new Error(`Data doesn't match the configuration! Errors: ${JSON.stringify(validator.errors)}`); } } } const headers = Object.assign(Object.assign({}, this._multipartHeaders), { Authorization: `Bearer ${this._accessToken.access_token}` }); const url = `${this._configuration.content.baseUrl}/api/mindconnect/v3/exchange`; log(`GetDataSourceConfiguration Headers ${JSON.stringify(headers)} Url ${url}`); const bulkDataMessage = (0, mindconnect_template_1.bulkDataTemplate)(timeStampedDataPoints, this._configuration.dataSourceConfiguration.configurationId); log(bulkDataMessage); const result = yield this.SendMessage("POST", url, bulkDataMessage, headers); return result; }); } /** * Upload file to MindSphere IOTFileService * * * This method is used to upload the files to the MindSphere. * * It supports standard and multipart upload which can be configured with the [optional.chunk] parameter. * * * The method will try to abort the multipart upload if an exception occurs. * * Multipart Upload is done in following steps: * * start multipart upload * * upload in parallel [optional.parallelUploadChunks] the file parts (retrying [optional.retry] times if configured) * * uploading last chunk. * * @param {string} entityId - asset id or agent.ClientId() for agent * @param {string} filepath - mindsphere file path * @param {(string | Buffer)} file - local path or Buffer * @param {fileUploadOptionalParameters} [optional] - optional parameters: enable chunking, define retries etc. * @param {(number | undefined)}[optional.part] multipart/upload part * @param {(Date | undefined)} [optional.timestamp] File timestamp in mindsphere. * @param {(string | undefined)} [optional.description] Description in mindsphere. * @param {(string | undefined)} [optional.type] Mime type in mindsphere. * @param {(number | undefined)} [optional.chunkSize] chunkSize. It must be bigger than 5 MB. Default 8 MB. * @param {(number | undefined)} [optional.retry] Number of retries * @param {(Function | undefined)} [optional.logFunction] log functgion is called every time a retry happens. * @param {(Function | undefined)} [optional.verboseFunction] verboseLog function. * @param {(boolean | undefined)} [optional.chunk] Set to true to enable multipart uploads * @param {(number | undefined)} [optional.parallelUploads] max paralell uploads for parts (default: 3) * @param {(number | undefined)} [optional.ifMatch] The etag for the upload. * @returns {Promise<string>} - md5 hash of the file * * @memberOf MindConnectAgent * * @example await agent.UploadFile (agent.GetClientId(), "some/mindsphere/path/file.txt", "file.txt"); * @example await agent.UploadFile (agent.GetClientId(), "some/other/path/10MB.bin", "bigFile.bin",{ chunked:true, retry:5 }); */ UploadFile(entityId, filepath, file, optional) { return __awaiter(this, void 0, void 0, function* () { const result = yield this.uploader.UploadFile(entityId, filepath, file, optional); yield (0, utils_1.retry)(5, () => this.SaveConfig()); return result; }); } /** * Uploads the file to mindsphere * * @deprecated please use UploadFile method instead this method will probably be removed in version 4.0.0 * * @param {string} file filename or buffer for upload * @param {string} fileType mime type (e.g. image/png) * @param {string} description description of the file * @param {boolean} [chunk=true] if this is set to false the system will only upload smaller files * @param {string} [entityId] entityid can be used to define the asset for upload, otherwise the agent is used. * @param {number} [chunkSize=8 * 1024 * 1024] - at the moment 8MB as per restriction of mindgate * @param {number} [maxSockets=3] - maxSockets for http Upload - number of parallel multipart uploads * @returns {Promise<string>} md5 hash of the uploaded file * * @memberOf MindConnectAgent */ Upload(file_1, fileType_1, description_1) { return __awaiter(this, arguments, void 0, function* (file, fileType, description, chunk = true, entityId, chunkSize = 8 * 1024 * 1024, maxSockets = 3, filePath) { const clientId = entityId || this.ClientId(); const filepath = filePath || (file instanceof Buffer ? "no-filepath-for-buffer" : path.basename(file)); return yield this.UploadFile(clientId, filepath, file, { type: fileType, description: description, chunk: chunk, chunkSize: chunkSize, parallelUploads: maxSockets, }); }); } SendMessage(method, url, dataMessage, headers) { return __awaiter(this, void 0, void 0, function* () { try { const response = yield (0, cross_fetch_1.default)(url, { method: method, body: dataMessage, headers: headers, agent: this._proxyHttpAgent, }); if (!response.ok) { log({ method: method, body: dataMessage, headers: headers, agent: this._proxyHttpAgent }); log(response); throw new Error(response.statusText); } const text = yield response.text(); if (response.status >= 200 && response.status <= 299) { const etag = response.headers.get("eTag"); return etag ? etag : true; } else { throw new Error(`Error occured response status ${response.status} ${text}`); } // process body } catch (err) { log(err); throw new Error(`Network error occured ${err.message}`); } }); } /** * Generates a Data Source Configuration for specified Asset Type * * you still have to generate the mappings (or use ConfigureAgentForAssetId method) * * @example * config = await agent.GenerateDataSourceConfiguration("castidev.Engine"); * * @param {string} assetTypeId * @param {("NUMERICAL" | "DESCRIPTIVE")} [mode="DESCRIPTIVE"] * * NUMERICAL MODE will use names like CF0001 for configurationId , DS0001,DS0002,DS0003... for data source ids and DP0001, DP0002... for dataPointIds * * DESCRIPTIVE MODE will use names like CF-assetName for configurationId , DS-aspectName... for data source ids and DP-variableName for data PointIds (default) * @returns {Promise<DataSourceConfiguration>} * * @memberOf MindConnectAgent */ GenerateDataSourceConfiguration(assetTypeId_1) { return __awaiter(this, arguments, void 0, function* (assetTypeId, mode = "DESCRIPTIVE") { const assetType = yield this.Sdk().GetAssetManagementClient().GetAssetType(assetTypeId, { exploded: true }); const dataSourceConfiguration = this.Sdk() .GetMindConnectApiClient() .GenerateDataSourceConfiguration(assetType, mode); return dataSourceConfiguration; }); } /** * Generate automatically the mappings for the specified target assetid * * !Important! this only works if you have created the data source coniguration automatically * * @example * config = await agent.GenerateDataSourceConfiguration("castidev.Engine"); * await agent.PutDataSourceConfiguration(config); * const mappings = await agent.GenerateMappings(targetassetId); * await agent.PutDataMappings (mappings); * * @param {string} targetAssetId * @returns {Mapping[]} * * @memberOf MindConnectAgent */ GenerateMappings(targetAssetId) { const mcapi = this.Sdk().GetMindConnectApiClient(); !this._configuration.dataSourceConfiguration && (0, utils_1.throwError)("no data source configuration! (have you forgotten to create / generate the data source configuration first?"); const mappings = mcapi.GenerateMappings(this._configuration.dataSourceConfiguration, this.ClientId(), targetAssetId); return mappings; } /** * This method can automatically create all necessary configurations and mappings for selected target asset id. * * * This method will automatically create all necessary configurations and mappings to start sending the data * * to an asset with selected assetid in Mindsphere * * @param {string} targetAssetId * @param {("NUMERICAL" | "DESCRIPTIVE")} mode * * * NUMERICAL MODE will use names like CF0001 for configurationId , DS0001,DS0002,DS0003... for data source ids and DP0001, DP0002... for dataPointIds * * DESCRIPTIVE MODE will use names like CF-assetName for configurationId , DS-aspectName... for data source ids and DP-variableName for data PointIds (default) * @param {boolean} [overwrite=true] ignore eTag will overwrite mappings and data source configuration * @memberOf MindConnectAgent */ ConfigureAgentForAssetId(targetAssetId_1) { return __awaiter(this, arguments, void 0, function* (targetAssetId, mode = "DESCRIPTIVE", overwrite = true) { const asset = yield this.Sdk().GetAssetManagementClient().GetAsset(targetAssetId); const configuration = yield this.GenerateDataSourceConfiguration(asset.typeId, mode); if (overwrite) { yield this.GetDataSourceConfiguration(); } yield this.PutDataSourceConfiguration(configuration, overwrite); if (overwrite) { yield this.DeleteAllMappings(); } const mappings = this.GenerateMappings(targetAssetId); yield this.PutDataMappings(mappings); }); } /** * MindSphere SDK using agent authentication * * ! important: not all APIs can be called with agent credentials, however MindSphere is currently working on making this possible. * * * Here is a list of some APIs which you can use: * * * AssetManagementClient (Read Methods) * * MindConnectApiClient * * @returns {MindSphereSdk} * * @memberOf MindConnectAgent */ Sdk() { if (!this._sdk) { this._sdk = new sdk_1.MindSphereSdk(this); } return this._sdk; } /** * Ajv Validator (@see https://github.com/ajv-validator/ajv) for the data points. Validates if the data points array is only * containing dataPointIds which are configured in the agent configuration. * * @returns {ajv.ValidateFunction} * * @memberOf MindConnectAgent */ GetValidator() { const model = this._configuration.dataSourceConfiguration; if (!model) { throw new Error("Invalid local configuration, Please get or crete the data source configuration."); } return (0, mindconnect_validators_1.dataValidator)(model); } /** * * Ajv Validator (@see https://github.com/ajv-validator/ajv) for the events. Validates the syntax of the mindsphere events. * * @returns {ajv.ValidateFunction} * * @memberOf MindConnectAgent */ GetEventValidator() { if (!this._eventValidator) { this._eventValidator = (0, mindconnect_validators_1.eventValidator)(); } return this._eventValidator; } /** * Get local configuration from the agent state storage. * * * This is a local agent state storage setting only. MindSphere API is not called. * * @see https://developer.siemens.com/industrial-iot-open-source/mindconnect-nodejs/agent-development/agent-state-storage.html * * @returns {IMindConnectConfiguration} * * @memberOf MindConnectAgent */ GetMindConnectConfiguration() { return this._configuration; } } exports.MindConnectAgent = MindConnectAgent; //# sourceMappingURL=mindconnect-agent.js.map