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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dbus-victron-virtual",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Add interoperability with victron dbus to a given dbus interface",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
@@ -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(`Unable to extract product from name, ensure name is of the form 'com.victronenergy.product.my_name', declaration.name=${declaration.name}`);
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(`Invalid product, ensure product name is in ${products.join(", ")}`);
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 === 'function') {
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 'd': // double/float
120
- return isNaN(parseFloat(stringValue)) ? '' : stringValue;
121
- case 'i': // integer
122
- return isNaN(parseInt(stringValue, 10)) ? '' : stringValue;
123
- case 's': // string
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
- getValue,
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 = { addVictronInterfaces };
364
+ module.exports = {
365
+ addVictronInterfaces,
366
+ addSettings,
367
+ removeSettings,
368
+ getValue,
369
+ setValue,
370
+ };