@rhizomatics/signalk-bluetti-plugin 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -1
- package/README.md +4 -4
- package/cli.js +10 -10
- package/index.js +16 -26
- package/lib/path-mapper.js +1 -1
- package/package.json +2 -2
- /package/{registers → devices}/ac180.yaml +0 -0
- /package/{registers → devices}/ac180p.yaml +0 -0
- /package/{registers → devices}/ac180t.yaml +0 -0
- /package/{registers → devices}/ac200l.yaml +0 -0
- /package/{registers → devices}/ac200m.yaml +0 -0
- /package/{registers → devices}/ac200p.yaml +0 -0
- /package/{registers → devices}/ac200pl.yaml +0 -0
- /package/{registers → devices}/ac2a.yaml +0 -0
- /package/{registers → devices}/ac2p.yaml +0 -0
- /package/{registers → devices}/ac300.yaml +0 -0
- /package/{registers → devices}/ac500.yaml +0 -0
- /package/{registers → devices}/ac50b.yaml +0 -0
- /package/{registers → devices}/ac60.yaml +0 -0
- /package/{registers → devices}/ac60p.yaml +0 -0
- /package/{registers → devices}/ac70.yaml +0 -0
- /package/{registers → devices}/ac70p.yaml +0 -0
- /package/{registers → devices}/ap300.yaml +0 -0
- /package/{registers → devices}/eb3a.yaml +0 -0
- /package/{registers → devices}/el100v2.yaml +0 -0
- /package/{registers → devices}/el30v2.yaml +0 -0
- /package/{registers → devices}/ep2000.yaml +0 -0
- /package/{registers → devices}/ep500.yaml +0 -0
- /package/{registers → devices}/ep500p.yaml +0 -0
- /package/{registers → devices}/ep600.yaml +0 -0
- /package/{registers → devices}/ep760.yaml +0 -0
- /package/{registers → devices}/ep800.yaml +0 -0
- /package/{registers → devices}/handsfree1.yaml +0 -0
- /package/{registers → devices}/pr100v2.yaml +0 -0
- /package/{registers → devices}/pr30v2.yaml +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this plugin are documented here. Format loosely follows [Keep a Changelog](https://keepachangelog.com/).
|
|
4
4
|
|
|
5
|
+
## [1.5.1]
|
|
6
|
+
|
|
7
|
+
- Simplify the custom device configuration override
|
|
8
|
+
- 'Register Map' is now 'Device Configuration'
|
|
9
|
+
- Bundled register maps moved from `registers/` to `devices/`
|
|
10
|
+
|
|
5
11
|
## [1.5.0]
|
|
6
12
|
|
|
7
|
-
- Expanded Bluetti model support
|
|
13
|
+
- Expanded Bluetti model support, over 20 now with register mappings
|
|
8
14
|
- Configuration moved from CSV to YAML
|
|
9
15
|
- Configurable directory to add YAML files for unsupported Bluetti models
|
|
10
16
|
- CLI `models` command lists supported register maps with field/constant counts
|
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ node cli.js dump aa:bb:cc:dd:ee:ff --timeout 30 # discovery timeout if device
|
|
|
73
73
|
Connect to a device and decode its live register values the same way the plugin itself does — same GATT UUIDs, same encryption handshake (including the AES/ECDH handshake used by "V2"-protocol models like the EL100V2), same register parsing. Requires `--registers`. If `mac` is omitted, scans and connects to the first Bluetti-matching device found.
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
|
-
# Decode live registers using a bundled
|
|
76
|
+
# Decode live registers using a bundled device configuration (see devices/*.yaml for available models)
|
|
77
77
|
node cli.js info aa:bb:cc:dd:ee:ff --registers ac200p
|
|
78
78
|
node cli.js info --registers ac200p # scan and use the first Bluetti device found
|
|
79
79
|
|
|
@@ -86,7 +86,7 @@ node cli.js info aa:bb:cc:dd:ee:ff --registers ac200p --timeout 30 # discovery
|
|
|
86
86
|
|
|
87
87
|
### `models`
|
|
88
88
|
|
|
89
|
-
List every
|
|
89
|
+
List every device configuration the CLI (and the plugin) can find — bundled ones plus anything dropped into the custom directory (`~/.signalk/bluetti` by default) — with a field and constant count per model. No device connection needed.
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
92
|
node cli.js models
|
|
@@ -140,9 +140,9 @@ This tells systemd to start `bluetoothd` first and wait for it before starting S
|
|
|
140
140
|
|
|
141
141
|
### My station isn't supported
|
|
142
142
|
|
|
143
|
-
Additional products can be added using a YAML file mapping modbus registers and providing static values — see [`docs/examples/model_definition.yaml`](docs/examples/model_definition.yaml) in the repo for an annotated template, and `
|
|
143
|
+
Additional products can be added using a YAML file mapping modbus registers and providing static values — see [`docs/examples/model_definition.yaml`](docs/examples/model_definition.yaml) in the repo for an annotated template, and `devices/*.yaml` for real examples.
|
|
144
144
|
|
|
145
|
-
Drop your file into the plugin's **Custom
|
|
145
|
+
Drop your file into the plugin's **Custom Device Configuration Directory** (a plugin setting, defaulting to `~/.signalk/bluetti`) and it'll appear in the **Device Configuration** dropdown for a device, named after the file (without `.yaml`). A file there takes priority over a bundled one of the same name, so it also doubles as a way to override a built-in map.
|
|
146
146
|
|
|
147
147
|
Check one of these projects to see if some of the mapping work has been done:
|
|
148
148
|
|
package/cli.js
CHANGED
|
@@ -5,11 +5,11 @@ const fs = require("fs");
|
|
|
5
5
|
const os = require("os");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const DEVICES_DIR = path.join(__dirname, "devices");
|
|
9
9
|
// Same default the plugin itself uses when running under a real SignalK
|
|
10
10
|
// server (see index.js) — lets a user's custom register maps be found by the
|
|
11
11
|
// CLI too, without needing to pass --registers-dir.
|
|
12
|
-
const
|
|
12
|
+
const DEFAULT_USER_DEVICES_DIR = path.join(os.homedir(), ".signalk", "bluetti");
|
|
13
13
|
|
|
14
14
|
function listModels(dir) {
|
|
15
15
|
try {
|
|
@@ -24,7 +24,7 @@ function listModels(dir) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function listBuiltinModels() {
|
|
27
|
-
return [...new Set([...listModels(
|
|
27
|
+
return [...new Set([...listModels(DEVICES_DIR), ...listModels(DEFAULT_USER_DEVICES_DIR)])].sort((a, b) => a.localeCompare(b));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// One row per model, checking the custom directory before the bundled one so
|
|
@@ -35,8 +35,8 @@ function listModelDetails() {
|
|
|
35
35
|
|
|
36
36
|
return listBuiltinModels().map((model) => {
|
|
37
37
|
for (const [dir, source] of [
|
|
38
|
-
[
|
|
39
|
-
[
|
|
38
|
+
[DEFAULT_USER_DEVICES_DIR, "custom"],
|
|
39
|
+
[DEVICES_DIR, "built-in"],
|
|
40
40
|
]) {
|
|
41
41
|
for (const ext of [".yaml", ".yml"]) {
|
|
42
42
|
const p = path.join(dir, `${model}${ext}`);
|
|
@@ -79,12 +79,12 @@ Commands:
|
|
|
79
79
|
Bluetti device found.
|
|
80
80
|
--registers <model|path> Built-in model (${builtins.join(", ") || "none bundled"}) or a register map
|
|
81
81
|
YAML path (required). Also checks
|
|
82
|
-
${
|
|
82
|
+
${DEFAULT_USER_DEVICES_DIR} for custom models.
|
|
83
83
|
--encryption-key <path> Path to the Bluetti-provided encryption CSV
|
|
84
84
|
(only needed for legacy XOR-scrambled models)
|
|
85
85
|
--timeout <seconds> Discovery/scan timeout if not already known (default: 20)
|
|
86
86
|
|
|
87
|
-
models List supported
|
|
87
|
+
models List supported device configurations, with a field and
|
|
88
88
|
constant count per model
|
|
89
89
|
|
|
90
90
|
help Show this help
|
|
@@ -180,7 +180,7 @@ async function findDevice(adapter, mac, timeoutMs) {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
function resolveRegistersArg(value) {
|
|
183
|
-
for (const dir of [
|
|
183
|
+
for (const dir of [DEFAULT_USER_DEVICES_DIR, DEVICES_DIR]) {
|
|
184
184
|
for (const ext of [".yaml", ".yml"]) {
|
|
185
185
|
const p = path.join(dir, `${value}${ext}`);
|
|
186
186
|
if (fs.existsSync(p)) return p;
|
|
@@ -188,7 +188,7 @@ function resolveRegistersArg(value) {
|
|
|
188
188
|
}
|
|
189
189
|
if (fs.existsSync(value)) return value;
|
|
190
190
|
const builtins = listBuiltinModels();
|
|
191
|
-
throw new Error(`
|
|
191
|
+
throw new Error(`Device configuration not found: "${value}" (not a built-in model [${builtins.join(", ")}] or an existing file path)`);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
async function printDeviceSummary(bleDevice, mac) {
|
|
@@ -406,7 +406,7 @@ async function cmdInfo(args) {
|
|
|
406
406
|
function cmdModels() {
|
|
407
407
|
const rows = listModelDetails();
|
|
408
408
|
if (rows.length === 0) {
|
|
409
|
-
console.log(`No
|
|
409
|
+
console.log(`No device configurations found (checked ${DEVICES_DIR} and ${DEFAULT_USER_DEVICES_DIR}).`);
|
|
410
410
|
return;
|
|
411
411
|
}
|
|
412
412
|
printTable(rows, ["model", "fields", "constants", "source"]);
|
package/index.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require("path");
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
|
|
7
7
|
const PLUGIN_ID = "signalk-bluetti-plugin";
|
|
8
|
-
const
|
|
8
|
+
const DEVICES_DIR = path.join(__dirname, "devices");
|
|
9
9
|
|
|
10
10
|
// Bluetti device BLE name prefixes (mirrors scanner.js).
|
|
11
11
|
const BLUETTI_PREFIXES = ["BT-TH-", "BLUETTI", "AC", "EP", "EB", "EL", "PR", "AP"];
|
|
@@ -38,7 +38,7 @@ function yamlModelNames(dir) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
// The directory a user can drop their own
|
|
41
|
+
// The directory a user can drop their own device configuration YAML files into,
|
|
42
42
|
// without needing to fork/publish the plugin — defaults to a `bluetti`
|
|
43
43
|
// subdirectory of the SignalK home directory (e.g. ~/.signalk/bluetti).
|
|
44
44
|
// `app.config.configPath` isn't part of the documented plugin API but is the
|
|
@@ -57,7 +57,7 @@ module.exports = function (app) {
|
|
|
57
57
|
let waitingStatusTimer = null;
|
|
58
58
|
|
|
59
59
|
const defaultUserDir = defaultUserRegistersDir(app);
|
|
60
|
-
const builtins = yamlModelNames(
|
|
60
|
+
const builtins = yamlModelNames(DEVICES_DIR);
|
|
61
61
|
// Models available for the dropdown at schema-render time — bundled plus
|
|
62
62
|
// whatever's already sitting in the default user directory. If `registersDir`
|
|
63
63
|
// is overridden away from the default, models unique to that directory won't
|
|
@@ -83,13 +83,13 @@ module.exports = function (app) {
|
|
|
83
83
|
scanOnStart: {
|
|
84
84
|
type: "boolean",
|
|
85
85
|
title: "Scan for new devices on plugin start",
|
|
86
|
-
description: "Runs a
|
|
86
|
+
description: "Runs a short BLE scan and logs discovered Bluetti devices. Useful for finding device addresses.",
|
|
87
87
|
default: true,
|
|
88
88
|
},
|
|
89
89
|
registersDir: {
|
|
90
90
|
type: "string",
|
|
91
|
-
title: "Custom
|
|
92
|
-
description: `Directory to scan for your own
|
|
91
|
+
title: "Custom Device Configuration Directory",
|
|
92
|
+
description: `Directory to scan for your own device configuration YAML files (e.g. for a model this plugin doesn't bundle yet), in addition to the ones built in. Leave blank to use the default: ${defaultUserDir || "<SignalK home>/bluetti"}.`,
|
|
93
93
|
default: "",
|
|
94
94
|
},
|
|
95
95
|
devices: {
|
|
@@ -117,17 +117,11 @@ module.exports = function (app) {
|
|
|
117
117
|
},
|
|
118
118
|
builtinModel: {
|
|
119
119
|
type: "string",
|
|
120
|
-
title: "
|
|
120
|
+
title: "Device Configuration",
|
|
121
121
|
description:
|
|
122
|
-
|
|
123
|
-
enum:
|
|
124
|
-
default: allModels.length > 0 ? allModels[0] : "
|
|
125
|
-
},
|
|
126
|
-
registerMapPath: {
|
|
127
|
-
type: "string",
|
|
128
|
-
title: "Custom register map YAML path",
|
|
129
|
-
description: 'Absolute path to a register map YAML file. Only used when "custom" is selected above.',
|
|
130
|
-
default: "",
|
|
122
|
+
"Select a device configuration for your model — bundled with the plugin, or dropped into the custom device configuration directory above.",
|
|
123
|
+
enum: allModels,
|
|
124
|
+
default: allModels.length > 0 ? allModels[0] : "",
|
|
131
125
|
},
|
|
132
126
|
encryptionCsvPath: {
|
|
133
127
|
type: "string",
|
|
@@ -154,7 +148,6 @@ module.exports = function (app) {
|
|
|
154
148
|
items: {
|
|
155
149
|
address: { "ui:placeholder": "aa:bb:cc:dd:ee:ff" },
|
|
156
150
|
name: { "ui:placeholder": "house" },
|
|
157
|
-
registerMapPath: { "ui:placeholder": "e.g. /path/to/my-device-registers.yaml" },
|
|
158
151
|
encryptionCsvPath: { "ui:placeholder": "e.g. /path/to/19e1646709e0421b755fa9dda74.csv" },
|
|
159
152
|
},
|
|
160
153
|
},
|
|
@@ -180,7 +173,7 @@ module.exports = function (app) {
|
|
|
180
173
|
try {
|
|
181
174
|
fs.mkdirSync(userRegistersDir, { recursive: true });
|
|
182
175
|
} catch (err) {
|
|
183
|
-
log(`Could not create custom
|
|
176
|
+
log(`Could not create custom device configuration directory "${userRegistersDir}": ${err.message}`);
|
|
184
177
|
}
|
|
185
178
|
}
|
|
186
179
|
|
|
@@ -298,7 +291,7 @@ module.exports = function (app) {
|
|
|
298
291
|
// Finds <model>.yaml (or .yml), checking the custom directory before the
|
|
299
292
|
// bundled one so a user's own file can override a built-in of the same name.
|
|
300
293
|
function findModelFile(model, userRegistersDir) {
|
|
301
|
-
const dirs = [userRegistersDir,
|
|
294
|
+
const dirs = [userRegistersDir, DEVICES_DIR].filter(Boolean);
|
|
302
295
|
for (const dir of dirs) {
|
|
303
296
|
for (const ext of [".yaml", ".yml"]) {
|
|
304
297
|
const p = path.join(dir, `${model}${ext}`);
|
|
@@ -309,15 +302,12 @@ module.exports = function (app) {
|
|
|
309
302
|
}
|
|
310
303
|
|
|
311
304
|
function resolveRegisterMapPath(cfg, userRegistersDir) {
|
|
312
|
-
const { builtinModel
|
|
313
|
-
if (!builtinModel
|
|
314
|
-
if (!registerMapPath) throw new Error("No register map: select a model or provide a custom YAML path");
|
|
315
|
-
return registerMapPath;
|
|
316
|
-
}
|
|
305
|
+
const { builtinModel } = cfg;
|
|
306
|
+
if (!builtinModel) throw new Error("No device configuration: select a model");
|
|
317
307
|
const found = findModelFile(builtinModel, userRegistersDir);
|
|
318
308
|
if (!found)
|
|
319
309
|
throw new Error(
|
|
320
|
-
`
|
|
310
|
+
`Device configuration "${builtinModel}" not found (checked ${userRegistersDir ? `${userRegistersDir} and ` : ""}${DEVICES_DIR})`,
|
|
321
311
|
);
|
|
322
312
|
return found;
|
|
323
313
|
}
|
|
@@ -339,7 +329,7 @@ module.exports = function (app) {
|
|
|
339
329
|
fields = loadRegisters(registerPath);
|
|
340
330
|
log(`[${name}] Loaded ${fields.length} registers from ${registerPath}`);
|
|
341
331
|
} catch (err) {
|
|
342
|
-
app.setPluginError(`[${name}] Failed to load
|
|
332
|
+
app.setPluginError(`[${name}] Failed to load device configuration "${registerPath}": ${err.message}`);
|
|
343
333
|
return;
|
|
344
334
|
}
|
|
345
335
|
|
package/lib/path-mapper.js
CHANGED
|
@@ -216,7 +216,7 @@ function buildDelta(registers, fields, deviceName, source, opts = {}) {
|
|
|
216
216
|
|
|
217
217
|
// Remaining capacity = nominal capacity × state of charge. Bluetti's own
|
|
218
218
|
// "remaining capacity" register drifts with cell aging and isn't available
|
|
219
|
-
// on every model; deriving it from a fixed nominal spec (see
|
|
219
|
+
// on every model; deriving it from a fixed nominal spec (see devices/*.yaml)
|
|
220
220
|
// plus the reported SoC is simpler and consistent across models.
|
|
221
221
|
const soc = cache.has("battery_soc") ? cache.get("battery_soc") : cache.has("battery_percent") ? cache.get("battery_percent") : null;
|
|
222
222
|
if (soc !== null && cache.has("total_capacity")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhizomatics/signalk-bluetti-plugin",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "SignalK plugin for Bluetti power station monitoring via Bluetooth LE. Comes with configuration for 20+ models.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"battery",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"lib/",
|
|
36
36
|
"docs/assets/",
|
|
37
37
|
"docs/examples/",
|
|
38
|
-
"
|
|
38
|
+
"devices/",
|
|
39
39
|
"README.md",
|
|
40
40
|
"CHANGELOG.md"
|
|
41
41
|
],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|