UNPKG

tibber-api

Version:

Node.js module for connecting to Tibber API and extract data from your connected homes, including realtime data from Tibber Pulse.

616 lines 22.4 kB
"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 { get timeoutCount() { return this._timeoutCount; } /** * 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) { super(); this._operationId = 0; if (!tibberQuery || !(tibberQuery instanceof TibberQueryBase_1.TibberQueryBase)) { throw new Error('Missing mandatory parameter [tibberQuery]'); } 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._timerHeartbeat = []; this._timerConnect = []; this._timerConnectionTimeout = []; 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 * 60 * 1; // 1 hour this._retryBackoff = 1000; this._timeoutCount = 0; 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() { const result = Date.now() > (this._lastRetry + this._retryBackoff); if (result) { this._lastRetry = Date.now(); if (this._retryBackoff < this._backoffDelayMax) this._connectionAttempts++; this._retryBackoff = this.getBackoffWithJitter(this._connectionAttempts); } this.log(`Can connect: ${result}. Last retry: ${this._lastRetry}. With backoff for: ${this._retryBackoff} ms.`); return result; } /** * 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.cancelTimeouts(this._timerHeartbeat); this.cancelTimeouts(this._timerConnect); this.cancelTimeouts(this._timerConnectionTimeout); 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.cancelTimeouts(this._timerHeartbeat); this.addTimeout(this._timerHeartbeat, () => { 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.connectWithDelayWorker(); } }, 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); } } /** * Add a timeout to an array of timeouts. * @param {NodeJS.Timeout[]} timers List og timeouts * @param {void} callback Callback function to call when timeout is reached. * @param {number} delayMs Delay in milliseconds before callback will be called. */ addTimeout(timers, callback, delayMs) { this._timeoutCount++; timers.push(setTimeout(callback, delayMs)); } /** * Clear timeout for a timer. * @param {NodeJS.Timeout[]} timers Timer handle to clear */ cancelTimeouts(timers) { try { while (timers.length) { const timer = timers.pop(); if (timer) { this._timeoutCount--; clearTimeout(timer); } } } catch (error) { this.error(error); } } /** * Connect to feed with built in delay, timeout and backoff. */ async connectWithTimeout() { var _a; if (this._isConnecting || this._isConnected) { return; } this._isConnecting = true; if (!this.canConnect) { this._isConnecting = false; return; } const unauthenticatedMessage = `Unauthenticated! Invalid token. Please provide a valid token and try again.`; if (this._isUnauthenticated) { this.error(unauthenticatedMessage); this._isConnecting = false; return; } if (this._realTimeConsumptionEnabled === null) { try { this._realTimeConsumptionEnabled = await this._tibberQuery.getRealTimeEnabled((_a = this._config.homeId) !== null && _a !== void 0 ? _a : ''); } 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 === null || error === void 0 ? void 0 : error.errors.find((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(unauthenticatedMessage); } else { this.error(`An error ocurred while trying to check if real time consumption is enabled.\n${JSON.stringify(error)}`); } this._isConnecting = false; return; } } else if (!this._realTimeConsumptionEnabled) { this.warn(`Unable to connect. Real Time Consumtion is not enabled.`); return; } this.cancelTimeouts(this._timerConnect); this.cancelTimeouts(this._timerConnectionTimeout); this.log('Connecting...'); this.emit('connecting', { timeout: this._retryBackoff + this._backoffDelayBase, retryBackoff: this._retryBackoff }); // Set connection timeout. this.addTimeout(this._timerConnectionTimeout, () => { this.error('Connection timeout'); this.emit('connection_timeout', { timeout: this._feedConnectionTimeout }); if (this._webSocket) this.terminateConnection(); if (this._active) this.connectWithDelayWorker(); }, this._feedConnectionTimeout); // Perform connection. await this.internalConnect(); } /** * Connect with a delay if the feed is still active. */ connectWithDelayWorker(delay = 1000) { this.cancelTimeouts(this._timerConnect); if (this._active) { this.addTimeout(this._timerConnect, () => { try { this.connectWithTimeout(); } catch (error) { this.error(error); } this.connectWithDelayWorker(); }, delay); } } /** * Internal connection method that handles the communication with tibber feed. */ async internalConnect() { const { apiEndpoint } = this._config; if (!apiEndpoint || !apiEndpoint.apiKey) { this.error('Missing mandatory parameters: apiEndpoint or apiKey. Execution will halt.'); throw new Error('Missing mandatory parameters: apiEndpoint or apiKey.'); } try { const url = await this._tibberQuery.getWebsocketSubscriptionUrl(); const options = { headers: { 'Authorization': `Bearer ${apiEndpoint.apiKey}`, 'User-Agent': this._headerManager.userAgent, } }; this._webSocket = new ws_1.default(url.href, ['graphql-transport-ws'], options); this._webSocket.onopen = this.onWebSocketOpen.bind(this); this._webSocket.onmessage = this.onWebSocketMessage.bind(this); this._webSocket.onclose = this.onWebSocketClose.bind(this); this._webSocket.onerror = this.onWebSocketError.bind(this); } catch (reason) { this.error(reason); } finally { this._isConnecting = false; } ; } /** * 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.'); } /** * 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(); break; case GQL_1.GQL.CONNECTION_ACK: this._isConnected = true; this.cancelTimeouts(this._timerConnectionTimeout); 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) { // this.log(`Message contains unexpected id and will be ignored.\n${JSON.stringify(msg)}`); 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) { // this.log(`Complete message contains unexpected id and will be ignored.\n${JSON.stringify(msg)}`); return; } this.log('Received complete message. Closing connection.'); this.close(); this.cancelTimeouts(this._timerConnect); const delay = this.getRandomInt(60000); 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) { this.error(`An error occurred: ${JSON.stringify(event)}`); this.close(); } /** * 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); } } } exports.TibberFeed = TibberFeed; //# sourceMappingURL=TibberFeed.js.map