UNPKG

ednl-liftstatus-web-components

Version:
315 lines (314 loc) 11.1 kB
import ky from "ky"; import io, { Socket } from "socket.io-client"; import dayjs from "dayjs"; import { keys, isEmpty, values, isNil } from "lodash-es"; import { createNewStore, getStore, updateStore, } from "../../store"; export class LsData { constructor() { this.store = null; /** * The list of relevant sensors that is used when performing API requests. */ this.defaultSensorIds = [ 104, 105, 106, 210, 500, 501, 502, 503, 513, 514, 515, 550, 590, 591 ]; this.accessToken = undefined; this.idKey = undefined; this.sensorIds = undefined; } async componentWillLoad() { var _a; // Check for a valid access token and if so decode it const { valid, objectId, objectType } = await this.decodeAccessToken(this.accessToken); if (!valid) return; // Check if the installation has a back door const hasBackDoorPromise = this.checkHasBackdoor(objectType, objectId, this.accessToken); const mergedSensorIds = [ // Remove duplicates using a Set ...new Set(this.defaultSensorIds.concat(this.sensorIds !== undefined ? this.sensorIds.split(",").map(Number) : [])), ]; // Get the historical data from the past hour const historicalDataPastHourPromise = this.getHistoricalData(dayjs().subtract(1, "hour").format(), dayjs().format(), mergedSensorIds, objectType, objectId, this.accessToken); // Connect to the socket server const connectedPromise = this.connectToSocketServer(this.accessToken); // We stored all the promises, so now we can fire them concurrently const [hasBackDoor, historicalDataPastHour, { connected, socket }] = await Promise.all([ hasBackDoorPromise, historicalDataPastHourPromise, connectedPromise, ]); // If something went wrong an error should already be thrown, so simply return here if (isNil(hasBackDoor) || isNil(historicalDataPastHour) || !connected) { return; } // We always need a key to identify the store data, so if no key is provided we create one ourselves const idKey = (_a = this.idKey) !== null && _a !== void 0 ? _a : `${objectType}-${objectId}`; // Save state in store createNewStore(idKey, { hasBackDoor, historicalSensorData: historicalDataPastHour, currentSensorData: this.getCurrentDataFromHistorical(historicalDataPastHour), }); try { this.store = await getStore(idKey); } catch (error) { this.throwError("Something went wrong while creating the store", "Token", error); } // Listen to future realtime updates this.listenForSensorUpdates(socket, idKey, mergedSensorIds); } /** * Checks for valid access token and decodes it * @param accessToken * @returns \{ valid: true, objectId, objectType \} if the token is valid * @returns \{ valid: false \} if the token is invalid */ async decodeAccessToken(accessToken) { if (isEmpty(accessToken)) { this.throwError("No Access token supplied", "Token", "Add the token to the <ls-data> component like so: <ls-data access-token='yourToken'></ls-data>"); return { valid: false, }; } try { const response = await ky .post("https://api.liftstatus.nl/liftstatus/auth", { headers: { Authorization: `Bearer ${accessToken}` }, json: { accessToken }, throwHttpErrors: true, }) .json(); const objectId = response.tokenData.oid; let objectType; switch (response.tokenData.otype) { case "int": objectType = "internal"; break; case "inst": objectType = "installation"; break; case "ext": objectType = "external"; break; } return { valid: true, objectId, objectType, }; } catch (error) { this.throwError("Access token is invalid", "Token", error.message); return { valid: false, }; } } /** * This function checks if the elevator has a back door. * It does this by checking for activity on sensor 517 for the past week. */ async checkHasBackdoor(objectType, objectId, accessToken) { var _a, _b; const fromTS = dayjs().subtract(7, "day").format(); try { const response = await ky .get(`https://api.liftstatus.nl/liftstatus/${objectType}/${objectId}/sensor/517/summary`, { headers: { Authorization: `Bearer ${accessToken}` }, searchParams: { from: fromTS }, throwHttpErrors: true, timeout: 60000, }) .json(); const start = (_a = response.rows[0]) === null || _a === void 0 ? void 0 : _a.start; const end = (_b = response.rows[0]) === null || _b === void 0 ? void 0 : _b.end; return end - start === 0 ? false : true; } catch (error) { this.throwError("Checking for back door failed", "API", error.message); } } /** * This function retrieves historical data from the API server for a specified amount of time. * @param {string} fromTS A formatted Day.js object. * @param {string} toTS A formatted Day.js object. */ async getHistoricalData(fromTS, toTS, sensorList, objectType, objectId, accessToken) { try { const response = await ky .post(`https://api.liftstatus.nl/liftstatus/${objectType}/${objectId}/sensor`, { headers: { Authorization: `Bearer ${accessToken}` }, searchParams: { from: fromTS, to: toTS, sensors: sensorList.join(), }, throwHttpErrors: true, timeout: 60000, }) .json(); return response; } catch (error) { this.throwError("Failed to get historical data", "API", error.message); } } connectToSocketServer(accessToken) { // Close any existing connections if (!isEmpty(this.socketId)) { this.closeSocketConnection(); } return new Promise((resolve) => { const socket = io("https://api.liftstatus.nl/", { transports: ["websocket"], path: "/liftstatus/socket.io", reconnectionAttempts: 5, query: { access_token: `${accessToken}`, }, }); // Wait until the io function returns a connection socket.on("connect", () => { resolve({ connected: true, socket, }); }); // Listen for any errors socket.on("error", (error) => { this.throwError("Failed to connect to socket server", "Socket", error, socket); resolve({ connected: false }); }); }); } listenForSensorUpdates(socket, idKey, sensorIds) { // Responds with live updates from the socket server socket.on("module", ({ message }) => { // Update data values and timestamp const data = message.data; const dataKey = keys(data)[0]; const dataValue = values(data)[0]; const timestamp = message.ts; // Check if the sensor id is in the list of sensors we want to listen to if (!sensorIds.includes(parseInt(dataKey, 10))) return; // Overwrite only the updated values updateStore({ lastUpdate: timestamp, values: Object.assign(Object.assign({}, this.store.state.currentSensorData.values), { [dataKey]: dataValue }), updated: Object.assign(Object.assign({}, this.store.state.currentSensorData.updated), { [dataKey]: timestamp }), }, idKey); }); // Listen to socket server updates (this triggers the onModule updates) socket.emit("listen", {}); } getCurrentDataFromHistorical(historicalData) { let lastUpdate = 0; let values = {}; let updated = {}; historicalData.forEach((sensorRow) => { const sensorId = sensorRow.sensor.id; const lastRow = sensorRow.rows[sensorRow.rows.length - 1]; const lastRowTsIsMostRecent = dayjs(lastRow.ts).isAfter(dayjs(lastUpdate)); // lastRow.val is always of type string, so we need to check if we can parse it to a number const lastRowTypeIsNumber = isFinite(parseInt(lastRow.val, 10)); if (lastRowTsIsMostRecent) { lastUpdate = dayjs(lastRow.ts).valueOf(); } values = Object.assign(Object.assign({}, values), { [sensorId]: lastRowTypeIsNumber ? parseInt(lastRow.val, 10) : lastRow.val }); updated = Object.assign(Object.assign({}, updated), { [sensorId]: dayjs(lastRow.ts).valueOf() }); }); return { lastUpdate, values, updated, }; } async throwError(errorMessage, errorType, errorDescription, additionalInfo) { var _a; // Log the error for the user console.group(`%cError%c [LsData] ${errorMessage}`, "color: #DC2626; background-color: #FEF2F2; display: inline-block; padding: 0 4px; border-radius: 2px;", ""); errorDescription && console.log(errorDescription); additionalInfo && console.log(additionalInfo); console.groupEnd(); if (isNil(this.store)) { // We always need a key to identify the store data, so if no key is provided we create one ourselves // Normally we use a combination of the objectType and objectId as fallback, but since this is an error we can't do that createNewStore((_a = this.idKey) !== null && _a !== void 0 ? _a : "error", {}); this.store = await getStore(this.idKey); } // Update the state with the error this.store.state.error = { message: errorMessage, type: errorType }; } closeSocketConnection() { Socket.close(); this.socketId = null; } disconnectedCallback() { this.closeSocketConnection(); } static get is() { return "ls-data"; } static get encapsulation() { return "shadow"; } static get properties() { return { "accessToken": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The token used to connect to the Socket.IO and API servers." }, "attribute": "access-token", "reflect": false }, "idKey": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The unique key that is used to identify store data." }, "attribute": "id-key", "reflect": false }, "sensorIds": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Extra sensor IDs that should be included in the API requests." }, "attribute": "sensor-ids", "reflect": false } }; } } //# sourceMappingURL=ls-data.js.map