UNPKG

homebridge-awattar

Version:

Plugin exposes virtual (switch, light, presence, temperature) accessories and enables HomeKit automation by aWattar electricity pricing in Austria.

395 lines 21.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Functions = void 0; const funcs_Elering_1 = require("./funcs_Elering"); const settings_1 = require("./settings"); const luxon_1 = require("luxon"); const asciichart = __importStar(require("asciichart")); const settings_2 = require("./settings"); class Functions { constructor(platform, accessory, service, api) { var _a, _b, _c, _d, _e; this.platform = platform; this.accessory = accessory; this.service = service; this.api = api; this.decimalPrecision = (_a = this.platform.config.decimalPrecision) !== null && _a !== void 0 ? _a : 1; this.excessivePriceMargin = (_b = this.platform.config.excessivePriceMargin) !== null && _b !== void 0 ? _b : 200; this.minPriciestMargin = (_c = this.platform.config.minPriciestMargin) !== null && _c !== void 0 ? _c : 0; this.plotTheChart = (_d = this.platform.config.plotTheChart) !== null && _d !== void 0 ? _d : false; this.dynamicCheapestConsecutiveHours = (_e = this.platform.config.dynamicCheapestConsecutiveHours) !== null && _e !== void 0 ? _e : false; this.pricesCache = (0, settings_2.defaultPricesCache)(this.api); } async initAccessories() { this.accessory.getService(this.platform.Service.AccessoryInformation) .setCharacteristic(this.platform.Characteristic.Manufacturer, settings_2.PLATFORM_MANUFACTURER) .setCharacteristic(this.platform.Characteristic.Model, settings_2.PLATFORM_MODEL) .setCharacteristic(this.platform.Characteristic.SerialNumber, settings_2.PLATFORM_SERIAL_NUMBER); // init light sensor for current price this.service.currently = this.accessory.getService('currentPrice') || this.accessory.addService(this.platform.Service.LightSensor, 'currentPrice', 'currentPrice'); // set default price level if (this.service.currently) { this.service.currently.getCharacteristic(this.platform.Characteristic.CurrentAmbientLightLevel) .updateValue(settings_2.pricing.currently); } // init light sensor for current price Negative this.service.currentlyNeg = this.accessory.getService('currentPriceNegative') || this.accessory.addService(this.platform.Service.LightSensor, 'currentPriceNegative', 'currentPriceNegative'); // set default price level Negative if (this.service.currentlyNeg) { this.service.currentlyNeg.getCharacteristic(this.platform.Characteristic.CurrentAmbientLightLevel) .updateValue(settings_2.pricing.currently); } // hourly ticker this.service.hourlyTickerSwitch = this.accessory.getService('hourlyTickerSwitch') || this.accessory.addService(this.platform.Service.Switch, 'hourlyTickerSwitch', 'hourlyTickerSwitch'); // current hour as temperature sensor if (this.platform.config['currentHour'] !== undefined && this.platform.config['currentHour']) { this.service.currentHour = this.accessory.getService('currentHour') || this.accessory.addService(this.platform.Service.TemperatureSensor, 'currentHour', 'currentHour'); if (this.service.currentHour) { this.service.currentHour.getCharacteristic(this.platform.Characteristic.CurrentTemperature) .updateValue((0, settings_2.fnc_currentHour)()); } } else { const currentHourService = this.accessory.getService('currentHour'); if (currentHourService !== undefined) { this.accessory.removeService(currentHourService); this.platform.log.debug('Accessory currentHour removed according to Plugin Config'); } } // turn OFF hourly ticker if its turned on by schedule or manually if (this.service.hourlyTickerSwitch) { this.service.hourlyTickerSwitch.getCharacteristic(this.platform.Characteristic.On) .on('set', (value, callback) => { if (value) { // If switch is manually turned on, start a timer to switch it back off after 1 second setTimeout(() => { this.service.hourlyTickerSwitch.updateCharacteristic(this.platform.Characteristic.On, false); }, 1000); } callback(null); }); } // init virtual occupancy sensors for price levels for (const key of Object.keys(this.service)) { if (/^(cheapest|priciest)/.test(key)) { const accessoryService = this.accessory.getService(`${key}`); if (this.platform.config[key] !== undefined && !this.platform.config[key]) { if (accessoryService !== undefined) { this.accessory.removeService(accessoryService); this.platform.log.debug(`Accessory ${key} removed according to Plugin Config`); } else { this.platform.log.debug(`Accessory ${key} skipped according to Plugin Config`); } continue; } this.service[key] = accessoryService || this.accessory.addService(this.platform.Service.OccupancySensor, `${key}`, key); if (this.service[key]) { this.service[key] .getCharacteristic(this.platform.Characteristic.OccupancyDetected) .setValue(this.platform.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED); } } } // make sure accessories cache on homebridge gets updated this.platform.api.updatePlatformAccessories([this.accessory]); } async pullNordpoolData() { // if (this.platform.config.area.match(/^(LT|LV|EE|FI|AT)$/) ) { return (0, funcs_Elering_1.eleringEE_getNordpoolData)(this.platform.log, this.platform.config); } } async checkSystemTimezone() { const systemTimezone = luxon_1.DateTime.local().toFormat('ZZ'); const preferredTimezone = luxon_1.DateTime.local().setZone(settings_2.defaultAreaTimezone).toFormat('ZZ'); if (systemTimezone !== preferredTimezone) { this.platform.log.warn(`WARN: System timezone ${systemTimezone} DOES NOT match with ${this.platform.config.area} area timezone ${preferredTimezone}.` + 'This may result in incorrect time-to-price coding. If possible, please update your system time setting to match timezone of ' + 'your specified area.'); } else { this.platform.log.debug(`OK: system timezone ${systemTimezone} match ${this.platform.config.area} area timezone ${preferredTimezone}`); } } applySolarOverride(config, force) { if (config.solarOverride === null || config.solarOverride === false) { return; } const today = luxon_1.DateTime.local(); if (today.month < 3 || today.month > 9) { this.platform.log.warn('Solar power plant override applies in March-September months only.'); return; } const todayKey = (0, settings_1.fnc_todayKey)(); if (!force && this.pricesCache.getSync(`solarOverrideApplied_${todayKey}`)) { return; } const latitude = config.latitude || 55; const daysDifference = today.diff(luxon_1.DateTime.fromObject({ year: today.year, month: 6, day: 24 }), 'days').days; const solarOffsetMinutes = Math.abs(daysDifference) * (1.6 + 0.04 * (latitude - 55)); const solarOverrideJuneHourStart = luxon_1.DateTime.fromObject({ hour: config.solarOverrideJuneHourStart }) .plus({ minutes: solarOffsetMinutes }); const solarOverrideJuneHourStartDecimal = Math.round(solarOverrideJuneHourStart.hour + solarOverrideJuneHourStart.minute / 60); // one hour added, to make configured value 'inclusive' const solarOverrideJuneHourEnd = luxon_1.DateTime.fromObject({ hour: config.solarOverrideJuneHourEnd + 1 }) .minus({ minutes: solarOffsetMinutes }); const solarOverrideJuneHourEndDecimal = Math.round(solarOverrideJuneHourEnd.hour + solarOverrideJuneHourEnd.minute / 60); if (solarOverrideJuneHourStartDecimal < solarOverrideJuneHourEndDecimal) { this.platform.log.debug(`solarOffsetMinutes: ${solarOffsetMinutes}`); this.platform.log.debug(`solarOverrideJuneHourStart: ${solarOverrideJuneHourStart.toJSON()}`); this.platform.log.debug(`solarOverrideJuneHourEnd: ${solarOverrideJuneHourEnd.toJSON()}`); this.platform.log.warn(`Hours from ${solarOverrideJuneHourStartDecimal} to ${solarOverrideJuneHourEndDecimal - 1} (inclusive) are overridden ` + 'price values to 0 because of solar plant settings.'); for (let i = solarOverrideJuneHourStartDecimal; i < solarOverrideJuneHourEndDecimal; i++) { settings_2.pricing.today[i].price = 0; } } this.pricesCache.set(todayKey, settings_2.pricing.today); this.pricesCache.set(`solarOverrideApplied_${todayKey}`, true); } getCheapestHoursToday() { // make sure these arrays are empty on each (new day) re-calculation for (const key of Object.keys(settings_2.pricing)) { if (!/^(cheapest|priciest|cheapest5HoursConsec)/.test(key)) { continue; } settings_2.pricing[key] = []; } const sortedPrices = [...settings_2.pricing.today].sort((a, b) => a.price - b.price); settings_2.pricing.median = parseFloat(((sortedPrices[Math.floor(sortedPrices.length / 2) - 1].price + sortedPrices[Math.ceil(sortedPrices.length / 2)].price) / 2).toFixed(this.decimalPrecision)); settings_2.pricing.today .map((price) => ({ value: price.price, hour: price.hour })) .forEach(({ value, hour }) => { if (value <= sortedPrices[0].price) { settings_2.pricing.cheapestHour.push(hour); } if (value <= sortedPrices[2].price) { settings_2.pricing.cheapest2Hours.push(hour); } if (value <= sortedPrices[2].price) { settings_2.pricing.cheapest3Hours.push(hour); } if (value <= sortedPrices[3].price) { settings_2.pricing.cheapest4Hours.push(hour); } if (value <= sortedPrices[4].price) { settings_2.pricing.cheapest5Hours.push(hour); } if (value <= sortedPrices[5].price) { settings_2.pricing.cheapest6Hours.push(hour); } if (value <= sortedPrices[6].price) { settings_2.pricing.cheapest7Hours.push(hour); } if (value <= sortedPrices[7].price) { settings_2.pricing.cheapest8Hours.push(hour); } if (value <= sortedPrices[8].price) { settings_2.pricing.cheapest9Hours.push(hour); } if (value <= sortedPrices[9].price) { settings_2.pricing.cheapest10Hours.push(hour); } if (value <= sortedPrices[10].price) { settings_2.pricing.cheapest11Hours.push(hour); } if (value <= sortedPrices[11].price) { settings_2.pricing.cheapest12Hours.push(hour); } // last element if ((value >= (sortedPrices[sortedPrices.length - 1].price * 0.9) || value >= settings_2.pricing.median * this.excessivePriceMargin / 100) && !settings_2.pricing.cheapest12Hours.includes(hour) && value > this.minPriciestMargin) { settings_2.pricing.priciestHour.push(hour); } }); this.platform.log.info(`Cheapest hour(s): ${settings_2.pricing.cheapestHour.join(', ')}`); for (let i = 4; i <= 12; i++) { const key = `cheapest${i}Hours`; if (this.platform.config[key] !== undefined && this.platform.config[key]) { this.platform.log.info(`${i} cheapest hours: ${settings_2.pricing[key].join(', ')}`); } } if (settings_2.pricing.priciestHour.length === 0) { this.platform.log.info(`Most expensive hour(s): N/A (all hours prices fall below ${this.minPriciestMargin} cents)`); } else { this.platform.log.info(`Most expensive hour(s): ${settings_2.pricing.priciestHour.join(', ')}`); } this.platform.log.info(`Median price today: ${settings_2.pricing.median} cents`); if (this.plotTheChart) { this.plotPricesChart().then().catch((error) => { this.platform.log.error('An error occurred plotting the chart for today\'s data: ', error); }); } } async getCheapestConsecutiveHours(numHours, pricesSequence) { // if not required on plugin config, just return empty if (this.platform.config['cheapest5HoursConsec'] !== undefined && !this.platform.config['cheapest5HoursConsec']) { return []; } // try cached from 2-days calculation - if not avail then calculate fresh let retVal = this.pricesCache.getSync('5consecutiveUpdated', []); if (retVal === undefined || retVal.length === 0) { const hourSequences = []; for (let i = 0; i <= pricesSequence.length - numHours; i++) { const totalSum = pricesSequence.slice(i, i + numHours).reduce((total, priceObj) => total + priceObj.price, 0); hourSequences.push({ startHour: i, total: totalSum }); } const cheapestHours = hourSequences.sort((a, b) => a.total - b.total)[0]; retVal = Array.from({ length: numHours }, (_, i) => pricesSequence[cheapestHours.startHour + i].hour); } this.platform.log.info(`Consecutive ${numHours} cheapest hours: ${retVal.join(', ')}`); return retVal; } async plotPricesChart() { const priceData = settings_2.pricing.today.map(elem => elem.price); const chart = asciichart.plot(priceData, { padding: ' ', // 6 spaces height: 9, }); const lines = chart.split('\n'); lines.forEach((line) => { this.platform.log.warn(line); }); } setOccupancyByHour(currentHour, accessoryName) { let characteristic = this.platform.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED; if (settings_2.pricing[accessoryName].includes(currentHour)) { characteristic = this.platform.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED; } const accessoryService = this.service[accessoryName]; if (accessoryService !== undefined && accessoryService !== null) { accessoryService.setCharacteristic(this.platform.Characteristic.OccupancyDetected, characteristic); } } async analyze_and_setServices(currentHour) { if (settings_2.pricing.today.length === 24 || settings_2.pricing.today.length === 23) { settings_2.pricing.currently = settings_2.pricing.today[currentHour]['price']; } else { this.platform.log.warn('WARN: Unable to determine current hour price because data not available'); return; } settings_2.pricing.currentHour = currentHour; if (this.service.currentHour) { this.service.currentHour.getCharacteristic(this.platform.Characteristic.CurrentTemperature) .updateValue(currentHour); } this.applySolarOverride(this.platform.config, false); // if new day or cheapest hours not calculated yet if (currentHour === 0 || settings_2.pricing.cheapest4Hours.length === 0) { this.getCheapestHoursToday(); } if (settings_2.pricing.cheapest5HoursConsec.length === 0 || currentHour === 0 || (currentHour === 7 && this.dynamicCheapestConsecutiveHours)) { this.getCheapestConsecutiveHours(5, settings_2.pricing.today).then((retVal) => { settings_2.pricing.cheapest5HoursConsec = retVal; this.setOccupancyByHour(currentHour, 'cheapest5HoursConsec'); }).catch((error) => { settings_2.pricing.cheapest5HoursConsec = []; // make sure its empty in case of error this.platform.log.error('An error occurred calculating cheapest 5 consecutive hours: ', error); }); } // set current price level on light sensor if (this.service.currently) { this.service.currently.getCharacteristic(this.platform.Characteristic.CurrentAmbientLightLevel) .updateValue(settings_2.pricing.currently >= 0.0001 ? settings_2.pricing.currently : 0.0001); } // set current price Negative level on light sensor if (this.service.currentlyNeg) { const currentNeg = settings_2.pricing.currently * -1; this.service.currentlyNeg.getCharacteristic(this.platform.Characteristic.CurrentAmbientLightLevel) .updateValue(currentNeg >= 0.0001 ? currentNeg : 0.0001); } // set price levels on relevant occupancy sensors for (const key of Object.keys(settings_2.pricing)) { if (!/^(cheapest|priciest)/.test(key)) { continue; } if (!this.service[key] || !Array.isArray(settings_2.pricing[key])) { continue; } this.setOccupancyByHour(currentHour, key); } this.platform.log.info(`Hour: ${currentHour}; Price: ${settings_2.pricing.currently} cents`); // toggle hourly ticker in 1s ON if (this.service.hourlyTickerSwitch) { setTimeout(() => { this.service.hourlyTickerSwitch.setCharacteristic(this.platform.Characteristic.On, true); }, 1000); } } async getCheapestHoursIn2days() { // make sure its not allowed to execute if not enabled on plugin config if (!this.dynamicCheapestConsecutiveHours) { return; } const tomorrowKey = (0, settings_2.fnc_tomorrowKey)(); const currentHour = (0, settings_2.fnc_currentHour)(); let tomorrow = []; tomorrow = this.pricesCache.getSync(tomorrowKey, []); let twoDaysPricing = []; // stop function if not full data if (settings_2.pricing.today.length !== 24 || tomorrow.length !== 24) { return; } const remainingHoursToday = Array.from({ length: Math.min(24 - currentHour, 24) }, (_, i) => currentHour + i); // Check if any of the remaining hours are within the cheapest consecutive hours if (settings_2.pricing.cheapest5HoursConsec.some(hour => remainingHoursToday.includes(hour))) { // from now till next day 6AM twoDaysPricing = settings_2.pricing.today.slice(currentHour, 24).concat(tomorrow.slice(0, 7)); } else { // do nothing, allow recalculate 0AM this.pricesCache.remove('5consecutiveUpdated'); return; } this.getCheapestConsecutiveHours(5, twoDaysPricing).then((retVal) => { settings_2.pricing.cheapest5HoursConsec = retVal; this.setOccupancyByHour(currentHour, 'cheapest5HoursConsec'); // ttl in seconds till next morning 7am const ttl = this.ttlSecondsTill_7AM(); this.pricesCache.set('5consecutiveUpdated', retVal, ttl); }).catch((error) => { this.platform.log.error('An error occurred calculating cheapest 5 consecutive hours: ', error); }); } ttlSecondsTill_7AM() { const now = luxon_1.DateTime.local(); let next7am = now.startOf('day').plus({ hours: 6, minutes: 59 }); if (now >= next7am) { next7am = next7am.plus({ days: 1 }); } return next7am.diff(now, 'seconds').seconds; } } exports.Functions = Functions; //# sourceMappingURL=functions.js.map