UNPKG

iobroker.lowpass-filter

Version:

Dieser Adapter ermöglicht es einen beliebigen State mit einem Tiefpass 1. Ordnung zu filtern.

413 lines (377 loc) 18.1 kB
'use strict'; /* * Created with @iobroker/create-adapter v2.1.0 */ // The adapter-core module gives you access to the core ioBroker functions // you need to create an adapter const utils = require('@iobroker/adapter-core'); const schedule = require('node-schedule'); // Load your modules here, e.g.: // const fs = require("fs"); class LowpassFilter extends utils.Adapter { /** * @param [options] options of adapter constructor */ constructor(options) { super({ ...options, name: 'lowpass-filter', }); this.on('ready', this.onReady.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); this.on('objectChange', this.onObjectChange.bind(this)); // this.on("message", this.onMessage.bind(this)); this.on('unload', this.onUnload.bind(this)); this.subscribecounterId = 'info.subscribedStatesCount'; this.subscribecounter = 0; // define arrays for selected states and calculation this.activeStates = {}; // define cron jobs this.cronJobs = {}; this.jobId = 'job'; } /*************************************************************************************** * ********************************** Init ********************************************* ***************************************************************************************/ async onReady() { // Initialize your adapter here //Read all states with custom configuration const customStateArray = await this.getObjectViewAsync('system', 'custom', {}); // Request if there is an object if (customStateArray && customStateArray.rows) { for (let index = 0; index < customStateArray.rows.length; index++) { if (customStateArray.rows[index].value !== null) { // Request if there is an object for this namespace an its enabled if ( customStateArray.rows[index].value[this.namespace] && customStateArray.rows[index].value[this.namespace].enabled === true ) { const id = customStateArray.rows[index].id; const obj = await this.getForeignObjectAsync(id); if (obj) { const common = obj.common; const state = await this.getForeignStateAsync(id); if (state) { await this.addObjectAndCreateState( id, common, customStateArray.rows[index].value[this.namespace], state, ); } } } } } } this.subscribeForeignObjects('*'); this.setState(this.subscribecounterId, this.subscribecounter, true); } /*************************************************************************************** * ********************************** Changes ****************************************** ***************************************************************************************/ async onObjectChange(id, obj) { if (obj) { try { if (!obj.common.custom || !obj.common.custom[this.namespace]) { if (this.activeStates[id]) { this.clearStateArrayElement(id, false); return; } } else { const customInfo = obj.common.custom[this.namespace]; if (this.activeStates[id]) { this.activeStates[id].filterTime = customInfo.filterTime; this.activeStates[id].separateFilterTimeForNegativeDifference = customInfo.separateFilterTimeForNegativeDifference; this.activeStates[id].filterTimeNegative = customInfo.filterTimeNegative; this.activeStates[id].refreshWithStatechange = customInfo.refreshWithStatechange; this.activeStates[id].limitInNegativeDirection = customInfo.limitInNegativeDirection; this.activeStates[id].negativeLimit = customInfo.negativeLimit; this.activeStates[id].limitInPositiveDirection = customInfo.limitInPositiveDirection; this.activeStates[id].positiveLimit = customInfo.positiveLimit; this.activeStates[id].limitDecimalplaces = customInfo.limitDecimalplaces; this.activeStates[id].decimalplaces = customInfo.decimalplaces; if (this.activeStates[id].refreshRate != customInfo.refreshRate) { this.removeIdFromSchedule(id); this.activeStates[id].refreshRate = customInfo.refreshRate; this.addIdToSchedule(id); } this.output(id); } else { const state = await this.getForeignStateAsync(id); if (state) { this.addObjectAndCreateState(id, obj.common, customInfo, state); } else { this.log.error(`could not read state ${id}`); } } } } catch (error) { this.log.error(error); this.clearStateArrayElement(id, false); } } else { // The object was deleted // Check if the object is kwnow const obj = await this.getObjectAsync(this.createStatestring(id)); if (this.activeStates[id] || obj) { if (obj) { this.clearStateArrayElement(id, true); } else { this.clearStateArrayElement(id, false); } } } } /** * Is called if a subscribed state changes * * @param id id of the changed state * @param state state (val & ack) of the changed ste-id */ onStateChange(id, state) { if (state) { if (this.activeStates[id]) { // Check null, ignored values and limit if (state.val != null) { if (!this.activeStates[id].ignoredValues[state.val.toString()]) { // Limit value if ( this.activeStates[id].limitInNegativeDirection && state.val < this.activeStates[id].negativeLimit ) { this.activeStates[id].currentValue = this.activeStates[id].negativeLimit; this.log.debug( `State ${id} is set to value ${state.val} and will be limitted to ${this.activeStates[id].currentValue}`, ); } else if ( this.activeStates[id].limitInPositiveDirection && state.val > this.activeStates[id].positiveLimit ) { this.activeStates[id].currentValue = this.activeStates[id].positiveLimit; this.log.debug( `State ${id} is set to value ${state.val} and will be limitted to ${this.activeStates[id].currentValue}`, ); } else { this.activeStates[id].currentValue = state.val; } // claculate or output values if (this.activeStates[id].refreshRate == 0 || this.activeStates[id].refreshWithStatechange) { this.output(id); } else { this.calculateLowpassValue(id); } } else { this.log.debug(`State ${id} is set to value ${state.val} and will be ignored`); } } else { this.log.debug(`State ${id} is set to value ${state.val} and will be ignored`); } } } else { // The state was deleted } } // If you need to accept messages in your adapter, uncomment the following block and the corresponding line in the constructor. // /** // * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ... // * Using this method requires "common.messagebox" property to be set to true in io-package.json // * @param {ioBroker.Message} obj // */ // onMessage(obj) { // if (typeof obj === "object" && obj.message) { // if (obj.command === "send") { // // e.g. send email or pushover or whatever // this.log.debug("send command"); // // Send response in callback if required // if (obj.callback) this.sendTo(obj.from, obj.command, "Message received", obj.callback); // } // } // } /*************************************************************************************** * *********************************** Unload ****************************************** ***************************************************************************************/ /** * Is called when adapter shuts down - callback has to be called under any circumstances! * * @param callback function wich is called after shutdown adapter */ onUnload(callback) { try { // clear all schedules for (const seconds in this.cronJobs) { schedule.cancelJob(this.cronJobs[seconds][this.jobId]); } callback(); } catch (e) { this.log.error(e); callback(); } } /*************************************************************************************** * ************************** own defined functions ************************************ ***************************************************************************************/ /*************************************************************************************** * **************************** custom objec handling ********************************** ***************************************************************************************/ createStatestring(id) { return `filtered_Values.${id.replace(/\./g, '_')}`; } async addObjectAndCreateState(id, common, customInfo, state) { // check if custominfo is available if (!customInfo) { return; } if (common.type != 'number') { this.log.error(`state ${id} is not type number, but ${common.type}`); return; } this.activeStates[id] = { stateId: id, lastValue: state.val, currentValue: state.val, lowpassValue: state.val, lastTimestamp: Date.now(), filterTime: customInfo.filterTime, separateFilterTimeForNegativeDifference: customInfo.separateFilterTimeForNegativeDifference, filterTimeNegative: customInfo.filterTimeNegative, refreshRate: customInfo.refreshRate, refreshWithStatechange: customInfo.refreshWithStatechange, limitInNegativeDirection: customInfo.limitInNegativeDirection, negativeLimit: customInfo.negativeLimit, limitInPositiveDirection: customInfo.limitInPositiveDirection, positiveLimit: customInfo.positiveLimit, limitDecimalplaces: customInfo.limitDecimalplaces, decimalplaces: customInfo.decimalplaces, ignoredValues: {}, }; // assign cronJob this.addIdToSchedule(id); // Create Object await this.setObjectNotExistsAsync(this.createStatestring(id), { type: 'state', common: { name: common.name, type: 'number', role: 'indicator', unit: common.unit, read: true, write: false, def: state.val, }, native: {}, }); this.log.debug(`state ${id} added`); this.subscribeForeignStates(id); this.subscribecounter += 1; this.setState(this.subscribecounterId, this.subscribecounter, true); await this.output(id); } // clear the state from the active array. if selected the state will be deleted clearStateArrayElement(id, deleteObject) { if (this.activeStates[id]) { this.removeIdFromSchedule(id); delete this.activeStates[id]; this.subscribecounter -= 1; this.setState(this.subscribecounterId, this.subscribecounter, true); this.unsubscribeForeignStates(id); this.log.debug(`state ${id} removed`); if (this.config.deleteStatesWithDisable || deleteObject) { this.delObjectAsync(this.createStatestring(id)); this.log.debug(`state ${this.namespace}.${this.createStatestring(id)} deleted`); } } else if (deleteObject) { this.delObjectAsync(this.createStatestring(id)); this.log.debug(`state ${this.namespace}.${this.createStatestring(id)} deleted`); } } /*************************************************************************************** * *********************************** Schedule **************************************** ***************************************************************************************/ addIdToSchedule(id) { if (this.activeStates[id].refreshRate != 0) { if (!this.cronJobs[this.activeStates[id].refreshRate]) { this.cronJobs[this.activeStates[id].refreshRate] = {}; if (this.activeStates[id].refreshRate != 60) { this.cronJobs[this.activeStates[id].refreshRate][this.jobId] = schedule.scheduleJob( `*/${this.activeStates[id].refreshRate} * * * * *`, this.outputAddedIds.bind(this, this.activeStates[id].refreshRate), ); } else { this.cronJobs[this.activeStates[id].refreshRate][this.jobId] = schedule.scheduleJob( `0 * * * * *`, this.outputAddedIds.bind(this, this.activeStates[id].refreshRate), ); } } // Add id to object this.cronJobs[this.activeStates[id].refreshRate][id] = {}; } } // if the id is scheduled, it will be deleted from active array removeIdFromSchedule(id) { if (this.activeStates[id].refreshRate != 0) { delete this.cronJobs[this.activeStates[id].refreshRate][id]; if (Object.keys(this.cronJobs[this.activeStates[id].refreshRate]).length <= 1) { schedule.cancelJob(this.cronJobs[this.activeStates[id].refreshRate][this.jobId]); delete this.cronJobs[this.activeStates[id].refreshRate]; } } } // output all added id of the given schedule outputAddedIds(seconds) { for (const id in this.cronJobs[seconds]) { if (id == this.jobId) { continue; } this.output(id); } } /*************************************************************************************** * **************************** calculation of filter ********************************** ***************************************************************************************/ calculateLowpassValue(id) { const timestamp = Date.now(); let filterTime = 0; if ( this.activeStates[id].currentValue >= this.activeStates[id].lowpassValue || !this.activeStates[id].separateFilterTimeForNegativeDifference ) { filterTime = this.activeStates[id].filterTime; } else { filterTime = this.activeStates[id].filterTimeNegative; } if (filterTime != 0) { this.activeStates[id].lowpassValue += (this.activeStates[id].lastValue - this.activeStates[id].lowpassValue) * (1 - Math.exp(-(timestamp - this.activeStates[id].lastTimestamp) / (filterTime * 200))); // 200 because 1000ms / 5tau = 200 } else { this.activeStates[id].lowpassValue = this.activeStates[id].currentValue; } // Output with the desired decimal places if (this.activeStates[id].limitDecimalplaces) { this.activeStates[id].lowpassValue = Math.round(this.activeStates[id].lowpassValue * this.activeStates[id].decimalplaces) / this.activeStates[id].decimalplaces; } this.activeStates[id].lastTimestamp = timestamp; this.activeStates[id].lastValue = this.activeStates[id].currentValue; } // output the calculated values async output(id) { this.calculateLowpassValue(id); // Forreign wird hier verwendet, damit der Adapter eigene States wiederum filtern kann (Filter des Filters) await this.setStateAsync(this.createStatestring(id), this.activeStates[id].lowpassValue, true); } } if (require.main !== module) { // Export the constructor in compact mode /** * @param [options] options of exported adapter */ module.exports = options => new LowpassFilter(options); } else { // otherwise start the instance directly new LowpassFilter(); }