UNPKG

homebridge-panasonic-ac-platform

Version:
672 lines 31.1 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; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const settings_1 = require("./settings"); const axios_1 = __importDefault(require("axios")); const tough_cookie_1 = require("tough-cookie"); const axios_cookiejar_support_1 = require("axios-cookiejar-support"); const cheerio = __importStar(require("cheerio")); const jssha_1 = __importDefault(require("jssha")); const crypto_1 = __importDefault(require("crypto")); /** * This class exposes login, device status fetching, and device status update functions. */ class ComfortCloudApi { constructor(config, log) { this.config = config; this.log = log; this.parameters = {}; } /** * Logs in the user with Comfort Cloud and * saves the retrieved token on the instance. */ async login() { this.log.debug('Comfort Cloud: login()'); // 2 FA TOTP (not necessary for know, it only calculate PIN, but Panasonic API not require it yet). // Check if the key is given and if it has 32 characters if (this.config.key2fa && this.config.key2fa.length === 32) { await this.setup2fa(); } else { this.log.debug('No 2FA key or incorrect key (not necessary for know).'); } // NEW API - START ---------------------------------------------------------------------------------- // How to check API: // https://github.com/homebridge-panasonic-ac-platform/homebridge-panasonic-ac-platform/blob/master/docs/app.md // Based on: // https://github.com/sockless-coding/panasonic_cc/blob/master/custom_components/panasonic_cc/pcomfortcloud/panasonicauthentication.py // https://github.com/lostfields/python-panasonic-comfort-cloud // https://github.com/craibo/panasonic_cc/ // https://github.com/little-quokka/python-panasonic-comfort-cloud/ // STEP 0 - prepare --------------------------------------------------------------- const auth0client = settings_1.AUTH_0_CLIENT; this.log.debug(`auth0client: ${auth0client}`); const app_client_id = settings_1.APP_CLIENT_ID; this.log.debug(`app_client_id: ${app_client_id}`); function generateRandomString(length) { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < length; i++) { result += characters[Math.floor(Math.random() * characters.length)]; } return result; } function getQuerystringParameterFromHeaderEntryUrl(response, headerEntry, querystringParameter, baseUrl) { const headerEntryValue = response.headers[headerEntry]; const parsedUrl = new URL(headerEntryValue.startsWith('/') ? baseUrl + headerEntryValue : headerEntryValue); const params = new URLSearchParams(parsedUrl.search); return params.get(querystringParameter) || null; } // taken from AuthO docs function base64URLEncode(str) { return str.toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } function sha256(buffer) { return crypto_1.default.createHash('sha256').update(buffer).digest(); } const code_verifier = base64URLEncode(crypto_1.default.randomBytes(32)); this.log.debug(`code_verifier: ${code_verifier}`); const code_challenge = base64URLEncode(sha256(code_verifier)); this.log.debug(`code_challenge: ${code_challenge}`); const state = generateRandomString(20); this.log.debug(`state: ${state}`); // Setup CookieJar const jar = new tough_cookie_1.CookieJar(); const client = (0, axios_cookiejar_support_1.wrapper)(axios_1.default.create({ jar })); // STEP 1 - authorize ---------------------------------------------------------------- await client.request({ method: 'get', url: 'https://authglb.digital.panasonic.com/authorize', headers: { 'user-agent': 'okhttp/4.10.0', }, params: { 'scope': 'openid offline_access comfortcloud.control a2w.control', 'audience': 'https://digital.panasonic.com/' + settings_1.APP_CLIENT_ID + '/api/v1/', 'protocol': 'oauth2', 'response_type': 'code', 'code_challenge': code_challenge, 'code_challenge_method': 'S256', 'auth0Client': settings_1.AUTH_0_CLIENT, 'client_id': settings_1.APP_CLIENT_ID, 'redirect_uri': settings_1.REDIRECT_URI, 'state': state, }, maxRedirects: 0, validateStatus: status => (status >= 200 && status < 300) || status === 302, }) .then((response) => { this.log.debug('Comfort Cloud Login - Step 1 - Success'); this.log.debug(response.data); this.location = response.headers.location; this.log.debug(`location: ${this.location}`); this.state = getQuerystringParameterFromHeaderEntryUrl(response, 'location', 'state', 'https://authglb.digital.panasonic.com'); this.log.debug(`state: ${this.state}`); }) .catch((error) => { this.log.error('Comfort Cloud Login - Step 1 - Error'); this.log.debug(JSON.stringify(error, null, 2)); return Promise.reject(error); }); // STEP 2 - authorize - follow redirect -------------------------------------------------- await client.request({ method: 'get', url: 'https://authglb.digital.panasonic.com' + this.location, maxRedirects: 0, validateStatus: status => (status >= 200 && status < 300) || status === 200, }) .then((response) => { var _a, _b; this.log.debug('Comfort Cloud Login - Step 2 - Success'); //this.log.debug(response.data); this.csrf = (_b = (_a = response.headers['set-cookie'] .find(cookie => cookie.includes('_csrf'))) === null || _a === void 0 ? void 0 : _a.match(new RegExp('^_csrf=(.+?);'))) === null || _b === void 0 ? void 0 : _b[1]; this.log.debug(`csrf: ${this.csrf}`); }) .catch((error) => { this.log.error('Comfort Cloud Login - Step 2 - Error'); this.log.debug(JSON.stringify(error, null, 2)); return Promise.reject(error); }); // STEP 3 - login ---------------------------------------------------------------- await client.request({ method: 'post', url: 'https://authglb.digital.panasonic.com/usernamepassword/login', headers: { 'Auth0-Client': settings_1.AUTH_0_CLIENT, 'user-agent': 'okhttp/4.10.0', }, data: { 'client_id': settings_1.APP_CLIENT_ID, 'redirect_uri': settings_1.REDIRECT_URI, 'tenant': 'pdpauthglb-a1', 'response_type': 'code', 'scope': 'openid offline_access comfortcloud.control a2w.control', 'audience': 'https://digital.panasonic.com/' + settings_1.APP_CLIENT_ID + '/api/v1/', '_csrf': this.csrf, 'state': this.state, '_intstate': 'deprecated', 'username': this.config.email, 'password': this.config.password, 'lang': 'en', 'connection': 'PanasonicID-Authentication', }, maxRedirects: 0, validateStatus: status => (status >= 200 && status < 300) || status === 200, }) .then((response) => { this.log.debug('Comfort Cloud Login - Step 3 - Success'); this.log.debug(response.data); // get wa, wresult, wctx from body const $ = cheerio.load(response.data); const elements = $('input[type="hidden"]'); this.log.debug(`elements: ${elements}`); // Extract hidden input parameters and store them in a dictionary for (const el of elements) { this.parameters[el.attribs.name] = el.attribs.value; } }) .catch((error) => { this.log.error('Comfort Cloud Login - Step 3 - Error'); this.log.debug(JSON.stringify(error, null, 2)); return Promise.reject(error); }); // STEP 4 - login callback ------------------------------------------------------------ await client.request({ method: 'post', url: 'https://authglb.digital.panasonic.com/login/callback', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/113.0.0.0 Mobile Safari/537.36', }, data: this.parameters, maxRedirects: 0, validateStatus: status => (status >= 200 && status < 300) || status === 302, }) .then((response) => { this.log.debug('Comfort Cloud Login - Step 4 - Success'); this.log.debug(response.data); this.location = response.headers.location; this.log.debug(`location: ${this.location}`); }) .catch((error) => { this.log.error('Comfort Cloud Login - Step 4 - Error'); this.log.debug(JSON.stringify(error, null, 2)); return Promise.reject(error); }); // STEP 5 - login follow redirect ---------------------------------------------------------- await client.request({ method: 'get', url: 'https://authglb.digital.panasonic.com' + this.location, maxRedirects: 0, validateStatus: status => (status >= 200 && status < 300) || status === 302, }) .then((response) => { this.log.debug('Comfort Cloud Login - Step 5 - Success'); this.log.debug(response.data); this.code = getQuerystringParameterFromHeaderEntryUrl(response, 'location', 'code', 'https://authglb.digital.panasonic.com'); this.log.debug(`code: ${this.code}`); }) .catch((error) => { this.log.error('Comfort Cloud Login - Step 5 - Error'); this.log.debug(JSON.stringify(error, null, 2)); return Promise.reject(error); }); // STEP 6 - get new token ------------------------------------------------------------------- await client.request({ method: 'post', url: 'https://authglb.digital.panasonic.com/oauth/token', headers: { 'Auth0-Client': settings_1.AUTH_0_CLIENT, 'user-agent': 'okhttp/4.10.0', }, data: { 'scope': 'openid', 'client_id': settings_1.APP_CLIENT_ID, 'grant_type': 'authorization_code', 'code': this.code, 'redirect_uri': settings_1.REDIRECT_URI, 'code_verifier': code_verifier, }, maxRedirects: 0, validateStatus: status => (status >= 200 && status < 300) || status === 302, }) .then((response) => { this.log.debug('Comfort Cloud Login - Step 6 - Success'); this.log.debug(response.data); this.token = response.data.access_token; this.log.debug(`token: ${this.token}`); this.tokenRefresh = response.data.refresh_token; this.log.debug(`tokenRefresh: ${this.tokenRefresh}`); }) .catch((error) => { this.log.error('Comfort Cloud Login - Step 6 - Error'); this.log.debug(JSON.stringify(error, null, 2)); return Promise.reject(error); }); // STEP 7 - get client id -------------------------------------------------------------- await client.request({ method: 'post', url: 'https://accsmart.panasonic.com/auth/v2/login', headers: { ...this.getBaseRequestHeaders(), 'X-User-Authorization-V2': 'Bearer ' + this.token, }, data: { 'language': 0, }, validateStatus: status => (status >= 200 && status < 300) || status === 200, }) .then((response) => { this.log.debug('Comfort Cloud Login - Step 7 - Success'); this.log.debug(response.data); this.clientId = response.data.clientId; this.log.debug(`clientId: ${this.clientId}`); }) .catch((error) => { this.log.error('Comfort Cloud Login - Step 7 - Error'); this.log.debug(JSON.stringify(error, null, 2)); return Promise.reject(error); }); // STEP 8 - set timer to refresh token -------------------------------------------- // Refresh token just a moment before the expiration of 24 hours setTimeout(this.refreshToken.bind(this), 86300000); } // refresh token -------------------------------------------------------------------- async refreshToken() { this.log.debug('Comfort Cloud - refreshToken()'); await axios_1.default.request({ method: 'post', url: 'https://authglb.digital.panasonic.com/oauth/token', headers: { 'Auth0-Client': settings_1.AUTH_0_CLIENT, 'Content-Type': 'application/json', 'User-Agent': 'okhttp/4.10.0', }, data: { 'scope': 'openid offline_access comfortcloud.control a2w.control', 'client_id': settings_1.APP_CLIENT_ID, 'refresh_token': this.tokenRefresh, 'grant_type': 'refresh_token', }, }) .then((response) => { this.log.debug('Comfort Cloud - refreshToken() - Success'); this.log.debug(response.data); this.token = response.data.access_token; this.log.debug(`token: ${this.token}`); this.tokenRefresh = response.data.refresh_token; this.log.debug(`tokenRefresh: ${this.tokenRefresh}`); // Refresh token just a moment before the expiration of 24 hours setTimeout(this.refreshToken.bind(this), 86300000); }) .catch((error) => { this.log.error('Comfort Cloud - refreshToken() - Error'); this.log.debug(JSON.stringify(error, null, 2)); this.token = null; this.tokenRefresh = null; this.login.bind(this); return Promise.reject(error); }); } // NEW API - END ---------------------------------------------------------------------------------- // 2 FA TOTP ---------------------------------------------------------------------------------- async setup2fa() { // Decoding Base32 to bytes function base32ToBytes(base32) { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let bits = 0; let value = 0; const bytes = []; for (let i = 0; i < base32.length; i++) { const char = base32.charAt(i).toUpperCase(); value = (value << 5) | alphabet.indexOf(char); bits += 5; if (bits >= 8) { bits -= 8; const bytes = []; bytes.push((value >>> bits) & 0xff); } } return bytes; } // Converting a number to bytes (for a timer) function intToBytes(num) { const bytes = new Array(8); for (let i = 7; i >= 0; i--) { bytes[i] = num & 0xff; num = num >>> 8; } return bytes; } // SHA1 function sha1(msg) { function rotateLeft(n, s) { return (n << s) | (n >>> (32 - s)); } let h0 = 0x67452301, h1 = 0xEFCDAB89, h2 = 0x98BADCFE, h3 = 0x10325476, h4 = 0xC3D2E1F0; let padded = msg.slice(); padded.push(0x80); while ((padded.length % 64) !== 56) { padded.push(0); } const len = msg.length * 8; padded = padded.concat([0, 0, 0, 0, (len >>> 24) & 0xff, (len >>> 16) & 0xff, (len >>> 8) & 0xff, len & 0xff]); for (let i = 0; i < padded.length; i += 64) { const w = new Array(80); for (let t = 0; t < 16; t++) { w[t] = (padded[i + t * 4] << 24) | (padded[i + t * 4 + 1] << 16) | (padded[i + t * 4 + 2] << 8) | padded[i + t * 4 + 3]; } for (let t = 16; t < 80; t++) { w[t] = rotateLeft(w[t - 3] ^ w[t - 8] ^ w[t - 14] ^ w[t - 16], 1); } let a = h0, b = h1, c = h2, d = h3, e = h4; for (let t = 0; t < 80; t++) { const f = t < 20 ? (b & c) | (~b & d) : t < 40 ? b ^ c ^ d : t < 60 ? (b & c) | (b & d) | (c & d) : b ^ c ^ d; const k = [0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xCA62C1D6][Math.floor(t / 20)]; const temp = (rotateLeft(a, 5) + f + e + k + w[t]) >>> 0; e = d; d = c; c = rotateLeft(b, 30); b = a; a = temp; } h0 = (h0 + a) >>> 0; h1 = (h1 + b) >>> 0; h2 = (h2 + c) >>> 0; h3 = (h3 + d) >>> 0; h4 = (h4 + e) >>> 0; } return [(h0 >>> 24) & 0xff, (h0 >>> 16) & 0xff, (h0 >>> 8) & 0xff, h0 & 0xff, (h1 >>> 24) & 0xff, (h1 >>> 16) & 0xff, (h1 >>> 8) & 0xff, h1 & 0xff, (h2 >>> 24) & 0xff, (h2 >>> 16) & 0xff, (h2 >>> 8) & 0xff, h2 & 0xff, (h3 >>> 24) & 0xff, (h3 >>> 16) & 0xff, (h3 >>> 8) & 0xff, h3 & 0xff, (h4 >>> 24) & 0xff, (h4 >>> 16) & 0xff, (h4 >>> 8) & 0xff, h4 & 0xff]; } // HMAC-SHA1 function hmacSha1(key, message) { const blockSize = 64; let keyBytes = key.slice(); if (keyBytes.length > blockSize) { keyBytes = sha1(keyBytes); } while (keyBytes.length < blockSize) { keyBytes.push(0); } const oKeyPad = keyBytes.map(b => b ^ 0x5c); const iKeyPad = keyBytes.map(b => b ^ 0x36); const inner = iKeyPad.concat(message); const innerHash = sha1(inner); const outer = oKeyPad.concat(innerHash); return sha1(outer); } // Generate TOTP code function generateTOTP(base32Secret) { const secretBytes = base32ToBytes(base32Secret); const timeStep = 30; // Standardowy interwał 30 sekund const epoch = Math.floor(Date.now() / 1000); const time = Math.floor(epoch / timeStep); const timeBytes = intToBytes(time); // Calculate HMAC-SHA1 const hmac = hmacSha1(secretBytes, timeBytes); // Dynamiczne obcinanie const offset = hmac[hmac.length - 1] & 0xf; const binary = ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) | ((hmac[offset + 2] & 0xff) << 8) | (hmac[offset + 3] & 0xff); // Generate a 6-digit code const code = (binary % 1000000).toString().padStart(6, '0'); return code; } // Show UTC time this.log.debug('UTC date: ' + this.getCurrentTimestamp()); // Generate 6 digit PIN code calculated by key const key2fa = this.config.key2fa; const key2fa_masked = key2fa.replace(key2fa.substring(4, 28), '(...)'); const code2fa = generateTOTP(key2fa); this.log.info('2FA TOTP, for key: ' + key2fa_masked + ', PIN code: ' + code2fa); } // Get devices ---------------------------------------------------------------------------------- /** * Fetches all devices that are registered with the user's Comfort Cloud account. * * @returns A promise of all the user's devices. */ async getDevices() { this.log.debug('Comfort Cloud: getDevices()'); const fetchedDevices = []; if (!this.token) { return Promise.reject('No auth token available (login probably failed). ' + 'Check your credentials and restart Homebridge.'); } return axios_1.default.request({ method: 'get', url: 'https://accsmart.panasonic.com/device/group', headers: { ...this.getBaseRequestHeaders(), 'X-Client-Id': this.clientId, 'X-User-Authorization-V2': 'Bearer ' + this.token, }, }) .then((response) => { this.log.debug('Comfort Cloud - getDevices(): Success'); this.log.debug(JSON.stringify(response.data, null, 2)); response.data.groupList.forEach(group => { group.deviceList.forEach(device => { fetchedDevices.push(device); }); }); if (fetchedDevices.length === 0) { this.log.error('No devices found. ' + 'Check whether you have added at least one device to your Comfort Cloud account.'); } return fetchedDevices; }) .catch((error) => { this.log.debug('Comfort Cloud - getDevices(): Error'); this.handleNetworkRequestError(error); return Promise.reject('Comfort Cloud - getDevices(): Error'); }); } // Get devices status ---------------------------------------------------------------------------------- /** * Retrieves the status of a device. * * @param deviceGuid Comfort Cloud's globally unique identifier for the device. * @returns A promise of the status of the requested device. */ getDeviceStatus(deviceGuid, deviceName) { this.log.debug(`${deviceName} (${deviceGuid}): Comfort Cloud: getDeviceStatus()`); if (!this.token) { return Promise.reject('No auth token available (login probably failed). ' + 'Check your credentials and restart Homebridge.'); } if (!deviceGuid) { return Promise.reject('Cannot get device status for undefined deviceGuid.'); } return axios_1.default.request({ method: 'get', url: `https://accsmart.panasonic.com/deviceStatus/now/${deviceGuid}`, headers: { ...this.getBaseRequestHeaders(), 'X-Client-Id': this.clientId, 'X-User-Authorization-V2': 'Bearer ' + this.token, }, }) .then((response) => { this.log.debug(`${deviceName} (${deviceGuid}): Comfort Cloud - getDeviceStatus() : Success`); this.log.debug(`${deviceName} (${deviceGuid}) - device status:\n${JSON.stringify(response.data, null, 2)}`); return response.data; }) .catch((error) => { this.log.error(`Comfort Cloud - getDeviceStatus() for GUID '${deviceGuid}': Error`); this.log.error('Try restarting the AC (turn it off from the power completely' + ' and turn it on again) and Internet router and Homebridge.'); this.log.error('Turn on debug for more info.'); this.handleNetworkRequestError(error); return Promise.reject(`${deviceName} (${deviceGuid}): Comfort Cloud - getDeviceStatus() : Error`); }); } // Set device status ---------------------------------------------------------------------------------- /** * Sets the status of a device. * * @param deviceGuid Comfort Cloud's globally unique identifier for the device. * @param parameters Payload containing status update parameters. * @returns */ setDeviceStatus(deviceGuid, deviceName, parameters) { this.log.debug(`${deviceName} (${deviceGuid}): Comfort Cloud: setDeviceStatus()`); this.log.debug(`${deviceName} (${deviceGuid}): ${JSON.stringify(parameters)}`); if (!this.token) { this.log.error('No auth token available (login probably failed). ' + 'Check your credentials and restart Homebridge.'); return; } if (!deviceGuid) { this.log.error('Cannot set device status for undefined deviceGuid.'); return; } if (this.config.suppressOutgoingUpdates) { this.log.debug(`${deviceName} (${deviceGuid}): Suppressing outgoing device update.`); return; } return axios_1.default.request({ method: 'post', url: 'https://accsmart.panasonic.com/deviceStatus/control', headers: { ...this.getBaseRequestHeaders(), 'X-Client-Id': this.clientId, 'X-User-Authorization-V2': 'Bearer ' + this.token, }, data: { 'deviceGuid': deviceGuid, 'parameters': parameters, }, }) .then((response) => { this.log.debug(`${deviceName} (${deviceGuid}): Comfort Cloud - setDeviceStatus(): Success`); this.log.debug(`${deviceName} (${deviceGuid}): ${JSON.stringify(response.data)}`); }) .catch((error) => { this.log.error(`${deviceName} (${deviceGuid}): Comfort Cloud - setDeviceStatus(): Error`); this.log.error('Turn on debug for more info.'); this.handleNetworkRequestError(error); }); } // ---------------------------------------------------------------------------------- /** * Generic Axios error handler that checks which type of * error occurred and prints the respective information. * * @see https://axios-http.com/docs/handling_errors * @param error The error that is passes into the Axios error handler */ handleNetworkRequestError(error) { if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx. this.log.debug(error.response); } else if (error.request) { // The request was made but no response was received. // `error.request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js this.log.debug(error.request); } else { // Something happened in setting up the request that triggered an error. this.log.debug(error.message); } } /** * @returns An object containing all required HTTP headers for Comfort Cloud requests. */ getBaseRequestHeaders() { var _a; return { 'Accept': 'application/json; charset=UTF-8', 'Content-Type': 'application/json', 'User-Agent': 'G-RAC', 'X-APP-NAME': 'Comfort Cloud', 'X-APP-TIMESTAMP': this.getCurrentTimestamp(), 'X-APP-TYPE': '1', 'X-APP-VERSION': this.config.overwriteVersion || settings_1.APP_VERSION, 'X-CFC-API-KEY': (_a = this.getCfcApiKey()) !== null && _a !== void 0 ? _a : '0', }; } getCfcApiKey() { try { // Parse the timestamp in 'YYYY-MM-DD HH:MM:SS' format and convert to UTC milliseconds const timestamp = this.getCurrentTimestamp(); this.log.debug('Timestamp used for key generation and header: ' + timestamp); const date = new Date(timestamp + ' UTC'); // Added UTC to ensure consistent timezone handling const timestampMs = date.getTime().toString(); const input = 'Comfort Cloud' + '521325fb2dd486bf4831b47644317fca' + timestampMs + 'Bearer ' + this.token; const shaObj = new jssha_1.default('SHA-256', 'TEXT'); shaObj.update(input); const hashStr = shaObj.getHash('HEX'); return hashStr.slice(0, 9) + 'cfc' + hashStr.slice(9); } catch (error) { this.log.error('Failed to generate API key', error); return undefined; } } getCurrentTimestamp() { const now = new Date(); return now.getUTCFullYear() + '-' + (now.getUTCMonth() + 1).toString().padStart(2, '0') + '-' + now.getUTCDate().toString().padStart(2, '0') + ' ' + now.getUTCHours().toString().padStart(2, '0') + ':' + now.getUTCMinutes().toString().padStart(2, '0') + ':' + now.getUTCSeconds().toString().padStart(2, '0'); } } exports.default = ComfortCloudApi; //# sourceMappingURL=comfort-cloud.js.map