UNPKG

iobroker.tibberlink

Version:

links tibber API data to be used in ioBroker

589 lines 35.6 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 () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const utils = __importStar(require("@iobroker/adapter-core")); const cron_1 = require("cron"); const date_fns_1 = require("date-fns"); const tibberAPICaller_js_1 = require("./lib/tibberAPICaller.js"); const tibberCalculator_js_1 = require("./lib/tibberCalculator.js"); const tibberCharts_js_1 = require("./lib/tibberCharts.js"); const tibberLocal_js_1 = require("./lib/tibberLocal.js"); const tibberPulse_js_1 = require("./lib/tibberPulse.js"); class Tibberlink extends utils.Adapter { constructor(options = {}) { super({ ...options, name: "tibberlink", }); this.on("ready", this.onReady.bind(this)); this.on("stateChange", this.onStateChange.bind(this)); this.on("message", this.onMessage.bind(this)); this.on("unload", this.onUnload.bind(this)); this.homeInfoList = []; this.cronList = []; this.queryUrl = "https://api.tibber.com/v1-beta/gql"; } cronList; homeInfoList = []; queryUrl = ""; tibberCalculator = new tibberCalculator_js_1.TibberCalculator(this); tibberCharts = new tibberCharts_js_1.TibberCharts(this); tibberLocal = new tibberLocal_js_1.TibberLocal(this); async onReady() { if (!this.config.TibberAPIToken && !this.config.UseLocalPulseData) { this.log.error(`Missing API Token - please check configuration`); void this.setState(`info.connection`, false, true); } if (this.config.UseLocalPulseData) { try { this.log.info(`Setting up local poll of consumption data for ${this.config.PulseList.length} pulse module(s)`); this.config.PulseList.forEach((_pulse, index) => { this.tibberLocal.setupOnePulseLocal(index); }); } catch (error) { this.log.warn(`Error in setup of local Pulse data poll: ${error}`); } } if (this.config.TibberAPIToken) { const tibberConfigAPI = { active: true, apiEndpoint: { apiKey: this.config.TibberAPIToken, queryUrl: this.queryUrl, userAgent: `${this.config.TibberAPIToken.slice(5, 20).split("").reverse().join("")}${Date.now()}`, }, }; const tibberAPICaller = new tibberAPICaller_js_1.TibberAPICaller(tibberConfigAPI, this); try { this.homeInfoList = await tibberAPICaller.updateHomesFromAPI(); if (this.config.HomesList.length > 0) { if (this.homeInfoList.length > 0) { const result = []; for (const home of this.config.HomesList) { const matchingHomeInfo = this.homeInfoList.find(info => info.ID === home.homeID); if (!matchingHomeInfo) { this.log.error(`Configured feed for Home ID: ${home.homeID} not found in current data from Tibber server - delete the configuration line or verify any faults in your Tibber connection`); continue; } if (result.some(info => info.ID === matchingHomeInfo.ID)) { this.log.warn(`Double configuration of Home ID: ${home.homeID} found - please remove obsolete line in config - data of first instance will be used`); continue; } matchingHomeInfo.FeedActive = home.feedActive; matchingHomeInfo.PriceDataPollActive = home.priceDataPollActive; result.push(matchingHomeInfo); } for (const homeInfo of this.homeInfoList) { this.log.debug(`Feed Config for Home: ${homeInfo.NameInApp} (${homeInfo.ID}) - realtime data available: ${homeInfo.RealTime} - feed configured as active: ${homeInfo.FeedActive}`); this.log.debug(`Price Poll Config for Home: ${homeInfo.NameInApp} (${homeInfo.ID}) - poll configured as active: ${homeInfo.PriceDataPollActive}`); } } } else { this.log.warn(`No configuration of Tibber Pulse feeds found! Please configure to get live data - or configure your home(s) to discard live data`); } } catch (error) { this.log.error(tibberAPICaller.generateErrorMessage(error, `pull of homes from Tibber-Server`)); } if (this.config.HomesList?.every(info => !info.feedActive)) { if (this.homeInfoList.length > 0) { void this.setState("info.connection", true, true); this.log.debug(`Connection Check: Feed not enabled and I received home list from api - good connection`); } else { void this.setState("info.connection", false, true); this.log.debug(`Connection Check: Feed not enabled and I do not get home list from api - bad connection`); } } if (this.supportsFeature && this.supportsFeature("PLUGINS")) { const sentryInstance = this.getPluginInstance("sentry"); const pulseLocal = this.config.UseLocalPulseData ? 1 : 0; const last = await this.getStateAsync("info.LastSentryLogDay"); const lastDay = Number(last?.val) || 0; const today = new Date(); const todayDay = today.getDate(); const isMonthTransition = todayDay < lastDay; if ((!isMonthTransition && lastDay < todayDay + 3) || (isMonthTransition && todayDay + 30 - lastDay >= 3)) { this.tibberCalculator.updateCalculatorUsageStats(); if (sentryInstance) { const Sentry = sentryInstance.getSentryObject(); Sentry && Sentry.withScope((scope) => { scope.setLevel("info"); scope.setTag("SentryDay", todayDay); scope.setTag("HomeIDs", this.homeInfoList.length); scope.setTag("LocalPulse", pulseLocal); scope.setTag("numBestCost", this.tibberCalculator.numBestCost); scope.setTag("numBestCostLTF", this.tibberCalculator.numBestCostLTF); scope.setTag("numBestHoursBlock", this.tibberCalculator.numBestHoursBlock); scope.setTag("numBestHoursBlockLTF", this.tibberCalculator.numBestHoursBlockLTF); scope.setTag("numBestSingleHours", this.tibberCalculator.numBestSingleHours); scope.setTag("numBestSingleHoursLTF", this.tibberCalculator.numBestSingleHoursLTF); scope.setTag("numSmartBatteryBuffer", this.tibberCalculator.numSmartBatteryBuffer); scope.setTag("numBestPercentage", this.tibberCalculator.numBestPercentage); scope.setTag("numBestPercentageLTF", this.tibberCalculator.numBestPercentageLTF); Sentry.captureMessage("Adapter TibberLink started", "info"); }); } await this.setState("info.LastSentryLogDay", { val: todayDay, ack: true }); } } if (this.homeInfoList.length === 0) { this.log.warn(`Got no homes in your account - probably by a Tibber Server Error - adapter restarts in 5 minutes`); await this.delay(5 * 60000); this.restart(); } if (this.homeInfoList.length > 0) { const tibberCalculator = new tibberCalculator_js_1.TibberCalculator(this); if (this.config.UseCalculator) { try { this.log.info(`Setting up calculator states for ${this.config.CalculatorList.length} channels`); this.config.CalculatorList.forEach(async (channel, index) => { await tibberCalculator.setupCalculatorStates(channel.chHomeID, index); }); } catch (error) { this.log.warn(tibberAPICaller.generateErrorMessage(error, `setup of calculator states`)); } } await tibberAPICaller.updateCurrentPriceAllHomes(this.homeInfoList, true); void this.jobPricesTodayLOOP(tibberAPICaller); void this.jobPricesTomorrowLOOP(tibberAPICaller); void tibberCalculator.startCalculatorTasks(false, true); void tibberAPICaller.updateConsumptionAllHomes(); void this.tibberCharts.generateFlexChartJSONAllHomes(this.homeInfoList); const jobCurrentPrice = cron_1.CronJob.from({ cronTime: "20 58 * * * *", onTick: async () => { let okPrice = false; let attempt = 0; do { attempt++; await this.delay(this.getRandomDelay(2, 4)); okPrice = await tibberAPICaller.updateCurrentPriceAllHomes(this.homeInfoList); this.log.debug(`Cron job CurrentPrice - attempt ${attempt}, okPrice: ${okPrice}`); } while (!okPrice && attempt < 4); void tibberAPICaller.updateConsumptionAllHomes(); await tibberCalculator.startCalculatorTasks(); void this.tibberCharts.generateFlexChartJSONAllHomes(this.homeInfoList); }, start: true, timeZone: "system", runOnInit: false, }); if (jobCurrentPrice) { this.cronList.push(jobCurrentPrice); } const jobPricesToday = cron_1.CronJob.from({ cronTime: "20 56 23 * * *", onTick: async () => { let okPrice = false; let attempt = 0; do { attempt++; await this.delay(this.getRandomDelay(4, 6)); await tibberAPICaller.updatePricesTomorrowAllHomes(this.homeInfoList); okPrice = await tibberAPICaller.updatePricesTodayAllHomes(this.homeInfoList); this.log.debug(`Cron job PricesToday - attempt ${attempt}, okPrice: ${okPrice}`); } while (!okPrice && attempt < 10); void tibberCalculator.startCalculatorTasks(); void this.tibberCharts.generateFlexChartJSONAllHomes(this.homeInfoList); }, start: true, timeZone: "system", runOnInit: true, }); if (jobPricesToday) { this.cronList.push(jobPricesToday); } const jobPricesTomorrow = cron_1.CronJob.from({ cronTime: "20 56 12 * * *", onTick: async () => { let okPrice = false; let attempt = 0; do { attempt++; await this.delay(this.getRandomDelay(4, 6)); okPrice = await tibberAPICaller.updatePricesTomorrowAllHomes(this.homeInfoList); this.log.debug(`Cron job PricesTomorrow - attempt ${attempt}, okPrice: ${okPrice}`); } while (!okPrice && attempt < 8); void tibberCalculator.startCalculatorTasks(); void this.tibberCharts.generateFlexChartJSONAllHomes(this.homeInfoList); }, start: true, timeZone: "system", runOnInit: true, }); if (jobPricesTomorrow) { this.cronList.push(jobPricesTomorrow); } if (this.homeInfoList.some(info => info.FeedActive)) { const tibberFeedConfigs = Array.from({ length: this.homeInfoList.length }, () => { return { active: true, apiEndpoint: { apiKey: this.config.TibberAPIToken, queryUrl: this.queryUrl, userAgent: `${this.config.TibberAPIToken.slice(5, 20).split("").reverse().join("")}${Date.now()}`, }, timestamp: true, }; }); const tibberPulseInstances = new Array(this.homeInfoList.length); if (!this.homeInfoList.some(homeInfo => homeInfo.ID == `None available - restart adapter after entering token`)) { await this.delObjectAsync(`Homes.None available - restart adapter after entering token`, { recursive: true }); } this.homeInfoList.forEach((homeInfo, index) => { if (!homeInfo.ID || !homeInfo.RealTime) { this.log.warn(`skipping feed of live data - no Pulse configured for this home ${homeInfo.ID} according to Tibber server`); return; } this.log.debug(`Trying to establish feed of live data for home: ${homeInfo.ID}`); try { tibberFeedConfigs[index].homeId = homeInfo.ID; tibberFeedConfigs[index].power = true; tibberFeedConfigs[index].powerProduction = true; if (this.config.FeedConfigLastMeterConsumption) { tibberFeedConfigs[index].lastMeterConsumption = true; } if (this.config.FeedConfigAccumulatedConsumption) { tibberFeedConfigs[index].accumulatedConsumption = true; } if (this.config.FeedConfigAccumulatedProduction) { tibberFeedConfigs[index].accumulatedProduction = true; } if (this.config.FeedConfigAccumulatedConsumptionLastHour) { tibberFeedConfigs[index].accumulatedConsumptionLastHour = true; } if (this.config.FeedConfigAccumulatedProductionLastHour) { tibberFeedConfigs[index].accumulatedProductionLastHour = true; } if (this.config.FeedConfigAccumulatedCost) { tibberFeedConfigs[index].accumulatedCost = true; } if (this.config.FeedConfigAccumulatedCost) { tibberFeedConfigs[index].accumulatedReward = true; } if (this.config.FeedConfigCurrency) { tibberFeedConfigs[index].currency = true; } if (this.config.FeedConfigMinPower) { tibberFeedConfigs[index].minPower = true; } if (this.config.FeedConfigAveragePower) { tibberFeedConfigs[index].averagePower = true; } if (this.config.FeedConfigMaxPower) { tibberFeedConfigs[index].maxPower = true; } if (this.config.FeedConfigMinPowerProduction) { tibberFeedConfigs[index].minPowerProduction = true; } if (this.config.FeedConfigMaxPowerProduction) { tibberFeedConfigs[index].maxPowerProduction = true; } if (this.config.FeedConfigLastMeterProduction) { tibberFeedConfigs[index].lastMeterProduction = true; } if (this.config.FeedConfigPowerFactor) { tibberFeedConfigs[index].powerFactor = true; } if (this.config.FeedConfigVoltagePhase1) { tibberFeedConfigs[index].voltagePhase1 = true; } if (this.config.FeedConfigVoltagePhase2) { tibberFeedConfigs[index].voltagePhase2 = true; } if (this.config.FeedConfigVoltagePhase3) { tibberFeedConfigs[index].voltagePhase3 = true; } if (this.config.FeedConfigCurrentL1) { tibberFeedConfigs[index].currentL1 = true; } if (this.config.FeedConfigCurrentL2) { tibberFeedConfigs[index].currentL2 = true; } if (this.config.FeedConfigCurrentL3) { tibberFeedConfigs[index].currentL3 = true; } if (this.config.FeedConfigSignalStrength) { tibberFeedConfigs[index].signalStrength = true; } tibberPulseInstances[index] = new tibberPulse_js_1.TibberPulse(tibberFeedConfigs[index], this); tibberPulseInstances[index].connectPulseStream(); } catch (error) { this.log.warn(error.message); } }); } } } } async jobPricesTodayLOOP(tibberAPICaller) { let okPrice = false; let attempt = 0; do { attempt++; okPrice = await tibberAPICaller.updatePricesTodayAllHomes(this.homeInfoList, true); this.log.debug(`Loop job PricesToday - attempt ${attempt}, okPrice: ${okPrice}`); await this.delay(this.getRandomDelay(4, 6)); } while (!okPrice && attempt < 10); } async jobPricesTomorrowLOOP(tibberAPICaller) { let okPrice = false; let attempt = 0; do { attempt++; okPrice = await tibberAPICaller.updatePricesTomorrowAllHomes(this.homeInfoList, true); this.log.debug(`Loop job PricesTomorrow - attempt ${attempt}, okPrice: ${okPrice}`); await this.delay(this.getRandomDelay(4, 6)); } while (!okPrice && attempt < 8); } getRandomDelay = (minMinutes, maxMinutes) => { if (minMinutes >= maxMinutes) { throw new Error("minMinutes should be less than maxMinutes"); } const randomMinutes = Math.random() * (maxMinutes - minMinutes) + minMinutes; return Math.floor(randomMinutes * 60 * 1000); }; onMessage(obj) { if (obj) { switch (obj.command) { case "HomesForConfig": if (obj.callback) { try { if (this.homeInfoList.length > 0) { this.sendTo(obj.from, obj.command, this.homeInfoList.map(item => ({ label: `${item.NameInApp} (${item.ID})`, value: item.ID, })), obj.callback); } else { this.log.warn(`No Homes available to config TibberLink`); this.sendTo(obj.from, obj.command, [{ label: "None available", value: "None available" }], obj.callback); } } catch { this.sendTo(obj.from, obj.command, [{ label: "None available", value: "None available" }], obj.callback); } } break; case "HomesForCalculator": if (obj.callback) { try { if (this.homeInfoList.length > 0) { this.sendTo(obj.from, obj.command, this.homeInfoList.map(item => ({ label: `${item.NameInApp} (...${item.ID.slice(-8)})`, value: item.ID, })), obj.callback); } else { this.log.warn(`No Homes available to config TibberLink Calculator`); this.sendTo(obj.from, obj.command, [{ label: "None available", value: "None available" }], obj.callback); } } catch { this.sendTo(obj.from, obj.command, [{ label: "None available", value: "None available" }], obj.callback); } } break; } } } async onUnload(callback) { try { for (const cronJob of this.cronList) { await cronJob.stop(); } if (this.config.UseLocalPulseData) { this.tibberLocal.clearIntervals(); } await this.setState("info.connection", false, true); callback(); } catch (e) { this.log.warn(e.message); callback(); } } onStateChange(id, state) { try { if (state) { if (!state.ack) { this.log.debug(`state change detected and parsing for id: ${id} - state: ${state.val}`); if (id.includes(`.Calculations.`)) { const statePath = id.split("."); const homeIDToMatch = statePath[3]; const calcChannel = parseInt(statePath[5]); const settingType = statePath[6]; if (!isNaN(calcChannel) && calcChannel < this.config.CalculatorList.length && settingType !== undefined) { if (this.config.CalculatorList[calcChannel].chHomeID === homeIDToMatch) { switch (settingType) { case "Active": if (typeof state.val === "boolean") { this.config.CalculatorList[calcChannel].chActive = state.val; this.log.debug(`calculator settings state in home: ${homeIDToMatch} - channel: ${calcChannel} - changed to Active: ${this.config.CalculatorList[calcChannel].chActive}`); void this.setState(id, state.val, true); } else { this.log.warn(`Wrong type for channel: ${calcChannel} - chActive: ${state.val}`); } break; case "TriggerPrice": if (typeof state.val === "number") { this.config.CalculatorList[calcChannel].chTriggerPrice = state.val; this.log.debug(`calculator settings state in home: ${homeIDToMatch} - channel: ${calcChannel} - changed to TriggerPrice: ${this.config.CalculatorList[calcChannel].chTriggerPrice}`); void this.setState(id, state.val, true); } else { this.log.warn(`Wrong type for channel: ${calcChannel} - chTriggerPrice: ${state.val}`); } break; case "AmountHours": if (typeof state.val === "number") { this.config.CalculatorList[calcChannel].chAmountHours = state.val; this.log.debug(`calculator settings state in home: ${homeIDToMatch} - channel: ${calcChannel} - changed to AmountHours: ${this.config.CalculatorList[calcChannel].chAmountHours}`); void this.setState(id, state.val, true); } else { this.log.warn(`Wrong type for channel: ${calcChannel} - chAmountHours: ${state.val}`); } break; case "StartTime": if (typeof state.val === "string") { const iso8601RegEx = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})[.]\d{3}Z?([+-]\d{2}:\d{2})?$/; if (iso8601RegEx.test(state.val)) { const dateWithTimeZone = new Date(state.val); dateWithTimeZone.setMinutes(0, 0, 0); this.config.CalculatorList[calcChannel].chStartTime = dateWithTimeZone; this.log.debug(`calculator settings state in home: ${homeIDToMatch} - channel: ${calcChannel} - changed to StartTime: ${(0, date_fns_1.format)(dateWithTimeZone, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")}`); void this.setState(id, (0, date_fns_1.format)(dateWithTimeZone, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"), true); } else { this.log.warn(`Invalid ISO-8601 format or missing timezone offset for channel: ${calcChannel} - chStartTime: ${state.val}`); } } else { this.log.warn(`Wrong type for channel: ${calcChannel} - chStartTime: ${state.val}`); } break; case "StopTime": if (typeof state.val === "string") { const iso8601RegEx = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})[.]\d{3}Z?([+-]\d{2}:\d{2})?$/; if (iso8601RegEx.test(state.val)) { const dateWithTimeZone = new Date(state.val); dateWithTimeZone.setMinutes(0, 0, 0); this.config.CalculatorList[calcChannel].chStopTime = dateWithTimeZone; const startTime = this.config.CalculatorList[calcChannel].chStartTime; if (!(0, date_fns_1.isSameDay)(dateWithTimeZone, startTime) && !(0, date_fns_1.isSameDay)(dateWithTimeZone, (0, date_fns_1.addDays)(startTime, 1))) { this.log.warn(`StopTime for channel ${calcChannel} is not the same or next day as StartTime! StartTime: ${startTime.toISOString()}, StopTime: ${dateWithTimeZone.toISOString()}`); this.log.warn(`Setting StopTime outside the feasible range (same or next day as StartTime) can lead to errors in calculations or unexpected behavior. Please verify your configuration.`); } this.log.debug(`calculator settings state in home: ${homeIDToMatch} - channel: ${calcChannel} - changed to StopTime: ${(0, date_fns_1.format)(dateWithTimeZone, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")}`); void this.setState(id, (0, date_fns_1.format)(dateWithTimeZone, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"), true); } else { this.log.warn(`Invalid ISO-8601 format or missing timezone offset for channel: ${calcChannel} - chStopTime: ${state.val}`); } } else { this.log.warn(`Wrong type for channel: ${calcChannel} - chStopTime: ${state.val}`); } break; case "RepeatDays": if (typeof state.val === "number") { this.config.CalculatorList[calcChannel].chRepeatDays = state.val; this.log.debug(`calculator settings state in home: ${homeIDToMatch} - channel: ${calcChannel} - changed to RepeatDays: ${this.config.CalculatorList[calcChannel].chRepeatDays}`); void this.setState(id, state.val, true); } else { this.log.warn(`Wrong type for channel: ${calcChannel} - chRepeatDays: ${state.val}`); } break; case "EfficiencyLoss": if (typeof state.val === "number") { this.config.CalculatorList[calcChannel].chEfficiencyLoss = state.val; this.log.debug(`calculator settings state in home: ${homeIDToMatch} - channel: ${calcChannel} - changed to EfficiencyLoss: ${this.config.CalculatorList[calcChannel].chEfficiencyLoss}`); void this.setState(id, state.val, true); } else { this.log.warn(`Wrong type for channel: ${calcChannel} - chEfficiencyLoss: ${state.val}`); } break; case "Percentage": if (typeof state.val === "number") { this.config.CalculatorList[calcChannel].chPercentage = state.val; this.log.debug(`calculator settings state in home: ${homeIDToMatch} - channel: ${calcChannel} - changed to Percentage: ${this.config.CalculatorList[calcChannel].chPercentage}`); void this.setState(id, state.val, true); } else { this.log.warn(`Wrong type for channel: ${calcChannel} - chPercentage: ${state.val}`); } break; default: this.log.debug(`unknown value for setting type: ${settingType}`); } this.tibberCalculator .startCalculatorTasks(true) .then(() => this.tibberCharts.generateFlexChartJSONAllHomes(this.homeInfoList)) .catch(error => this.log.error(`unknown error calling tasks after parameter update: ${error}`)); } else { this.log.debug(`wrong index values in state ID or missing value for settingType`); } } } } } else { this.log.warn(`state ${id} deleted`); } } catch (e) { this.log.error(`Unhandled exception processing onstateChange: ${e}`); } } } if (require.main !== module) { module.exports = (options) => new Tibberlink(options); } else { (() => new Tibberlink())(); } //# sourceMappingURL=main.js.map