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
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 {
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