UNPKG

node-red-contrib-sun-position

Version:
926 lines (889 loc) 89.9 kB
// @ts-check /* * This code is licensed under the Apache License Version 2.0. * * Copyright (c) 2022 Robert Gester * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * */ /******************************************** * blind-control: *********************************************/ 'use strict'; /** --- Type Defs --- * @typedef {import('./types/typedefs.js').runtimeRED} runtimeRED * @typedef {import('./types/typedefs.js').runtimeNode} runtimeNode * @typedef {import('./types/typedefs.js').runtimeNodeConfig} runtimeNodeConfig * @typedef {import("./lib/dateTimeHelper").ITimeObject} ITimeObject * @typedef {import("./10-position-config.js").ITypedValue} ITypedValue * @typedef {import("./lib/timeControlHelper.js").ITimeControlNode} ITimeControlNode * @typedef {import("./lib/timeControlHelper.js").IPositionConfigNode} IPositionConfigNode */ /** * @typedef {Object} IBlindNodeData Node data object * @property {boolean} isDisabled - is the node disabled * @property {number} levelTop - the blind top level * @property {number} levelBottom - the blind bottom level * @property {number} [levelTopOffset] - the blind top level offset * @property {number} [levelBottomOffset] - the blind bottom level * @property {number} increment - open/closing increment * @property {ITypedValue} levelDefault - defaulot level * @property {ITypedValue} levelMin - minimum level * @property {ITypedValue} levelMax - maximum levell * @property {ITypedValue} slat - default slat setting * @property {string} topic - default topic * @property {ITypedValue} addId - additional id of the node * @property {Object} overwrite - open/closing increment * @property {boolean} overwrite.active - overwrite active or not * @property {number} overwrite.importance - importance of the overwrite * @property {number} overwrite.expireDuration - expireDuration */ /** * @typedef {Object} IBlindWindowSettings the window settings * @property {*} top - the top of the window * @property {string} topType - type of the top of the window * @property {*} bottom - the bottom of the window * @property {string} bottomType - type of the bottom of the window * @property {('startEnd'|'orientation')} setMode - mode of the start/end angles * @property {*} azimuthStart - the start position angle to the geographical north * @property {string} azimuthStartType - type of the start position angle to the geographical north * @property {*} azimuthEnd - the end position angle to the geographical north * @property {string} azimuthEndType - type of the end position angle to the geographical north * @property {*} windowOrientation - the orientation angle to the geographical north * @property {string} windowOrientationType - type of the the orientation angle to the geographical north * @property {number} windowOffsetP - an offset for the angle clockwise offset * @property {number} windowOffsetN - an offset for the angle anti-clockwise offset */ /** * @typedef {Object} IOversteerSettings the window settings * @property {boolean} isChecked - the top of the window * @property {boolean} active - type of the top of the window * @property {string} topic - the topic of the oversteer */ /** * @typedef {Object} IOversteerData the window settings * @property {number} pos - position * @property {(0|1|3|16)} mode - the top of the window * @property {any} value - type of the top of the window * @property {string} valueType - type of the value operator 1 * @property {function} valueExpr - value operator 1 * @property {string} operator - compare operator * @property {string} [operatorText] - compare operator text * @property {string} thresholdType - type of the value operator 2 * @property {string} threshold - value operator 2 * @property {ITypedValue} blindPos - blind position * @property {ITypedValue} slatPos - slat position * @property {boolean} onlySunInWindow - slat position */ /** * @typedef {Object} IBlindControlNodeInstance Extensions for the nodeInstance object type * @property {IBlindNodeData} nodeData get/set generic Data of the node * @property {IBlindWindowSettings} windowSettings - the window settings Object * @property {number} smoothTime smoothTime * @property {Array.<IOversteerData>} oversteers - tbd * @property {IOversteerSettings} oversteer - tbd * @property {Object} level - tbd * @property {Array.<Object>} results - tbd * ... obviously there are more ... */ /** * @typedef {ITimeControlNode & IBlindControlNodeInstance} IBlindControlNode Combine nodeInstance with additional, optional functions */ /******************************************************************************************/ /** Export the function that defines the node * @type {runtimeRED} */ module.exports = function (/** @type {runtimeRED} */ RED) { 'use strict'; const hlp = require('./lib/dateTimeHelper.js'); const ctrlLib = require('./lib/timeControlHelper.js'); const util = require('util'); const clonedeep = require('lodash.clonedeep'); const isEqual = require('lodash.isequal'); const cautoTriggerTimeBeforeSun = 10 * hlp.TIME_1min; // 10 min const cautoTriggerTimeSun = 5 * hlp.TIME_1min; // 5 min const cWinterMode = 1; const cMinimizeMode = 3; const cSummerMode = 16; /******************************************************************************************/ /** * get the absolute level from percentage level * @param {IBlindControlNode} node the node settings * @param {number} levelPercent the level in percentage (0-1) */ function posPrcToAbs_(node, levelPercent) { return posRound_(node, ((node.nodeData.levelTop - node.nodeData.levelBottom) * levelPercent) + node.nodeData.levelBottom); } /** * get the percentage level from absolute level (0-1) * @param {IBlindControlNode} node the node settings * @param {number} levelAbsolute the level absolute * @return {number} get the level percentage */ function posAbsToPrc_(node, levelAbsolute) { return (levelAbsolute - node.nodeData.levelBottom) / (node.nodeData.levelTop - node.nodeData.levelBottom); } /** * get the absolute inverse level * @param {IBlindControlNode} node the node settings * @param {number} level the level absolute * @return {number} get the inverse level */ function getInversePos_(node, level) { return posPrcToAbs_(node, 1 - posAbsToPrc_(node, level)); } /** * get the absolute inverse level * @param {IBlindControlNode} node the node settings * @param {Object} prevData the nodes previous data * @return {number} get the current level */ function getRealLevel_(node, prevData) { if (node.levelReverse) { return isNaN(node.level.currentInverse) ? prevData.levelInverse: node.level.currentInverse; } return isNaN(node.level.current) ? prevData.level : node.level.current; } /** * round a level to the next increment * @param {IBlindControlNode} node node data * @param {number} pos level * @return {number} rounded level number */ function posRound_(node, pos) { // node.debug(`levelPrcToAbs_ ${pos} - increment is ${node.nodeData.increment}`); // pos = Math.ceil(pos / node.nodeData.increment) * node.nodeData.increment; // pos = Math.floor(pos / node.nodeData.increment) * node.nodeData.increment; pos = Math.round(pos / node.nodeData.increment) * node.nodeData.increment; pos = Number(pos.toFixed(hlp.countDecimals(node.nodeData.increment))); if (pos > node.nodeData.levelTop) { pos = node.nodeData.levelTop; } if (pos < node.nodeData.levelBottom) { pos = node.nodeData.levelBottom; } // node.debug(`levelPrcToAbs_ result ${pos}`); return pos; } /** * check if angle is between start and end * @param {number} angle - angle in decimal degree * @param {number} start - angle in decimal degree or rad, based on angleType * @param {number} end - angle in decimal degree * @param {('deg'|'rad')} angleType - angle type * @return {Boolean} */ function angleBetween_(angle, start, end, angleType) { if (angleType === 'rad') { start = hlp.toDec(start); end = hlp.toDec(end); } start = hlp.angleNorm(start); end = hlp.angleNorm(end); if(start<end) return start<=angle && angle<=end; return start<=angle || angle<=end; } /******************************************************************************************/ /** * check the oversteering data * @param {IBlindControlNode} node node data * @param {Object} msg the message object * @param {Object} tempData the temporary data holder object * @param {ITimeObject} oNow the now Object * @return {IOversteerData|undefined} */ function checkOversteer(node, msg, tempData, sunPosition, oNow) { // node.debug(`checkOversteer ${util.inspect(node.oversteers, { colors: true, compact: 5, breakLength: Infinity, depth: 10 })}`); try { node.oversteer.isChecked = true; return node.oversteers.find(el => ((el.mode === 0 || el.mode === node.sunData.mode) && (!el.onlySunInWindow || sunPosition.InWindow) && node.positionConfig.comparePropValue(node, msg, { value: el.value, type: el.valueType, expr: el.valueExpr, callback: (result, _obj, cachable) => { return ctrlLib.evalTempData(node, _obj.type, _obj.value, result, tempData, cachable); }, noError:false, now:oNow.now }, el.operator, { value: el.threshold, type: el.thresholdType, callback: (result, _obj, cachable) => { return ctrlLib.evalTempData(node, _obj.type, _obj.value, result, tempData, cachable); }, noError:false, now:oNow.now }))); } catch (err) { node.error(RED._('blind-control.errors.getOversteerData', err)); node.log(util.inspect(err)); } // node.debug('node.oversteers=' + util.inspect(node.oversteers, { colors: true, compact: 5, breakLength: Infinity, depth: 10 })); return undefined; } /******************************************************************************************/ /** * get the blind level from a typed input * @param {IBlindControlNode} node - node data * @param {*} msg - message object * @param {string} type - type field * @param {string} value - value field * @param {number|NaN} def - default value * @param {Object} tempData - the temporary data holder object * @returns {number|NaN} blind level as number or NaN if not defined */ function getBlindPosFromTI(node, msg, type, value, def, tempData) { // node.debug(`getBlindPosFromTI - type=${type} value=${value} def=${def} nodeData=${ util.inspect(node.nodeData, { colors: true, compact: 5, breakLength: Infinity, depth: 10 }) }`); def = def || NaN; if (type === 'none' || type === ''|| type === 'levelND') { return def; } try { if (type === 'levelFixed') { const val = parseFloat(value); if (isNaN(val)) { if (value.includes('close')) { return node.nodeData.levelBottom; } else if (value.includes('open')) { return node.nodeData.levelTop; } else if (value === '') { return def; } } else { if (val < 1) { return node.nodeData.levelBottom; } else if (val > 99) { return node.nodeData.levelTop; } return posPrcToAbs_(node, val / 100); } throw new Error(`unknown value "${value}" of type "${type}"` ); } const res = node.positionConfig.getFloatProp(node, msg, { type, value, def, callback: (result, _obj, cachable) => { return ctrlLib.evalTempData(node, _obj.type, _obj.value, result, tempData, cachable); } }); if (node.levelReverse) { return getInversePos_(node, res); } return res; } catch (err) { node.error(RED._('blind-control.errors.getBlindPosData', err)); node.log(util.inspect(err)); } return def; } /******************************************************************************************/ /** * check if a manual overwrite should be set * @param {IBlindControlNode} node node data * @param {Object} msg message object * @param {ITimeObject} oNow Now Date object * @param {Object} prevData the nodes previous data * @returns {boolean} true if override is active, otherwise false */ function checkPosOverwrite(node, msg, oNow, prevData) { // node.debug(`checkPosOverwrite act=${node.nodeData.overwrite.active} `); let isSignificant = false; const exactImportance = hlp.getMsgBoolValue(msg, ['exactImportance', 'exactSignificance', 'exactPriority', 'exactPrivilege'], ['exactImporta', 'exactSignifican', 'exactPrivilege', 'exactPrio']); const nImportance = hlp.getMsgNumberValue(msg, ['importance', 'significance', 'prio', 'priority', 'privilege'], ['importa', 'significan', 'prio', 'alarm', 'privilege'], p => { if (exactImportance) { isSignificant = (node.nodeData.overwrite.importance === p); } else { isSignificant = (node.nodeData.overwrite.importance <= p); } ctrlLib.checkOverrideReset(node, msg, oNow, isSignificant); return p; }, () => { ctrlLib.checkOverrideReset(node, msg, oNow, true); return 0; }); if (node.nodeData.overwrite.active && (node.nodeData.overwrite.importance > 0) && !isSignificant) { // if (node.nodeData.overwrite.active && (node.nodeData.overwrite.importance > 0) && (node.nodeData.overwrite.importance > importance)) { // node.debug(`overwrite exit true node.nodeData.overwrite.active=${node.nodeData.overwrite.active}, importance=${nImportance}, node.nodeData.overwrite.importance=${node.nodeData.overwrite.importance}`); // if active, the importance must be 0 or given with same or higher as current overwrite otherwise this will not work node.debug(`do not check any overwrite, importance of message ${nImportance} not matches current overwrite importance ${node.nodeData.overwrite.importance}`); return ctrlLib.setOverwriteReason(node); } const onlyTrigger = hlp.getMsgBoolValue(msg, ['trigger', 'noOverwrite'], ['triggerOnly', 'noOverwrite']); if (onlyTrigger) { return ctrlLib.setOverwriteReason(node); } let newPos = hlp.getMsgNumberValue(msg, ['blindPosition', 'position', 'level', 'blindLevel'], ['manual', 'levelOverwrite']); let nExpire = hlp.getMsgNumberValue(msg, 'expire', 'expire'); if (msg.topic && String(msg.topic).includes('noExpir')) { // hlp.getMsgTopicContains(msg, 'noExpir')) { nExpire = -1; } if (!isNaN(newPos)) { node.debug(`needOverwrite nImportance=${nImportance} nExpire=${nExpire} newPos=${newPos}`); if (newPos === -1) { node.level.current = NaN; node.level.currentInverse = NaN; } else if (!isNaN(newPos)) { const allowRound = (msg.topic ? (msg.topic.includes('roundLevel') || msg.topic.includes('roundLevel')) : false); if (!ctrlLib.validPosition(node, newPos, allowRound)) { node.error(RED._('blind-control.errors.invalid-blind-level', { pos: newPos })); return false; } if (allowRound) { newPos = posRound_(node, newPos); } node.debug(`overwrite newPos=${newPos}`); if (hlp.getMsgBoolValue(msg, 'resetOnSameAsLastValue') && (prevData.level === newPos)) { node.debug(`resetOnSameAsLastValue active, reset overwrite and exit newPos=${newPos}`); ctrlLib.posOverwriteReset(node); return ctrlLib.setOverwriteReason(node); } else if (hlp.getMsgBoolValue(msg, 'ignoreSameValue') && (prevData.level === newPos)) { node.debug(`overwrite exit true (ignoreSameValue), newPos=${newPos}`); return ctrlLib.setOverwriteReason(node); } node.level.current = newPos; node.level.currentInverse = newPos; node.level.topic = msg.topic; if (typeof msg.slat !== 'undefined' && msg.slat !== null) { node.level.slat = msg.slat; } else if (typeof msg.blindSlat !== 'undefined' && msg.blindSlat !== null) { node.level.slat = msg.blindSlat; } else if (typeof msg.topic === 'string' && msg.topic.includes('slatOverwrite')) { node.level.slat = msg.payload; } else { node.level.slat = node.positionConfig.getPropValue(node, msg, node.nodeData.slat, false, oNow.now); } } if (Number.isFinite(nExpire) || (nImportance <= 0)) { // will set expiring if importance is 0 or if expire is explicit defined node.debug(`set expiring - expire is explicit defined "${nExpire}"`); ctrlLib.setExpiringOverwrite(node, oNow, nExpire, 'set expiring time by message'); } else if ((!exactImportance && (node.nodeData.overwrite.importance < nImportance)) || (!node.nodeData.overwrite.expireTs)) { // isSignificant // no expiring on importance change or no existing expiring node.debug(`no expire defined, using default or will not expire`); ctrlLib.setExpiringOverwrite(node, oNow, NaN, 'no special expire defined'); } if (nImportance > 0) { node.nodeData.overwrite.importance = nImportance; } node.nodeData.overwrite.active = true; node.context().set('overwrite', node.nodeData.overwrite, node.contextStore); } else if (node.nodeData.overwrite.active) { node.debug(`overwrite active, check of nImportance=${nImportance} or nExpire=${nExpire}`); if (Number.isFinite(nExpire)) { node.debug(`set to new expiring time nExpire="${nExpire}"`); // set to new expiring time ctrlLib.setExpiringOverwrite(node, oNow, nExpire, 'set new expiring time by message'); } if (nImportance > 0) { // set to new importance node.nodeData.overwrite.importance = nImportance; } node.context().set('overwrite', node.nodeData.overwrite, node.contextStore); } // node.debug(`overwrite exit node.nodeData.overwrite.active=${node.nodeData.overwrite.active}; expire=${nExpire}; newPos=${newPos}`); return ctrlLib.setOverwriteReason(node); } /******************************************************************************************/ /** * calculates for the blind the new level * @param {IBlindControlNode} node - the node data * @param {Object} msg - the message object * @param {ITimeObject} oNow - the now Object * @param {Object} tempData - the temporary data holder object * @param {Object} prevData - the nodes previous data * @returns {Object} the sun position object */ function calcBlindSunPosition(node, msg, oNow, tempData, prevData) { // node.debug('calcBlindSunPosition: calculate blind position by sun'); // sun control is active const sunPosition = node.positionConfig.getSunCalc(oNow.now, false, false); // node.debug('sunPosition: ' + util.inspect(sunPosition, { colors: true, compact: 5, breakLength: Infinity, depth: 10 })); let /** @type {number} */ azimuthStart, /** @type {number} */ azimuthEnd; if (node.windowSettings.setMode === 'orientation') { const orientationValue = node.positionConfig.getFloatProp(node, msg, { type: node.windowSettings.windowOrientationType, value: node.windowSettings.windowOrientation, def: NaN, callback: (result, _obj, cachable) => { return ctrlLib.evalTempData(node, _obj.type, _obj.value, result, tempData, cachable); }, /* callback: (result, _obj) => { if (result !== null && typeof result !== 'undefined') { tempData[_obj.type + '.' + _obj.value] = result; } return result; }, */ noError: true, now: oNow.now }); azimuthStart = orientationValue - node.windowSettings.windowOffsetN; azimuthEnd = orientationValue + node.windowSettings.windowOffsetP; } else { azimuthStart = node.positionConfig.getFloatProp(node, msg, { type: node.windowSettings.azimuthStartType, value: node.windowSettings.azimuthStart, def: NaN, callback: (result, _obj, cachable) => { return ctrlLib.evalTempData(node, _obj.type, _obj.value, result, tempData, cachable); }, /* callback: (result, _obj) => { if (result !== null && typeof result !== 'undefined') { tempData[_obj.type + '.' + _obj.value] = result; } return result; }, */ noError: true, now: oNow.now }); azimuthEnd = node.positionConfig.getFloatProp(node, msg, { type: node.windowSettings.azimuthEndType, value: node.windowSettings.azimuthEnd, def: NaN, callback: (result, _obj, cachable) => { return ctrlLib.evalTempData(node, _obj.type, _obj.value, result, tempData, cachable); }, /* callback: (result, _obj) => { if (result !== null && typeof result !== 'undefined') { tempData[_obj.type + '.' + _obj.value] = result; } return result; }, */ noError: true, now: oNow.now }); } sunPosition.InWindow = angleBetween_(sunPosition.azimuthDegrees, azimuthStart, azimuthEnd, node.positionConfig.angleType); // node.debug(`sunPosition: InWindow=${sunPosition.InWindow} azimuthDegrees=${sunPosition.azimuthDegrees} AzimuthStart=${azimuthStart} AzimuthEnd=${azimuthEnd}`); if (node.autoTrigger ) { if ((sunPosition.altitudeDegrees <= 0)) { node.autoTrigger.type = 3; // Sun not on horizon } else if (sunPosition.azimuthDegrees <= 72) { node.autoTrigger.type = 4; // Sun not visible } else if (!sunPosition.InWindow) { node.autoTrigger.time = Math.min(node.autoTrigger.time, cautoTriggerTimeBeforeSun); node.autoTrigger.type = 5; // sun before in window } else if (sunPosition.InWindow) { if (node.smoothTime > 0) { node.autoTrigger.time = Math.min(node.autoTrigger.time, node.smoothTime); node.autoTrigger.type = 6; // sun in window (smooth time set) } else { node.autoTrigger.time = Math.min(node.autoTrigger.time, (cautoTriggerTimeSun)); node.autoTrigger.type = 7; // sun in window } } } if (node.oversteer.active) { const res = checkOversteer(node, msg, tempData, sunPosition, oNow); if (res) { node.level.current = getBlindPosFromTI(node, msg, res.blindPos.type, res.blindPos.value, node.nodeData.levelTop); node.level.currentInverse = getInversePos_(node, node.level.current); node.level.slat = node.positionConfig.getPropValue(node, msg, res.slatPos, false, oNow.now); node.level.topic = node.oversteer.topic; prevData.last.sunLevel = node.level.current; node.reason.code = 10; node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.oversteer', { pos: res.pos+1 }); node.reason.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.oversteer', { pos: res.pos+1 }); sunPosition.oversteer = res; sunPosition.oversteerAll = node.oversteers; return sunPosition; } sunPosition.oversteerAll = node.oversteers; } // const summerMode = 2; if (!sunPosition.InWindow) { if (node.sunData.mode === cWinterMode) { node.level.current = getBlindPosFromTI(node, msg, node.nodeData.levelMin.type, node.nodeData.levelMin.value, node.nodeData.levelBottom); node.level.currentInverse = getInversePos_(node, node.level.current); node.level.topic = node.sunData.topic; node.level.slat = node.positionConfig.getPropValue(node, msg, node.sunData.slat, false, oNow.now); prevData.last.sunLevel = node.level.current; node.reason.code = 13; node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.sunNotInWinMin'); node.reason.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.sunNotInWinMin'); } else if (node.sunData.mode === cMinimizeMode) { node.level.current = getBlindPosFromTI(node, msg, node.nodeData.levelMax.type, node.nodeData.levelMax.value, node.nodeData.levelTop); node.level.currentInverse = getInversePos_(node, node.level.current); node.level.topic = node.sunData.topic; node.level.slat = node.positionConfig.getPropValue(node, msg, node.sunData.slat, false, oNow.now); prevData.last.sunLevel = node.level.current; node.reason.code = 13; node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.sunNotInWinMax'); node.reason.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.sunNotInWinMax'); } else { node.reason.code = 8; node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.sunNotInWin'); node.reason.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.sunNotInWin'); } return sunPosition; } if (node.sunData.mode === cWinterMode) { node.level.current = node.nodeData.levelMax; node.level.currentInverse = getInversePos_(node, node.level.current); node.level.slat = node.positionConfig.getPropValue(node, msg, node.sunData.slat, false, oNow.now); node.level.topic = node.sunData.topic; prevData.last.sunLevel = node.level.current; node.reason.code = 12; node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.sunInWinMax'); node.reason.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.sunInWinMax'); return sunPosition; } else if (node.sunData.mode === cMinimizeMode) { node.level.current = getBlindPosFromTI(node, msg, node.nodeData.levelMin.type, node.nodeData.levelMin.value, node.nodeData.levelBottom); node.level.currentInverse = getInversePos_(node, node.level.current); node.level.slat = node.positionConfig.getPropValue(node, msg, node.sunData.slat, false, oNow.now); node.level.topic = node.sunData.topic; prevData.last.sunLevel = node.level.current; node.reason.code = 12; node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.sunInWinMin'); node.reason.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.sunInWinMin'); return sunPosition; } const floorLength = node.positionConfig.getFloatProp(node, msg, { type: node.sunData.floorLengthType, value: node.sunData.floorLength, def: NaN, callback: (result, _obj, cachable) => { return ctrlLib.evalTempData(node, _obj.type, _obj.value, result, tempData, cachable); }, /* callback: (result, _obj) => { if (result !== null && typeof result !== 'undefined') { tempData[_obj.type + '.' + _obj.value] = result; } return result; }, */ noError: true, now: oNow.now }); const wTop = node.positionConfig.getFloatProp(node, msg, { type: node.windowSettings.topType, value: node.windowSettings.top, def: NaN, callback: (result, _obj, cachable) => { return ctrlLib.evalTempData(node, _obj.type, _obj.value, result, tempData, cachable); }, /* callback: (result, _obj) => { if (result !== null && typeof result !== 'undefined') { tempData[_obj.type + '.' + _obj.value] = result; } return result; }, */ noError: true, now: oNow.now }); const wBottom = node.positionConfig.getFloatProp(node, msg, { type: node.windowSettings.bottomType, value: node.windowSettings.bottom, def: NaN, callback: (result, _obj, cachable) => { return ctrlLib.evalTempData(node, _obj.type, _obj.value, result, tempData, cachable); }, /* callback: (result, _obj) => { if (result !== null && typeof result !== 'undefined') { tempData[_obj.type + '.' + _obj.value] = result; } return result; }, */ noError: true, now: oNow.now }); const height = Math.tan(sunPosition.altitudeRadians) * floorLength; // node.debug(`height=${height} - altitude=${sunPosition.altitudeRadians} - floorLength=${floorLength}`); if (height <= wBottom) { node.level.current = node.nodeData.levelBottom; node.level.currentInverse = node.nodeData.levelTop; } else if (height >= wTop) { node.level.current = node.nodeData.levelTop; node.level.currentInverse = node.nodeData.levelBottom; } else { const levelPercent = (height - wBottom) / (wTop - wBottom); node.level.current = posRound_(node, ((node.nodeData.levelTopSun - node.nodeData.levelBottomSun) * levelPercent) + node.nodeData.levelBottomSun); node.level.currentInverse = getInversePos_(node, node.level.current); } node.level.slat = node.positionConfig.getPropValue(node, msg, node.sunData.slat, false, oNow.now); node.level.topic = node.sunData.topic; const delta = Math.abs(prevData.level - node.level.current); if ((node.smoothTime > 0) && (node.sunData.changeAgain > oNow.nowNr)) { node.debug(`no change smooth - smoothTime= ${node.smoothTime} changeAgain= ${node.sunData.changeAgain}`); node.reason.code = 11; node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.smooth', { pos: getRealLevel_(node, prevData).toString()}); node.reason.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.smooth', { pos: getRealLevel_(node, prevData).toString()}); node.level.current = prevData.level; node.level.currentInverse = prevData.levelInverse; node.level.slat = prevData.slat; node.level.topic = prevData.topic; } else if ((node.sunData.minDelta > 0) && (delta < node.sunData.minDelta) && (node.level.current > node.nodeData.levelBottom) && (node.level.current < node.nodeData.levelTop)) { node.reason.code = 14; node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.sunMinDelta', { pos: getRealLevel_(node, prevData).toString()}); node.reason.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.sunMinDelta', { pos: getRealLevel_(node, prevData).toString() }); node.level.current = prevData.level; node.level.currentInverse = prevData.levelInverse; node.level.slat = prevData.slat; node.level.topic = prevData.topic; } else { node.reason.code = 9; node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.sunCtrl'); node.reason.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.sunCtrl'); node.sunData.changeAgain = oNow.nowNr + node.smoothTime; // node.debug(`set next time - smoothTime= ${node.smoothTime} changeAgain= ${node.sunData.changeAgain} nowNr=` + oNow.nowNr); } const levelMin = getBlindPosFromTI(node, msg, node.nodeData.levelMin.type, node.nodeData.levelMin.value, node.nodeData.levelBottom); // const levelMax = getBlindPosFromTI(node, msg, node.nodeData.levelMin.type, node.nodeData.levelMin.value, node.nodeData.levelBottom); const levelMax = getBlindPosFromTI(node, msg, node.nodeData.levelMax.type, node.nodeData.levelMax.value, node.nodeData.levelTop); if (node.level.current < levelMin) { // min node.debug(`${node.level.current} is below ${levelMin} (min)`); node.reason.code = 5; node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.sunCtrlMin', {org: node.reason.state}); node.reason.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.sunCtrlMin', {org: node.reason.description, level:node.level.current}); node.level.current = levelMin; node.level.currentInverse = getInversePos_(node, node.level.current); } else if (node.level.current > levelMax) { // max node.debug(`${node.level.current} is above ${levelMax} (max)`); node.reason.code = 6; node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.sunCtrlMax', {org: node.reason.state}); node.reason.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.sunCtrlMax', {org: node.reason.description, level:node.level.current}); node.level.current = levelMax; node.level.currentInverse = getInversePos_(node, node.level.current); } prevData.last.sunLevel = node.level.current; // node.debug(`calcBlindSunPosition end pos=${node.level.current} reason=${node.reason.code} description=${node.reason.description}`); return sunPosition; } /******************************************************************************************/ /** * changes the rule settings * @param {IBlindControlNode} node node data * @param {number} [rulePos] the position of the rule which should be changed * @param {string} [ruleName] the name of the rule which should be changed * @param {Object} [ruleData] the properties of the rule which should be changed */ function changeRules(node, rulePos, ruleName, ruleData) { // node.debug(`changeRules: ${ node.rules.count } ruleData:' ${util.inspect(ruleData, {colors:true, compact:10})}`); for (let i = 0; i <= node.rules.count; ++i) { const rule = node.rules[i]; if (((typeof rulePos !== 'undefined') && rule.pos === rulePos) || ((typeof ruleName !== 'undefined') && rule.name === ruleName)) { node.rules[i] = Object.assign(node.rules[i], ruleData); } } } /******************************************************************************************/ /** * check all rules and determinate the active rule * @param {IBlindControlNode} node node data * @param {Object} msg the message object * @param {ITimeObject} oNow the *current* date Object * @param {Object} tempData the object storing the temporary caching data * @returns {Object} the active rule or null */ function checkRules(node, msg, oNow, tempData) { // node.debug('checkRules --------------------'); ctrlLib.prepareRules(node, msg, tempData, oNow.now); const rule = ctrlLib.getActiveRule(node, msg, oNow, tempData); const livingRuleData = {}; livingRuleData.importance = 0; livingRuleData.resetOverwrite = false; if (rule.ruleTopicOvs) { livingRuleData.topicOversteer = { id: rule.ruleTopicOvs.pos, name: rule.ruleTopicOvs.name, conditional: rule.ruleTopicOvs.conditional, timeLimited: (!!rule.ruleTopicOvs.time), conditon: rule.ruleTopicOvs.conditonResult, time: rule.ruleTopicOvs.timeResult, importance: rule.ruleTopicOvs.importance, topic : rule.ruleTopicOvs.topic || '' }; delete rule.ruleTopicOvs; } if (rule.ruleSlatOvs) { livingRuleData.slatOversteer = { id: rule.ruleSlatOvs.pos, name: rule.ruleSlatOvs.name, conditional: rule.ruleSlatOvs.conditional, timeLimited: (!!rule.ruleSlatOvs.time), conditon: rule.ruleSlatOvs.conditonResult, time: rule.ruleSlatOvs.timeResult, slat: node.positionConfig.getPropValue(node, msg, rule.ruleSlatOvs.slat, false, oNow.now), importance: rule.ruleSlatOvs.importance }; delete rule.ruleSlatOvs; } livingRuleData.hasMinimum = false; if (rule.ruleSelMin) { const lev = getBlindPosFromTI(node, msg, rule.ruleSelMin.level.type, rule.ruleSelMin.level.value, -1); // node.debug('rule.ruleSelMin ' + lev + ' -- ' + util.inspect(rule.ruleSelMin, { colors: true, compact: 5, breakLength: Infinity, depth: 10 })); if (lev > -1) { livingRuleData.levelMinimum = lev; livingRuleData.hasMinimum = true; livingRuleData.minimum = { id: rule.ruleSelMin.pos, name: rule.ruleSelMin.name, conditional: rule.ruleSelMin.conditional, timeLimited: (!!rule.ruleSelMin.time), conditon: rule.ruleSelMin.conditonResult, time: rule.ruleSelMin.timeResult // slat: node.positionConfig.getPropValue(node, msg, rule.ruleSelMin.slat, false, oNow.now) // importance: rule.ruleSelMin.importance, // resetOverwrite: rule.ruleSelMin.resetOverwrite, // topic : rule.ruleSelMin.topic }; } delete rule.ruleSelMin; } livingRuleData.hasMaximum = false; if (rule.ruleSelMax) { const lev = getBlindPosFromTI(node, msg, rule.ruleSelMax.level.type, rule.ruleSelMax.level.value, -1); // node.debug('rule.ruleSelMax ' + lev + ' -- ' + util.inspect(rule.ruleSelMax, { colors: true, compact: 5, breakLength: Infinity, depth: 10 }) ); if (lev > -1) { livingRuleData.levelMaximum = lev; livingRuleData.hasMaximum = true; livingRuleData.maximum = { id: rule.ruleSelMax.pos, name: rule.ruleSelMax.name, conditional: rule.ruleSelMax.conditional, timeLimited: (!!rule.ruleSelMax.time), conditon: rule.ruleSelMax.conditonResult, time: rule.ruleSelMax.timeResult // slat: node.positionConfig.getPropValue(node, msg, rule.ruleSelMax.slat, false, oNow.now) // importance: rule.ruleSelMax.importance, // resetOverwrite: rule.ruleSelMax.resetOverwrite, // topic : rule.ruleSelMax.topic }; } delete rule.ruleSelMax; } if (rule.ruleSel) { // rule.ruleSel.text = ''; // node.debug('rule.ruleSel ' + util.inspect(rule.ruleSel, {colors:true, compact:10, breakLength: Infinity })); livingRuleData.id = rule.ruleSel.pos; livingRuleData.name = rule.ruleSel.name; livingRuleData.importance = rule.ruleSel.importance; livingRuleData.resetOverwrite = rule.ruleSel.resetOverwrite; livingRuleData.code = 4; livingRuleData.topic = rule.ruleSel.topic; livingRuleData.active = true; livingRuleData.conditional = rule.ruleSel.conditional; livingRuleData.timeLimited = (!!rule.ruleSel.time); const data = { number: rule.ruleSel.pos, name: rule.ruleSel.name }; let name = 'rule'; if (rule.ruleSel.conditional) { livingRuleData.conditon = rule.ruleSel.conditonResult; data.text = rule.ruleSel.conditonResult.text; data.textShort = rule.ruleSel.conditonResult.textShort; name = 'ruleCond'; } if (rule.ruleSel.time && rule.ruleSel.timeResult) { livingRuleData.time = rule.ruleSel.timeResult; if (livingRuleData.time.start) { livingRuleData.time.start.timeLocal = node.positionConfig.toTimeString(rule.ruleSel.timeResult.start.value); livingRuleData.time.start.timeLocalDate = node.positionConfig.toDateString(rule.ruleSel.timeResult.start.value); livingRuleData.time.start.dateISO= rule.ruleSel.timeResult.start.value.toISOString(); livingRuleData.time.start.dateUTC= rule.ruleSel.timeResult.start.value.toUTCString(); } if (livingRuleData.time.end) { livingRuleData.time.end.timeLocal = node.positionConfig.toTimeString(rule.ruleSel.timeResult.end.value); livingRuleData.time.end.timeLocalDate = node.positionConfig.toDateString(rule.ruleSel.timeResult.end.value); livingRuleData.time.end.dateISO= rule.ruleSel.timeResult.end.value.toISOString(); livingRuleData.time.end.dateUTC= rule.ruleSel.timeResult.end.value.toUTCString(); } // data.timeOp = rule.ruleSel.time.operatorText; // data.timeLocal = livingRuleData.time.timeLocal; // data.time = livingRuleData.time.dateISO; name = (rule.ruleSel.conditional) ? 'ruleTimeCond' : 'ruleTime'; } livingRuleData.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.'+name, data); livingRuleData.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.'+name, data); // node.debug(`checkRules end livingRuleData=${util.inspect(livingRuleData, { colors: true, compact: 5, breakLength: Infinity, depth: 10 })}`); if (rule.ruleSel.level.operator === ctrlLib.cRuleType.off) { livingRuleData.isOff = true; } else if (rule.ruleSel.level.operator === ctrlLib.cRuleType.absolute) { // absolute rule livingRuleData.level = getBlindPosFromTI(node, msg, rule.ruleSel.level.type, rule.ruleSel.level.value, -1); livingRuleData.slat = node.positionConfig.getPropValue(node, msg, rule.ruleSel.slat, false, oNow.now); livingRuleData.active = (livingRuleData.level > -1); } else { livingRuleData.active = false; livingRuleData.level = getBlindPosFromTI(node, msg, node.nodeData.levelDefault.type, node.nodeData.levelDefault.value, node.nodeData.levelTop); livingRuleData.slat = node.positionConfig.getPropValue(node, msg, node.nodeData.slat, false, oNow.now); } return livingRuleData; } livingRuleData.active = false; livingRuleData.id = ctrlLib.cRuleDefault; livingRuleData.importance = 0; livingRuleData.resetOverwrite = false; livingRuleData.level = getBlindPosFromTI(node, msg, node.nodeData.levelDefault.type, node.nodeData.levelDefault.value, node.nodeData.levelTop); livingRuleData.slat = node.positionConfig.getPropValue(node, msg, node.nodeData.slat, false, oNow.now); livingRuleData.topic = node.nodeData.topic; livingRuleData.code = 1; livingRuleData.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.default'); livingRuleData.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.default'); // node.debug(`checkRules end livingRuleData=${util.inspect(livingRuleData, { colors: true, compact: 5, breakLength: Infinity, depth: 10 })}`); return livingRuleData; } /******************************************************************************************/ /******************************************************************************************/ /** * standard Node-Red Node handler for the sunBlindControlNode * @param {*} config the Node-Red Configuration property of the Node */ function sunBlindControlNode(config) { RED.nodes.createNode(this, config); /** Copy 'this' object in case we need it in context of callbacks of other functions. * @type {IBlindControlNode} */ // @ts-ignore const node = this; /** @type {IPositionConfigNode} */ node.positionConfig = RED.nodes.getNode(config.positionConfig); if (!node.positionConfig) { node.error(RED._('node-red-contrib-sun-position/position-config:errors.config-missing')); node.status({fill: 'red', shape: 'dot', text: RED._('node-red-contrib-sun-position/position-config:errors.config-missing-state') }); return; } if (node.positionConfig.checkNode( error => { const text = RED._('node-red-contrib-sun-position/position-config:errors.config-error', { error }); node.error(text); node.status({fill: 'red', shape: 'dot', text }); return true; }, false)) { return; } if (!Array.isArray(config.results)) { config.results = [{ p: '', pt: 'msgPayload', v: '', vt: 'level' }, { p: 'slat', pt: 'msg', v: '', vt: 'slat'