// bluetti.mjs — Bluetti Open Platform (cloud) client, signed per the official spec. // // Node 18+ only (built-in fetch + node:crypto; no npm install): // BLUETTI_APPKEY=xxx BLUETTI_APPSECRET=yyy BLUETTI_SN=EL300... node bluetti.mjs // // Auth (from open.bluetti.com OpenAPI "Signature" section): // Authorization = SHA-256( "appKey=&appSecret=&nonceStr=&timeStamp=" ) // sent with headers: x-app-key=, ETag=, Date=. // The AppSecret is hashed in, never transmitted. // // Register an app + request the IoT/telemetry API permission at // https://open.bluetti.com/developers — and the device SN must be authorized to // your account, or the telemetry endpoints return a permission error. import { createHash, randomBytes } from 'node:crypto'; const BASE = process.env.BLUETTI_BASE || 'https://open.bluetti.com'; const APP_KEY = process.env.BLUETTI_APPKEY || process.env.BLUETTI_ID; const APP_SECRET = process.env.BLUETTI_APPSECRET || process.env.BLUETTI_SECRET; const DEVICE_SN = process.env.BLUETTI_SN || 'EL3002546110146262'; if (!APP_KEY || !APP_SECRET) { console.error('Set BLUETTI_APPKEY and BLUETTI_APPSECRET (from open.bluetti.com).'); process.exit(1); } function authHeaders() { const timeStamp = String(Math.floor(Date.now() / 1000)); const nonceStr = randomBytes(16).toString('hex'); const signStr = `appKey=${APP_KEY}&appSecret=${APP_SECRET}&nonceStr=${nonceStr}&timeStamp=${timeStamp}`; const signature = createHash('sha256').update(signStr).digest('hex').toUpperCase(); return { 'x-app-key': APP_KEY, 'ETag': nonceStr, 'Date': timeStamp, 'Authorization': signature, 'Content-Type': 'application/json', 'Accept-Language': 'en_US', }; } // POST a JSON body to an /open/... path; returns the UnifyResponse `data`. async function call(path, body = {}) { const r = await fetch(`${BASE}${path}`, { method: 'POST', headers: authHeaders(), body: JSON.stringify(body), }); const text = await r.text(); let j; try { j = JSON.parse(text); } catch { throw new Error(`HTTP ${r.status}: ${text}`); } // UnifyResponse: msgCode 0 == success if (j.msgCode !== 0 && j.code !== 200) { throw new Error(`API error msgCode=${j.msgCode} code=${j.code}: ${j.message || text}`); } return j.data; } // List the telemetry devices bound to this account. function listDevices(pageNo = 1, pageSize = 20) { return call('/open/bluiotdata/device/telemetry/v1/userTelemetryDeviceList', { pageNo, pageSize }); } // Reported (telemetry) data for one device over a time window (timestamps in ms). function reportedData(deviceSn, beginTimestamp, endTimestamp, pageNo = 1, pageSize = 20) { return call('/open/bluiotdata/device/telemetry/v1/telemetryDeviceReportedData', { deviceSn, beginTimestamp, endTimestamp, pageNo, pageSize }); } const devices = await listDevices(); console.log('Devices:', JSON.stringify(devices, null, 2)); const now = Date.now(); const data = await reportedData(DEVICE_SN, now - 10 * 60 * 1000, now); console.log(`\nReported data for ${DEVICE_SN}:`); console.log(JSON.stringify(data, null, 2));