/** * Javascript module for Nibe helper methods. * * Copyright (c) 2022 Markus Sipilä. * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */ /** * Exports. */ module.exports = { getForecastTemp: getForecastTemp, calculateNumberOfHours: calculateNumberOfHours, determineHours: determineHours }; /** * Reads forecasted temperatures from the database and calculates an average. * * @param Date start * Start of the time range. * @param Date stop * Stop of the time range. * * @return float * Average temperature for the range. */ function getForecastTemp(start, stop) { console.log('nibe.js: Calculating forecasted average temperature...'); influx = require('kolapuuntie/influx.js'); const csv = influx.getPoints('fmi_forecast_temperature', start, stop); const points = influx.parseCSV(csv, 5, 6); let sum = null; let avg = null; for (let i = 0; i < points.length; i++) { sum += points[i].value; } if (points.length) { avg = sum / points.length; } console.log('nibe.js: average temperature: ' + avg); return avg; } /** * Calculates number of needed hours for given average temperature. * * @param float temperature * Average temperateure for the day. * * @return float * Number of hours the heat pump should be allowed to run. */ function calculateNumberOfHours(temperature) { console.log('nibe.js: Calculating number of ON hours for Nibe...'); // Early exit if temperature is null. if (temperature == null) { console.warn('nibe.js: No temperature given! Number of needed hours defaulted to 24!'); return 24; } // Calculate curve based on two constant points. // y = kx + b // x = temperature, y = number of needed hours. const p1 = { x : -20, y : 22 }; const p2 = { x: 20, y: 2 } const k = (p1.y-p2.y) / (p1.x-p2.x); const b = p2.y - (k * p2.x); console.debug('nibe.js: y = ' + k + 'x + ' + b); let y = k * temperature + b; if (temperature < p1.x) { y = p1.y; } if (temperature > p2.x) { y = p2.y; } console.log('nibe.js: Number of needed hours: ' + y); return y; } /** * Calculates the on/off hours for the heatpump. * * @param Date start * Start of the time range. * @param Date stop * Stop of the time range. * @param int num * Number of hours the heat pump must be on. * @param int slices * Divide day into this many slices to balance heating during the day. * Caller is responsible for ensuring that slices is > 0 and < length of the time range. * Example: 2 would result in 2 x 12h slices if the range is 24 hours * @paramn float min * Minimum share of heating hours each slice must have. * Caller is responsible for ensuring that the value is a sensible float between 0 and 1. * Example: * Let's say 10h of heating is required and the day is split into 2 slices. * Value 0.1 (10%) means that both slices must have at least 0.1x10h = 1 hour of heating. * The minimum number of hours per slice is rounded down. * * @return array * Array of point objects. */ function determineHours(start, stop, num, slices, min) { console.log('nibe.js: Determining on/off hours for Nibe...'); let selectedHours = []; // Read the spot prices from the database and slice the day. const prices = influx.getPrices(start, stop); const priceSlices = slicePrices(prices, slices); // Pick min number of hours from each slice to the final array. const minPerSlice = Math.floor(num*min); console.debug('nibe.js: Minimum number of hours for each slice: ' + minPerSlice); for (let i = 0; i < priceSlices.length; i++) { for (let j = 0; j < minPerSlice; j++) { selectedHours.push(priceSlices[i][j]); priceSlices[i].splice(j, 1); // Removes price from the slice since it's used. num--; } } // Rest of the needed hours can be chosen freely based on the price. const merged = mergeSlices(priceSlices); const rest = merged.slice(0, num); selectedHours = selectedHours.concat(rest); const unselectedHours = merged.slice(num); // Prepare control points. const points = preparePoints(selectedHours, unselectedHours); return points; } /** * Slices the spot prices array. * * @param array prices * Array of spot price objects. * @param int slices * Number of slices the 'prices' array should be split. * * @return array * Array of sliced spot price arrays. Each slice is sorted by spot price. */ function slicePrices(prices, slices) { const priceSlices = []; const n = prices.length; const start = 0; const length = Math.ceil(n/slices); for (let i = 0; i < slices; i++) { let slice = prices.slice(i * length, (i+1) * length); // Sort slice by prices. slice.sort((a, b) => (a.value > b.value) ? 1 : -1); priceSlices.push(slice); } return priceSlices; } /** * Merges slices to one array. * * @param array priceSlices * * @return array * Merged array. */ function mergeSlices(priceSlices) { let merged = []; for (let i = 0; i < priceSlices.length; i++) { for (let j = 0; j < priceSlices[i].length; j++) { merged.push(priceSlices[i][j]); } } // Sort by prices. merged.sort((a, b) => (a.value > b.value) ? 1 : -1); return merged; } /** * Prepares the control points to be written to the database. * * @param array selectedHours * @param array unselectedHours * * @return array * Array of point objects. */ function preparePoints(selectedHours, unselectedHours) { let points = []; // Value 1 for the selected hours of the day. for (let i = 0; i < selectedHours.length; i++) { let point = { datetime: selectedHours[i].datetime, value: 1 } points.push(point); } // Value 0 for the unselected hours of the day. for (let i = 0; i < unselectedHours.length; i++) { let point = { datetime: unselectedHours[i].datetime, value: 0 } points.push(point); } // Sort by datetime points.sort((a, b) => (a.datetime > b.datetime) ? 1 : -1); return points; }