UNPKG

dt-common-device

Version:

A secure and robust device management library for IoT applications

546 lines (545 loc) 29.3 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 __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); var _, done = false; for (var i = decorators.length - 1; i >= 0; i--) { var context = {}; for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; for (var p in contextIn.access) context.access[p] = contextIn.access[p]; context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); if (kind === "accessor") { if (result === void 0) continue; if (result === null || typeof result !== "object") throw new TypeError("Object expected"); if (_ = accept(result.get)) descriptor.get = _; if (_ = accept(result.set)) descriptor.set = _; if (_ = accept(result.init)) initializers.unshift(_); } else if (_ = accept(result)) { if (kind === "field") initializers.unshift(_); else descriptor[key] = _; } } if (target) Object.defineProperty(target, contextIn.name, descriptor); done = true; }; var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { var useValue = arguments.length > 2; for (var i = 0; i < initializers.length; i++) { value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); } return useValue ? value : void 0; }; 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 __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) { if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : ""; return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LocalDeviceService = void 0; const EventHandler_1 = require("../../../../events/EventHandler"); const lodash_1 = require("lodash"); const Device_repository_1 = require("../repository/Device.repository"); const Issue_service_1 = require("../../../../issues/Issue.service"); const Service_1 = require("../../../../constants/Service"); const typedi_1 = __importStar(require("typedi")); const IAuditProperties_1 = require("../../../../audit/IAuditProperties"); const constants_1 = require("../../../../constants"); const audit_1 = require("../../../../audit"); const issue_types_1 = require("../../../../issues/issue.types"); const property_1 = require("../../../property"); const Monitoring_repository_1 = require("../../../monitoring/Monitoring.repository"); let LocalDeviceService = (() => { let _classDecorators = [(0, typedi_1.Service)()]; let _classDescriptor; let _classExtraInitializers = []; let _classThis; var LocalDeviceService = _classThis = class { constructor() { // Use dependency injection instead of creating new instances this.eventHandler = typedi_1.default.get(EventHandler_1.EventHandler); this.deviceRepository = typedi_1.default.get(Device_repository_1.DeviceRepository); this.issueService = typedi_1.default.get(Issue_service_1.IssueService); this.localPropertyService = typedi_1.default.get(property_1.LocalPropertyService); this.monitoringRepository = typedi_1.default.get(Monitoring_repository_1.MonitoringRepository); } async createDevice(body) { const device = await this.deviceRepository.createDevice(body); await this.eventHandler.onDeviceCreate(device); return device; } async getDevice(deviceId, withHubDetails = false) { if (!deviceId) { throw new Error("Device ID is required"); } return await this.deviceRepository.getDevice(deviceId, withHubDetails); } async getDevices(deviceIds, withHubDetails = false) { if (!deviceIds.length) { throw new Error("At least one device ID is required"); } return await this.deviceRepository.getDevices(deviceIds, withHubDetails); } async getPropertyDevices(propertyId, selectDeviceId = false, type, withHubDetails = false) { if (!propertyId) { throw new Error("Property ID is required"); } return await this.deviceRepository.getPropertyDevices(propertyId, selectDeviceId, type, withHubDetails); } async updateDevice(deviceId, body, auditBody) { if (!deviceId) { throw new Error("Device ID is required"); } const device = await this.getDevice(deviceId); if (!device) { throw new Error("Device not found"); } if (body?.status) { if (body?.status?.online) { await this.handleOnlineStatus(device, device.status, body.status, auditBody?.source || Service_1.Source.SYSTEM, auditBody); } else { await this.handleOfflineStatus(device, device.status, body.status, auditBody?.source || Service_1.Source.SYSTEM, auditBody, body.status.error?.message); } // delete the status from the body, so it doesn't get updated in the database again! delete body.status; } if (body.state?.batteryPercentage?.value || body.state?.batteryPercentage?.value === 0) { await this.setBatteryLevel(deviceId, body.state?.batteryPercentage?.value, auditBody?.source || Service_1.Source.SYSTEM, auditBody); // delete the battery percentage and battery state from the body, so it doesn't get updated in the database again! delete body.state?.batteryPercentage; delete body.state?.batteryState; } if (body.state?.lockState?.value) { await this.setState(deviceId, { lockState: body.state.lockState }, auditBody); // delete the lock state from the body, so it doesn't get updated in the database again! delete body.state?.lockState; } await this.deviceRepository.updateDevice(deviceId, body); return await this.eventHandler.onDeviceUpdate(deviceId, body, auditBody); } async updateDevices(query, updateData) { if (!query || !updateData) { throw new Error("Query and update data are required"); } if (updateData?.status) { const auditType = updateData?.status?.online ? constants_1.DT_EVENT_TYPES.DEVICE.STATUS.ONLINE : constants_1.DT_EVENT_TYPES.DEVICE.STATUS.OFFLINE; const devices = await this.deviceRepository.queryDevices(query); for (const device of devices) { const auditBody = { source: Service_1.Source.DEVICE_ACTION, resource: IAuditProperties_1.Resource.DEVICE, propertyId: device.propertyId, zoneId: device.zoneId, deviceId: device.deviceId, }; if (updateData?.status?.online) { await this.handleOnlineStatus(device, device.status, updateData.status, Service_1.Source.DEVICE_ACTION, auditBody); } else { await this.handleOfflineStatus(device, device.status, updateData.status, Service_1.Source.DEVICE_ACTION, auditBody); } (0, audit_1.pushAudit)({ auditType, auditData: { resource: IAuditProperties_1.Resource.DEVICE, source: Service_1.Source.DEVICE_ACTION, propertyId: device.propertyId, deviceId: device.deviceId, zoneId: device.zoneId, deviceType: device.deviceType, deviceName: device.name, deviceStatus: device.status, oldStatus: device.status, newStatus: updateData.status, }, }); } } return await this.deviceRepository.updateDevices(query, updateData); } async deleteDevice(deviceId, auditBody) { if (!deviceId) { throw new Error("Device ID is required"); } await this.deviceRepository.deleteDevice(deviceId); return await this.eventHandler.onDeviceDelete(deviceId, auditBody); } async getState(deviceId) { if (!deviceId) { throw new Error("Device ID is required"); } return await this.deviceRepository.getState(deviceId); } async setState(deviceId, newState, auditProperties, eventType) { if (!deviceId || !newState) { throw new Error("Device ID and new state are required"); } const oldState = (await this.getState(deviceId))?.state ?? {}; const changedKeys = Object.keys(newState).filter((key) => { const oldVal = oldState[key]?.value; const newVal = newState[key]?.value; // Convert both to string (handles string/number/boolean safely) return !(0, lodash_1.isEqual)(String(oldVal).toLowerCase(), String(newVal).toLowerCase()); }); if (changedKeys.length > 0) { await this.deviceRepository.setState(deviceId, newState); return await this.eventHandler.onStateChange(deviceId, newState, auditProperties, eventType); } if (auditProperties.source === Service_1.Source.CLOUD_EVENT) { return await this.eventHandler.onStateChange(deviceId, newState, auditProperties, eventType); } } async getStatus(deviceId) { if (!deviceId) { throw new Error("Device ID is required"); } return await this.deviceRepository.getStatus(deviceId); } async setStatus(deviceId, newStatus, source, auditBody, reason) { if (!deviceId || !newStatus) { throw new Error("Device ID and new status are required"); } const device = await this.getDevice(deviceId); //NOTE: Baseline logic only apply for device type of LOCK if (device?.deviceType?.type === "LOCK") { const oldStatus = device.status; const currentTime = new Date().toISOString(); // Determine if the new status is ONLINE or OFFLINE const isNewStatusOnline = newStatus?.liveStatus === "ONLINE"; const isNewStatusOffline = newStatus?.liveStatus === "OFFLINE"; if (isNewStatusOffline) { // New Status = OFFLINE await this.handleOfflineStatus(device, oldStatus, newStatus, source, auditBody, reason, currentTime); } else if (isNewStatusOnline) { // New Status = ONLINE await this.handleOnlineStatus(device, oldStatus, newStatus, source, auditBody, reason, currentTime); } else { // For any other status, just update normally await this.deviceRepository.setStatus(deviceId, newStatus); await this.eventHandler.onStatusChange(deviceId, newStatus, auditBody, constants_1.DT_EVENT_TYPES.DEVICE.STATUS.UNKNOWN); } } else { // For DeviceTypes other than LOCK, just update normally (HUB, THERMOSTATS) const eventType = newStatus.online ? constants_1.DT_EVENT_TYPES.DEVICE.STATUS.ONLINE : constants_1.DT_EVENT_TYPES.DEVICE.STATUS.OFFLINE; await this.deviceRepository.setStatus(deviceId, newStatus); await this.eventHandler.onStatusChange(deviceId, newStatus, auditBody, eventType); } } async setStatusMany(query, newStatus, source, auditBody) { if (!query || !newStatus) { throw new Error("Query and new status are required"); } newStatus.lastUpdated = new Date().toISOString(); const eventType = newStatus.online ? constants_1.DT_EVENT_TYPES.DEVICE.STATUS.ONLINE : constants_1.DT_EVENT_TYPES.DEVICE.STATUS.OFFLINE; const devices = await this.deviceRepository.queryDevices(query); for (const device of devices) { if (device?.deviceType?.type === "LOCK") { if (newStatus.online) { await this.handleOnlineStatus(device, device.status, newStatus, source, auditBody); } else { await this.handleOfflineStatus(device, device.status, newStatus, source, auditBody); } } else { await this.deviceRepository.setStatus(device.deviceId, newStatus); await this.eventHandler.onStatusChange(device.deviceId, newStatus, auditBody, eventType); } } } async handleOfflineStatus(device, oldStatus, newStatus, source, auditBody, reason, currentTime) { // if deviceProfile is active, only then use baseline for the device, if its calibrating or inactive mark device offline immediately const preferences = await this.localPropertyService.getPropertyPreferences(device.propertyId, ["deviceProfile"]); const deviceProfile = preferences?.settings?.deviceProfile?.value || "calibrating"; currentTime = currentTime ?? new Date().toISOString(); const isExistingStatusOnline = oldStatus?.liveStatus === "ONLINE"; const isExistingStatusOffline = oldStatus?.liveStatus === "OFFLINE"; if (deviceProfile === "active") { if (isExistingStatusOnline) { // Existing status is Online newStatus.online = true; newStatus.liveStatus = "OFFLINE"; newStatus.lastUpdated = currentTime; newStatus.error = { type: "offline", message: reason || "Device went offline", default: {}, }; await this.deviceRepository.setStatus(device.deviceId, newStatus); await this.eventHandler.onStatusChange(device.deviceId, newStatus, auditBody, constants_1.DT_EVENT_TYPES.DEVICE.STATUS.OFFLINE); } else if (isExistingStatusOffline) { // Existing status is Offline const timeLapsed = oldStatus?.lastUpdated ? Date.now() - new Date(oldStatus.lastUpdated).getTime() : 0; const baselineTime = await this.getDeviceBaseline(device.deviceId); if (timeLapsed > baselineTime) { // When the time lapsed is higher than the baseline time newStatus.online = false; newStatus.liveStatus = "OFFLINE"; newStatus.lastUpdated = currentTime; newStatus.error = { type: "offline", message: reason || "Device has been offline for longer than baseline", default: {}, }; await this.deviceRepository.setStatus(device.deviceId, newStatus); await this.eventHandler.onStatusChange(device.deviceId, newStatus, auditBody, constants_1.DT_EVENT_TYPES.DEVICE.STATUS.OFFLINE); // Raise issue when the device goes offline if longer than the baseline (OPERATIONAL only) await this.issueService.createDeviceOfflineIssue(device, source, reason); } } } else { if (!newStatus.online) { // Raise issue when the device goes offline if longer than the baseline (OPERATIONAL only) await this.issueService.createDeviceOfflineIssue(device, source, reason); } await this.deviceRepository.setStatus(device.deviceId, newStatus); await this.eventHandler.onStatusChange(device.deviceId, newStatus, auditBody, constants_1.DT_EVENT_TYPES.DEVICE.STATUS.OFFLINE); } } async handleOnlineStatus(device, oldStatus, newStatus, source, auditBody, reason, currentTime) { currentTime = currentTime ?? new Date().toISOString(); const isExistingStatusOnline = oldStatus?.online === true; const isExistingStatusOffline = oldStatus?.online === false; if (isExistingStatusOnline) { // Existing status is Online if (oldStatus?.liveStatus === "ONLINE") { // liveStatus = Online → Do nothing return; } else if (oldStatus?.liveStatus === "OFFLINE") { // liveStatus = Offline → Follow the step #2 (same as existing status is Offline) newStatus.online = true; newStatus.liveStatus = "ONLINE"; newStatus.lastUpdated = currentTime; newStatus.error = {}; // Clear the error await this.deviceRepository.setStatus(device.deviceId, newStatus); await this.eventHandler.onStatusChange(device.deviceId, newStatus, auditBody, constants_1.DT_EVENT_TYPES.DEVICE.STATUS.ONLINE); } } else if (isExistingStatusOffline) { // Existing status is Offline newStatus.online = true; newStatus.liveStatus = "ONLINE"; newStatus.lastUpdated = currentTime; newStatus.error = undefined; // Clear the error await this.deviceRepository.setStatus(device.deviceId, newStatus); await this.eventHandler.onStatusChange(device.deviceId, newStatus, auditBody, constants_1.DT_EVENT_TYPES.DEVICE.STATUS.ONLINE); //Resolve issue if exists await this.issueService.performIssueAction({ entityId: device.deviceId, entityType: issue_types_1.EntityType.DEVICE, type: issue_types_1.IssueType.DEVICE_OFFLINE, propertyId: device.propertyId, zoneId: device.zoneId, }, "resolve", source); } } async getDeviceBaseline(deviceId) { const response = await this.monitoringRepository.getDeviceBaseLine(deviceId); const baselineInMilliseconds = response?.baseline && response?.baseline * 60 * 1000; return baselineInMilliseconds ?? 3600000; // 1 hour in milliseconds } async getBatteryLevel(deviceId) { if (!deviceId) { throw new Error("Device ID is required"); } return await this.deviceRepository.getBatteryLevel(deviceId); } async setBatteryLevel(deviceId, batteryLevel, source, auditBody) { if (!deviceId || batteryLevel === undefined || batteryLevel === null) { throw new Error("Device ID and battery level are required"); } // Get device information const device = await this.getDevice(deviceId); // Fetch the old battery level state const oldBatteryState = device.state?.batteryPercentage; // Check if battery level has changed const isDifferent = !(0, lodash_1.isEqual)(oldBatteryState?.value, batteryLevel); if (isDifferent) { // // Check if current time is greater than or equal to last updated time (8-hour logic) // const shouldUpdate = this.shouldUpdateBatteryLevel( // oldBatteryState?.lastUpdated // ); // if (shouldUpdate) { // Save the battery level in the device await this.deviceRepository.setBatteryLevel(deviceId, batteryLevel); await this.eventHandler.onBatteryLevelChange(deviceId, batteryLevel, auditBody); if (oldBatteryState?.value < batteryLevel && batteryLevel - oldBatteryState?.value > 50) { // Battery level has increased more than 50% (considering as battery has been replaced) const batteryState = { batteryState: { value: "replaced", lastUpdated: new Date().toISOString(), }, }; await this.setState(deviceId, batteryState, auditBody); await this.eventHandler.onBatteryReplaced(deviceId, batteryLevel, auditBody); // Cancel any existing battery issue await this.issueService.performIssueAction({ entityId: deviceId, entityType: issue_types_1.EntityType.DEVICE, type: issue_types_1.IssueType.BATTERY_LOW, propertyId: device.propertyId, zoneId: device.zoneId, }, "resolve", source); } // Get property threshold const propertyThreshold = await this.getPropertyBatteryThreshold(device.propertyId); // Check if battery level is below threshold if (batteryLevel < propertyThreshold) { // Raise issue when the level is below threshold await this.issueService.createDeviceBatteryIssue(device, batteryLevel, propertyThreshold, issue_types_1.IssuePriority.CRITICAL, source); } else if (batteryLevel >= propertyThreshold) { // Resolve issue when the level is above threshold await this.issueService.performIssueAction({ entityId: deviceId, entityType: issue_types_1.EntityType.DEVICE, type: issue_types_1.IssueType.BATTERY_LOW, propertyId: device.propertyId, zoneId: device.zoneId, }, "resolve", source); } // } } } shouldUpdateBatteryLevel(lastUpdated) { if (!lastUpdated) { return true; // No previous update, so we should update } const lastUpdateTime = new Date(lastUpdated).getTime(); const currentTime = Date.now(); const eightHoursInMs = 8 * 60 * 60 * 1000; // 8 hours in milliseconds // Return true if current time is greater than or equal to last updated time + 8 hours return currentTime >= lastUpdateTime + eightHoursInMs; } async updateBatteryState(deviceId, batteryState) { try { await this.deviceRepository.updateDevice(deviceId, batteryState); } catch (error) { console.error("Error updating battery state:", error); } } async getPropertyBatteryThreshold(propertyId) { const preferences = await this.localPropertyService.getPropertyPreferences(propertyId, ["batteryPreference"]); if (!preferences) { return 20; // 20% default threshold } return preferences?.settings?.batteryPreference?.value ?? 20; } async getMetaData(deviceId) { if (!deviceId) { throw new Error("Device ID is required"); } return await this.deviceRepository.getMetaData(deviceId); } async setMetaData(deviceId, metaData, auditBody) { if (!deviceId || !metaData) { throw new Error("Device ID and meta data are required"); } await this.eventHandler.onDeviceMetaChange(deviceId, metaData, auditBody); return await this.deviceRepository.setMetaData(deviceId, metaData); } async getDevicesByZone(zoneId) { if (!zoneId) { throw new Error("Zone ID is required"); } return await this.deviceRepository.getDevicesByZone(zoneId); } async getDevicesByAccessGroup(accessGroupId) { if (!accessGroupId) { throw new Error("Access Group ID is required"); } return await this.deviceRepository.getDevicesByAccessGroup(accessGroupId); } async querySelect(query, fields) { if (!query || Object.keys(query).length === 0) { throw new Error("Query is required"); } return await this.deviceRepository.querySelect(query, fields); } async queryCount(query) { if (!query || Object.keys(query).length === 0) { throw new Error("Query is required"); } const count = await this.deviceRepository.queryCount(query); return count; } async deleteDevices(query) { if (!query || Object.keys(query).length === 0) { throw new Error("Query is required"); } // TODO: Add Audit here as well onDeleteDevices return await this.deviceRepository.deleteDevices(query); } async queryDevices(query) { if (!query || Object.keys(query).length === 0) { throw new Error("Query is required"); } const res = await this.deviceRepository.queryDevices(query); return res; } }; __setFunctionName(_classThis, "LocalDeviceService"); (() => { const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); LocalDeviceService = _classThis = _classDescriptor.value; if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); __runInitializers(_classThis, _classExtraInitializers); })(); return LocalDeviceService = _classThis; })(); exports.LocalDeviceService = LocalDeviceService;