tibber-api
Version:
Node.js module for connecting to Tibber API and extract data from your connected homes, including realtime data from Tibber Pulse.
796 lines • 30.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TibberFeed = void 0;
const events_1 = require("events");
const ws_1 = __importDefault(require("ws"));
const GQL_1 = require("./models/GQL");
const TibberQueryBase_1 = require("./TibberQueryBase");
const HeaderManager_1 = require("../tools/HeaderManager");
class TibberFeed extends events_1.EventEmitter {
/// <summary>
/// Number of timeouts that have been created.
/// </summary>
get timeoutCount() {
return this._timeoutCount;
}
/// <summary>
/// The number of connection attempts that can be made before a hard reset is performed.
/// This is used to prevent the feed from trying to connect indefinitely.
/// The default value is 10.
/// You can tune this value to your needs.
/// </summary>
get maxFailedConnectionAttempts() {
return this._maxFailedConnectionAttempts;
}
set maxFailedConnectionAttempts(value) {
this._maxFailedConnectionAttempts = value;
}
/**
* Constructor for creating a new instance if TibberFeed.
* @constructor
* @param {TibberQueryBase} tibberQuery TibberQueryBase object.
* @param {number} timeout Feed idle timeout in milliseconds. The feed will reconnect after being idle for more than the specified number of milliseconds. Min 5000 ms.
* @param {boolean} returnAllFields Specify if you want to return all fields from the data feed.
* @param {number} connectionTimeout Feed connection timeout.
* @see {@linkcode TibberQueryBase}
*/
constructor(tibberQuery, timeout = 60000, returnAllFields = false, connectionTimeout = 30000, autoReconnect = false, webSocketFactory) {
super();
this._operationId = 0;
this._failedAttempts = 0;
this._timeouts = new Map();
this._rateLimitUntil = 0; // timestamp until which we should not reconnect
this._autoReconnect = false;
if (!tibberQuery || !(tibberQuery instanceof TibberQueryBase_1.TibberQueryBase)) {
throw new Error('Missing mandatory parameter [tibberQuery]');
}
this._webSocketFactory = webSocketFactory;
this._feedConnectionTimeout = connectionTimeout > 5000 ? connectionTimeout : 5000;
this._feedIdleTimeout = timeout > 5000 ? timeout : 5000;
this._tibberQuery = tibberQuery;
this._config = tibberQuery.config;
this._headerManager = new HeaderManager_1.HeaderManager(this._config);
this._active = this._config.active;
this._isConnected = false;
this._isConnecting = false;
this._isClosing = false;
this._isUnauthenticated = false;
this._gql = '';
this._realTimeConsumptionEnabled = null;
this._lastRetry = 0;
this._connectionAttempts = 0;
this._backoffDelayBase = 1000; // 1 second
this._backoffDelayMax = 1000 * 60 * 10; // 10 minutes
this._retryBackoff = 1000;
this._timeoutCount = 0;
this._maxFailedConnectionAttempts = 12;
this._lastConnectedAt = 0;
this._autoReconnect = autoReconnect;
const { apiEndpoint, homeId } = this._config;
if (!apiEndpoint || !apiEndpoint.apiKey || !homeId) {
this._active = false;
this._config.active = false;
this.warn('Missing mandatory parameters. Execution will halt.');
return;
}
this._gql = 'subscription($homeId:ID!){liveMeasurement(homeId:$homeId){';
if (this._config.timestamp || returnAllFields) {
this._gql += 'timestamp ';
}
if (this._config.power || returnAllFields) {
this._gql += 'power ';
}
if (this._config.lastMeterConsumption || returnAllFields) {
this._gql += 'lastMeterConsumption ';
}
if (this._config.accumulatedConsumption || returnAllFields) {
this._gql += 'accumulatedConsumption ';
}
if (this._config.accumulatedProduction || returnAllFields) {
this._gql += 'accumulatedProduction ';
}
if (this._config.accumulatedConsumptionLastHour || returnAllFields) {
this._gql += 'accumulatedConsumptionLastHour ';
}
if (this._config.accumulatedProductionLastHour || returnAllFields) {
this._gql += 'accumulatedProductionLastHour ';
}
if (this._config.accumulatedCost || returnAllFields) {
this._gql += 'accumulatedCost ';
}
if (this._config.accumulatedReward || returnAllFields) {
this._gql += 'accumulatedReward ';
}
if (this._config.currency || returnAllFields) {
this._gql += 'currency ';
}
if (this._config.minPower || returnAllFields) {
this._gql += 'minPower ';
}
if (this._config.averagePower || returnAllFields) {
this._gql += 'averagePower ';
}
if (this._config.maxPower || returnAllFields) {
this._gql += 'maxPower ';
}
if (this._config.powerProduction || returnAllFields) {
this._gql += 'powerProduction ';
}
if (this._config.minPowerProduction || returnAllFields) {
this._gql += 'minPowerProduction ';
}
if (this._config.maxPowerProduction || returnAllFields) {
this._gql += 'maxPowerProduction ';
}
if (this._config.lastMeterProduction || returnAllFields) {
this._gql += 'lastMeterProduction ';
}
if (this._config.powerFactor || returnAllFields) {
this._gql += 'powerFactor ';
}
if (this._config.voltagePhase1 || returnAllFields) {
this._gql += 'voltagePhase1 ';
}
if (this._config.voltagePhase2 || returnAllFields) {
this._gql += 'voltagePhase2 ';
}
if (this._config.voltagePhase3 || returnAllFields) {
this._gql += 'voltagePhase3 ';
}
if (this._config.currentL1 || returnAllFields) {
this._gql += 'currentL1 ';
}
if (this._config.currentL2 || returnAllFields) {
this._gql += 'currentL2 ';
}
if (this._config.currentL3 || returnAllFields) {
this._gql += 'currentL3 ';
}
if (this._config.signalStrength || returnAllFields) {
this._gql += 'signalStrength ';
}
this._gql += '}}';
}
get active() {
return this._active;
}
set active(value) {
if (value === this._active) {
if (!this._active)
this.close();
return;
}
this._active = value;
if (this._active) {
this.connectWithDelayWorker(1);
}
else {
this.close();
}
}
get connected() {
return this._isConnected;
}
get feedIdleTimeout() {
return this._feedIdleTimeout;
}
set feedIdleTimeout(value) {
if (value === this._feedIdleTimeout) {
return;
}
this._feedIdleTimeout = value;
}
get feedConnectionTimeout() {
return this._feedConnectionTimeout;
}
set feedConnectionTimeout(value) {
if (value === this._feedConnectionTimeout) {
return;
}
this._feedConnectionTimeout = value;
}
get queryRequestTimeout() {
var _a;
return (_a = this._tibberQuery) === null || _a === void 0 ? void 0 : _a.requestTimeout;
}
set queryRequestTimeout(value) {
var _a;
if (value === ((_a = this._tibberQuery) === null || _a === void 0 ? void 0 : _a.requestTimeout)) {
return;
}
if (this._tibberQuery)
this._tibberQuery.requestTimeout = value;
}
get config() {
return this._tibberQuery.config;
}
set config(value) {
this._tibberQuery.config = value;
}
get canConnect() {
return Date.now() > (this._lastRetry + this._retryBackoff);
}
/**
* PUBLIC METHODS
* ----------------
* These methods are used to interact with the TibberFeed.
* ----------------
* */
/**
* Connect to Tibber feed.
*/
async connect() {
this.connectWithDelayWorker(1);
}
/**
* Close the Tibber feed.
*/
close() {
this._isClosing = true;
this.cancelTimeout('heartbeat');
this.cancelTimeout('connect');
this.cancelTimeout('connection_timeout');
if (this._webSocket) {
if (this._isConnected && this._webSocket.readyState === ws_1.default.OPEN) {
this.closeConnection();
}
}
this._isClosing = false;
this.log('Closed Tibber Feed.');
}
/**
* Heartbeat function used to keep connection alive.
* Mostly for internal use, even if it is public.
*/
heartbeat() {
if (this._isClosing)
return;
this.cancelTimeout('heartbeat');
this.addTimeout('heartbeat', () => {
if (this._webSocket) {
this.terminateConnection();
}
this.warn(`Connection timed out after ${this._feedIdleTimeout} ms.`);
this.emit('heartbeat_timeout', { timeout: this._feedIdleTimeout });
if (this._active) {
this.emit('heartbeat_reconnect', { connection_attempt: this._connectionAttempts, backoff: this._retryBackoff });
this.updateBackoff();
this.connectWithDelayWorker(this._retryBackoff);
}
}, this._feedIdleTimeout);
}
/**
* PRIVATE METHODS
* ----------------
* These methods are used internally by the TibberFeed.
* ----------------
* */
/**
* Generate random number with a max value.
* @param {number} max Maximum number
* @returns {number} Random number.
*/
getRandomInt(max) {
return Math.floor(Math.random() * max);
}
/**
* Exponential backoff with jitter
* @param {number} attempt Connection attempt
* @returns {number}
*/
getBackoffWithJitter(attempt) {
const exponential = Math.min(Math.pow(2, attempt) * this._backoffDelayBase, this._backoffDelayMax);
return exponential / 2 + this.getRandomInt(exponential / 2);
}
/**
* Decreases the connection backoff.
*/
decreaseConnectionBackoff() {
if (this._connectionAttempts > 0) {
this._connectionAttempts--;
this._retryBackoff = this.getBackoffWithJitter(this._connectionAttempts);
}
}
/**
* addTimeout
* Adds a timeout to the list of timeouts.
* @param {string} name Name of the timeout.
* @param {() => void} fn Function to call when timeout is reached.
* @param {number} ms Delay in milliseconds before callback will be called.
*/
addTimeout(name, fn, ms) {
const existing = this._timeouts.get(name);
if (existing)
clearTimeout(existing);
const timeout = setTimeout(fn, ms);
this._timeouts.set(name, timeout);
}
/**
* cancelTimeout
* Cancels a timeout with the specified name.
* @param name Name of the timeout to cancel.
*/
cancelTimeout(name) {
const timeout = this._timeouts.get(name);
if (timeout) {
clearTimeout(timeout);
this._timeouts.delete(name);
}
}
/**
* Connect to feed with built in delay, timeout and backoff.
*/
async connectWithTimeout() {
var _a;
if (this._isConnecting || this._isConnected)
return;
// If we're in a rate limit cooldown, skip connection attempts
if (Date.now() < this._rateLimitUntil) {
const wait = this._rateLimitUntil - Date.now();
this.warn(`Rate limited. Waiting ${Math.ceil(wait / 1000)} seconds before reconnecting.`);
this.connectWithDelayWorker(wait + 1000);
return;
}
this._isConnecting = true;
try {
if (!this.canConnect) {
this.log(`Skipping connect attempt: waiting for backoff (retryBackoff: ${this._retryBackoff} ms, lastRetry: ${this._lastRetry})`);
return;
}
this._lastRetry = Date.now();
if (this._isUnauthenticated) {
this.error(`Unauthenticated! Invalid token. Please provide a valid token and try again.`);
return;
}
if (this._realTimeConsumptionEnabled === null) {
try {
const homeId = (_a = this._config.homeId) !== null && _a !== void 0 ? _a : '';
this._realTimeConsumptionEnabled = await this._tibberQuery.getRealTimeEnabled(homeId);
}
catch (error) {
if ((error === null || error === void 0 ? void 0 : error.httpCode) === 400 &&
Array.isArray(error === null || error === void 0 ? void 0 : error.errors) &&
error.errors.some((x) => { var _a; return ((_a = x === null || x === void 0 ? void 0 : x.extensions) === null || _a === void 0 ? void 0 : _a.code) === 'UNAUTHENTICATED'; })) {
this._isUnauthenticated = true;
this.error(`Unauthenticated! Invalid token. Please provide a valid token and try again.`);
}
else if ((error === null || error === void 0 ? void 0 : error.httpCode) === 429 || (error === null || error === void 0 ? void 0 : error.statusCode) === 429) {
// Too many requests: set a long backoff (e.g., 10 minutes)
const cooldown = 10 * 60 * 1000 + this.getRandomInt(60 * 1000); // 10-11 min
this._rateLimitUntil = Date.now() + cooldown;
this._retryBackoff = cooldown;
this.warn(`Received 429 Too Many Requests. Backing off for ${Math.round(cooldown / 1000)} seconds.`);
this.emit('rate_limited', { until: this._rateLimitUntil, cooldown });
// Only schedule a reconnect after the cooldown, then return
this.connectWithDelayWorker(cooldown + 1000);
return;
}
else {
this.error(`Error checking real-time consumption status.\n${JSON.stringify(error)}`);
}
this.incrementFailedAttempts();
this.updateBackoff(error);
return;
}
}
if (!this._realTimeConsumptionEnabled) {
this.warn(`Unable to connect. Real Time Consumption is not enabled.`);
this.incrementFailedAttempts();
this.updateBackoff();
return;
}
this.cancelTimeout('connect');
this.cancelTimeout('connection_timeout');
this.log('Connecting...');
this.emit('connecting', {
timeout: this._retryBackoff + this._backoffDelayBase,
retryBackoff: this._retryBackoff
});
this.addTimeout('connection_timeout', () => {
this.error('Connection timeout');
this.emit('connection_timeout', { timeout: this._feedConnectionTimeout });
if (this._webSocket) {
this.terminateConnection();
}
this.incrementFailedAttempts();
this.updateBackoff();
if (this._active) {
this.connectWithDelayWorker(this._retryBackoff);
}
}, this._feedConnectionTimeout);
await this.internalConnect();
}
catch (error) {
// Detect 429 Too Many Requests
if ((error === null || error === void 0 ? void 0 : error.httpCode) === 429 || /429/.test(error === null || error === void 0 ? void 0 : error.message)) {
const cooldown = Math.max(this._retryBackoff, 10 * 60 * 1000); // 10 minutes
this._rateLimitUntil = Date.now() + cooldown;
this._retryBackoff = cooldown;
this.warn(`Received 429 Too Many Requests. Backing off for ${Math.ceil(cooldown / 1000)} seconds.`);
this.emit('rate_limited', { until: this._rateLimitUntil, cooldown });
// Only schedule a reconnect after the cooldown, then return
this.connectWithDelayWorker(cooldown + 1000);
return;
}
this.error(error);
this.incrementFailedAttempts();
this.updateBackoff(error);
if (!this._isConnected && this._active) {
this.updateBackoff();
this.connectWithDelayWorker(this._retryBackoff);
}
}
finally {
this._isConnecting = false;
}
}
updateBackoff(error) {
// If 429, don't increase attempts, just use the long backoff already set
if ((error === null || error === void 0 ? void 0 : error.httpCode) === 429 || (error === null || error === void 0 ? void 0 : error.statusCode) === 429) {
return;
}
if (this._retryBackoff < this._backoffDelayMax) {
this._connectionAttempts++;
}
this._retryBackoff = this.getBackoffWithJitter(this._connectionAttempts);
}
incrementFailedAttempts() {
const now = Date.now();
const STABLE_CONNECTION_THRESHOLD = 5 * 60 * 1000; // 5 minutes
if (now - this._lastConnectedAt > STABLE_CONNECTION_THRESHOLD) {
// Connection was stable, reset backoff
this._connectionAttempts = 0;
this._retryBackoff = this._backoffDelayBase;
}
this._failedAttempts++;
if (this._failedAttempts >= this._maxFailedConnectionAttempts) {
this.warn(`Max failed attempts (${this._maxFailedConnectionAttempts}) reached. Performing hard reset.`);
this.hardReset();
}
}
resetFailedAttempts() {
this._failedAttempts = 0;
}
hardReset() {
this.log('Performing hard reset of TibberFeed...');
this._isConnecting = false;
this._isConnected = false;
this._isClosing = false;
this._isUnauthenticated = false;
this._realTimeConsumptionEnabled = null;
this._connectionAttempts = 0;
this._retryBackoff = this._backoffDelayBase;
this._lastRetry = 0;
this.resetFailedAttempts();
this.cancelTimeout('heartbeat');
this.cancelTimeout('connect');
this.cancelTimeout('connection_timeout');
if (this._webSocket) {
try {
this._webSocket.terminate();
}
catch (e) { }
this._webSocket = undefined;
}
// Wait for rate limit cooldown if set
const now = Date.now();
const delay = this._rateLimitUntil > now ? this._rateLimitUntil - now : 5000;
if (this._active) {
this.updateBackoff();
this.connectWithDelayWorker(delay);
}
}
/**
* Connect with a delay if the feed is still active.
*/
connectWithDelayWorker(delay, isRetry = false, reason) {
this.cancelTimeout('connect');
if (!this._active)
return;
// Prevent overlapping connection attempts
if (this._isConnecting || this._isConnected)
return;
const nextDelay = delay !== undefined ? delay : this._retryBackoff;
this.addTimeout('connect', async () => {
if (this._isConnecting || this._isConnected)
return;
// Log the reason and delay for reconnect (now we know the context)
if (reason) {
this.log(`Attempting reconnect after ${Math.ceil(nextDelay / 1000)} seconds due to: ${reason}`);
}
else if (isRetry) {
this.log(`Attempting reconnect after ${Math.ceil(nextDelay / 1000)} seconds due to: previous attempt failed`);
}
else {
this.log(`Attempting initial connect after ${Math.ceil(nextDelay / 1000)} seconds (backoff: ${this._retryBackoff} ms)`);
}
try {
if (this.canConnect) {
await this.connectWithTimeout();
}
}
catch (error) {
this.error(error);
}
// Only schedule another reconnect if not connected and still active
if (!this._isConnected && this._active) {
this.updateBackoff();
this.connectWithDelayWorker(this._retryBackoff, true);
}
}, nextDelay);
}
/**
* Internal connection method that handles the communication with tibber feed.
*/
async internalConnect() {
const { apiEndpoint } = this._config;
if (!apiEndpoint || !apiEndpoint.apiKey) {
const msg = 'Missing mandatory parameters: apiEndpoint or apiKey. Execution will halt.';
this.error(msg);
throw new Error(msg);
}
try {
const url = await this._tibberQuery.getWebsocketSubscriptionUrl();
const options = {
headers: {
'Authorization': `Bearer ${apiEndpoint.apiKey}`,
'User-Agent': this._headerManager.userAgent,
}
};
const ws = this._webSocketFactory
? this._webSocketFactory(url.href, ['graphql-transport-ws'], options)
: new ws_1.default(url.href, ['graphql-transport-ws'], options);
this.attachWebSocketHandlers(ws);
this._webSocket = ws;
}
catch (error) {
this.error(error);
throw error;
}
}
attachWebSocketHandlers(ws) {
ws.onopen = this.onWebSocketOpen.bind(this);
ws.onmessage = this.onWebSocketMessage.bind(this);
ws.onclose = this.onWebSocketClose.bind(this);
ws.onerror = this.onWebSocketError.bind(this);
}
/**
* Event: onWebSocketOpen
* Called when websocket connection is established.
*/
onWebSocketOpen(event) {
if (!this._webSocket) {
return;
}
this.initConnection();
}
/**
* Event: onWebSocketClose
* Called when feed is closed.
* @param {WebSocket.CloseEvent} event Close event
*/
onWebSocketClose(event) {
this._isConnected = false;
this.emit('disconnected', 'Disconnected from Tibber feed.');
if (this._active && this._autoReconnect) {
this.updateBackoff();
this.connectWithDelayWorker(this._retryBackoff, true, 'auto-reconnect after disconnect');
}
}
/**
* Event: onWebSocketMessage
* Called when data is received from the feed.
* @param {WebSocket.MessageEvent} event Message event
*/
onWebSocketMessage(message) {
if (message.data && message.data.toString().startsWith('{')) {
const msg = JSON.parse(message.data.toString());
switch (msg.type) {
case GQL_1.GQL.CONNECTION_ERROR:
this.error(`A connection error occurred: ${JSON.stringify(msg)}`);
// this.close();
this.handleConnectionError();
break;
case GQL_1.GQL.CONNECTION_ACK:
this._isConnected = true;
this._lastConnectedAt = Date.now(); // Track when we last connected
this.resetFailedAttempts();
this.cancelTimeout('connection_timeout');
this.startSubscription(this._gql, { homeId: this._config.homeId });
this.heartbeat();
this.emit('connected', 'Connected to Tibber feed.');
this.emit(GQL_1.GQL.CONNECTION_ACK, msg);
break;
case GQL_1.GQL.NEXT:
if (msg.payload && msg.payload.errors) {
this.emit('error', msg.payload.errors);
}
if (Number(msg.id) !== this._operationId) {
return;
}
if (!msg.payload || !msg.payload.data) {
return;
}
this.decreaseConnectionBackoff();
const data = msg.payload.data.liveMeasurement;
this.heartbeat();
this.emit('data', data);
break;
case GQL_1.GQL.ERROR:
this.error(`An error occurred: ${JSON.stringify(msg)}`);
break;
case GQL_1.GQL.COMPLETE:
if (Number(msg.id) !== this._operationId) {
return;
}
this.log('Received complete message. Closing connection.');
this.close();
this.cancelTimeout('connect');
const delay = this.getRandomInt(60000);
this.updateBackoff();
this.connectWithDelayWorker(delay);
break;
default:
this.warn(`Unrecognized message type: ${JSON.stringify(msg)}`);
break;
}
}
}
;
/**
* Event: onWebSocketError
* Called when an error has occurred.
* @param {WebSocket.ErrorEvent} event Error event
*/
onWebSocketError(event) {
let errorMsg = 'An error occurred';
if (event && typeof event === 'object') {
if ('message' in event && event.message) {
errorMsg += `: ${event.message}`;
}
else if ('error' in event && event.error) {
errorMsg += `: ${event.error}`;
}
else if (Object.keys(event).length > 0) {
errorMsg += `: ${JSON.stringify(event)}`;
}
else {
errorMsg += ': [unknown websocket error]';
}
}
this.error(errorMsg);
if (!this._isClosing) {
this.handleConnectionError();
}
}
/**
* Gracefully close connection with Tibber.
*/
closeConnection() {
this.stopSubscription();
this._webSocket.close(1000, 'Normal Closure');
this._webSocket.terminate();
}
/**
* Forcefully terminate connection with Tibber.
*/
terminateConnection() {
this.stopSubscription();
this._webSocket.terminate();
this._isConnected = false;
}
/**
* Initialize connection with Tibber.
*/
initConnection() {
const query = {
type: GQL_1.GQL.CONNECTION_INIT,
payload: { 'token': this._config.apiEndpoint.apiKey },
};
this.sendQuery(query);
this.emit('init_connection', 'Initiating Tibber feed.');
}
/**
* Subscribe to a specified resource.
* @param subscription @typedef string Name of the resource to subscribe to.
* @param variables @typedef Record<string, unknown> Variable to use with the resource.
*/
startSubscription(subscription, variables) {
const query = {
id: `${++this._operationId}`,
type: GQL_1.GQL.SUBSCRIBE,
payload: {
variables,
extensions: {},
operationName: null,
query: subscription,
},
};
this.sendQuery(query);
}
/**
* Stops subscribing to a resource with a specified operation Id
* @param {number} operationId Operation Id to stop subscribing to.
*/
stopSubscription(operationId) {
const query = {
id: `${operationId !== null && operationId !== void 0 ? operationId : this._operationId}`,
type: GQL_1.GQL.COMPLETE,
};
this.sendQuery(query);
this.emit('disconnecting', 'Sent stop to Tibber feed.');
}
/**
* Send websocket query to Tibber
* @param {IQuery} query Tibber GQL query
*/
sendQuery(query) {
if (!this._webSocket) {
this.error('Invalid websocket.');
return;
}
try {
if (this._webSocket.readyState === ws_1.default.OPEN) {
this._webSocket.send(JSON.stringify(query));
}
}
catch (error) {
this.error(error);
}
}
/**
* Log function to emit log data to subscribers.
* @param {string} message Log message
*/
log(message) {
try {
this.emit('log', message);
}
catch (error) {
// console.error(error);
}
}
/**
* Log function to emit warning log data to subscribers.
* @param {string} message Log message
*/
warn(message) {
try {
this.emit('warn', message);
}
catch (error) {
// console.error(error);
}
}
/**
* Log function to emit error log data to subscribers.
* @param {any} message Log message
*/
error(message) {
try {
this.emit('error', message);
}
catch (error) {
// console.error(error);
}
}
handleConnectionError() {
this.terminateConnection();
if (this._active) {
if (Date.now() < this._rateLimitUntil) {
const wait = this._rateLimitUntil - Date.now();
this.log(`Currently rate limited due to a previous 429 response. Waiting ${Math.ceil(wait / 1000)} seconds before next reconnect attempt.`);
this.connectWithDelayWorker(wait + 1000);
}
else {
this.updateBackoff();
this.connectWithDelayWorker(this._retryBackoff);
}
}
}
}
exports.TibberFeed = TibberFeed;
//# sourceMappingURL=TibberFeed.js.map