ednl-liftstatus-web-components
Version:
The EDNL LiftStatus web components
315 lines (314 loc) • 11.1 kB
JavaScript
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