dbus-victron-virtual 0.1.0
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/.husky/pre-commit +2 -0
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/eslint.config.mjs +10 -0
- package/package.json +38 -0
- package/src/__tests__/dummyTest.js +5 -0
- package/src/__tests__/emitItemsChangedTest.js +31 -0
- package/src/__tests__/getValueTest.js +57 -0
- package/src/__tests__/inputsTest.js +94 -0
- package/src/__tests__/integrationTest.js +162 -0
- package/src/__tests__/setValueTest.js +121 -0
- package/src/index.js +213 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Victron Energy BV
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# About
|
|
2
|
+
|
|
3
|
+
`dbus-victron-virtual` is a wrapper around
|
|
4
|
+
[dbus-native](https://www.npmjs.com/package/dbus-native), which allows you to
|
|
5
|
+
connect to [dbus](https://www.freedesktop.org/wiki/Software/dbus/), and
|
|
6
|
+
simplify integrating with the [Victron](https://www.victronenergy.com/)
|
|
7
|
+
infrastructure: To do this, `dbus-victron-virtual` provides functions to
|
|
8
|
+
|
|
9
|
+
* expose your dbus interface as a Victron service, by implementing the dbus interface `com.victronenergy.BusItem`,
|
|
10
|
+
* emit the Victron-specific event `ItemsChanged`, and
|
|
11
|
+
* define and modify settings which are then available through Victron's settings interface.
|
|
12
|
+
|
|
13
|
+
See `dbus-victron-virtual` in action [here](https://github.com/Chris927/dbus-victron-virtual-test).
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
This package may be for you if
|
|
17
|
+
|
|
18
|
+
* you want to define virtual devices for testing on a Victron device, like a [Victron Cerbo GX](https://www.victronenergy.com/media/pg/Cerbo_GX/en/index-en.html), e.g. to use it in [Node-RED](https://www.victronenergy.com/live/venus-os:large), or
|
|
19
|
+
* you need to integrate a device via dbus that is not (yet) supported by Victron natively.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Usage
|
|
23
|
+
|
|
24
|
+
(TODO)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Development
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Prerequisites
|
|
31
|
+
|
|
32
|
+
You can develop on a device that runs [Venus OS](https://github.com/victronenergy/venus). This way, the dbus environment as required by this package will be available. Alternatively, you can develop in any environment that support node 18 or higher, but you won't be able to run integration tests.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## Steps
|
|
36
|
+
|
|
37
|
+
* clone the repository
|
|
38
|
+
* `npm install`
|
|
39
|
+
* `npm run test`
|
|
40
|
+
* run integration tests with `DBUS_SESSION_BUS_ADDRESS=unix:socket=/var/run/dbus/system_bus_socket TEST_INTEGRATION=1 npm run test`
|
|
41
|
+
|
|
42
|
+
The implementation is in `./src/index.js`, tests are in `./src/__tests__`.
|
|
43
|
+
|
|
44
|
+
Test coverage stats, when run with the integration `./src/__tests__/integrationTest.js`:
|
|
45
|
+
|
|
46
|
+
----------|---------|----------|---------|---------|---------------------------
|
|
47
|
+
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
|
|
48
|
+
----------|---------|----------|---------|---------|---------------------------
|
|
49
|
+
All files | 85.18 | 69.23 | 100 | 84.9 |
|
|
50
|
+
index.js | 85.18 | 69.23 | 100 | 84.9 | 28,35,39-41,94-95,130,170
|
|
51
|
+
----------|---------|----------|---------|---------|---------------------------
|
|
52
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import globals from "globals";
|
|
2
|
+
import pluginJs from "@eslint/js";
|
|
3
|
+
import jest from "eslint-plugin-jest"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export default [
|
|
7
|
+
{ languageOptions: { sourceType: 'commonjs', globals: { ...globals.browser, ...globals.node, ...globals.jest } } },
|
|
8
|
+
pluginJs.configs.recommended,
|
|
9
|
+
{ files: ["src/__tests__/*.js"], plugins: { jest: jest } }
|
|
10
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dbus-victron-virtual",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Add interoperability with victron dbus to a given dbus interface",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "jest",
|
|
9
|
+
"lint": "eslint 'src/**/*.js'",
|
|
10
|
+
"prepare": "husky"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/Chris927/dbus-victron-virtual.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"dbus",
|
|
18
|
+
"victron"
|
|
19
|
+
],
|
|
20
|
+
"author": "chris@uber5.com",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/Chris927/dbus-victron-virtual/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/Chris927/dbus-victron-virtual#readme",
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@eslint/js": "^9.6.0",
|
|
28
|
+
"eslint": "^9.6.0",
|
|
29
|
+
"eslint-plugin-jest": "^28.6.0",
|
|
30
|
+
"globals": "^15.7.0",
|
|
31
|
+
"husky": "^9.0.11",
|
|
32
|
+
"jest": "^29.7.0",
|
|
33
|
+
"lint-staged": "^15.2.7"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"dbus-native": "^0.4.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* eslint-env node */
|
|
2
|
+
const { addVictronInterfaces } = require('../index');
|
|
3
|
+
|
|
4
|
+
describe('victron-dbus-virtual, emitItemsChanged tests', () => {
|
|
5
|
+
|
|
6
|
+
const noopBus = { exportInterface: () => { } };
|
|
7
|
+
|
|
8
|
+
it('works for the case without props', () => {
|
|
9
|
+
const declaration = { name: 'foo' };
|
|
10
|
+
const definition = {};
|
|
11
|
+
const { emitItemsChanged } = addVictronInterfaces(noopBus, declaration, definition);
|
|
12
|
+
emitItemsChanged();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('works for the happy case', () => {
|
|
16
|
+
const declaration = { name: 'foo', properties: { StringProp: 's' } };
|
|
17
|
+
const definition = { StringProp: 'hello' };
|
|
18
|
+
const emit = jest.fn();
|
|
19
|
+
const bus = {
|
|
20
|
+
exportInterface: (iface /* , _path, _ifaceDesc */) => {
|
|
21
|
+
iface.emit = emit;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const { emitItemsChanged } = addVictronInterfaces(bus, declaration, definition);
|
|
25
|
+
emitItemsChanged();
|
|
26
|
+
expect(emit.mock.calls.length).toBe(1);
|
|
27
|
+
expect(emit.mock.calls[0][0]).toBe('ItemsChanged');
|
|
28
|
+
expect(emit.mock.calls[0][1]).toEqual([['StringProp', [['Value', ['s', 'hello']], ['Text', ['s', 'hello']]]]]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const { addVictronInterfaces } = require('../index');
|
|
2
|
+
|
|
3
|
+
describe('victron-dbus-virtual, getValue tests', () => {
|
|
4
|
+
|
|
5
|
+
it('works for the happy case', async () => {
|
|
6
|
+
const declaration = { name: 'foo' };
|
|
7
|
+
const definition = {};
|
|
8
|
+
const bus = {
|
|
9
|
+
exportInterface: () => { },
|
|
10
|
+
invoke: function(args, cb) {
|
|
11
|
+
process.nextTick(() => cb(null, args));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const { getValue } = addVictronInterfaces(bus, declaration, definition);
|
|
15
|
+
|
|
16
|
+
// NOTE: calling getValue() is useful to retrieve the value of a setting.
|
|
17
|
+
// See https://github.com/Chris927/dbus-victron-virtual-test/blob/master/index.js for an example.
|
|
18
|
+
|
|
19
|
+
const result = await getValue({
|
|
20
|
+
path: '/StringProp',
|
|
21
|
+
interface_: 'foo',
|
|
22
|
+
destination: 'foo'
|
|
23
|
+
});
|
|
24
|
+
expect(result.member).toBe('GetValue');
|
|
25
|
+
expect(result.path).toBe('/StringProp');
|
|
26
|
+
expect(result.interface).toBe('foo');
|
|
27
|
+
expect(result.destination).toBe('foo');
|
|
28
|
+
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('fails if invoke fails', async () => {
|
|
32
|
+
const declaration = { name: 'foo' };
|
|
33
|
+
const definition = {};
|
|
34
|
+
const bus = {
|
|
35
|
+
exportInterface: () => { },
|
|
36
|
+
invoke: function(args, cb) {
|
|
37
|
+
process.nextTick(() => cb(new Error('oops')));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const { getValue } = addVictronInterfaces(bus, declaration, definition);
|
|
41
|
+
|
|
42
|
+
// NOTE: calling getValue() is useful to retrieve the value of a setting.
|
|
43
|
+
// See https://github.com/Chris927/dbus-victron-virtual-test/blob/master/index.js for an example.
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await getValue({
|
|
47
|
+
path: '/StringProp',
|
|
48
|
+
interface_: 'foo',
|
|
49
|
+
destination: 'foo'
|
|
50
|
+
});
|
|
51
|
+
expect(false, 'should have thrown');
|
|
52
|
+
} catch (err) {
|
|
53
|
+
expect(err.message).toBe('oops');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/* eslint-env node */
|
|
2
|
+
const { addVictronInterfaces } = require('../index');
|
|
3
|
+
|
|
4
|
+
describe('victron-dbus-virtual, input parameters tests', () => {
|
|
5
|
+
|
|
6
|
+
const noopBus = { exportInterface: () => { } };
|
|
7
|
+
|
|
8
|
+
it('works for the trivial case', () => {
|
|
9
|
+
const declaration = { name: 'foo' };
|
|
10
|
+
const definition = {};
|
|
11
|
+
const result = addVictronInterfaces(noopBus, declaration, definition);
|
|
12
|
+
expect(!!result).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('fails in some scenarios', () => {
|
|
16
|
+
try {
|
|
17
|
+
addVictronInterfaces(noopBus, {}, {});
|
|
18
|
+
} catch (e) {
|
|
19
|
+
expect(e.message.includes('Interface name')).toBe(true);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
addVictronInterfaces(noopBus, { name: '' }, {});
|
|
23
|
+
} catch (e) {
|
|
24
|
+
expect(e.message.includes('Interface name')).toBe(true);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('works for an example with properties', () => {
|
|
29
|
+
const declaration = {
|
|
30
|
+
name: 'com.victronenergy.myservice',
|
|
31
|
+
properties: {
|
|
32
|
+
'foo': 'i',
|
|
33
|
+
'bar': 's',
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const definition = {
|
|
37
|
+
foo: 42,
|
|
38
|
+
bar: 'hello',
|
|
39
|
+
emit: function() { }
|
|
40
|
+
};
|
|
41
|
+
const result = addVictronInterfaces(noopBus, declaration, definition);
|
|
42
|
+
expect(!!result).toBe(true);
|
|
43
|
+
expect(result.warnings.length).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('provides a warning if the interface name contains problematic characters', () => {
|
|
47
|
+
const declaration = {
|
|
48
|
+
name: 'com.victronenergy.my-service-with-dashes',
|
|
49
|
+
};
|
|
50
|
+
const { warnings } = addVictronInterfaces(noopBus, declaration, {});
|
|
51
|
+
expect(warnings.length).toBe(1);
|
|
52
|
+
expect(warnings[0].includes('problematic characters')).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('provides a warning if the interface name does not start with com.victronenergy', () => {
|
|
56
|
+
const declaration = {
|
|
57
|
+
name: 'com.example.my_service',
|
|
58
|
+
};
|
|
59
|
+
const { warnings } = addVictronInterfaces(noopBus, declaration, {});
|
|
60
|
+
expect(warnings.length).toBe(1);
|
|
61
|
+
expect(warnings[0].includes('start with com.victronenergy')).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('exports GetItems, and SetValue for each property', () => {
|
|
65
|
+
const declaration = {
|
|
66
|
+
name: 'some_name',
|
|
67
|
+
properties: {
|
|
68
|
+
'foo': 'i',
|
|
69
|
+
'bar': 's',
|
|
70
|
+
'baz': 'b',
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const bus = {
|
|
74
|
+
exportInterface: jest.fn()
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
addVictronInterfaces(bus, declaration, {});
|
|
78
|
+
expect(bus.exportInterface.mock.calls.length).toBe(4);
|
|
79
|
+
|
|
80
|
+
const call0 = bus.exportInterface.mock.calls[0];
|
|
81
|
+
expect(Object.keys(call0[0])).toStrictEqual(['GetItems', 'emit']);
|
|
82
|
+
|
|
83
|
+
const call1 = bus.exportInterface.mock.calls[1];
|
|
84
|
+
expect(Object.keys(call1[0])).toStrictEqual(['SetValue']);
|
|
85
|
+
expect(call1[1]).toStrictEqual('/foo');
|
|
86
|
+
expect(call1[2].name).toBe('com.victronenergy.BusItem');
|
|
87
|
+
|
|
88
|
+
const call3 = bus.exportInterface.mock.calls[3];
|
|
89
|
+
expect(call3[1]).toStrictEqual('/baz');
|
|
90
|
+
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const dbus = require('dbus-native');
|
|
2
|
+
const { addVictronInterfaces } = require('../index');
|
|
3
|
+
|
|
4
|
+
const describeIf = (condition, ...args) =>
|
|
5
|
+
condition ? describe(...args) : describe.skip(...args);
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
describeIf(process.env.TEST_INTEGRATION, "run integration tests", () => {
|
|
9
|
+
test("this is a dummy integration test", async () => {
|
|
10
|
+
|
|
11
|
+
// example adopted from https://github.com/sidorares/dbus-native/blob/master/examples/basic-service.js
|
|
12
|
+
const serviceName = 'com.victronenergy.my_integration_test_service1';
|
|
13
|
+
const interfaceName = serviceName;
|
|
14
|
+
const objectPath = `/${serviceName.replace(/\./g, '/')}`;
|
|
15
|
+
|
|
16
|
+
const sessionBus = dbus.sessionBus();
|
|
17
|
+
if (!sessionBus) {
|
|
18
|
+
throw new Error('Could not connect to the DBus session bus.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// request service name from the bus
|
|
22
|
+
await new Promise((resolve, reject) => {
|
|
23
|
+
sessionBus.requestName(serviceName, 0x4, (err, retCode) => {
|
|
24
|
+
// If there was an error, warn user and fail
|
|
25
|
+
if (err) {
|
|
26
|
+
return reject(new Error(
|
|
27
|
+
`Could not request service name ${serviceName}, the error was: ${err}.`
|
|
28
|
+
));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Return code 0x1 means we successfully had the name
|
|
32
|
+
if (retCode === 1) {
|
|
33
|
+
console.log(`Successfully requested service name "${serviceName}"!`);
|
|
34
|
+
resolve();
|
|
35
|
+
} else {
|
|
36
|
+
/* Other return codes means various errors, check here
|
|
37
|
+
(https://dbus.freedesktop.org/doc/api/html/group__DBusShared.html#ga37a9bc7c6eb11d212bf8d5e5ff3b50f9) for more
|
|
38
|
+
information
|
|
39
|
+
*/
|
|
40
|
+
return reject(new Error(
|
|
41
|
+
`Failed to request service name "${serviceName}". Check what return code "${retCode}" means.`
|
|
42
|
+
));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// First, we need to create our interface description (here we will only expose method calls)
|
|
48
|
+
var ifaceDesc = {
|
|
49
|
+
name: interfaceName,
|
|
50
|
+
methods: {
|
|
51
|
+
// Simple types
|
|
52
|
+
SayHello: ['', 's', [], ['hello_sentence']],
|
|
53
|
+
GiveTime: ['', 's', [], ['current_time']],
|
|
54
|
+
Capitalize: ['s', 's', ['initial_string'], ['capitalized_string']]
|
|
55
|
+
},
|
|
56
|
+
properties: {
|
|
57
|
+
Flag: 'b',
|
|
58
|
+
StringProp: 's',
|
|
59
|
+
RandValue: 'i'
|
|
60
|
+
},
|
|
61
|
+
signals: {
|
|
62
|
+
Rand: ['i', 'random_number']
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Then we need to create the interface implementation (with actual functions)
|
|
67
|
+
var iface = {
|
|
68
|
+
SayHello: function() {
|
|
69
|
+
return 'Hello, world!';
|
|
70
|
+
},
|
|
71
|
+
GiveTime: function() {
|
|
72
|
+
return new Date().toString();
|
|
73
|
+
},
|
|
74
|
+
Capitalize: function(str) {
|
|
75
|
+
return str.toUpperCase();
|
|
76
|
+
},
|
|
77
|
+
Flag: true,
|
|
78
|
+
StringProp: 'initial string',
|
|
79
|
+
RandValue: 43,
|
|
80
|
+
emit: function() {
|
|
81
|
+
// no nothing, as usual
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
// Now we need to actually export our interface on our object
|
|
87
|
+
sessionBus.exportInterface(iface, objectPath, ifaceDesc);
|
|
88
|
+
|
|
89
|
+
// Then we can add the required Victron interfaces, and receive some funtions to use
|
|
90
|
+
const {
|
|
91
|
+
emitItemsChanged,
|
|
92
|
+
addSettings,
|
|
93
|
+
removeSettings,
|
|
94
|
+
getValue,
|
|
95
|
+
setValue
|
|
96
|
+
} = addVictronInterfaces(sessionBus, ifaceDesc, iface);
|
|
97
|
+
|
|
98
|
+
console.log('Interface exposed to DBus, ready to receive function calls!');
|
|
99
|
+
|
|
100
|
+
async function proceed() {
|
|
101
|
+
const settingsResult = await addSettings([
|
|
102
|
+
{ path: '/Settings/Basic2/OptionA', default: 3, min: 0, max: 5 },
|
|
103
|
+
{ path: '/Settings/Basic2/OptionB', default: 'x' },
|
|
104
|
+
{ path: '/Settings/Basic2/OptionC', default: 'y' },
|
|
105
|
+
{ path: '/Settings/Basic2/OptionD', default: 'y' },
|
|
106
|
+
]);
|
|
107
|
+
console.log('settingsResult', JSON.stringify(settingsResult, null, 2));
|
|
108
|
+
|
|
109
|
+
const interval = setInterval(async () => {
|
|
110
|
+
|
|
111
|
+
// emit a random value (not relevant for our Victron interfaces)
|
|
112
|
+
var rand = Math.round(Math.random() * 100);
|
|
113
|
+
if (rand > 75) {
|
|
114
|
+
iface.emit('Rand', Math.round(Math.random() * 100));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// set a random value. By calling emitItemsChanged afterwards, the
|
|
118
|
+
// Victron-specific signal 'ItemsChanged' will be emitted
|
|
119
|
+
iface.RandValue = Math.round(Math.random() * 100);
|
|
120
|
+
emitItemsChanged();
|
|
121
|
+
|
|
122
|
+
// change a setting programmatically
|
|
123
|
+
const setValueResult = await setValue({
|
|
124
|
+
path: '/Settings/Basic2/OptionB',
|
|
125
|
+
value: 'changed via SetValue ' + Math.round(Math.random() * 100),
|
|
126
|
+
interface: 'com.victronenergy.BusItem',
|
|
127
|
+
destination: 'com.victronenergy.settings'
|
|
128
|
+
});
|
|
129
|
+
console.log('setValueResult', setValueResult);
|
|
130
|
+
|
|
131
|
+
// or get a configuration value
|
|
132
|
+
getValue({
|
|
133
|
+
path: '/Settings/Basic2/OptionB',
|
|
134
|
+
interface: 'com.victronenergy.BusItem',
|
|
135
|
+
destination: 'com.victronenergy.settings'
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
}, 1_000);
|
|
139
|
+
|
|
140
|
+
await new Promise((resolve) => {
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
console.log('CLEARING INTERVAL', interval);
|
|
143
|
+
clearInterval(interval);
|
|
144
|
+
removeSettings([
|
|
145
|
+
{ path: '/Settings/Basic2/OptionC', default: 'y' },
|
|
146
|
+
{ path: '/Settings/Basic2/OptionD', default: 'y' }
|
|
147
|
+
]);
|
|
148
|
+
resolve();
|
|
149
|
+
}, 5000);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await proceed();
|
|
155
|
+
sessionBus.connection.end();
|
|
156
|
+
|
|
157
|
+
// wait a bit more, until all logs are written
|
|
158
|
+
await new Promise(res => setTimeout(res, 2_000));
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
}, /* timeout in milliseconds */ 20_000);
|
|
162
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* eslint-env node */
|
|
2
|
+
const { addVictronInterfaces } = require('../index');
|
|
3
|
+
|
|
4
|
+
describe('victron-dbus-virtual, setValue tests', () => {
|
|
5
|
+
|
|
6
|
+
it('works for the happy case', async () => {
|
|
7
|
+
const declaration = { name: 'foo', properties: { StringProp: 's' } };
|
|
8
|
+
const definition = { StringProp: 'hello' };
|
|
9
|
+
const bus = {
|
|
10
|
+
exportInterface: () => { },
|
|
11
|
+
invoke: function(args, cb) {
|
|
12
|
+
process.nextTick(() => cb(null, args));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const { setValue } = addVictronInterfaces(bus, declaration, definition);
|
|
16
|
+
|
|
17
|
+
const result = await setValue({
|
|
18
|
+
path: '/StringProp',
|
|
19
|
+
value: 'fourty-two',
|
|
20
|
+
destination: 'foo',
|
|
21
|
+
interface_: 'foo'
|
|
22
|
+
});
|
|
23
|
+
expect(result.member).toBe('SetValue');
|
|
24
|
+
expect(result.body).toStrictEqual([['s', 'fourty-two']]);
|
|
25
|
+
expect(result.path).toBe('/StringProp');
|
|
26
|
+
expect(result.interface).toBe('foo');
|
|
27
|
+
expect(result.destination).toBe('foo');
|
|
28
|
+
|
|
29
|
+
// NOTE: calling setValue() does *not* change the definition. If you want to update the definition,
|
|
30
|
+
// re-assign it: "definition.StringProp = 'fourty-two';"
|
|
31
|
+
// ... if you want to notify other processes of the change, you can call emitItemsChanged().
|
|
32
|
+
// The purpose of setValue() is to change the value in the dbus object, not the definition. This is useful
|
|
33
|
+
// for settings.
|
|
34
|
+
expect(definition.StringProp).toBe('hello');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('works for the happy case for settings', async () => {
|
|
38
|
+
const declaration = { name: 'foo', properties: { StringProp: 's' } };
|
|
39
|
+
const definition = { StringProp: 'hello' };
|
|
40
|
+
const bus = {
|
|
41
|
+
exportInterface: () => { },
|
|
42
|
+
invoke: function(args, cb) {
|
|
43
|
+
process.nextTick(() => cb(null, args));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const { addSettings, setValue, removeSettings } = addVictronInterfaces(bus, declaration, definition);
|
|
47
|
+
|
|
48
|
+
// first, add a setting
|
|
49
|
+
const settingsResult = await addSettings([
|
|
50
|
+
{ path: '/Settings/MySettings/Setting', default: 3, min: 0, max: 10 },
|
|
51
|
+
]);
|
|
52
|
+
expect(settingsResult.member).toBe('AddSettings');
|
|
53
|
+
|
|
54
|
+
// then, we set its value
|
|
55
|
+
const setValueResult = await setValue({
|
|
56
|
+
path: '/Settings/MySettings/Setting',
|
|
57
|
+
value: 7,
|
|
58
|
+
interface: 'com.victronenergy.BusItem',
|
|
59
|
+
destination: 'com.victronenergy.settings',
|
|
60
|
+
});
|
|
61
|
+
expect(setValueResult.member).toBe('SetValue');
|
|
62
|
+
|
|
63
|
+
// lastly, we remove the setting
|
|
64
|
+
const removeSettingsResult = await removeSettings([
|
|
65
|
+
{ path: '/Settings/MySettings/Setting' },
|
|
66
|
+
]);
|
|
67
|
+
expect(removeSettingsResult.member).toBe('RemoveSettings');
|
|
68
|
+
expect(removeSettingsResult.body).toStrictEqual([['/Settings/MySettings/Setting']]);
|
|
69
|
+
expect(removeSettingsResult.path).toBe('/');
|
|
70
|
+
expect(removeSettingsResult.interface).toBe('com.victronenergy.Settings');
|
|
71
|
+
expect(removeSettingsResult.destination).toBe('com.victronenergy.settings');
|
|
72
|
+
expect(removeSettingsResult.signature).toBe('as');
|
|
73
|
+
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('works for the happy case when we get called with SetValue', async () => {
|
|
77
|
+
const declaration = { name: 'foo', properties: { StringProp: 's' } };
|
|
78
|
+
const definition = { StringProp: 'hello' };
|
|
79
|
+
const emit = jest.fn();
|
|
80
|
+
const interfaces = []
|
|
81
|
+
const bus = {
|
|
82
|
+
exportInterface: (iface /* , _path, _ifaceDesc */) => {
|
|
83
|
+
interfaces.push(iface);
|
|
84
|
+
iface.emit = emit;
|
|
85
|
+
},
|
|
86
|
+
invoke: function(args, cb) {
|
|
87
|
+
process.nextTick(() => cb(null, args));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
addVictronInterfaces(bus, declaration, definition);
|
|
91
|
+
expect(!!interfaces[1].SetValue).toBe(true);
|
|
92
|
+
interfaces[1].SetValue([[{ type: 's' }], ['hello']]);
|
|
93
|
+
expect(emit.mock.calls[0][0]).toBe('ItemsChanged');
|
|
94
|
+
expect(emit.mock.calls[0][1]).toEqual([['StringProp', [['Value', ['s', 'hello']], ['Text', ['s', 'hello']]]]]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('fails if the underlying invoke() fails', async () => {
|
|
98
|
+
const declaration = { name: 'foo', properties: { StringProp: 's' } };
|
|
99
|
+
const definition = { StringProp: 'hello' };
|
|
100
|
+
const bus = {
|
|
101
|
+
exportInterface: () => { },
|
|
102
|
+
invoke: function(_args, cb) {
|
|
103
|
+
process.nextTick(() => cb(new Error('testing ... invoke failed')));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const { setValue } = addVictronInterfaces(bus, declaration, definition);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await setValue({
|
|
110
|
+
path: '/StringProp',
|
|
111
|
+
value: 'fourty-two',
|
|
112
|
+
destination: 'foo',
|
|
113
|
+
interface_: 'foo',
|
|
114
|
+
});
|
|
115
|
+
expect(false, 'should have thrown');
|
|
116
|
+
} catch (e) {
|
|
117
|
+
expect(e.message).toBe('testing ... invoke failed');
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
});
|
package/src/index.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
|
|
2
|
+
function addVictronInterfaces(bus, declaration, definition) {
|
|
3
|
+
|
|
4
|
+
const warnings = [];
|
|
5
|
+
|
|
6
|
+
if (!declaration.name) {
|
|
7
|
+
throw new Error('Interface name is required');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!declaration.name.match(/^[a-zA-Z0-9_.]+$/)) {
|
|
11
|
+
warnings.push(
|
|
12
|
+
`Interface name contains problematic characters, only a-zA-Z0-9_ allowed.`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
if (!declaration.name.match(/^com.victronenergy/)) {
|
|
16
|
+
warnings.push('Interface name should start with com.victronenergy');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function wrapValue(t, v) {
|
|
20
|
+
switch (t) {
|
|
21
|
+
case 'b':
|
|
22
|
+
return ['b', v];
|
|
23
|
+
case 's':
|
|
24
|
+
return ['s', v];
|
|
25
|
+
case 'i':
|
|
26
|
+
return ['i', v];
|
|
27
|
+
default:
|
|
28
|
+
return v;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function unwrapValue([t, v]) {
|
|
33
|
+
switch (t[0].type) {
|
|
34
|
+
case 'b':
|
|
35
|
+
return !!v[0];
|
|
36
|
+
case 's':
|
|
37
|
+
return v[0];
|
|
38
|
+
case 'i':
|
|
39
|
+
return Number(v[0]);
|
|
40
|
+
default:
|
|
41
|
+
throw new Error(`Unsupported value type: ${JSON.stringify(t)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// we use this for GetItems and ItemsChanged
|
|
46
|
+
function getProperties() {
|
|
47
|
+
return Object.entries(declaration.properties || {}).map(([k, v]) => {
|
|
48
|
+
console.log('getProperties, entries, (k,v):', k, v);
|
|
49
|
+
|
|
50
|
+
return [
|
|
51
|
+
k,
|
|
52
|
+
[
|
|
53
|
+
['Value', wrapValue(v, definition[k])],
|
|
54
|
+
['Text', ['s', '' + definition[k]]]
|
|
55
|
+
]
|
|
56
|
+
];
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const iface = {
|
|
61
|
+
GetItems: function() {
|
|
62
|
+
return getProperties();
|
|
63
|
+
},
|
|
64
|
+
emit: function() { }
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const ifaceDesc = {
|
|
68
|
+
name: 'com.victronenergy.BusItem',
|
|
69
|
+
methods: {
|
|
70
|
+
GetItems: ['', 'a{sa{sv}}', [], []]
|
|
71
|
+
},
|
|
72
|
+
signals: {
|
|
73
|
+
ItemsChanged: ['a{sa{sv}}', '', [], []]
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
bus.exportInterface(iface, '/', ifaceDesc);
|
|
78
|
+
|
|
79
|
+
// support GetValue for each property
|
|
80
|
+
for (const [k,] of Object.entries(declaration.properties || {})) {
|
|
81
|
+
bus.exportInterface(
|
|
82
|
+
{
|
|
83
|
+
SetValue: function(value, /* msg */) {
|
|
84
|
+
console.log(
|
|
85
|
+
'SetValue',
|
|
86
|
+
JSON.stringify(arguments[0]),
|
|
87
|
+
JSON.stringify(arguments[1])
|
|
88
|
+
);
|
|
89
|
+
try {
|
|
90
|
+
definition[k] = unwrapValue(value);
|
|
91
|
+
iface.emit('ItemsChanged', getProperties());
|
|
92
|
+
return 0;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.error(e);
|
|
95
|
+
return -1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
`/${k}`,
|
|
100
|
+
{
|
|
101
|
+
name: 'com.victronenergy.BusItem',
|
|
102
|
+
methods: {
|
|
103
|
+
SetValue: ['v', 'i', [], []]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function addSettings(settings) {
|
|
110
|
+
const body = [
|
|
111
|
+
settings.map(setting => [
|
|
112
|
+
['path', wrapValue('s', setting.path)],
|
|
113
|
+
['default', wrapValue('s', '' + setting.default)] // TODO: forcing value to be string
|
|
114
|
+
// TODO: incomplete, min and max missing
|
|
115
|
+
])
|
|
116
|
+
];
|
|
117
|
+
return await new Promise((resolve, reject) => {
|
|
118
|
+
bus.invoke(
|
|
119
|
+
{
|
|
120
|
+
interface: 'com.victronenergy.Settings',
|
|
121
|
+
path: '/',
|
|
122
|
+
member: 'AddSettings',
|
|
123
|
+
destination: 'com.victronenergy.settings',
|
|
124
|
+
type: undefined,
|
|
125
|
+
signature: 'aa{sv}',
|
|
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 removeSettings(settings) {
|
|
139
|
+
const body = [settings.map(setting => setting.path)];
|
|
140
|
+
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
bus.invoke(
|
|
143
|
+
{
|
|
144
|
+
interface: 'com.victronenergy.Settings',
|
|
145
|
+
path: '/',
|
|
146
|
+
member: 'RemoveSettings',
|
|
147
|
+
destination: 'com.victronenergy.settings',
|
|
148
|
+
type: undefined,
|
|
149
|
+
signature: 'as',
|
|
150
|
+
body: body
|
|
151
|
+
},
|
|
152
|
+
function(err, result) {
|
|
153
|
+
if (err) {
|
|
154
|
+
return reject(err);
|
|
155
|
+
}
|
|
156
|
+
return resolve(result);
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function setValue({ path, interface_, destination, value }) {
|
|
163
|
+
return await new Promise((resolve, reject) => {
|
|
164
|
+
bus.invoke(
|
|
165
|
+
{
|
|
166
|
+
interface: interface_,
|
|
167
|
+
path: path || '/',
|
|
168
|
+
member: 'SetValue',
|
|
169
|
+
destination,
|
|
170
|
+
signature: 'v',
|
|
171
|
+
body: [wrapValue('s', '' + value)] // TODO: only supports string type for now
|
|
172
|
+
},
|
|
173
|
+
function(err, result) {
|
|
174
|
+
if (err) {
|
|
175
|
+
return reject(err);
|
|
176
|
+
}
|
|
177
|
+
resolve(result);
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function getValue({ path, interface_, destination }) {
|
|
184
|
+
return await new Promise((resolve, reject) => {
|
|
185
|
+
bus.invoke(
|
|
186
|
+
{
|
|
187
|
+
interface: interface_,
|
|
188
|
+
path: path || '/',
|
|
189
|
+
member: 'GetValue',
|
|
190
|
+
destination
|
|
191
|
+
},
|
|
192
|
+
function(err, result) {
|
|
193
|
+
if (err) {
|
|
194
|
+
return reject(err);
|
|
195
|
+
}
|
|
196
|
+
resolve(result);
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
emitItemsChanged: () => iface.emit('ItemsChanged', getProperties()),
|
|
204
|
+
addSettings,
|
|
205
|
+
removeSettings,
|
|
206
|
+
setValue,
|
|
207
|
+
getValue,
|
|
208
|
+
warnings
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
module.exports = { addVictronInterfaces };
|