dt-common-device
Version:
A secure and robust device management library for IoT applications
546 lines (545 loc) • 29.3 kB
JavaScript
"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;