UNPKG

@yachteye/signalk-engineroom-plugin

Version:

Get EngineRoom data from the source (database or other) and add it to the SignalK graph.

346 lines (345 loc) 17 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()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const _1 = require("."); const mssql_1 = __importDefault(require("mssql")); const utils = require('@signalk/nmea0183-utilities'); const cloudwatch_1 = require("./cloudwatch"); /** * Access to the BDE database with Engine Data. */ class SqlBDE { constructor(app, settings, definitions) { this.pool = null; this.retryTimeOutSeconds = 60; this.heartbeatTimer = null; this.reconnecting = false; this.lastHeartbeat = null; this.lastHeartbeatError = null; this.lastConnected = null; this.lastDisconnect = null; this.lastError = null; this.reconnectCount = 0; this.app = app; this.dataDefinitions = definitions; this.config = { server: settings.sqlServer, database: settings.sqlDb, user: settings.sqlUser, password: settings.sqlPassword, pool: { max: 10, min: 0, idleTimeoutMillis: 30000, }, options: { trustServerCertificate: settings.trustServerCertificate, }, }; if (settings.sqlPort > 0) { this.config.port = settings.sqlPort; } this.connect(); } /** * Connect to the SQL database and get a Connection Pool. Retry when the connect fails. */ connect() { var _a; this.app.debug(`[SqlBDE] connect(): connect-status=${(_a = this.pool) === null || _a === void 0 ? void 0 : _a.connected}`); mssql_1.default.connect(this.config) .then((pool) => { this.app.setPluginStatus(`[SqlBDE]: connection pool created, connected-status=${pool.connected}.`); pool.on('error', (err) => { this.app.debug(`[SqlBDE] pool error: ${err}`); this.app.setPluginError(`[SqlBDE] pool error: ${err === null || err === void 0 ? void 0 : err.message}`); this.lastError = (err === null || err === void 0 ? void 0 : err.message) || String(err); this.lastDisconnect = new Date(); this.reconnect(); (0, cloudwatch_1.log)('warn', 'SQL disconnected', { error: (err === null || err === void 0 ? void 0 : err.message) || String(err) }); }); pool.on('warn', (warn) => { console.error(`[SqlBDE] pool warn: ${warn}`); }); pool.on('info', (info) => { console.error(`[SqlBDE] pool info: ${info}`); }); this.app.debug(`[SqlBDE] connect(): connecting-status=${pool.connecting}, connected-status=${pool.connected}.`); this.pool = pool; this.retryTimeOutSeconds = 60; this.reconnecting = false; this.lastConnected = new Date(); this.lastError = null; this.lastHeartbeatError = null; this.startHeartbeat(); (0, cloudwatch_1.log)('info', 'SQL connected', { server: this.config.server, database: this.config.database }); this.getIds(); }) .catch((err) => { this.app.debug(`[SqlBDE] connect(): Error creating connection pool ${err}`); this.app.setPluginError(`[SqlBDE]: ${err === null || err === void 0 ? void 0 : err.message}`); this.lastError = (err === null || err === void 0 ? void 0 : err.message) || String(err); this.reconnecting = false; (0, cloudwatch_1.log)('error', 'SQL connection failed', { error: (err === null || err === void 0 ? void 0 : err.message) || String(err), retryInSeconds: this.retryTimeOutSeconds }); this.retryTimeOutSeconds = this.retryTimeOutSeconds > 10 * 60 ? 10 * 60 : this.retryTimeOutSeconds * 2; setTimeout(() => { this.connect(); }, this.retryTimeOutSeconds * 1000); }); } /** * Start a periodic heartbeat that checks the SQL connection health. * If the connection is lost, triggers a reconnect. */ startHeartbeat() { this.stopHeartbeat(); this.heartbeatTimer = setInterval(() => __awaiter(this, void 0, void 0, function* () { if (!this.pool || !this.pool.connected) { this.app.debug(`[SqlBDE] heartbeat: pool not connected, triggering reconnect.`); this.lastHeartbeatError = 'Pool not connected'; this.lastDisconnect = new Date(); this.reconnect(); (0, cloudwatch_1.log)('warn', 'SQL heartbeat failed', { error: 'Pool not connected' }); return; } try { yield this.pool.query('SELECT 1'); this.lastHeartbeat = new Date(); this.lastHeartbeatError = null; } catch (err) { this.app.debug(`[SqlBDE] heartbeat: query failed (${err === null || err === void 0 ? void 0 : err.message}), triggering reconnect.`); this.lastHeartbeatError = (err === null || err === void 0 ? void 0 : err.message) || String(err); this.lastDisconnect = new Date(); this.reconnect(); (0, cloudwatch_1.log)('warn', 'SQL heartbeat failed', { error: (err === null || err === void 0 ? void 0 : err.message) || String(err) }); } }), SqlBDE.HEARTBEAT_INTERVAL_MS); } /** * Stop the heartbeat timer. */ stopHeartbeat() { if (this.heartbeatTimer !== null) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } } /** * Close the current pool (if any) and reconnect. Guards against concurrent reconnect attempts. */ reconnect() { if (this.reconnecting) { return; } this.reconnecting = true; this.reconnectCount++; this.stopHeartbeat(); this.app.debug(`[SqlBDE] reconnect(): closing current pool and reconnecting (attempt #${this.reconnectCount}).`); this.app.setPluginStatus(`[SqlBDE]: reconnecting...`); (0, cloudwatch_1.log)('warn', 'SQL reconnecting', { attempt: this.reconnectCount }); const closeAndReconnect = () => __awaiter(this, void 0, void 0, function* () { if (this.pool) { try { this.pool.removeAllListeners(); yield this.pool.close(); } catch (err) { this.app.debug(`[SqlBDE] reconnect(): error closing pool: ${err === null || err === void 0 ? void 0 : err.message}`); } this.pool = null; } this.connect(); }); closeAndReconnect(); } /** * Get the current status of the SQL connection for diagnostics. */ getStatus() { var _a, _b, _c, _d, _e, _f, _g, _h, _j; return { connected: (_b = (_a = this.pool) === null || _a === void 0 ? void 0 : _a.connected) !== null && _b !== void 0 ? _b : false, reconnecting: this.reconnecting, server: this.config.server, database: (_c = this.config.database) !== null && _c !== void 0 ? _c : '', port: this.config.port, lastHeartbeat: (_e = (_d = this.lastHeartbeat) === null || _d === void 0 ? void 0 : _d.toISOString()) !== null && _e !== void 0 ? _e : null, lastHeartbeatError: this.lastHeartbeatError, lastConnected: (_g = (_f = this.lastConnected) === null || _f === void 0 ? void 0 : _f.toISOString()) !== null && _g !== void 0 ? _g : null, lastDisconnect: (_j = (_h = this.lastDisconnect) === null || _h === void 0 ? void 0 : _h.toISOString()) !== null && _j !== void 0 ? _j : null, lastError: this.lastError, reconnectCount: this.reconnectCount, retryTimeOutSeconds: this.retryTimeOutSeconds, }; } /** * Disconnect and stop the heartbeat. Called when the plugin is stopped. */ disconnect() { return __awaiter(this, void 0, void 0, function* () { var _a; this.app.debug(`[SqlBDE] disconnect(): connected-status=${(_a = this.pool) === null || _a === void 0 ? void 0 : _a.connected}`); this.stopHeartbeat(); if (this.pool) { try { this.pool.removeAllListeners(); yield this.pool.close(); this.pool = null; } catch (err) { this.app.setPluginError(`[SqlBDE] disconnect() error: ${err}`); } } }); } /** * Get the database [Id] and [Scale] values for the data we need to retrieve. */ getIds() { console.log(`[SqlBDE] getIds()`); this.dataDefinitions.forEach((dataDef) => { if (this.pool) { this.pool .query(`SELECT [Id], [Scale] from [BDE].[dbo].[T_Tag] WHERE [DisplayName] LIKE '${dataDef.displayNameDb}'`) .then((r) => { if (r.recordset.length === 0) { if (!dataDef.displayNameDb.startsWith('N.A. ')) { console.warn(`[SqlBDE] getIds() No data found for '${dataDef.displayNameDb}' path='${dataDef.path}' `); } } else { dataDef.Id = r.recordset[0].Id; if (r.recordset[0].Scale) { dataDef.scaleDb = r.recordset[0].Scale; } } }) .catch((reason) => { this.app.debug(`[SqlBDE] getIds() error for ${dataDef.displayNameDb}: ${reason}`); }); } }); } /** * Get the specified value from the database table [T_Data]. The SQL Value is specified as bigint or null. * @param id The database Id to query. * @returns A Promise. */ queryDataById(id) { return __awaiter(this, void 0, void 0, function* () { // console.log(`[SqlBDE] queryDataById( ${id} )`); if (this.pool) { try { // const result = await this.pool.request() // .input('input_parameter', sql.Int, id) // .output('Value', sql.BigInt) // .output('Time',sql.DateTime2(7)) // .query('SELECT TOP(1) * FROM [BDE].[dbo].[T_Data] WHERE [Id] = @input_parameter ORDER BY [Time] DESC'); const result = yield this.pool.query(`SELECT TOP(1) [Time], [Value] FROM [BDE].[dbo].[T_Data] WHERE [Id] = ${id} ORDER BY [Time] DESC`); // console.log('Q', id, JSON.stringify(result)); // {"recordsets":[[{"Time":"2024-06-25T05:44:02.096Z","Value":"0"}]],"recordset":[{"Time":"2024-06-25T05:44:02.096Z","Value":"0"}],"output":{},"rowsAffected":[1]} const v = result.recordset[0].Value === null ? null : parseInt(result.recordset[0].Value, 10); return { sqlValue: v, timestamp: result.recordset[0].Time }; } catch (err) { console.error(`[SqlBDE] queryDataById( ${id} ) error: ${err}`); } } else { // console.error(`[SqlBDE] queryDataById( ${id} ) No SQL connection pool`); } return null; }); } /** * Get all the required data from the database. * @returns A Promise with a dictionary with the data (keyed by path). */ getData() { return __awaiter(this, void 0, void 0, function* () { var _a, _b; if (this.pool === null || this.pool.connected === false) { this.app.setPluginError(`[SqlBDE] getData(): no pool or not connected, connected-status=${(_a = this.pool) === null || _a === void 0 ? void 0 : _a.connected}`); (0, cloudwatch_1.log)('warn', 'SQL getData called while disconnected'); } else { this.app.setPluginStatus(`[SqlBDE] getData(): connected-status=${(_b = this.pool) === null || _b === void 0 ? void 0 : _b.connected} `); } const data = {}; const raw = {}; for (const dataDef of this.dataDefinitions) { if (dataDef.Id) { const value = yield this.queryDataById(dataDef.Id); let dataValue = (value === null) ? null : value.sqlValue; // console.warn('[SqlBDE] Value for', dataDef.Id, dataDef.displayNameDb, dataValue); if (dataValue !== null) { if (dataDef.scaleDb !== undefined && dataDef.scaleDb !== 1) { dataValue = dataValue / dataDef.scaleDb; } if (dataDef.type === _1.IDataType.Ratio) { if (dataValue > 100 || dataValue < -100) { console.warn(`[SqlBDE] getData(): unexpected percentage value ${dataValue} for ${JSON.stringify(dataDef)} .`); } dataValue /= 100; } else if (dataDef.type === 'running_off') { // Values are not just 0 or 1 as one would expect, so check for 0 and treat everything else as 'running'. dataValue = dataValue === 0 ? 'off' : 'running'; } if (dataDef.unit === 'W' && typeof dataValue === 'number') { dataValue *= 1000; // kW to W. } else if (dataDef.unit === 'K' && typeof dataValue === 'number') { dataValue = utils.transform(dataValue, 'C', 'K'); } else if (dataDef.unit === 'Pa' && typeof dataValue === 'number') { dataValue *= 100000; // bar to Pa. } else if (dataDef.unit === 'degrees' && dataDef.type === _1.IDataType.Number && typeof dataValue === 'number') { dataValue = utils.transform(dataValue, 'deg', 'rad'); } if (dataDef.type === _1.IDataType.Number && typeof dataValue === 'number') { if (dataDef.min !== undefined && dataValue < dataDef.min) { console.log(`[SqlBDE] getData(): unexpected value ${dataValue} for ${JSON.stringify(dataDef)} .`); } if (dataDef.max !== undefined && dataValue > dataDef.max) { console.log(`[SqlBDE] getData(): unexpected value ${dataValue} for ${JSON.stringify(dataDef)} .`); } } } raw[dataDef.path] = { id: String(dataDef.Id), value: (value === null) ? null : value.sqlValue, parsed: dataValue, path: dataDef.path, displayNameDb: dataDef.displayNameDb, lastUpdate: (value === null) ? null : value.timestamp, }; data[dataDef.path] = dataValue; } else if (dataDef.type === _1.IDataType.Fixed) { data[dataDef.path] = dataDef.fixed === undefined ? 0 : dataDef.fixed; } else { // console.warn('[SqlBDE] getData() no ID for', dataDef.displayNameDb, dataDef.Id); data[dataDef.path] = null; } } return { data, raw }; }); } } SqlBDE.HEARTBEAT_INTERVAL_MS = 60 * 1000; exports.default = SqlBDE;