node-red-contrib-sun-position
Version:
NodeRED nodes to get sun and moon position
926 lines (889 loc) • 89.9 kB
JavaScript
// @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'