dbus-victron-virtual 0.1.3 → 0.1.4
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/package.json +1 -1
- package/src/__tests__/addSettingsTest.js +30 -0
- package/src/index.js +203 -163
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const { addSettings } = require("..");
|
|
2
|
+
|
|
3
|
+
describe("victron-dbus-virtual, addSettings tests, without calling addVictronInterfaces", () => {
|
|
4
|
+
it("works for the happy case", async () => {
|
|
5
|
+
const bus = {
|
|
6
|
+
invoke: function (args, cb) {
|
|
7
|
+
process.nextTick(() => cb(null, args));
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
const settingsResult = await addSettings(bus, [
|
|
11
|
+
{
|
|
12
|
+
path: "/Settings/MySettings/Setting",
|
|
13
|
+
default: 3,
|
|
14
|
+
min: 0,
|
|
15
|
+
max: 10,
|
|
16
|
+
},
|
|
17
|
+
]);
|
|
18
|
+
expect(settingsResult.member).toBe("AddSettings");
|
|
19
|
+
expect(settingsResult.path).toBe("/");
|
|
20
|
+
expect(settingsResult.interface).toBe("com.victronenergy.Settings");
|
|
21
|
+
expect(settingsResult.body).toStrictEqual([
|
|
22
|
+
[
|
|
23
|
+
[
|
|
24
|
+
["path", ["s", "/Settings/MySettings/Setting"]],
|
|
25
|
+
["default", ["i", 3]],
|
|
26
|
+
],
|
|
27
|
+
],
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
});
|
package/src/index.js
CHANGED
|
@@ -6,9 +6,182 @@ const products = {
|
|
|
6
6
|
temperature: 0xc060,
|
|
7
7
|
meteo: 0xc061,
|
|
8
8
|
grid: 0xc062,
|
|
9
|
-
tank: 0xc063
|
|
9
|
+
tank: 0xc063,
|
|
10
|
+
heatpump: 0xc064,
|
|
11
|
+
battery: 0xc065,
|
|
12
|
+
pvinverter: 0xc066,
|
|
10
13
|
};
|
|
11
14
|
|
|
15
|
+
function getType(value) {
|
|
16
|
+
return value === null
|
|
17
|
+
? "d"
|
|
18
|
+
: typeof value === "undefined"
|
|
19
|
+
? (() => {
|
|
20
|
+
throw new Error("Value cannot be undefined");
|
|
21
|
+
})()
|
|
22
|
+
: typeof value === "string"
|
|
23
|
+
? "s"
|
|
24
|
+
: typeof value === "number"
|
|
25
|
+
? isNaN(value)
|
|
26
|
+
? (() => {
|
|
27
|
+
throw new Error("NaN is not a valid input");
|
|
28
|
+
})()
|
|
29
|
+
: Number.isInteger(value)
|
|
30
|
+
? "i"
|
|
31
|
+
: "d"
|
|
32
|
+
: (() => {
|
|
33
|
+
throw new Error("Unsupported type: " + typeof value);
|
|
34
|
+
})();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function wrapValue(t, v) {
|
|
38
|
+
if (v === null) {
|
|
39
|
+
return ["ai", []];
|
|
40
|
+
}
|
|
41
|
+
switch (t) {
|
|
42
|
+
case "b":
|
|
43
|
+
return ["b", v];
|
|
44
|
+
case "s":
|
|
45
|
+
return ["s", v];
|
|
46
|
+
case "i":
|
|
47
|
+
return ["i", v];
|
|
48
|
+
case "d":
|
|
49
|
+
return ["d", v];
|
|
50
|
+
default:
|
|
51
|
+
return t.type ? wrapValue(t.type, v) : v;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function unwrapValue([t, v]) {
|
|
56
|
+
switch (t[0].type) {
|
|
57
|
+
case "b":
|
|
58
|
+
return !!v[0];
|
|
59
|
+
case "s":
|
|
60
|
+
return v[0];
|
|
61
|
+
case "i":
|
|
62
|
+
return Number(v[0]);
|
|
63
|
+
case "d":
|
|
64
|
+
return Number(v[0]);
|
|
65
|
+
case "ai":
|
|
66
|
+
if (v[0].length === 0) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
throw new Error(
|
|
70
|
+
'Unsupported value type "ai", only supported as empty array',
|
|
71
|
+
);
|
|
72
|
+
default:
|
|
73
|
+
throw new Error(`Unsupported value type: ${JSON.stringify(t)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function addSettings(bus, settings) {
|
|
78
|
+
const body = [
|
|
79
|
+
settings.map((setting) => [
|
|
80
|
+
["path", wrapValue("s", setting.path)],
|
|
81
|
+
[
|
|
82
|
+
"default",
|
|
83
|
+
wrapValue(
|
|
84
|
+
typeof setting.type !== "undefined"
|
|
85
|
+
? setting.type
|
|
86
|
+
: getType(setting.default),
|
|
87
|
+
setting.default,
|
|
88
|
+
),
|
|
89
|
+
],
|
|
90
|
+
// TODO: incomplete, min and max missing
|
|
91
|
+
]),
|
|
92
|
+
];
|
|
93
|
+
return await new Promise((resolve, reject) => {
|
|
94
|
+
bus.invoke(
|
|
95
|
+
{
|
|
96
|
+
interface: "com.victronenergy.Settings",
|
|
97
|
+
path: "/",
|
|
98
|
+
member: "AddSettings",
|
|
99
|
+
destination: "com.victronenergy.settings",
|
|
100
|
+
type: undefined,
|
|
101
|
+
signature: "aa{sv}",
|
|
102
|
+
body: body,
|
|
103
|
+
},
|
|
104
|
+
function (err, result) {
|
|
105
|
+
if (err) {
|
|
106
|
+
return reject(err);
|
|
107
|
+
}
|
|
108
|
+
return resolve(result);
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function removeSettings(bus, settings) {
|
|
115
|
+
const body = [settings.map((setting) => setting.path)];
|
|
116
|
+
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
bus.invoke(
|
|
119
|
+
{
|
|
120
|
+
interface: "com.victronenergy.Settings",
|
|
121
|
+
path: "/",
|
|
122
|
+
member: "RemoveSettings",
|
|
123
|
+
destination: "com.victronenergy.settings",
|
|
124
|
+
type: undefined,
|
|
125
|
+
signature: "as",
|
|
126
|
+
body: body,
|
|
127
|
+
},
|
|
128
|
+
function (err, result) {
|
|
129
|
+
if (err) {
|
|
130
|
+
return reject(err);
|
|
131
|
+
}
|
|
132
|
+
return resolve(result);
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function setValue(bus, { path, interface_, destination, value, type }) {
|
|
139
|
+
return await new Promise((resolve, reject) => {
|
|
140
|
+
if (path === "/DeviceInstance") {
|
|
141
|
+
console.warn(
|
|
142
|
+
"setValue called for path /DeviceInstance, this will be ignored by Victron services.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
bus.invoke(
|
|
146
|
+
{
|
|
147
|
+
interface: interface_,
|
|
148
|
+
path: path || "/",
|
|
149
|
+
member: "SetValue",
|
|
150
|
+
destination,
|
|
151
|
+
signature: "v",
|
|
152
|
+
body: [
|
|
153
|
+
wrapValue(typeof type !== "undefined" ? type : getType(value), value),
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
function (err, result) {
|
|
157
|
+
if (err) {
|
|
158
|
+
return reject(err);
|
|
159
|
+
}
|
|
160
|
+
resolve(result);
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function getValue(bus, { path, interface_, destination }) {
|
|
167
|
+
return await new Promise((resolve, reject) => {
|
|
168
|
+
bus.invoke(
|
|
169
|
+
{
|
|
170
|
+
interface: interface_,
|
|
171
|
+
path: path || "/",
|
|
172
|
+
member: "GetValue",
|
|
173
|
+
destination,
|
|
174
|
+
},
|
|
175
|
+
function (err, result) {
|
|
176
|
+
if (err) {
|
|
177
|
+
return reject(err);
|
|
178
|
+
}
|
|
179
|
+
resolve(result);
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
12
185
|
function addVictronInterfaces(
|
|
13
186
|
bus,
|
|
14
187
|
declaration,
|
|
@@ -34,11 +207,15 @@ function addVictronInterfaces(
|
|
|
34
207
|
debug("addDefaults, declaration.name:", declaration.name);
|
|
35
208
|
const productInName = declaration.name.split(".")[2];
|
|
36
209
|
if (!productInName) {
|
|
37
|
-
throw new Error(
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Unable to extract product from name, ensure name is of the form 'com.victronenergy.product.my_name', declaration.name=${declaration.name}`,
|
|
212
|
+
);
|
|
38
213
|
}
|
|
39
214
|
const product = products[productInName];
|
|
40
215
|
if (!product) {
|
|
41
|
-
throw new Error(
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Invalid product, ensure product name is in ${products.join(", ")}`,
|
|
218
|
+
);
|
|
42
219
|
}
|
|
43
220
|
declaration["properties"]["Mgmt/Connection"] = "s";
|
|
44
221
|
definition["Mgmt/Connection"] = "Virtual";
|
|
@@ -49,8 +226,7 @@ function addVictronInterfaces(
|
|
|
49
226
|
|
|
50
227
|
declaration["properties"]["ProductId"] = {
|
|
51
228
|
type: "i",
|
|
52
|
-
format: (/* v */) =>
|
|
53
|
-
product.toString(16),
|
|
229
|
+
format: (/* v */) => product.toString(16),
|
|
54
230
|
};
|
|
55
231
|
definition["ProductId"] = products[declaration["name"].split(".")[2]];
|
|
56
232
|
declaration["properties"]["ProductName"] = "s";
|
|
@@ -61,66 +237,26 @@ function addVictronInterfaces(
|
|
|
61
237
|
addDefaults();
|
|
62
238
|
}
|
|
63
239
|
|
|
64
|
-
function wrapValue(t, v) {
|
|
65
|
-
if (v === null) {
|
|
66
|
-
return ["ai", []];
|
|
67
|
-
}
|
|
68
|
-
switch (t) {
|
|
69
|
-
case "b":
|
|
70
|
-
return ["b", v];
|
|
71
|
-
case "s":
|
|
72
|
-
return ["s", v];
|
|
73
|
-
case "i":
|
|
74
|
-
return ["i", v];
|
|
75
|
-
case "d":
|
|
76
|
-
return ["d", v];
|
|
77
|
-
default:
|
|
78
|
-
return t.type ? wrapValue(t.type, v) : v;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function unwrapValue([t, v]) {
|
|
83
|
-
switch (t[0].type) {
|
|
84
|
-
case "b":
|
|
85
|
-
return !!v[0];
|
|
86
|
-
case "s":
|
|
87
|
-
return v[0];
|
|
88
|
-
case "i":
|
|
89
|
-
return Number(v[0]);
|
|
90
|
-
case "d":
|
|
91
|
-
return Number(v[0]);
|
|
92
|
-
case "ai":
|
|
93
|
-
if (v[0].length === 0) {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
throw new Error(
|
|
97
|
-
'Unsupported value type "ai", only supported as empty array',
|
|
98
|
-
);
|
|
99
|
-
default:
|
|
100
|
-
throw new Error(`Unsupported value type: ${JSON.stringify(t)}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
240
|
const getFormatFunction = (v) => {
|
|
105
|
-
if (v.format && typeof v.format ===
|
|
241
|
+
if (v.format && typeof v.format === "function") {
|
|
106
242
|
// Wrap the custom format function to ensure it always returns a string
|
|
107
243
|
return (value) => {
|
|
108
244
|
const formatted = v.format(value);
|
|
109
|
-
return formatted != null ? String(formatted) :
|
|
245
|
+
return formatted != null ? String(formatted) : "";
|
|
110
246
|
};
|
|
111
247
|
} else {
|
|
112
248
|
return (value) => {
|
|
113
|
-
if (value == null) return
|
|
249
|
+
if (value == null) return "";
|
|
114
250
|
|
|
115
251
|
let stringValue = String(value);
|
|
116
252
|
|
|
117
253
|
// Handle potential type mismatches
|
|
118
254
|
switch (v.type) {
|
|
119
|
-
case
|
|
120
|
-
return isNaN(parseFloat(stringValue)) ?
|
|
121
|
-
case
|
|
122
|
-
return isNaN(parseInt(stringValue, 10)) ?
|
|
123
|
-
case
|
|
255
|
+
case "d": // double/float
|
|
256
|
+
return isNaN(parseFloat(stringValue)) ? "" : stringValue;
|
|
257
|
+
case "i": // integer
|
|
258
|
+
return isNaN(parseInt(stringValue, 10)) ? "" : stringValue;
|
|
259
|
+
case "s": // string
|
|
124
260
|
return stringValue;
|
|
125
261
|
default:
|
|
126
262
|
return stringValue;
|
|
@@ -145,17 +281,6 @@ function addVictronInterfaces(
|
|
|
145
281
|
});
|
|
146
282
|
}
|
|
147
283
|
|
|
148
|
-
function getType(value) {
|
|
149
|
-
return value === null ? 'd'
|
|
150
|
-
: typeof value === 'undefined' ? (() => { throw new Error('Value cannot be undefined'); })()
|
|
151
|
-
: typeof value === 'string' ? 's'
|
|
152
|
-
: typeof value === 'number'
|
|
153
|
-
? (isNaN(value)
|
|
154
|
-
? (() => { throw new Error('NaN is not a valid input'); })()
|
|
155
|
-
: Number.isInteger(value) ? 'i' : 'd')
|
|
156
|
-
: (() => { throw new Error('Unsupported type: ' + typeof value); })();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
284
|
const iface = {
|
|
160
285
|
GetItems: function () {
|
|
161
286
|
return getProperties(true);
|
|
@@ -166,7 +291,7 @@ function addVictronInterfaces(
|
|
|
166
291
|
return [k.replace(/^(?!\/)/, "/"), wrapValue(v, definition[k])];
|
|
167
292
|
});
|
|
168
293
|
},
|
|
169
|
-
emit: function () {},
|
|
294
|
+
emit: function () { },
|
|
170
295
|
};
|
|
171
296
|
|
|
172
297
|
const ifaceDesc = {
|
|
@@ -224,107 +349,22 @@ function addVictronInterfaces(
|
|
|
224
349
|
);
|
|
225
350
|
}
|
|
226
351
|
|
|
227
|
-
async function addSettings(settings) {
|
|
228
|
-
const body = [
|
|
229
|
-
settings.map((setting) => [
|
|
230
|
-
["path", wrapValue("s", setting.path)],
|
|
231
|
-
["default", wrapValue(typeof setting.type !== 'undefined' ? setting.type: getType(setting.default), setting.default)],
|
|
232
|
-
// TODO: incomplete, min and max missing
|
|
233
|
-
]),
|
|
234
|
-
];
|
|
235
|
-
return await new Promise((resolve, reject) => {
|
|
236
|
-
bus.invoke(
|
|
237
|
-
{
|
|
238
|
-
interface: "com.victronenergy.Settings",
|
|
239
|
-
path: "/",
|
|
240
|
-
member: "AddSettings",
|
|
241
|
-
destination: "com.victronenergy.settings",
|
|
242
|
-
type: undefined,
|
|
243
|
-
signature: "aa{sv}",
|
|
244
|
-
body: body,
|
|
245
|
-
},
|
|
246
|
-
function (err, result) {
|
|
247
|
-
if (err) {
|
|
248
|
-
return reject(err);
|
|
249
|
-
}
|
|
250
|
-
return resolve(result);
|
|
251
|
-
},
|
|
252
|
-
);
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async function removeSettings(settings) {
|
|
257
|
-
const body = [settings.map((setting) => setting.path)];
|
|
258
|
-
|
|
259
|
-
return new Promise((resolve, reject) => {
|
|
260
|
-
bus.invoke(
|
|
261
|
-
{
|
|
262
|
-
interface: "com.victronenergy.Settings",
|
|
263
|
-
path: "/",
|
|
264
|
-
member: "RemoveSettings",
|
|
265
|
-
destination: "com.victronenergy.settings",
|
|
266
|
-
type: undefined,
|
|
267
|
-
signature: "as",
|
|
268
|
-
body: body,
|
|
269
|
-
},
|
|
270
|
-
function (err, result) {
|
|
271
|
-
if (err) {
|
|
272
|
-
return reject(err);
|
|
273
|
-
}
|
|
274
|
-
return resolve(result);
|
|
275
|
-
},
|
|
276
|
-
);
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
async function setValue({ path, interface_, destination, value, type }) {
|
|
281
|
-
return await new Promise((resolve, reject) => {
|
|
282
|
-
bus.invoke(
|
|
283
|
-
{
|
|
284
|
-
interface: interface_,
|
|
285
|
-
path: path || "/",
|
|
286
|
-
member: "SetValue",
|
|
287
|
-
destination,
|
|
288
|
-
signature: "v",
|
|
289
|
-
body: [wrapValue(typeof type !== 'undefined' ? type: getType(value), value)],
|
|
290
|
-
},
|
|
291
|
-
function (err, result) {
|
|
292
|
-
if (err) {
|
|
293
|
-
return reject(err);
|
|
294
|
-
}
|
|
295
|
-
resolve(result);
|
|
296
|
-
},
|
|
297
|
-
);
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
async function getValue({ path, interface_, destination }) {
|
|
302
|
-
return await new Promise((resolve, reject) => {
|
|
303
|
-
bus.invoke(
|
|
304
|
-
{
|
|
305
|
-
interface: interface_,
|
|
306
|
-
path: path || "/",
|
|
307
|
-
member: "GetValue",
|
|
308
|
-
destination,
|
|
309
|
-
},
|
|
310
|
-
function (err, result) {
|
|
311
|
-
if (err) {
|
|
312
|
-
return reject(err);
|
|
313
|
-
}
|
|
314
|
-
resolve(result);
|
|
315
|
-
},
|
|
316
|
-
);
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
|
|
320
352
|
return {
|
|
321
353
|
emitItemsChanged: () => iface.emit("ItemsChanged", getProperties()),
|
|
322
|
-
addSettings,
|
|
323
|
-
removeSettings,
|
|
324
|
-
setValue,
|
|
325
|
-
|
|
354
|
+
addSettings: (settings) => addSettings(bus, settings),
|
|
355
|
+
removeSettings: (settings) => removeSettings(bus, settings),
|
|
356
|
+
setValue: ({ path, interface_, destination, value, type }) =>
|
|
357
|
+
setValue(bus, { path, interface_, destination, value, type }),
|
|
358
|
+
getValue: ({ path, interface_, destination }) =>
|
|
359
|
+
getValue(bus, { path, interface_, destination }),
|
|
326
360
|
warnings,
|
|
327
361
|
};
|
|
328
362
|
}
|
|
329
363
|
|
|
330
|
-
module.exports = {
|
|
364
|
+
module.exports = {
|
|
365
|
+
addVictronInterfaces,
|
|
366
|
+
addSettings,
|
|
367
|
+
removeSettings,
|
|
368
|
+
getValue,
|
|
369
|
+
setValue,
|
|
370
|
+
};
|