UNPKG

signalk-to-venus

Version:

Injects battery, tank, temperature, humidity, and switch data from Signal K into the Victron Cerbo GX D-Bus via Venus OS.

324 lines (286 loc) 9.77 kB
import dbusNative from 'dbus-native'; import EventEmitter from 'events'; export class VenusClient extends EventEmitter { constructor(settings, deviceType) { super(); this.settings = settings; this.deviceType = deviceType; this.bus = null; this.switchData = {}; this.lastInitAttempt = 0; this.switchIndex = 0; // For unique switch indexing this.VBUS_SERVICE = `com.victronenergy.virtual.${deviceType}`; this.managementProperties = {}; } // Helper function to wrap values in D-Bus variant format wrapValue(type, value) { return [type, value]; } // Helper function to get D-Bus type for JavaScript values getType(value) { if (typeof value === 'string') return 's'; if (typeof value === 'number' && Number.isInteger(value)) return 'i'; if (typeof value === 'number') return 'd'; if (typeof value === 'boolean') return 'b'; return 'v'; // variant for unknown types } async init() { try { // Create D-Bus connection using dbus-native with anonymous authentication this.bus = dbusNative.createClient({ host: this.settings.venusHost, port: 78, authMethods: ['ANONYMOUS'] }); // Request service name await new Promise((resolve, reject) => { this.bus.requestName(this.VBUS_SERVICE, 0, (err, result) => { if (err) reject(err); else resolve(result); }); }); this._exportMgmt(); this._exportRootInterface(); // Export root interface for VRM compatibility } catch (err) { // Convert errors to more user-friendly messages if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED') { throw new Error(`Cannot connect to Venus OS at ${this.settings.venusHost}:78 - ${err.code}`); } else if (err.message && err.message.includes('timeout')) { throw new Error(`Connection timeout to Venus OS at ${this.settings.venusHost}:78`); } throw new Error(err.message || err.toString()); } } _exportMgmt() { // Define the BusItem interface descriptor with enhanced signatures const busItemInterface = { name: "com.victronenergy.BusItem", methods: { GetValue: ["", "v", [], ["value"]], SetValue: ["v", "i", ["value"], ["result"]], GetText: ["", "s", [], ["text"]], }, signals: { PropertiesChanged: ["a{sv}", ["changes"]] } }; // Export management properties const mgmtInterface = { GetValue: () => { return this.wrapValue('i', 1); // Connected = 1 (integer) }, SetValue: (val) => { return 0; // Success }, GetText: () => { return 'Connected'; } }; this.bus.exportInterface(mgmtInterface, '/Mgmt/Connection', busItemInterface); this.managementProperties['/Mgmt/Connection'] = { value: 1, text: 'Connected' }; // Product Name - Required for Venus OS recognition const productNameInterface = { GetValue: () => { return this.wrapValue('s', 'SignalK Virtual Switch'); }, SetValue: (val) => { return 0; }, GetText: () => { return 'Product name'; } }; this.bus.exportInterface(productNameInterface, '/ProductName', busItemInterface); this.managementProperties['/ProductName'] = { value: 'SignalK Virtual Switch', text: 'Product name' }; // Device Instance - Required for unique identification const deviceInstanceInterface = { GetValue: () => { return this.wrapValue('u', 102); // Unsigned integer for device instance }, SetValue: (val) => { return 0; }, GetText: () => { return 'Device instance'; } }; this.bus.exportInterface(deviceInstanceInterface, '/DeviceInstance', busItemInterface); this.managementProperties['/DeviceInstance'] = { value: 102, text: 'Device instance' }; // Custom Name const customNameInterface = { GetValue: () => { return this.wrapValue('s', 'SignalK Switch'); }, SetValue: (val) => { return 0; }, GetText: () => { return 'Custom name'; } }; this.bus.exportInterface(customNameInterface, '/CustomName', busItemInterface); this.managementProperties['/CustomName'] = { value: 'SignalK Switch', text: 'Custom name' }; // Process Name and Version - Required for VRM registration const processNameInterface = { GetValue: () => { return this.wrapValue('s', 'signalk-switch-sensor'); }, SetValue: (val) => { return 0; }, GetText: () => { return 'Process name'; } }; this.bus.exportInterface(processNameInterface, '/Mgmt/ProcessName', busItemInterface); this.managementProperties['/Mgmt/ProcessName'] = { value: 'signalk-switch-sensor', text: 'Process name' }; const processVersionInterface = { GetValue: () => { return this.wrapValue('s', '1.0.12'); }, SetValue: (val) => { return 0; }, GetText: () => { return 'Process version'; } }; this.bus.exportInterface(processVersionInterface, '/Mgmt/ProcessVersion', busItemInterface); this.managementProperties['/Mgmt/ProcessVersion'] = { value: '1.0.12', text: 'Process version' }; } _exportRootInterface() { // Export root interface for VRM compatibility const rootInterface = { name: "com.victronenergy.BusItem", methods: { GetValue: ["", "v", [], ["value"]], SetValue: ["v", "i", ["value"], ["result"]], GetText: ["", "s", [], ["text"]], }, signals: { PropertiesChanged: ["a{sv}", ["changes"]] } }; const rootImpl = { GetValue: () => { return this.wrapValue('s', 'SignalK Virtual Switch Service'); }, SetValue: (val) => { return 0; }, GetText: () => { return 'SignalK Virtual Switch Service'; } }; this.bus.exportInterface(rootImpl, '/', rootInterface); } _exportProperty(path, config) { // Define the BusItem interface descriptor for dbus-native const busItemInterface = { name: "com.victronenergy.BusItem", methods: { GetValue: ["", "v", [], ["value"]], SetValue: ["v", "i", ["value"], ["result"]], GetText: ["", "s", [], ["text"]], }, signals: { PropertiesChanged: ["a{sv}", ["changes"]] } }; // Store initial value this.switchData[path] = config.value; const propertyInterface = { GetValue: () => { return this.wrapValue(config.type, this.switchData[path] || (config.type === 's' ? '' : 0)); }, SetValue: (val) => { const actualValue = Array.isArray(val) ? val[1] : val; this.switchData[path] = actualValue; this.emit('valueChanged', path, actualValue); return 0; // Success }, GetText: () => { return config.text; // Native string return } }; this.bus.exportInterface(propertyInterface, path, busItemInterface); } _updateValue(path, value) { if (this.switchData.hasOwnProperty(path)) { this.switchData[path] = value; } } _getSwitchName(path) { // Extract switch name from Signal K path like electrical.switches.navigation.state const pathParts = path.split('.'); if (pathParts.length < 3) return 'Switch'; // Use the switch name from the path const switchName = pathParts[2]; // Convert camelCase to proper names return switchName.charAt(0).toUpperCase() + switchName.slice(1).replace(/([A-Z])/g, ' $1'); } async handleSignalKUpdate(path, value) { try { // Validate input parameters if (value === null || value === undefined) { // Skip invalid switch values silently return; } if (!this.bus) { // Only try to initialize once every 30 seconds to avoid spam const now = Date.now(); if (!this.lastInitAttempt || (now - this.lastInitAttempt) > 30000) { this.lastInitAttempt = now; await this.init(); } else { // Skip silently if we recently failed to connect return; } } const switchName = this._getSwitchName(path); const index = this.switchIndex++; if (path.includes('state')) { // Switch state (0 = off, 1 = on) const switchState = value ? 1 : 0; const statePath = `/Switch/${index}/State`; this._exportProperty(statePath, { value: switchState, type: 'd', text: `${switchName} state` }); this.emit('dataUpdated', 'Switch State', `${switchName}: ${value ? 'ON' : 'OFF'}`); } else if (path.includes('dimmingLevel')) { // Dimming level (0-1 to 0-100 percentage) if (typeof value === 'number' && !isNaN(value)) { const dimmingPercent = value * 100; const dimmingPath = `/Switch/${index}/DimmingLevel`; this._exportProperty(dimmingPath, { value: dimmingPercent, type: 'd', text: `${switchName} dimming level` }); this.emit('dataUpdated', 'Switch Dimming', `${switchName}: ${dimmingPercent.toFixed(1)}%`); } } else { // Silently ignore unknown switch paths // Silently ignore unknown switch paths return; } } catch (err) { throw new Error(err.message); } } async disconnect() { if (this.bus) { try { this.bus.end(); } catch (err) { // Ignore disconnect errors } this.bus = null; this.switchData = {}; } } }