ramp-thermostat.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. "use strict";
  2. var fs = require('fs');
  3. var path = require('path');
  4. module.exports = function(RED) {
  5. function Profile(n) {
  6. RED.nodes.createNode(this,n);
  7. this.n = n;
  8. this.name = n.name;
  9. }
  10. RED.nodes.registerType("profile",Profile);
  11. function RampThermostat(config) {
  12. RED.nodes.createNode(this, config);
  13. //this.warn(node_name+" - "+JSON.stringify(this));
  14. var node_name;
  15. if (typeof this.name !== "undefined" ) {
  16. node_name = this.name.replace(/\s/g, "_");
  17. } else {
  18. node_name = this.id;
  19. }
  20. var globalContext = this.context().global;
  21. this.current_status = {};
  22. this.points_result = {};
  23. this.h_plus = Math.abs(parseFloat(config.hysteresisplus)) || 0;
  24. this.h_minus = Math.abs(parseFloat(config.hysteresisminus)) || 0;
  25. this.ramp_limit = Math.abs(parseInt(config.ramplimit)) || 1440;
  26. // experimental
  27. //this.profile = globalContext.get(node_name);
  28. //if (typeof this.profile === "undefined") {
  29. this.profile = RED.nodes.getNode(config.profile);
  30. this.points_result = getPoints(this.profile.n);
  31. if (this.points_result.isValid) {
  32. this.profile.points = this.points_result.points;
  33. } else {
  34. this.warn("Profile temperature not numeric");
  35. }
  36. globalContext.set(node_name, this.profile);
  37. //}
  38. this.current_status = {fill:"green",shape:"dot",text:"profile set to "+this.profile.name};
  39. this.status(this.current_status);
  40. //this.warn(node_name+" - "+JSON.stringify(this.profile));
  41. this.on('input', function(msg) {
  42. var msg1 = {"topic":"state"};
  43. var msg2 = {"topic":"current"};
  44. var msg3 = {"topic":"target"};
  45. var result = {};
  46. //this.warn(JSON.stringify(msg));
  47. if (typeof msg.payload === "undefined") {
  48. this.warn("msg.payload undefined");
  49. } else {
  50. switch (msg.topic) {
  51. case "setCurrent":
  52. case "setcurrent":
  53. case "":
  54. case undefined:
  55. if (typeof msg.payload === "string") {
  56. msg.payload = parseFloat(msg.payload);
  57. }
  58. if (isNaN(msg.payload)) {
  59. this.warn("Non numeric input");
  60. } else {
  61. result = this.getState(msg.payload, this.profile);
  62. if (result.state !== null) {
  63. msg1.payload = result.state;
  64. } else {
  65. msg1 = null;
  66. }
  67. msg2.payload = msg.payload;
  68. msg3.payload = result.target;
  69. this.send([msg1, msg2, msg3]);
  70. this.current_status = result.status;
  71. this.status(this.current_status);
  72. }
  73. break;
  74. case "setTarget":
  75. case "settarget":
  76. result = setTarget(msg.payload);
  77. if (result.isValid) {
  78. this.profile = result.profile;
  79. globalContext.set(node_name, this.profile);
  80. this.current_status = result.status;
  81. this.status(this.current_status);
  82. }
  83. break;
  84. case "setProfile":
  85. case "setprofile":
  86. //this.warn(JSON.stringify(msg.payload));
  87. result = setProfile(msg.payload);
  88. if (result.found) {
  89. this.profile = result.profile;
  90. if (this.profile.name === "default") {
  91. this.profile = RED.nodes.getNode(config.profile);
  92. this.points_result = getPoints(this.profile.n);
  93. if (this.points_result.isValid) {
  94. this.profile.points = this.points_result.points;
  95. } else {
  96. this.warn("Profile temperature not numeric.");
  97. }
  98. //this.warn("default "+this.profile.name);
  99. this.current_status = {fill:"green",shape:"dot",text:"profile set to default ("+this.profile.name+")"};
  100. } else {
  101. this.current_status = result.status;
  102. }
  103. globalContext.set(node_name, this.profile);
  104. } else {
  105. this.current_status = result.status;
  106. this.warn(msg.payload+" not found");
  107. }
  108. this.status(this.current_status);
  109. break;
  110. case "setHysteresisPlus":
  111. case "sethysteresisplus":
  112. result = setHysteresisPlus(msg.payload);
  113. if (result.isValid) {
  114. this.h_plus = result.hysteresis_plus;
  115. }
  116. this.status(result.status);
  117. break;
  118. case "setHysteresisMinus":
  119. case "sethysteresisminus":
  120. result = setHysteresisMinus(msg.payload);
  121. if (result.isValid) {
  122. this.h_minus = Math.abs(result.hysteresis_minus);
  123. }
  124. this.status(result.status);
  125. break;
  126. case "checkUpdate":
  127. case "checkupdate":
  128. var version = readNodeVersion();
  129. var pck_name = "node-red-contrib-ramp-thermostat";
  130. read_npmVersion(pck_name, function(npm_version) {
  131. if (npm_version > version) {
  132. this.warn("A new "+pck_name+" version "+npm_version+" is avaiable.");
  133. } else {
  134. this.warn(pck_name+" "+version+" is up to date.");
  135. }
  136. }.bind(this));
  137. this.warn("ramp-thermostat version: "+version);
  138. this.status({fill:"green",shape:"dot",text:"version: "+version});
  139. var set_timeout = setTimeout(function() {
  140. this.status(this.current_status);
  141. }.bind(this), 4000);
  142. break;
  143. default:
  144. this.warn("invalid topic >"+msg.topic+"< - set msg.topic to e.g. 'setCurrent'");
  145. }
  146. }
  147. });
  148. }
  149. RED.nodes.registerType("ramp-thermostat",RampThermostat);
  150. /**
  151. * ramp-thermostat specific functions
  152. **/
  153. RampThermostat.prototype.getState = function(current, profile) {
  154. var point_mins, pre_mins, pre_target, point_target, target, gradient;
  155. var state;
  156. var status = {};
  157. current = parseFloat((current).toFixed(1));
  158. var date = new Date();
  159. var current_mins = date.getHours()*60 + date.getMinutes();
  160. // Objects do no guarentee order, lets sort it
  161. var points_arr = Object.keys(profile.points)
  162. .map(key=>{return profile.points[key]})
  163. .sort( (a,b)=> { return a.m - b.m });
  164. //console.log("name " + profile.name + " profile.points " + JSON.stringify(profile.points));
  165. for( var k=0; k<points_arr.length; k++ ) {
  166. point_mins = points_arr[k].m;
  167. //console.log("mins " + point_mins + " temp " + profile.points[k]);
  168. target = point_target = points_arr[k].t;
  169. if( points_arr.length == 1 ) {
  170. // no need to go any further
  171. break;
  172. }
  173. if (current_mins < point_mins) {
  174. if( point_mins - current_mins > this.ramp_limit ) {
  175. // Not yet in window to start ramping thermostat, stay constant
  176. target = pre_target;
  177. break;
  178. }
  179. if( point_mins - pre_mins > this.ramp_limit && point_mins - current_mins <= this.ramp_limit ) {
  180. // bring the previous point forward to match ramp max
  181. pre_mins = point_mins - this.ramp_limit;
  182. }
  183. gradient = (point_target - pre_target) / (point_mins - pre_mins);
  184. target = pre_target + (gradient * (current_mins - pre_mins));
  185. //this.warn("k=" + k +" gradient " + gradient + " target " + target);
  186. break;
  187. }
  188. pre_mins = point_mins;
  189. pre_target = point_target;
  190. }
  191. if(isNaN(target)) {
  192. this.warn("target undefined");
  193. return {"state":null, "target":0, "status": {fill:"red",shape:"dot",text:"No points in profile"}};
  194. }
  195. var target_plus = parseFloat((target + this.h_plus).toFixed(1));
  196. var target_minus = parseFloat((target - this.h_minus).toFixed(1));
  197. //this.warn(target_minus+" - "+target+" - "+target_plus);
  198. if (current > target_plus) {
  199. state = false;
  200. status = {fill:"grey",shape:"ring",text:current+" > "+target_plus+" ("+profile.name+")"};
  201. } else if (current < target_minus) {
  202. state = true;
  203. status = {fill:"yellow",shape:"dot",text:current+" < "+target_minus+" ("+profile.name+")"};
  204. } else if (current == target_plus) {
  205. state = null;
  206. status = {fill:"grey",shape:"ring",text:current+" = "+target_plus+" ("+profile.name+")"};
  207. } else if (current == target_minus) {
  208. state = null;
  209. status = {fill:"grey",shape:"ring",text:current+" = "+target_minus+" ("+profile.name+")"};
  210. } else {
  211. state = null;
  212. status = {fill:"grey",shape:"ring",text:target_minus+" < "+current+" < "+target_plus+" ("+profile.name+")"};
  213. }
  214. return {"state":state, "target":parseFloat(target.toFixed(1)), "status":status};
  215. }
  216. function setTarget(target) {
  217. var valid;
  218. var status = {};
  219. var profile = {};
  220. if (typeof target === "string") {
  221. target = parseFloat(target);
  222. }
  223. if (typeof target === "number") {
  224. profile.name = "manual";
  225. profile.points = {"1":{"m":0,"t":target},"2":{"m":1440,"t":target}};
  226. valid = true;
  227. status = {fill:"green",shape:"dot",text:"set target to "+target+" ("+profile.name+")"};
  228. } else {
  229. valid = false;
  230. status = {fill:"red",shape:"dot",text:"invalid type of target"};
  231. }
  232. return {"profile":profile, "status":status, "isValid": valid};
  233. }
  234. function setProfile(input) {
  235. var found = false;
  236. var status = {};
  237. var profile = {};
  238. var result = {};
  239. //var count = 0;
  240. var type = typeof input;
  241. switch (type) {
  242. case "string":
  243. if (input === "default") {
  244. profile.name = "default";
  245. found = true;
  246. } else {
  247. RED.nodes.eachNode(function(n) {
  248. if (n.type === "profile" && n.name === input) {
  249. profile.n = n;
  250. profile.name = n.name;
  251. result = getPoints(profile.n);
  252. if (result.isValid) {
  253. profile.points = result.points;
  254. } else {
  255. this.warn("Profile temperature not numeric");
  256. }
  257. found = true;
  258. }
  259. //count++;
  260. });
  261. //console.log("count " + count);
  262. if (found) {
  263. status = {fill:"green",shape:"dot",text:"profile set to "+profile.name};
  264. } else {
  265. status = {fill:"red",shape:"dot",text:input+" not found"};
  266. }
  267. }
  268. break;
  269. case "object":
  270. profile.name = input.name || "input profile";
  271. var points = {};
  272. var arr, minutes;
  273. var i = 1;
  274. for (var k in input.points) {
  275. arr = k.split(":");
  276. minutes = parseInt(arr[0])*60 + parseInt(arr[1]);
  277. points[i] = JSON.parse('{"m":' + minutes + ',"t":' + input.points[k] + '}');
  278. //points[minutes] = input.points[k];
  279. i++;
  280. }
  281. profile.points = points;
  282. found = true;
  283. status = {fill:"green",shape:"dot",text:"profile set to "+profile.name};
  284. //console.log(points);
  285. break;
  286. default:
  287. status = {fill:"red",shape:"dot",text:"invalid type "+type};
  288. }
  289. return {"profile":profile, "status":status, "found":found};
  290. }
  291. function setHysteresisPlus(hp) {
  292. var valid;
  293. var status = {};
  294. if (typeof hp === "string") {
  295. hp = parseFloat(hp);
  296. }
  297. if (typeof hp === "number") {
  298. status = {fill:"green",shape:"dot",text:"hysteresis [+] set to "+hp};
  299. valid = true;
  300. } else {
  301. valid = false;
  302. status = {fill:"red",shape:"dot",text:"invalid type of hysteresis [+]"};
  303. }
  304. return {"hysteresis_plus":hp, "status":status, "isValid":valid};
  305. }
  306. function setHysteresisMinus(hm) {
  307. var valid;
  308. var status = {};
  309. if (typeof hm === "string") {
  310. hm = parseFloat(hm);
  311. }
  312. if (typeof hm === "number") {
  313. status = {fill:"green",shape:"dot",text:"hysteresis [-] set to "+hm};
  314. valid = true;
  315. } else {
  316. valid = false;
  317. status = {fill:"red",shape:"dot",text:"invalid type of hysteresis [-]"};
  318. }
  319. return {"hysteresis_minus":hm, "status":status, "isValid":valid};
  320. }
  321. function getPoints(n) {
  322. var timei, tempi, arr, minutes;
  323. var points = {};
  324. var valid = true;
  325. var points_str = '{';
  326. for (var i=1; i<=10; i++) {
  327. timei = "time"+i;
  328. tempi = "temp"+i;
  329. if (isNaN(n[tempi])) {
  330. valid = false;
  331. } else {
  332. if (typeof(n[timei]) !== "undefined" && n[timei] !== "") {
  333. arr = n[timei].split(":");
  334. minutes = parseInt(arr[0])*60 + parseInt(arr[1]);
  335. points_str += '"' + i + '":{"m":' + minutes + ',"t":' + n[tempi] + '},';
  336. }
  337. }
  338. }
  339. if (valid) {
  340. points_str = points_str.slice(0,points_str.length-1);
  341. points_str += '}';
  342. //console.log(points_str);
  343. points = JSON.parse(points_str);
  344. //console.log(JSON.stringify(points));
  345. }
  346. return {"points":points, "isValid":valid};
  347. }
  348. function readNodeVersion () {
  349. var package_json = "../package.json";
  350. //console.log(__dirname+" - "+package_json);
  351. var packageJSONPath = path.join(__dirname, package_json);
  352. var packageJSON = JSON.parse(fs.readFileSync(packageJSONPath));
  353. return packageJSON.version;
  354. }
  355. function read_npmVersion(pck, callback) {
  356. var exec = require('child_process').exec;
  357. var cmd = 'npm view '+pck+' version';
  358. var npm_version;
  359. exec(cmd, function(error, stdout, stderr) {
  360. npm_version = stdout.trim();
  361. callback(npm_version);
  362. //console.log("npm_version "+npm_version);
  363. });
  364. }
  365. }