UNPKG

@corvina/device-client

Version:
655 lines 29.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DeviceService = void 0; const types_1 = require("./../common/types"); const messagepublisherpolicies_1 = require("./messagepublisherpolicies"); const pem_1 = __importDefault(require("pem")); const fs_1 = __importDefault(require("fs")); const licensesaxiosinstance_1 = __importDefault(require("./licensesaxiosinstance")); const mqtt_1 = __importDefault(require("mqtt")); const bson_1 = __importDefault(require("bson")); const lodash_1 = __importDefault(require("lodash")); const simulation_1 = require("./simulation"); const types_2 = require("../common/types"); const zlib = __importStar(require("zlib")); const assert = require("assert"); const corvinadatainterface_1 = __importDefault(require("./corvinadatainterface")); const logger_service_1 = require("./logger.service"); const stream_1 = require("stream"); const https = __importStar(require("https")); const mqtt_level_store_1 = __importDefault(require("mqtt-level-store")); const x509 = require("x509.js"); const ONLY_TEST_CONNECTION = process.env["ONLY_TEST_CONNECTION"] === "true" || false; class DeviceService extends stream_1.EventEmitter { inited; initPending; readyToTransmit; licenseData; mqttClient; msgSentStats = 0; byteSentStats = 0; lastDateStats = Date.now(); lastTriedBrokerEndpoint = 0; empyCacheTopic; introspectionTopic; static baseIntrospection = "com.corvina.control.sub.Config:0:2;com.corvina.control.pub.Config:0:2;com.corvina.control.pub.DeviceAlarm:2:0;com.corvina.control.sub.DeviceAlarm:1:0"; customIntrospections; applyConfigTopic; consumerPropertiesTopic; actionAlarmTopic; configTopic; availableTagsTopic; lastConfig; _deviceConfig; axios; dataInterface; messageStore; defaultQoS = process.env["MQTT_DEFAULT_QOS"] ? parseInt(process.env["MQTT_DEFAULT_QOS"]) : 0; constructor() { super(); if (process.env["MQTT_MSG_STORE_PATH"]) { this.messageStore = new mqtt_level_store_1.default(process.env["MQTT_MSG_STORE_PATH"]); } this._deviceConfig = {}; this.dataInterface = new corvinadatainterface_1.default({ sendMessage: this.sendMessage.bind(this), }); if (process.env["NODE_TLS_REJECT_UNAUTHORIZED"] !== "0") { let currentCa = https.globalAgent?.options?.ca; if (currentCa) { currentCa = currentCa + "\n" + this.getCA(); } else { currentCa = this.getCA(); } https.globalAgent.options.ca = currentCa; } } get status() { return { msgSent: this.getMsgSent(), bytesSent: this.getBytesSent(), ready: this.isReady(), connected: this.isConnected(), inited: this.isInited(), }; } get deviceConfig() { return this._deviceConfig; } getMsgSent() { return this.msgSentStats; } getBytesSent() { return this.byteSentStats; } getAppliedConfig() { return this.lastConfig; } getDeviceConfig() { return this._deviceConfig; } getLicenseData() { return this.licenseData; } setCycleTime(cycleTime) { this.dataInterface.setCycleTime(cycleTime); } reinit(deviceConfig, doInit = false) { this.inited = false; this.readyToTransmit = false; if (this.mqttClient) { logger_service_1.l.debug("Going to end mqtt client"); this.mqttClient.end(true); this.mqttClient = null; } else { logger_service_1.l.debug("No mqtt client to end"); } simulation_1.DataSimulator.clear(); this.initPending = null; this.licenseData = {}; this.customIntrospections = ""; this.lastConfig = ""; Object.assign(this._deviceConfig, deviceConfig); this._deviceConfig.dynamicTags = new Map(); logger_service_1.l.info("Init with %j", this._deviceConfig); this.axios = new licensesaxiosinstance_1.default(this._deviceConfig.pairingEndpoint, this._deviceConfig.activationKey); this.init(); return this._deviceConfig; } isInited() { return this.inited; } isReady() { return this.readyToTransmit; } isConnected() { return this.mqttClient && this.mqttClient.connected; } setReady(ready) { if (this.readyToTransmit != ready) { this.readyToTransmit = ready; if (ready) { this.emit("ready", ready); } else { this.emit("not_ready", ready); } } } createCSR(logicalId) { return new Promise((resolve, reject) => { pem_1.default.createCSR({ organization: "System", commonName: `${this.licenseData.logicalId}`, }, (err, obj) => { if (err == null) { logger_service_1.l.debug("PEM RETURNED %s %s", err, obj); resolve(obj); } else { reject(err); } }); }); } async applyConfig(config) { if (JSON.stringify(config) == JSON.stringify(this.lastConfig)) { logger_service_1.l.info("Found same config => return"); return; } if (this.initPending) { await this.initPending; } this.setReady(false); this.customIntrospections = ""; logger_service_1.l.debug("Apply config: %s", JSON.stringify(config)); this.dataInterface.applyConfig(config); this.dataInterface.config.interfaceNames.forEach((interfaceName) => { this.customIntrospections += `;${interfaceName}`; }); logger_service_1.l.debug("Applied config done!"); this.lastConfig = config; setTimeout(async () => { logger_service_1.l.debug("Going to end mqtt client"); await this.mqttClient.end(); setTimeout(async () => await this.mqttClient.reconnect({ incomingStore: this.messageStore?.incoming, outgoingStore: this.messageStore?.outgoing, }), 1000); }, 0); } serializeMessage(msg) { if (this._deviceConfig.packetFormat == types_1.PacketFormatEnum.BSON) { return bson_1.default.serialize({ v: msg.v, t: new Date(msg.t), m: msg.m }); } else { return JSON.stringify(msg); } } getCA() { if (process.env["BROKER_CA_FILE"]) { try { return fs_1.default.readFileSync(process.env["BROKER_CA_FILE"]); } catch (e) { logger_service_1.l.error("Error reading CA file: %s", e); return ""; } } return `-----BEGIN CERTIFICATE----- MIICWTCCAf+gAwIBAgIUAkkMEwP0AejpBDLeXUiBRJSDv7UwCgYIKoZIzj0EAwIw eTELMAkGA1UEBhMCSVQxDjAMBgNVBAgMBUl0YWx5MSgwJgYDVQQKDB9FeG9yIERl dmljZXMgRGlnaXRhbCBJZGVudGl0aWVzMTAwLgYDVQQDDCdFeG9yIERldmljZXMg RGlnaXRhbCBJZGVudGl0aWVzIFJvb3QgQ0EwIBcNMjAxMjEwMTAwOTQ5WhgPMjA2 MjAxMDQxMDA5NDlaMHkxCzAJBgNVBAYTAklUMQ4wDAYDVQQIDAVJdGFseTEoMCYG A1UECgwfRXhvciBEZXZpY2VzIERpZ2l0YWwgSWRlbnRpdGllczEwMC4GA1UEAwwn RXhvciBEZXZpY2VzIERpZ2l0YWwgSWRlbnRpdGllcyBSb290IENBMFkwEwYHKoZI zj0CAQYIKoZIzj0DAQcDQgAEQGKIj1KpHpRk5ZOYvf9g33ENs2gOBu3RsCneaYKQ Jhhl8wzVnt8vA4wzgv7B9Jui5+efYIk9N19jZ9H8JAjDZKNjMGEwHQYDVR0OBBYE FO3l09dQYmSZ5+VuR8IDyNDSrP8cMB8GA1UdIwQYMBaAFO3l09dQYmSZ5+VuR8ID yNDSrP8cMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49 BAMCA0gAMEUCIEBfvBPKnQSGQhk/JLvtdsC9AUhzmpnmXKqztImkkkfJAiEAqEOc fLibdXgfUjlbFwApfXoXZsYZMwyFq/HjIKS1pyA= -----END CERTIFICATE-----`; } connectClient(broker_url, key, crt) { logger_service_1.l.info("Connecting to mqtt broker %s", broker_url); return new Promise(async (resolve, reject) => { const mqttClientOptions = {}; mqttClientOptions.rejectUnauthorized = process.env["NODE_TLS_REJECT_UNAUTHORIZED"] === "0" ? false : true; mqttClientOptions.ca = this.getCA(); mqttClientOptions.key = key; mqttClientOptions.cert = crt; mqttClientOptions.clean = true; mqttClientOptions.clientId = x509.parseCert(crt).subject.commonName; mqttClientOptions.reconnectPeriod = 10000; logger_service_1.l.debug(mqttClientOptions, "MQTT options"); mqttClientOptions.incomingStore = this.messageStore?.incoming; mqttClientOptions.outgoingStore = this.messageStore?.outgoing; this.mqttClient = mqtt_1.default.connect(broker_url, mqttClientOptions); logger_service_1.l.debug("MQTT client created"); this.mqttClient.on("connect", async (v) => { logger_service_1.l.info(`Successfully connected to mqtt broker! ${JSON.stringify(v)}`); this.subscribeChannel(this.consumerPropertiesTopic); this.subscribeChannel(this.applyConfigTopic); this.subscribeChannel(this.actionAlarmTopic); if (ONLY_TEST_CONNECTION) { logger_service_1.l.info("Connection test successful!"); process.exit(0); } logger_service_1.l.debug("Published introspection " + DeviceService.baseIntrospection + this.customIntrospections); await this.sendStringMessage(this.introspectionTopic, DeviceService.baseIntrospection + this.customIntrospections, { qos: 2 }); logger_service_1.l.debug("Published empty cache"); await this.sendStringMessage(this.empyCacheTopic, "1", { qos: 2, }); logger_service_1.l.debug("Published configuration"); await this.sendStringMessage(this.configTopic, this.serializeMessage({ v: JSON.stringify(this.lastConfig), t: Date.now(), }), { qos: 2 }); this.throttledUpdateAvailableTags(); if (this.dataInterface.config) { this.dataInterface.config.subscribedTopics.forEach((topic, topicName) => { this.subscribeChannel(this.licenseData.realm + "/" + this.licenseData.logicalId + topicName); }); } this.setReady(true); logger_service_1.l.info("Ready to transmit!"); simulation_1.DataSimulator.clear(); if (this._deviceConfig.simulateTags) { this._deviceConfig.availableTags.forEach((value) => { if (value.simulation === null) { return; } new simulation_1.DataSimulator(value.name, value.type, async (t, v, ts) => { if (this.isReady()) { return this._internalPost([{ tagName: t, value: v, timestamp: ts }], true); } return false; }, value.simulation); }); if (this._deviceConfig.simulateAlarms) { this._deviceConfig.availableAlarms.forEach((value) => { new simulation_1.AlarmSimulator(value, async (data) => { if (this.isReady()) { return this.postAlarm(data); } return false; }); }); } } resolve(true); }); this.mqttClient.on("close", () => { simulation_1.DataSimulator.clear(); logger_service_1.l.warn("Stream closed!"); }); this.mqttClient.on("reconnect", () => { simulation_1.DataSimulator.clear(); logger_service_1.l.warn("Stream reconnected!"); }); this.mqttClient.on("error", (error) => { logger_service_1.l.error(error, "Stream error!"); this.lastTriedBrokerEndpoint++; reject(error); }); this.mqttClient.on("message", async (topic, message) => { logger_service_1.l.info(`Received message on ${topic}\n`); let decodedMsg; switch (topic) { case this.consumerPropertiesTopic.toString(): decodedMsg = zlib.unzipSync(message.slice(4)).toString(); logger_service_1.l.debug("Received consumer properties!"); logger_service_1.l.trace(`<<<< %s %j %d %j`, topic, decodedMsg, message.length, message); break; case this.applyConfigTopic.toString(): decodedMsg = bson_1.default.deserialize(message); logger_service_1.l.trace(`<<<< %s %j %d %j`, topic, decodedMsg, message.length, message); this.applyConfig(JSON.parse(decodedMsg.v)); break; case this.actionAlarmTopic.toString(): decodedMsg = bson_1.default.deserialize(message); logger_service_1.l.trace(`<<<< %s %j %d %j`, topic, decodedMsg, message.length, message); const x = decodedMsg.v; const sim = simulation_1.BaseSimulator.simulatorsByTagName.get(simulation_1.AlarmSimulator.alarmSimulatorMapkey(x.name)); if (!sim) { logger_service_1.l.error("Trying to perform action on unknown alarm %s", x.name); } else { switch (x.command) { case "ack": sim.acknowledge(x.evTs, x.user, x.comment); break; case "reset": sim.reset(x.evTs, x.user, x.comment); break; } } break; default: decodedMsg = bson_1.default.deserialize(message); logger_service_1.l.trace(`<<<< %s %j %d %j`, topic, decodedMsg, message.length, message); const topicKey = topic.slice(this.licenseData.logicalId.length + this.licenseData.realm.length + 1); const subscriber = this.dataInterface.config.subscribedTopics.get(topicKey); if (subscriber) { this.onWrite(subscriber, decodedMsg); if (this._deviceConfig.simulateTags) { this.applyBackToSimulation(subscriber.targetTag, decodedMsg.v); } } else { logger_service_1.l.info(`Nothing to do for topic ${topic}`); } } }); }); } subscribeChannel(channel) { return new Promise((resolve, reject) => { this.mqttClient.subscribe(channel, function (err) { if (!err) { resolve(true); } else { logger_service_1.l.warn(err, `Error subscribing %s`, channel); reject(err); } }); }); } async sendStringMessage(channel, message, options = {}) { logger_service_1.l.debug("Going to publish %s", channel); return new Promise((resolve, reject) => { this.mqttClient.publish(channel, message, options, (err) => { if (!err) { resolve(true); } else { logger_service_1.l.warn(`Error publishing to ${channel}: %j`, err); reject(err); } }); }); } async sendMessage(topic, payload, options) { topic = this.licenseData.realm + "/" + this.licenseData.logicalId + topic; const message = this.serializeMessage(payload); this.byteSentStats += message.length; this.msgSentStats += 1; const timeDiff = Date.now() - this.lastDateStats; if (timeDiff > 10000) { this.byteSentStats = 0; this.msgSentStats = 0; this.lastDateStats = this.lastDateStats + timeDiff; } logger_service_1.l.debug("Going to send to topic %s", topic); try { if (!this.readyToTransmit) { const err = `Cannot publish if not ready to transmit`; logger_service_1.l.warn(err); if (options?.cb) { options.cb(new Error(err), undefined); } throw "Cannot publish if not ready to transmit"; } options = options || { qos: this.defaultQoS }; if (!options.qos) { options.qos = this.defaultQoS; } logger_service_1.l.trace(">>>> %s %j %d %j QoS %d", topic, payload, message.length, message, options.qos); if (options?.cb) { await this.mqttClient.publish(topic, message, options, (err, packet) => { options.cb(err, packet); }); } else { await this.mqttClient.publish(topic, message, options); } } catch (e) { logger_service_1.l.error("Got error while publishing: "); logger_service_1.l.error(e); return false; } } async _asyncInit() { try { this.licenseData = await this.axios.init(); logger_service_1.l.debug("Got api key %j ", this.licenseData); this.inited = true; const csr = await this.createCSR(this.licenseData.logicalId); logger_service_1.l.info({ msg: "CSR created", csr }); const crt = await this.axios.doPairing(csr.csr); logger_service_1.l.info({ msg: "Certificate signed", crt }); assert(await this.axios.verify(crt.client_crt)); this.empyCacheTopic = `${this.licenseData.realm}/${this.licenseData.logicalId}/control/emptyCache`; this.introspectionTopic = `${this.licenseData.realm}/${this.licenseData.logicalId}`; this.consumerPropertiesTopic = `${this.licenseData.realm}/${this.licenseData.logicalId}/control/consumer/properties`; this.applyConfigTopic = `${this.licenseData.realm}/${this.licenseData.logicalId}/com.corvina.control.sub.Config/applyConfiguration`; this.actionAlarmTopic = `${this.licenseData.realm}/${this.licenseData.logicalId}/com.corvina.control.sub.DeviceAlarm/a`; this.configTopic = `${this.licenseData.realm}/${this.licenseData.logicalId}/com.corvina.control.pub.Config/configuration`; this.availableTagsTopic = `${this.licenseData.realm}/${this.licenseData.logicalId}/com.corvina.control.pub.Config/availableTags`; await this.connectClient(this.licenseData.brokerUrls[this.lastTriedBrokerEndpoint % this.licenseData.brokerUrls.length], csr.clientKey, crt.client_crt); } catch (err) { this.inited = false; throw err; } return this.inited; } async init() { if (this.inited == false && this.initPending == null) { this.initPending = this._asyncInit(); try { await this.initPending; } catch (err) { logger_service_1.l.error("Error initing:"); logger_service_1.l.error(err); this.initPending = null; const randomRetry = 5 + 10 * Math.random(); logger_service_1.l.warn(`Retry init in ${randomRetry} secs`); if (this.mqttClient) { logger_service_1.l.debug("Going to end mqtt client"); this.mqttClient.end(true); this.mqttClient = null; } else { logger_service_1.l.debug("No mqtt client to end"); } setTimeout(() => { this.init(); }, randomRetry * 1000); } this.initPending = null; } return this.inited; } throttledUpdateAvailableTags = lodash_1.default.throttle(async () => { try { await this.sendStringMessage(this.availableTagsTopic, this.serializeMessage({ v: JSON.stringify([ ...this._deviceConfig.availableTags.values(), ...(this._deviceConfig.dynamicTags ? this._deviceConfig.dynamicTags.values() : []), ]), t: Date.now(), }), { qos: 2 }); } catch (e) { } }, 1000, { leading: false, trailing: true }); jsToCorvinaType(value) { switch (typeof value) { case "number": return "double"; case "string": return "string"; case "object": if (lodash_1.default.isArray(value)) { if (value.length > 0 && typeof value[0] === "string") { return "stringarray"; } return "doublearray"; } else { return "struct"; } break; default: return undefined; } } applyBackToSimulation(tagName, value) { const sim = simulation_1.BaseSimulator.simulatorsByTagName?.get(tagName); if (sim) { sim.value = value; sim.lastSentValue = value; if (sim.desc?.type == types_2.SimulationType.CONST) { sim.desc.value = value; } } else { if (this._deviceConfig.simulateTags) { const sim = new simulation_1.DataSimulator(tagName, this.jsToCorvinaType(value), async (t, v, ts) => { if (this.isReady()) { return this._internalPost([{ tagName: t, value: v, timestamp: ts }], true); } return false; }, { type: types_2.SimulationType.CONST, value: value }); logger_service_1.l.info({ sim }, "Inited new const simulator"); } } } recurseNotifyObject = (prefix, rootValue, ts, calledFromSimulation, options) => { lodash_1.default.mapKeys(rootValue, (value, key) => { const decoratedName = `${prefix}${key}`; if (lodash_1.default.isArray(value) && value.length > 0 && !lodash_1.default.isObject(value[0])) { for (const e in value) { this.recurseNotifyObject(`${decoratedName}[${e}]`, value[e], ts, calledFromSimulation, options); } } else if (lodash_1.default.isObject(value)) { this.recurseNotifyObject(decoratedName + ".", value, ts, calledFromSimulation, options); } if (this._deviceConfig.dynamicTags && !this._deviceConfig.dynamicTags.has(decoratedName) && !this._deviceConfig.availableTags.has(decoratedName) && value != undefined) { this._deviceConfig.dynamicTags.set(decoratedName, { name: decoratedName, type: this.jsToCorvinaType(value), }); this.throttledUpdateAvailableTags(); } if (this.dataInterface.config) { if (!calledFromSimulation) { this.applyBackToSimulation(decoratedName, value); } this.dataInterface.notifyTag(decoratedName, new messagepublisherpolicies_1.State(value, ts), options); } }); if (this.dataInterface.config && prefix.length > 0) { this.dataInterface.notifyTag(prefix.endsWith(".") ? prefix.slice(0, -1) : prefix, new messagepublisherpolicies_1.State(rootValue, ts), options); } }; async post(dataPoints, options) { return this._internalPost(dataPoints, false, options); } async _internalPost(dataPoints, calledFromSimulation, options) { if (!this.readyToTransmit) { const err = `Cannot process ${JSON.stringify(dataPoints)}. Device not ready to transmit!`; if (options?.cb) { options.cb(new Error(err), undefined, undefined); } logger_service_1.l.info(err); return false; } for (const dp of dataPoints) { if (dp.tagName == undefined) { assert(lodash_1.default.isObject(dp.value) && !lodash_1.default.isArray(dp.value)); this.recurseNotifyObject("", dp.value, dp.timestamp, calledFromSimulation, options); } else { if (lodash_1.default.isObject(dp.value) && !options?.recurseNotifyOnlyWholeObject && !lodash_1.default.isArray(dp.value)) { this.recurseNotifyObject(dp.tagName + ".", dp.value, dp.timestamp, calledFromSimulation, options); } else { if (this.dataInterface.config) { if (!calledFromSimulation && this._deviceConfig.simulateTags) { this.applyBackToSimulation(dp.tagName, dp.value); } this.dataInterface.notifyTag(dp.tagName, new messagepublisherpolicies_1.State(dp.value, dp.timestamp), options); } } } } if (!this.dataInterface.config) { const err = `Cannot process ${JSON.stringify(dataPoints)}. Device is not configured yet!`; logger_service_1.l.info(err); if (options?.cb) { options.cb(new Error(err), undefined, undefined); } return false; } return true; } async postAlarm(alarmData) { const payload = this.serializeMessage({ t: Date.now(), v: alarmData }); const topic = `${this.licenseData.realm}/${this.licenseData.logicalId}/com.corvina.control.pub.DeviceAlarm/a`; logger_service_1.l.debug("Going to send alarm "); logger_service_1.l.trace(">>>> %s %j %d %j", topic, { t: Date.now(), v: alarmData }, payload.length, payload); await this.mqttClient.publish(topic, payload, { qos: 2 }); return true; } onWrite(subscriber, message) { logger_service_1.l.debug("CorvinaDataInterface.onWrite %j", message); this.emit("write", { topic: subscriber.topic, modelPath: subscriber.modelPath, fieldName: subscriber.fieldName, tagName: subscriber.targetTag, v: (0, types_1.castCorvinaType)(message.v, subscriber.topicType), }); } } exports.DeviceService = DeviceService; //# sourceMappingURL=device.service.js.map