UNPKG

node-red-contrib-blindcontroller-v2

Version:

A collection of Node-Red nodes that automates the control of household roller blinds on the current position of the sun

1,174 lines (1,128 loc) 38.6 kB
/** * Copyright 2020, Jean-Noël Canches * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ module.exports = function(RED) { "use strict"; /* * Tests the validity of the input msg.payload before using this payload to * run the calculation. As the node consumes 4 different message type, the * topic is used as the mechanism of identifying the type. The validations * differ for each message type. * * The validations performed are: * - existence of mandatory properties e.g. channel must exist * - range checks on property values e.g. orientation should be a value * between 0 and 360 inclusive * - consistency checks between property values e.g. bottom of window should * be higher than the top */ function validateMsg(node, msg) { var validMsg = true; if (!(typeof msg.payload === "object")) { node.error(RED._("blindcontroller.error.invalid-msg-payload"), msg); validMsg = false; } else { switch (msg.topic) { case "sun": validMsg = validateSunPositionMsg(node, msg); break; case "blindPosition": validMsg = validateBlindPositionMsg(node, msg); break; case "blindPositionReset": validMsg = validateBlindPositionResetMsg(node, msg); break; case "blind": validMsg = validateBlindMsg(node, msg); break; case "weather": validMsg = validateWeatherMsg(node, msg); break; case "mode": validMsg = validateModeMsg(node, msg); break; default: node.error( RED._("blindcontroller.error.unknown-msg-topic") + msg.topic, msg ); validMsg = false; } } return validMsg; } /* * Validate Sun Position message */ function validateSunPositionMsg(node, msg) { var validMsg = true; var sunProperty = ["sunInSky", "azimuth", "altitude"]; var i; for (i in sunProperty) { if (!(sunProperty[i] in msg.payload)) { node.error( RED._("blindcontroller.error.sunPosition.missing-property") + sunProperty[i], msg ); validMsg = false; } } if (validMsg) { if (typeof msg.payload.sunInSky != "boolean") { node.error( RED._("blindcontroller.error.sunPosition.invalid-sunInSky") + typeof msg.payload.sunInSky, msg ); validMsg = false; } if ( typeof msg.payload.altitude != "number" || msg.payload.altitude > 90 ) { node.error( RED._("blindcontroller.error.sunPosition.invalid-altitude") + msg.payload.altitude, msg ); validMsg = false; } if ( typeof msg.payload.azimuth != "number" || msg.payload.azimuth < 0 || msg.payload.azimuth > 360 ) { node.error( RED._("blindcontroller.error.sunPosition.invalid-azimuth") + msg.payload.azimuth, msg ); validMsg = false; } } return validMsg; } function invalidPosition(position, increment) { return ( position && (typeof position != "number" || position < 0 || position > 100 || position % increment != 0) ); } /* * Validate Blind message */ function validateBlindMsg(node, msg) { var validMsg = true; var blindProperty = [ "channel", "orientation", "top", "bottom", "depth", "increment" ]; var modes = ["Summer", "Winter"]; var i; for (i in blindProperty) { if (!(blindProperty[i] in msg.payload)) { node.error( RED._("blindcontroller.error.blind.missing-property") + blindProperty[i], msg ); validMsg = false; } } if (validMsg) { if ( typeof msg.payload.orientation != "number" || msg.payload.orientation < 0 || msg.payload.orientation > 360 ) { node.error( RED._("blindcontroller.error.blind.invalid-orientation") + msg.payload.orientation, msg ); validMsg = false; } if ( msg.payload.mode && (typeof msg.payload.mode != "string" || modes.indexOf(msg.payload.mode) == -1) ) { node.error( RED._("blindcontroller.error.blind.invalid-mode") + msg.payload.mode, msg ); validMsg = false; } if ( msg.payload.noffset && (typeof msg.payload.noffset != "number" || msg.payload.noffset < 0 || msg.payload.noffset > 90) ) { node.error( RED._("blindcontroller.error.blind.invalid-noffset") + msg.payload.noffset, msg ); validMsg = false; } if ( msg.payload.poffset && (typeof msg.payload.poffset != "number" || msg.payload.poffset < 0 || msg.payload.poffset > 90) ) { node.error( RED._("blindcontroller.error.blind.invalid-poffset") + msg.payload.poffset, msg ); validMsg = false; } if (typeof msg.payload.top != "number" || msg.payload.top <= 0) { node.error( RED._("blindcontroller.error.blind.invalid-top") + msg.payload.top, msg ); validMsg = false; } if (typeof msg.payload.bottom != "number" || msg.payload.bottom < 0) { node.error( RED._("blindcontroller.error.blind.invalid-bottom") + msg.payload.bottom, msg ); validMsg = false; } if (typeof msg.payload.depth != "number" || msg.payload.depth < 0) { node.error( RED._("blindcontroller.error.blind.invalid-depth") + msg.payload.depth, msg ); validMsg = false; } if ( typeof msg.payload.top == "number" && typeof msg.payload.bottom == "number" && msg.payload.top < msg.payload.bottom ) { node.error( RED._("blindcontroller.error.blind.invalid-dimensions") + msg.payload.top + " - " + msg.payload.bottom, msg ); validMsg = false; } if ( typeof msg.payload.increment != "number" || msg.payload.increment < 0 || msg.payload.increment > 100 || 100 % msg.payload.increment != 0 ) { node.error( RED._("blindcontroller.error.blind.invalid-increment") + msg.payload.increment, msg ); validMsg = false; } if (invalidPosition(msg.payload.maxopen, msg.payload.increment)) { node.error( RED._("blindcontroller.error.blind.invalid-maxopen") + msg.payload.maxopen, msg ); validMsg = false; } if (invalidPosition(msg.payload.maxclosed, msg.payload.increment)) { node.error( RED._("blindcontroller.error.blind.invalid-maxclosed") + msg.payload.maxclosed, msg ); validMsg = false; } if (msg.payload.maxopen > msg.payload.maxclosed) { node.error( RED._("blindcontroller.error.blind.invalid-max-settings") + msg.payload.maxopen + " - " + msg.payload.maxclosed, msg ); validMsg = false; } if ( msg.payload.altitudethreshold && (typeof msg.payload.altitudethreshold != "number" || msg.payload.altitudethreshold < 0 || msg.payload.altitudethreshold > 90) ) { node.error( RED._("blindcontroller.error.blind.invalid-altitudethreshold") + msg.payload.altitudethreshold, msg ); validMsg = false; } if ( msg.payload.cloudsthreshold && (typeof msg.payload.cloudsthreshold != "number" || msg.payload.cloudsthreshold < 0 || msg.payload.cloudsthreshold > 1) ) { node.error( RED._("blindcontroller.error.blind.invalid-cloudsthreshold") + msg.payload.cloudsthreshold, msg ); validMsg = false; } if ( invalidPosition( msg.payload.cloudsthresholdposition, msg.payload.increment ) ) { node.error( RED._("blindcontroller.error.blind.invalid-cloudsthresholdposition") + msg.payload.cloudsthresholdposition, msg ); validMsg = false; } if ( msg.payload.uvindexthreshold && (typeof msg.payload.uvindexthreshold != "number" || msg.payload.uvindexthreshold < 0 || msg.payload.uvindexthreshold > 20) ) { node.error( RED._("blindcontroller.error.blind.invalid-uvindexthreshold") + msg.payload.uvindexthreshold, msg ); validMsg = false; } if ( invalidPosition( msg.payload.uvindexthresholdposition, msg.payload.increment ) ) { node.error( RED._( "blindcontroller.error.blind.invalid-uvindexthresholdposition" ) + msg.payload.uvindexthresholdposition, msg ); validMsg = false; } if ( invalidPosition( msg.payload.temperaturethresholdposition, msg.payload.increment ) ) { node.error( RED._( "blindcontroller.error.blind.invalid-temperaturethresholdposition" ) + msg.payload.temperaturethresholdposition, msg ); validMsg = false; } if (invalidPosition(msg.payload.nightposition, msg.payload.increment)) { node.error( RED._("blindcontroller.error.blind.invalid-nightposition") + msg.payload.nightposition, msg ); validMsg = false; } if ( msg.payload.expiryperiod && (typeof msg.payload.expiryperiod != "number" || msg.payload.expiryposition < 0) ) { node.error( RED._("blindcontroller.error.blind.invalid-expiryperiod") + msg.payload.expiryperiod, msg ); validMsg = false; } } return validMsg; } /* * Validate Blind Position message */ function validateBlindPositionMsg(node, msg) { var validMsg = true; var blindProperty = ["channel", "blindPosition"]; var i; for (i in blindProperty) { if (!(blindProperty[i] in msg.payload)) { node.error( RED._("blindcontroller.error.blindPosition.missing-property") + blindProperty[i], msg ); validMsg = false; } } if (validMsg) { if ( msg.payload.expiryperiod && (typeof msg.payload.expiryperiod != "number" || msg.payload.expiryperiod < 0) ) { node.error( RED._("blindcontroller.error.blindPosition.invalid-expiryperiod") + msg.payload.expiryperiod, msg ); validMsg = false; } if ( msg.payload.blindPosition && (typeof msg.payload.blindPosition != "number" || msg.payload.blindPosition < 0 || msg.payload.blindPosition > 100) ) { node.error( RED._("blindcontroller.error.blindPosition.invalid-blindPosition") + msg.payload.blindPosition, msg ); validMsg = false; } } return validMsg; } /* * Validate Blind Position Reset message */ function validateBlindPositionResetMsg(node, msg) { var validMsg = true; var blindProperty = ["channel", "reset"]; var i; for (i in blindProperty) { if (!(blindProperty[i] in msg.payload)) { node.error( RED._("blindcontroller.error.blindPosition.missing-property") + blindProperty[i], msg ); validMsg = false; } } if (validMsg) { if (msg.payload.reset && typeof msg.payload.reset != "boolean") { node.error( RED._("blindcontroller.error.blindPosition.invalid-reset") + msg.payload.reset, msg ); validMsg = false; } } return validMsg; } /* * Validate Weather message */ function validateWeatherMsg(node, msg) { var validMsg = true; if (msg.payload.clouds < 0 || msg.payload.clouds > 1) { node.error( RED._("blindcontroller.error.weather.invalid-clouds") + msg.payload.clouds, msg ); validMsg = false; } if (msg.payload.uvindex < 0 || msg.payload.uvindex > 20) { node.error( RED._("blindcontroller.error.weather.invalid-uvindex") + msg.payload.uvindex, msg ); validMsg = false; } return validMsg; } /* * Validate Mode message */ function validateModeMsg(node, msg) { var validMsg = true; var modeProperty = ["mode"]; var modes = ["Summer", "Winter"]; var i; for (i in modeProperty) { if (!(modeProperty[i] in msg.payload)) { node.error( RED._("blindcontroller.error.mode.missing-property") + modeProperty[i], msg ); validMsg = false; } } if (validMsg) { if ( msg.payload.mode && (typeof msg.payload.mode != "string" || modes.indexOf(msg.payload.mode) == -1) ) { node.error( RED._("blindcontroller.error.mode.invalid-mode") + msg.payload.mode, msg ); validMsg = false; } } return validMsg; } /* * Function to determine whether the sun is considered to be in the window * based on the orientation of the window and the azimuth of the sun */ function isSunInWindow(blind, azimuth) { var sunInWindow = false; /* * Checks the sun azimuth is between window orientation +/- offset. * Where the range includes ranges each side of north, separate checks * need to be performed either side of north */ if (blind.orientation - blind.noffset < 0) { if ( (360 + blind.orientation - blind.noffset <= azimuth) & (azimuth <= 360) || (0 <= azimuth && azimuth <= blind.orientation + blind.poffset) ) { sunInWindow = true; } } else if (blind.orientation + blind.poffset > 360) { if ( (0 <= azimuth) & (azimuth <= blind.orientation + blind.poffset - 360) || (blind.orientation - blind.noffset <= azimuth && azimuth <= 360) ) { sunInWindow = true; } } else { if ( blind.orientation - blind.noffset <= azimuth && azimuth <= blind.orientation + blind.poffset ) { sunInWindow = true; } } return sunInWindow; } function getBlindPositionReasonDesc(blindPositionReasonCode) { return RED._("blindcontroller.positionReason." + blindPositionReasonCode); } /* * Function to calculate the appropriate blind position based on the * altitude of the sun, characteristics of the window, with the target of * restricting or maximising the extent to which direct sunlight enters * the room. * * The function works in two modes, Summer and Winter. In Summer mode, it * restricts direct sunlight entering the room. When the sun is considered * to be in the window, the function calculates the minimum height of an * object that casts a shadow to the depth property based on the sun * altitude of the sun. This height is converted into a blind position * using the dimensions of the window and the increments by which the blind * position can be controlled. * * The calculation also takes into account the following (in order of * precedence): * - if the blind's position has been manually specified, the calculation * is not performed for that blind until that position expires * - if the forecasted temperature for the day exceeds a threshold, the * blind will be closed fully while the sun is in the sky. This feature * of the function is intended to allow blinds to be used to block out * extreme heat. * - if the UV index is low, the blind will be set to a fully open position. * - if it is deemed to be sufficiently overcast, the blind will be set to a * fully open position. * - if the sun is below an altitude threshold, the blind will be set to a * fully open position. * * In winter mode, the calculation is based on whether the sun is in * window and whether it is suffiently overcast. If the sun is in the * window, it will be opened to a configured Open position unless it is * overcast it which case it will be closed. If the sun is not in the * window, it is closed to a configured Closed position. * * Outside daylight hours, the blind is closed to a configured position. */ function calcBlindPosition(blind, sunPosition, weather) { /* * For the given altitude of the sun, calculate the minimum height of * an object that casts a shadow to the specified depth. Convert this * height into a blind position based on the dimensions of the window */ var isTemperatureAConcern = weather.maxtemp && blind.temperaturethreshold ? weather.maxtemp > blind.temperaturethreshold : false; var isOvercast = weather.clouds && blind.cloudsthreshold ? weather.clouds > blind.cloudsthreshold : false; var isHighUV = weather.uvindex && blind.uvindexthreshold ? weather.uvindex > blind.uvindexthreshold : false; var now = new Date(); if (hasBlindPositionExpired(blind.blindPositionExpiry)) { blind.blindPosition = blind.maxopen; if (sunPosition.sunInSky) { if (isTemperatureAConcern) { blind.blindPosition = blind.temperaturethresholdposition; blind.blindPositionReasonCode = "07"; blind.blindPositionReasonDesc = getBlindPositionReasonDesc("07"); } else { blind.sunInWindow = isSunInWindow(blind, sunPosition.azimuth); switch (blind.mode) { case "Winter": if (blind.sunInWindow) { if (isOvercast) { blind.blindPosition = blind.cloudsthresholdposition; blind.blindPositionReasonCode = "06"; blind.blindPositionReasonDesc = getBlindPositionReasonDesc( "06" ); } else if (isHighUV) { blind.blindPosition = blind.uvindexthresholdposition; blind.blindPositionReasonCode = "08"; blind.blindPositionReasonDesc = getBlindPositionReasonDesc( "08" ); } else { blind.blindPosition = blind.maxopen; blind.blindPositionReasonCode = "05"; blind.blindPositionReasonDesc = getBlindPositionReasonDesc( "05" ); } } else { blind.blindPosition = blind.maxclosed; blind.blindPositionReasonCode = "04"; blind.blindPositionReasonDesc = getBlindPositionReasonDesc( "04" ); } break; default: if (blind.sunInWindow) { if ( ((blind.altitudethreshold && sunPosition.altitude >= blind.altitudethreshold) || !blind.altitudethreshold) && !isOvercast && !isHighUV ) { var height = Math.tan((sunPosition.altitude * Math.PI) / 180) * blind.depth; if (height <= blind.bottom) { blind.blindPosition = blind.maxclosed; } else if (height >= blind.top) { blind.blindPosition = blind.maxopen; } else { blind.blindPosition = Math.ceil( 100 * (1 - (height - blind.bottom) / (blind.top - blind.bottom)) ); blind.blindPosition = Math.ceil(blind.blindPosition / blind.increment) * blind.increment; blind.blindPosition = blind.blindPosition > blind.maxclosed ? blind.maxclosed : blind.blindPosition; blind.blindPosition = blind.blindPosition < blind.maxopen ? blind.maxopen : blind.blindPosition; } blind.blindPositionReasonCode = "05"; blind.blindPositionReasonDesc = getBlindPositionReasonDesc( "05" ); } else if ( blind.altitudethreshold && sunPosition.altitude < blind.altitudethreshold ) { blind.blindPositionReasonCode = "03"; blind.blindPositionReasonDesc = getBlindPositionReasonDesc( "03" ); } else if (isOvercast) { blind.blindPosition = blind.cloudsthresholdposition; blind.blindPositionReasonCode = "06"; blind.blindPositionReasonDesc = getBlindPositionReasonDesc( "06" ); } else if (isHighUV) { blind.blindPosition = blind.uvindexthresholdposition; blind.blindPositionReasonCode = "08"; blind.blindPositionReasonDesc = getBlindPositionReasonDesc( "08" ); } } else { blind.blindPositionReasonCode = "04"; blind.blindPositionReasonDesc = getBlindPositionReasonDesc( "04" ); } if (weather) { blind.weather = weather; } break; } } } else { blind.blindPosition = blind.nightposition; blind.blindPositionReasonCode = "02"; blind.blindPositionReasonDesc = getBlindPositionReasonDesc("02"); blind.sunInWindow = false; } if (blind.blindPositionExpiry) { delete blind.blindPositionExpiry; } } } /* * Checks whether the current Blind Position has expired. */ function hasBlindPositionExpired(expiryTimestampString) { var now = new Date(); if (expiryTimestampString) { var expiry = new Date(expiryTimestampString); return now > expiry; } else { return true; } } /* * For each blind, run the blind calculation process and if a different * position is determined send a message with the new position for the * channel. */ function runCalc(node, send, msg, allBlinds, sunPosition, weather) { var i; var blinds = []; var resultMsgs = []; if (msg.payload.channel && allBlinds[msg.payload.channel]) { blinds.push(allBlinds[msg.payload.channel]); } for (i in blinds) { var previousBlindPosition = blinds[i].blindPosition; var previousSunInWindow = blinds[i].sunInWindow; var previousBlindPositionReasonCode = blinds[i].blindPositionReasonCode; calcBlindPosition(blinds[i], sunPosition, weather); blinds[i].logicalBlindPosition = blinds[i].blindPosition; blinds[i].blindPosition = blinds[i].opposite ? 100 - blinds[i].blindPosition : blinds[i].blindPosition; if ( blinds[i].blindPosition != previousBlindPosition || blinds[i].sunInWindow != previousSunInWindow || blinds[i].blindPositionReasonCode != previousBlindPositionReasonCode ) { var resultMsg = RED.util.cloneMessage(msg); resultMsg.payload = blinds[i]; resultMsg.data = { channel: blinds[i].channel, altitude: sunPosition.altitude, azimuth: sunPosition.azimuth, blindPosition: blinds[i].blindPosition }; resultMsg.topic = "blind"; resultMsgs.push(resultMsg); } } if (resultMsgs.length > 0) { send(resultMsgs); } } /* * When the blind position is manually specified, this function is used to * prepare the message and the expiry timestamp. */ function setPosition(node, send, msg, blind) { blind.blindPosition = blind.opposite ? 100 - msg.payload.blindPosition : msg.payload.blindPosition; blind.logicalBlindPosition = msg.payload.blindPosition; blind.blindPositionExpiry = calcBlindPositionExpiry( msg.payload.expiryperiod ? msg.payload.expiryperiod : blind.expiryperiod ); blind.blindPositionReasonCode = "01"; blind.blindPositionReasonDesc = getBlindPositionReasonDesc("01"); msg.payload = blind; msg.topic = "blind"; send(msg); } /* * When the blind position is manually specified and a reset message is received, this function is used to * remove the expiry timestamp and set it back to "defaults". */ function resetPosition(blind) { delete blind.blindPositionExpiry; } /* * Calculates the expiry timestamp */ function calcBlindPositionExpiry(expiryperiod) { var expiryTimestamp = new Date(); expiryTimestamp.setMinutes(expiryTimestamp.getMinutes() + expiryperiod); return expiryTimestamp; } /* * Tests whether val has been set, and returns defaultVal if it hasn't */ function defaultIfUndefined(val, defaultVal) { return typeof val == "undefined" || val == "" ? defaultVal : val; } function convertToBoolean(val) { switch (typeof val) { case "boolean": return val; case "string": switch (val.toLowerCase().trim()) { case "true": case "yes": case "1": return true; case "false": case "no": case "0": case null: return false; default: return Boolean(string); } } } /* * Function which is exported when the node-RED runtime loads the node on * start-up, and the basis of the Multi Blind Controller node. The node * responds to four different input messages: * - blind: the input configuration parameters for a blind. One message is * expected to be received per blind on startup * - sun: the output of the Sun Position node containing the sun's current * altitude and azimuth. A message is expected to be received * periodically, as this is the trigger to recalculate the blind * position * - weather: the current weather conditions. A message is expected to be * received periodically. * - blindPosition: message containing a specified blind position. * * The function maintains a state machine which allows the messages to be * received in any sequence. */ function BlindControllerWithoutConfig(config) { RED.nodes.createNode(this, config); /* * Initialise node with value of configurable properties */ this.name = config.name; var node = this; var weather = {}; var sunPosition = {}; /* * Registers a listener on the input event to receive messages from the * up-stream nodes in a flow. This function does not any values from * the input message. */ this.on("input", function(msg, send, done) { var validMsg = validateMsg(node, msg); if (validMsg) { var blinds = getBlinds(node); switch (msg.topic) { case "sun": sunPosition = msg.payload; runCalc(node, send, msg, blinds, sunPosition, weather); break; case "blindPosition": setPosition(node, send, msg, blinds[msg.payload.channel]); break; case "blindPositionReset": resetPosition(blinds[msg.payload.channel]); break; case "blind": var channel = msg.payload.channel; blinds[channel] = msg.payload; /* * Default settings if not specified in input msg */ blinds[channel].mode = defaultIfUndefined( msg.payload.mode, RED._("blindcontroller.placeholder.mode") ); blinds[channel].noffset = defaultIfUndefined( msg.payload.noffset, Number(RED._("blindcontroller.placeholder.noffset")) ); blinds[channel].poffset = defaultIfUndefined( msg.payload.poffset, Number(RED._("blindcontroller.placeholder.poffset")) ); blinds[channel].maxopen = defaultIfUndefined( msg.payload.maxopen, Number(RED._("blindcontroller.placeholder.maxopen")) ); blinds[channel].maxclosed = defaultIfUndefined( msg.payload.maxclosed, Number(RED._("blindcontroller.placeholder.maxclosed")) ); blinds[channel].nightposition = defaultIfUndefined( msg.payload.nightposition, Number(RED._("blindcontroller.placeholder.nightposition")) ); blinds[channel].temperaturethresholdposition = defaultIfUndefined( msg.payload.temperaturethresholdposition, Number( RED._( "blindcontroller.placeholder.temperaturethresholdposition" ) ) ); blinds[channel].cloudsthresholdposition = defaultIfUndefined( msg.payload.cloudsthresholdposition, Number( RED._("blindcontroller.placeholder.cloudsthresholdposition") ) ); blinds[channel].uvindexthresholdposition = defaultIfUndefined( msg.payload.uvindexthresholdposition, RED._("blindcontroller.placeholder.uvindexthresholdposition") ); blinds[channel].expiryperiod = defaultIfUndefined( msg.payload.expiryperiod, Number(RED._("blindcontroller.placeholder.expiryperiod")) ); blinds[channel].opposite = convertToBoolean( defaultIfUndefined( msg.payload.opposite, RED._("blindcontroller.placeholder.opposite") ) ); break; case "weather": sunPosition = msg.payload; weather = msg.payload; runCalc(node, send, msg, blinds, sunPosition, weather); break; case "mode": var mode = msg.payload.mode; var i; for (i in blinds) { blinds[i].mode = mode; } runCalc(node, send, msg, blinds, sunPosition, weather); break; default: break; } setBlinds(node, blinds); } }); } function getBlinds(node) { var globalContext = node.context().global; var blinds = globalContext.get("blinds"); if (!blinds) { blinds = {}; globalContext.set("blinds", blinds); } return blinds; } function setBlinds(node, blinds) { var globalContext = node.context().global; globalContext.set("blinds", blinds); } /* * Function which is exported when the node-RED runtime loads the node on * start-up, and the basis of the Blind Controller node. The node responds * to three different input messages: * - sun: the output of the Sun Position node containing the sun's current * altitude and azimuth * - blindPosition: message containing a specified blind position * - weather: the current weather conditions */ function BlindControllerWithConfig(config) { RED.nodes.createNode(this, config); /* * Initialise node with value of configurable properties */ this.name = config.name; var channel = config.channel; var blinds = getBlinds(this); blinds[channel] = { channel: channel, mode: config.mode, orientation: Number(config.orientation), noffset: Number( defaultIfUndefined( config.noffset, RED._("blindcontroller.placeholder.noffset") ) ), poffset: Number( defaultIfUndefined( config.poffset, RED._("blindcontroller.placeholder.poffset") ) ), top: Number(config.top), bottom: Number(config.bottom), depth: Number(config.depth), altitudethreshold: Number(config.altitudethreshold), increment: Number(config.increment), maxopen: Number( defaultIfUndefined( config.maxopen, RED._("blindcontroller.placeholder.maxopen") ) ), maxclosed: Number( defaultIfUndefined( config.maxclosed, RED._("blindcontroller.placeholder.maxclosed") ) ), temperaturethreshold: config.temperaturethreshold, temperaturethresholdposition: Number( defaultIfUndefined( config.temperaturethresholdposition, RED._("blindcontroller.placeholder.temperaturethresholdposition") ) ), cloudsthreshold: config.cloudsthreshold, cloudsthresholdposition: Number( defaultIfUndefined( config.cloudsthresholdposition, RED._("blindcontroller.placeholder.cloudsthresholdposition") ) ), uvindexthreshold: config.uvindexthreshold, uvindexthresholdposition: Number( defaultIfUndefined( config.uvindexthresholdposition, RED._("blindcontroller.placeholder.uvindexthresholdposition") ) ), nightposition: Number( defaultIfUndefined( config.nightposition, RED._("blindcontroller.placeholder.nightposition") ) ), expiryperiod: Number( defaultIfUndefined( config.expiryperiod, RED._("blindcontroller.placeholder.expiryperiod") ) ), opposite: convertToBoolean( defaultIfUndefined( config.opposite, RED._("blindcontroller.placeholder.opposite") ) ) }; this.blind = blinds[channel]; setBlinds(this, blinds); var node = this; var sunPosition = {}; var weather = {}; /* * Registers a listener on the input event to receive messages from the * up-stream nodes in a flow. This function does not any values from * the input message. */ var previousBlindPosition = -1; this.on("input", function(msg, send, done) { if (msg.topic == "blindPosition" || msg.topic == "blindPositionReset") { msg.payload.channel = defaultIfUndefined(msg.payload.channel, channel); } var validMsg = validateMsg(node, msg); if (validMsg) { switch (msg.topic) { case "sun": sunPosition = msg.payload; runCalc(node, send, msg, blinds, sunPosition, weather); break; case "blindPosition": setPosition(node, send, msg, blinds[msg.payload.channel]); break; case "blindPositionReset": resetPosition(blinds[msg.payload.channel]); break; case "weather": sunPosition = msg.payload; weather = msg.payload; runCalc(node, send, msg, blinds, sunPosition, weather); break; case "mode": var mode = msg.payload.mode; var i; for (i in blinds) { blinds[i].mode = mode; } runCalc(node, send, msg, blinds, sunPosition, weather); break; default: break; } setBlinds(this, blinds); node.status({ fill: blinds[channel].blindPositionReasonCode == "01" ? "red" : sunPosition.sunInSky ? "yellow" : "blue", shape: blinds[channel].logicalBlindPosition == 100 ? "dot" : "ring", text: blinds[channel].logicalBlindPosition + "%" }); } }); } RED.nodes.registerType("multiblindcontroller", BlindControllerWithoutConfig); RED.nodes.registerType("blindcontroller", BlindControllerWithConfig); };