77 lines
3.1 KiB
JavaScript
77 lines
3.1 KiB
JavaScript
// 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=<k>&appSecret=<s>&nonceStr=<n>&timeStamp=<t>" )
|
|
// sent with headers: x-app-key=<k>, ETag=<nonceStr>, Date=<timeStamp(sec)>.
|
|
// 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));
|