UNPKG

node-red-contrib-power-saver

Version:

A module for Node-RED that you can use to turn on and off a switch based on power prices

283 lines (266 loc) 9.38 kB
"use strict"; const cloneDeep = require("lodash.clonedeep"); const { DateTime } = require("luxon"); const expect = require("chai").expect; const helper = require("node-red-node-test-helper"); const node = require("../src/strategy-heat-capacitor.js"); const prices = require("./data/converted-prices.json"); const multiTrade = require("./data/multiple-trades.json"); const nanTest = require("./data/heat-capacitor-prices-NaN-test.json"); helper.init(require.resolve("node-red")); describe("ps-strategy-heat-capacitor node", function () { beforeEach(function (done) { helper.startServer(done); }); afterEach(function (done) { helper.unload().then(function () { helper.stopServer(done); }); }); it("should be loaded", function (done) { const flow = [{ id: "n1", type: "ps-strategy-heat-capacitor", name: "Temp. Adj." }]; helper.load(node, flow, function () { const n1 = helper.getNode("n1"); expect(n1).to.have.property("name", "Temp. Adj."); done(); }); }); it("should log error when illegal data is received", function (done) { const flow = [{ id: "n1", type: "ps-strategy-heat-capacitor", name: "Temp. Adj." }]; helper.load(node, flow, function () { const n1 = helper.getNode("n1"); n1.receive({}); n1.warn.should.be.calledWithExactly("No payload"); n1.receive({ payload: "Error" }); n1.warn.should.be.calledWithExactly("Payload is not an object"); n1.receive({ payload: [] }); n1.warn.should.be.calledWithExactly("Payload is missing priceData"); n1.receive({ payload: { priceData: [] } }); n1.warn.should.be.calledWithExactly("priceData is empty"); n1.receive({ payload: { priceData: { today: [], tomorrow: [] } } }); n1.warn.should.be.calledWithExactly("Illegal priceData in payload. Did you use the receive-price node?"); ["start", "value"].forEach((attr) => { const testData1 = cloneDeep(prices); delete testData1.priceData[3][attr]; n1.receive({ payload: testData1 }); n1.warn.should.be.calledWithExactly( "Malformed entries in priceData. All entries must contain start and value." ); }); n1.receive({ payload: cloneDeep(prices) }); n1.warn.should.not.be.called; done(); }); }); it("should be configurable", function (done) { const flow = [{ id: "n1", type: "ps-strategy-heat-capacitor", name: "Heat Capacitor" }]; helper.load(node, flow, function () { const n1 = helper.getNode("n1"); n1.receive({ payload: { config: { timeHeat1C: 1, timeCool1C: 2, setpoint: 3, maxTempAdjustment: 4, minSavings: 5, boostTempHeat: 6, boostTempCool: 7, }, }, }); expect(n1).to.have.property("timeHeat1C", 1); expect(n1).to.have.property("timeCool1C", 2); expect(n1).to.have.property("boostTempHeat", 6); expect(n1).to.have.property("boostTempCool", 7); expect(n1).to.have.property("setpoint", 3); expect(n1).to.have.property("maxTempAdjustment", 4); expect(n1).to.have.property("minSavings", 5); n1.receive({ payload: { config: { setpoint: 24 } } }); expect(n1).to.have.property("setpoint", 24); done(); }); }); it("should plan correctly", function (done) { const result = 0.5; const flow = makeFlow(); helper.load(node, flow, function () { const n1 = helper.getNode("n1"); const n2 = helper.getNode("n2"); const n3 = helper.getNode("n3"); let bothReceived = false; n2.on("input", function (msg) { expect(msg).to.have.deep.property("payload", 22.5); n1.warn.should.not.be.called; bothReceived ? done() : (bothReceived = true); }); n3.on("input", function (msg) { expect(msg).to.have.deep.property("payload", -0.5); n1.warn.should.not.be.called; bothReceived ? done() : (bothReceived = true); }); const time = DateTime.fromISO(prices.priceData[10].start); const p = cloneDeep(prices); p.time = time; n1.receive({ payload: p }); }); }); it("should plan correctly, multiTrade", function (done) { const result = 0.5; const flow = makeFlow(); helper.load(node, flow, function () { const n1 = helper.getNode("n1"); const n2 = helper.getNode("n2"); const n3 = helper.getNode("n3"); const n4 = helper.getNode("n4"); const n5 = helper.getNode("n5"); let bothReceived = false; n2.on("input", function (msg) { expect(msg).to.have.deep.property("payload", 24.5); n1.warn.should.not.be.called; bothReceived ? done() : (bothReceived = true); }); n3.on("input", function (msg) { expect(msg).to.have.deep.property("payload", 1.5); n1.warn.should.not.be.called; bothReceived ? done() : (bothReceived = true); }); n5.on("input", function (msg) { expect(msg).to.have.deep.property("payload.current_setpoint", 24.5); n1.warn.should.not.be.called; }); const time = DateTime.fromISO(multiTrade.priceData[4].start).plus({ minutes: 10 }); multiTrade.time = time; n1.receive({ payload: multiTrade }); }); }); it("should merge and trim priceData", function (done) { const flow = makeFlow(); helper.load(node, flow, function () { const n1 = helper.getNode("n1"); n1.receive({ payload: multiTrade }); n1.receive({ payload: prices }); expect(n1.priceData.length).to.equal(72); done(); }); }); it("should plan correctly, NaN test", function (done) { const result = 0.5; const flow = [ { id: "n1", type: "ps-strategy-heat-capacitor", name: "Temp. Adj.", timeHeat1C: 480, timeCool1C: 360, boostTempHeat: 2, boostTempCool: 2, setpoint: 20, maxTempAdjustment: 1, minSavings: 0.08, wires: [["n2"], ["n3"], ["n4"], ["n5"]], }, { id: "n2", type: "helper" }, { id: "n3", type: "helper" }, { id: "n4", type: "helper" }, { id: "n5", type: "helper" }, ]; helper.load(node, flow, function () { const n1 = helper.getNode("n1"); const n2 = helper.getNode("n2"); const n3 = helper.getNode("n3"); const n4 = helper.getNode("n4"); const n5 = helper.getNode("n5"); let bothReceived = false; n2.on("input", function (msg) { expect(msg).to.have.deep.property("payload", 17); n1.warn.should.not.be.called; bothReceived ? done() : (bothReceived = true); }); n3.on("input", function (msg) { expect(msg).to.have.deep.property("payload", -3); n1.warn.should.not.be.called; bothReceived ? done() : (bothReceived = true); }); n5.on("input", function (msg) { expect(msg).to.have.deep.property("payload.current_setpoint", 17); n1.warn.should.not.be.called; }); const time = DateTime.fromISO("2022-12-06T10:51:48.126+01:00"); nanTest.time = time; n1.receive({ payload: nanTest }); }); }); it("should support dynamic commands", function (done) { const result = 0.5; const flow = makeFlow(); helper.load(node, flow, function () { const n1 = helper.getNode("n1"); const n2 = helper.getNode("n2"); const n3 = helper.getNode("n3"); const n4 = helper.getNode("n4"); const n5 = helper.getNode("n5"); const numInputs = [0, 0, 0, 0]; const expectedNumInputs = [2, 2, 2, 2]; function testNumInputs() { if (numInputs.every((e, i) => e === expectedNumInputs[i])) { done(); } } n2.on("input", function (msg) { numInputs[0]++; testNumInputs(); }); n3.on("input", function (msg) { numInputs[1]++; testNumInputs(); }); n4.on("input", function (msg) { numInputs[2]++; testNumInputs(); }); n5.on("input", function (msg) { numInputs[3]++; testNumInputs(); }); const time = DateTime.fromISO(multiTrade.priceData[4].start).plus({ minutes: 10 }); const outputCommand = { payload: { commands: { sendOutput: true } } }; const scheduleCommand = { payload: { commands: { sendSchedule: true } } }; multiTrade.time = time; n1.receive({ payload: multiTrade }); n1.receive(outputCommand); n1.receive(scheduleCommand); }); }); }); function makeFlow() { return [ { id: "n1", type: "ps-strategy-heat-capacitor", name: "Temp. Adj.", timeHeat1C: 60, timeCool1C: 50, boostTempHeat: 1, boostTempCool: 1, setpoint: 23, maxTempAdjustment: 0.5, minSavings: 0.08, wires: [["n2"], ["n3"], ["n4"], ["n5"]], }, { id: "n2", type: "helper" }, { id: "n3", type: "helper" }, { id: "n4", type: "helper" }, { id: "n5", type: "helper" }, ]; } function makePayload(prices, time) { const payload = cloneDeep(prices); payload.time = time; let entryTime = DateTime.fromISO(payload.time); payload.priceData.forEach((e) => { e.start = entryTime.toISO(); entryTime = entryTime.plus({ milliseconds: 10 }); }); return payload; }