Cloud version, requires approval
This commit is contained in:
@@ -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 SHA‑256 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.
|
||||
@@ -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));
|
||||
Reference in New Issue
Block a user