UNPKG

thing-it-device-enocean-ip

Version:

[thing-it-node] Device Plugin for EnOcean IP products.

380 lines (325 loc) 13.9 kB
module.exports = { metadata: { plugin: "occupancySensor", label: "EnOcean IP PIR Sensor", role: "sensor", family: "occupancySensor", deviceTypes: ["enocean-ip/gateway"], dataTypes: { sensorType: { family: "enumeration", values: [{ id: "SOLARSENSOR", label: "Solar Sensor" }, { id: "BATTERYSENSOR", label: "Battery Sensor" }] } }, services: [], events: [{ id: "motionDetected", label: "Motion detected" }, { id: "tic", label: "Motion Tic" }, { id: "noMoreMotion", label: "No more Motion" }], state: [{ label: "Occupied", id: "occupied", type: { id: "boolean" } }, { label: "Ticks/Minute", id: "ticksPerMinute", type: { id: "integer" } }, { label: "Last Motion Timestamp", id: "lastMotionTimestamp", type: { id: "string" } }], configuration: [{ label: "Device ID", id: "deviceId", type: { id: "string" } }, { label: "Ghost Ticks Interval", id: "ghostTickInterval", type: { id: "integer" }, defaultValue: 2, unit: "s" }, { label: "Release Time", id: "releaseTime", type: { id: "integer" }, defaultValue: 240, unit: "s" }, { label: "Ticks Counting Interval", id: "tickCountTimeMinutes", type: { id: "integer" }, defaultValue: 5, }, { label: "Sensor Type", id: "sensorType", type: { family: "reference", id: "sensorType" }, defaultValue: "SOLARSENSOR" }] }, create: function () { return new OccupancySensor(); } }; var q = require('q'); var moment = require('moment'); /** * */ function OccupancySensor() { /** * */ OccupancySensor.prototype.start = function () { var deferred = q.defer(); // this.logLevel = 'debug'; this.state = {occupied: false, ticksPerMinute: 0}; this.ticksTimestampsArray = []; this.ticCountArray = []; this.lastProcessedTS = Date.now(); this.lastReceivedTS = Date.now(); if (!this.configuration.ghostTickInterval) { this.configuration.ghostTickInterval = 2; } if (this.isSimulated()) { this.interval = setInterval(function () { this.state.occupied = !this.state.occupied; if (this.state.occupied) { this.state.ticksPerMinute = Math.round(Math.random() * 10); this.state.lastMotionTimestamp = moment().toISOString(); } else { this.state.ticksPerMinute = 0; } this.publishStateChange(); }.bind(this), 20000); deferred.resolve(); } else { //TODO this should be only started if the device is initialized and connected //TODO Interval only if battery sensor if (this.configuration.sensorType === "BATTERYSENSOR") { this.ticksUpdateInterval = setInterval(function () { this.updateTicsOverTime(); this.publishStateChange(); }.bind(this), 60000); } this.device.adapter.listeners.push(telegram => { if (telegram.deviceId === this.configuration.deviceId) { //console.log('Device ' + telegram.friendlyId + ' is processing ', telegram.functions); if (this.configuration.sensorType === "BATTERYSENSOR") { for (var n in telegram.functions) { if (telegram.functions[n].key === 'motionDetected') { if (telegram.functions[n].value === 'true') { this.publishEvent('tic', {}); this.state.lastMotionTimestamp = moment().toISOString(); this.ticksTimestampsArray.unshift(Date.now()); this.updateTicsOverTime(); this.publishStateChange(); if (this.occupancyTimeout) { clearTimeout(this.occupancyTimeout); } if (!this.state.occupied) { this.state.occupied = true; this.publishEvent('motionDetected', {}); this.publishStateChange(); } // Simulates repeated ticks if (this.configuration.ghostTickInterval) { this.tickRepeatInterval = setInterval(() => { this.logDebug('Sending Ghost Tick'); this.publishEvent('tic', {}); this.state.lastMotionTimestamp = moment().toISOString(); this.ticksTimestampsArray.unshift(Date.now()); this.updateTicsOverTime(); this.publishStateChange(); }, this.configuration.ghostTickInterval * 1000); } this.occupancyTimeout = setTimeout(() => { this.state.occupied = false; this.publishEvent('noMoreMotion', {}); this.publishStateChange(); }, this.configuration.releaseTime * 1000); } else { if (this.tickRepeatInterval) { clearInterval(this.tickRepeatInterval); } } break; } } } else { let ticCounter; let tic; let duration; let dataArray = []; dataArray = telegram.telegramInfo.data.match(/.{1,2}/g); let timeStamp = Date.now(); // "OD" means motion detected? Means if we do not get "0D" we can assume that there is also no motion data? switch (dataArray[3]) { case "0D": //MOTION DETECTED this.state.lastMotionTimestamp = moment().toISOString(); if (!this.state.occupied) { this.state.occupied = true; this.publishEvent('motionDetected', {}); this.publishStateChange({occupied: this.state.occupied}); //Only publish the changed state TODO Not really needed here? } //RESET OCCUPANCY TIMOUT if (this.occupancyTimeout) { clearTimeout(this.occupancyTimeout); } //SET NEW OCCUPANCY TIMEOUT this.occupancyTimeout = setTimeout(() => { this.state.occupied = false; this.publishEvent('noMoreMotion', {}); this.publishStateChange({occupied: this.state.occupied}); //Only publish the changed state }, this.configuration.releaseTime * 1000); //EXTRACT TICS FROM THE LATEST TELEGRAM ticCounter = dataArray[1] + dataArray[2]; this.ticCountArray.unshift(parseInt(ticCounter, 16)); if (this.ticCountArray[0] < this.ticCountArray[1]) { tic = this.ticCountArray[0] + (65535 - this.ticCountArray[1]); } else { tic = this.ticCountArray[0] - this.ticCountArray[1]; } //DEBUG if (tic > 60) { this.logDebug("######### TELEGRAM OVERFLOW : ", tic); this.logDebug("######### TELEGRAM: ", telegram); tic = 60; } //SPREAD "tic" TIMESTAMPS TO tickTimestampArray let timestampDifference = timeStamp - this.lastReceivedTS; if (timestampDifference > 120000) timestampDifference = 120000; for (let i = tic; i >= 0; i--) { this.ticksTimestampsArray.push(Math.round(timeStamp - (timestampDifference / tic * i))); } this.lastReceivedTS = timeStamp; break; case "09": //NO MOTION DETECTED. Normally after 10 minutes this.lastReceivedTS = timeStamp; break; default: this.logDebug("Unknown telegram type..."); break; } let ticHistory = this.processHistoricalTics(); if (ticHistory !== null) { this.publishStateChangeHistory(ticHistory); } } } }); deferred.resolve(); } return deferred.promise; }; /** * */ OccupancySensor.prototype.processHistoricalTics = function () { const minute = 60000; if (this.lastReceivedTS - this.lastProcessedTS >= minute) { let stateHistory = []; while ((this.lastReceivedTS - this.lastProcessedTS) >= minute) { if ((this.ticksTimestampsArray.length !== 0) && (this.ticksTimestampsArray[0] <= (this.lastProcessedTS + minute))) { let ticsInSelectedMinute = []; while (this.ticksTimestampsArray[0] <= (this.lastProcessedTS + minute)) { ticsInSelectedMinute.push(this.ticksTimestampsArray.shift()); } let tic; //DEBUG if (ticsInSelectedMinute.length > 60) { this.logDebug("######### HISTORICAL TIC CALCULATION: ", ticsInSelectedMinute); tic = 60; } else { tic = ticsInSelectedMinute.length; } stateHistory.push({ timestamp: new Date(this.lastProcessedTS + minute).toISOString(), state: {ticksPerMinute: tic}, }); } else { stateHistory.push({ timestamp: new Date(this.lastProcessedTS + minute).toISOString(), state: {ticksPerMinute: 0}, }); } this.lastProcessedTS += 60000; } return stateHistory; } else { return null; } }; /** * */ OccupancySensor.prototype.updateTicsOverTime = function () { let timestamp = Date.now(); let tickCountTimeMinutes = this.configuration.tickCountTimeMinutes || 5; while ((timestamp - this.ticksTimestampsArray[this.ticksTimestampsArray.length - 1]) > (tickCountTimeMinutes * 60000)) { this.ticksTimestampsArray.pop(); } this.state.ticksPerMinute = Math.round(this.ticksTimestampsArray.length / tickCountTimeMinutes); }; /** * */ OccupancySensor.prototype.getState = function () { return this.state; }; /** * */ OccupancySensor.prototype.setState = function (state) { this.state = state; }; /** * */ OccupancySensor.prototype.stop = function () { if (this.isSimulated()) { if (this.interval) { clearInterval(this.interval); } } else { if (this.occupancyTimeout) { clearTimeout(this.occupancyTimeout); } if (this.ticksUpdateInterval) { clearInterval(this.ticksUpdateInterval); } if (this.tickRepeatInterval) { clearInterval(this.tickRepeatInterval) } } } }