Cloud version, requires approval

This commit is contained in:
2026-06-04 22:48:26 +10:00
parent 890feae060
commit 71fc3abb90
2 changed files with 136 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
# Bluetti Cloud (Open Platform) client
A tiny Node.js client for reading device telemetry from Bluetti's **cloud** Open
Platform API. Use this for the **newer encrypted models** (Elite 300 / EL300, V2,
EP600, AC180, AC200L …) where the local BLE channel is locked — the cloud path
works today and needs no AES key.
> This is *not* the ESP32 library — it's a separate, optional helper that talks to
> Bluetti's cloud over HTTPS. For older/plaintext models read locally with
> `BluettiBLE`; `BluettiADV` will cover newer models locally once Bluetti exposes
> the broadcast AES key.
Implemented from the official spec at <https://open.bluetti.com/developers>
(`basePath: https://open.bluetti.com/open`).
## Auth: signed requests (not OAuth)
Each request carries four headers; the **AppSecret is hashed in, never sent**:
```
Authorization = SHA-256( "appKey=<AppKey>&appSecret=<AppSecret>&nonceStr=<nonce>&timeStamp=<unixSeconds>" )
x-app-key: <AppKey>
ETag: <nonce> # random hex, new per request
Date: <unixSeconds> # must match the timeStamp used in the hash
```
## Setup
1. Register at <https://open.bluetti.com/developers>, create an application to get
an **AppKey** and **AppSecret**, and **apply for the IoT/telemetry API
permission**. Your device SN must also be authorized to the account, or the
telemetry endpoints return a permission error even with a valid signature.
2. Run it (Node 18+, no `npm install`):
```bash
BLUETTI_APPKEY=xxx BLUETTI_APPSECRET=yyy BLUETTI_SN=EL3002546110146262 node bluetti.mjs
```
## Endpoints used
| Purpose | Path (POST) | Body |
|---|---|---|
| List devices | `/open/bluiotdata/device/telemetry/v1/userTelemetryDeviceList` | `{pageNo, pageSize}` |
| Reported data | `/open/bluiotdata/device/telemetry/v1/telemetryDeviceReportedData` | `{deviceSn, beginTimestamp, endTimestamp, pageNo, pageSize}` (ms) |
| Control (set) | `/open/bluiotdata/device/telemetry/v1/telemetryDeviceSetUp` | `{deviceSn, functionCode, setValue}` |
Responses use the `UnifyResponse` envelope — `{ code, msgCode, message, data, ... }`,
where `msgCode == 0` means success. `data` for reported data is a paginated list
(`content[]`) of `key→value` telemetry maps.
## When it works / doesn't
- **403 / permission error** → your app lacks the telemetry permission, or the SN
isn't authorized to your account. Apply on the developer portal.
- **Signature failure** → check that the `Date` header equals the `timeStamp`
string used inside the SHA256 input, and that you hashed the literal
`appKey=…&appSecret=…&nonceStr=…&timeStamp=…` string.
Once you can see a sample telemetry response, paste it back and the script can be
tightened into a poller that pulls out SoC/power and logs on change.
+76
View File
@@ -0,0 +1,76 @@
// 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));