@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
JavaScript
;
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