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
JavaScript
"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