signalk-to-venus
Version:
Injects batteries, tanks, environment sensors, and switches as virtual devices and battery monitor into the Victron Cerbo GX Venus OS.
819 lines (720 loc) • 32.7 kB
JavaScript
import { VenusClientFactory } from './venusClientFactory.js';
import settings from './settings.js';
import dbusNative from 'dbus-native';
// Signal K plugin entry point
export default function(app) {
// Dynamic tracking of discovered Signal K paths
let discoveredPaths = {
batteries: new Map(),
tanks: new Map(),
environment: new Map(),
switches: new Map()
};
let lastSchemaUpdate = 0;
const plugin = {
id: 'signalk-to-venus',
name: 'Signal K to Venus OS Bridge',
description: 'Bridges Signal K data to Victron Venus OS via D-Bus',
venusConnected: false,
unsubscribe: null,
clients: {},
connectivityInterval: null,
// Function to generate dynamic schema based on discovered paths
schema: function() {
const baseSchema = {
type: 'object',
properties: {
venusHost: {
type: 'string',
title: 'Venus OS Host',
default: 'venus.local'
},
interval: {
type: 'number',
title: 'Update Interval (ms)',
default: 1000
},
batteryMonitor: {
type: 'object',
title: 'Battery Monitor Configuration',
properties: {
batteryCapacity: {
type: 'number',
title: 'Battery Capacity (Ah)',
description: 'Total battery capacity in Amp-hours for TTG calculation and monitoring',
default: 800
}
}
}
}
};
// Add discovered devices grouped by type if any have been found
if (hasDiscoveredPaths()) {
// Add each device type with discovered paths
Object.entries(discoveredPaths).forEach(([deviceType, pathMap]) => {
if (pathMap.size > 0) {
const deviceTitles = {
'batteries': 'Batteries',
'tanks': 'Tanks',
'environment': 'Environment',
'switches': 'Switches & Dimmers'
};
baseSchema.properties[deviceType] = {
type: 'object',
title: deviceTitles[deviceType],
properties: {}
};
// Add each discovered path as a simple checkbox
pathMap.forEach((pathInfo, devicePath) => {
const safePathKey = devicePath.replace(/[^a-zA-Z0-9]/g, '_');
baseSchema.properties[deviceType].properties[safePathKey] = {
type: 'boolean',
title: `${pathInfo.displayName} (${devicePath})`,
default: false // Disable devices by default to prevent unwanted connections
};
});
}
});
}
return baseSchema;
},
// UI Schema to enhance the configuration interface
uiSchema: function() {
const uiSchema = {};
// Make each device type collapsible if devices have been discovered
if (hasDiscoveredPaths()) {
Object.keys(discoveredPaths).forEach(deviceType => {
if (discoveredPaths[deviceType].size > 0) {
uiSchema[deviceType] = {
'ui:field': 'collapsible',
collapse: {
field: 'ObjectField',
wrapClassName: 'panel-group'
}
};
}
});
}
return uiSchema;
},
start: function(options) {
app.setPluginStatus('Starting Signal K to Venus OS bridge');
app.debug('Starting Signal K to Venus OS bridge');
const config = { ...settings, ...options };
plugin.clients = {};
plugin.venusConnected = false; // Track Venus connection status
const activeClientTypes = new Set();
let venusReachable = false; // Track Venus OS reachability (assume unreachable until proven otherwise)
const deviceTypeNames = {
'batteries': 'Batteries',
'tanks': 'Tanks',
'environment': 'Environment',
'switches': 'Switches'
};
// Test Venus OS connectivity before processing any data
async function testVenusConnectivity() {
let testBus = null;
try {
// Create D-Bus connection with anonymous authentication for Venus OS using dbus-native
try {
testBus = dbusNative.createClient({
host: config.venusHost,
port: 78,
authMethods: ['ANONYMOUS'] // Try anonymous auth first for Venus OS
});
} catch (createErr) {
throw createErr;
}
// Test the connection by trying to list names (simple D-Bus operation)
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Connection timeout'));
}, 3000);
// Try a simple D-Bus operation to test connectivity
try {
testBus.listNames((err, names) => {
clearTimeout(timeout);
if (err) {
reject(err);
} else {
resolve();
}
});
} catch (syncErr) {
clearTimeout(timeout);
reject(syncErr);
}
});
venusReachable = true;
plugin.venusConnected = true;
app.setPluginStatus(`Venus OS reachable at ${config.venusHost}`);
return true;
} catch (err) {
venusReachable = false;
plugin.venusConnected = false;
let errorMsg = `Venus OS not reachable at ${config.venusHost}`;
if (err.code === 'ENOTFOUND') {
errorMsg += ' (DNS resolution failed)';
} else if (err.code === 'ECONNREFUSED') {
errorMsg += ' (connection refused - check D-Bus TCP setting)';
} else if (err.message.includes('timeout')) {
errorMsg += ' (connection timeout)';
} else if (err.message.includes('dbus-keyrings') || err.message.includes('ENOENT')) {
errorMsg += ' (D-Bus authentication failed - this is a known issue)';
}
app.setPluginError(errorMsg);
// Clear all existing clients when Venus becomes unreachable
Object.keys(plugin.clients).forEach(key => {
if (plugin.clients[key] && typeof plugin.clients[key] === 'object') {
// Disconnect existing clients gracefully
if (plugin.clients[key].disconnect) {
plugin.clients[key].disconnect().catch(() => {});
}
}
delete plugin.clients[key];
});
return false;
} finally {
// Always disconnect the test bus
if (testBus) {
try {
testBus.end();
} catch (disconnectErr) {
// Silent disconnect error handling
}
}
}
}
// Subscribe to Signal K updates for discovery and processing
// We always subscribe to all device types for discovery, filtering happens later
const subscriptions = [
{ path: 'electrical.batteries.*', period: config.interval },
{ path: 'tanks.*', period: config.interval },
{ path: 'environment.*', period: config.interval },
{ path: 'electrical.switches.*', period: config.interval }
];
// Subscribe to Signal K delta stream using multiple approaches for compatibility
app.setPluginStatus('Setting up Signal K subscription');
let deltaCount = 0;
let lastDataTime = Date.now();
// Try multiple subscription methods in order of compatibility
if (app.streambundle && app.streambundle.getSelfBus) {
// Method 1: Stream bundle getSelfBus (correct API usage)
try {
// Subscribe to all paths and filter in the callback
plugin.unsubscribe = app.streambundle.getSelfBus().onValue(data => {
// Validate the streambundle data before conversion
if (!data || typeof data !== 'object' || !data.path) {
return;
}
// Filter paths early - only process paths we care about
const deviceType = identifyDeviceType(data.path);
if (!deviceType) {
// Path doesn't match any enabled device types, skip silently
return;
}
// Skip null/undefined values at the source - don't process them at all
if (data.value === null || data.value === undefined) {
return;
}
// Convert the normalized delta format to standard delta format
const delta = {
context: data.context || 'vessels.self',
updates: [{
source: data.source || { label: 'streambundle' },
timestamp: data.timestamp || new Date().toISOString(),
values: [{
path: data.path,
value: data.value
}]
}]
};
processDelta(delta);
});
} catch (err) {
app.error('getSelfBus method failed:', err);
}
} else if (app.signalk && app.signalk.subscribe) {
// Method 2: Direct signalk subscription (common method)
try {
const subscription = {
context: 'vessels.self',
subscribe: [
{ path: '*', period: config.interval, format: 'delta', policy: 'ideal', minPeriod: 200 }
]
};
plugin.unsubscribe = app.signalk.subscribe(subscription, delta => {
processDelta(delta);
});
} catch (err) {
app.error('signalk.subscribe method failed:', err);
}
} else if (app.registerDeltaInputHandler) {
// Method 3: Delta input handler (fallback)
try {
app.registerDeltaInputHandler((delta, next) => {
if (delta.context === 'vessels.self') {
processDelta(delta);
}
next(delta);
});
} catch (err) {
app.error('registerDeltaInputHandler method failed:', err);
}
} else {
app.setPluginError('No compatible subscription method found');
return;
}
// Test Venus OS connectivity initially and periodically
async function runConnectivityTest() {
try {
const isReachable = await testVenusConnectivity();
} catch (err) {
app.error('Connectivity test error:', err);
}
}
runConnectivityTest(); // Run initial test
plugin.connectivityInterval = setInterval(runConnectivityTest, 120000); // Check every 2 minutes when testing (reduced frequency)
// Function to process delta messages
function processDelta(delta) {
try {
deltaCount++;
lastDataTime = Date.now();
if (delta.updates) {
delta.updates.forEach(update => {
// Check if update and update.values are valid
if (!update || !Array.isArray(update.values)) {
return;
}
// Check for Venus OS sources to prevent feedback loops
const sourceLabel = update.source?.label || update.$source || 'unknown';
if (sourceLabel.includes('venus.com.victronenergy')) {
return;
}
update.values.forEach(async pathValue => {
try {
// Check if pathValue exists and has required properties
if (!pathValue || typeof pathValue !== 'object') {
return;
}
if (!pathValue.path) {
return;
}
// Skip null/undefined values - this should be rare if streambundle filtering works
if (pathValue.value === undefined || pathValue.value === null) {
return;
}
const deviceType = identifyDeviceType(pathValue.path);
if (deviceType) {
// Track this discovered path (always do discovery regardless of Venus OS connection)
addDiscoveredPath(deviceType, pathValue.path, pathValue.value, config);
// Only proceed with Venus OS operations if Venus is reachable and path is enabled
if (venusReachable !== true) {
// Venus OS not reachable, skip Venus operations but continue discovery
return;
}
// Check if this specific path is enabled
if (!isPathEnabled(deviceType, pathValue.path, config)) {
return; // Skip disabled paths
}
if (!plugin.clients[deviceType]) {
app.setPluginStatus(`Connecting to Venus OS at ${config.venusHost} for ${deviceTypeNames[deviceType]}`);
try {
plugin.clients[deviceType] = VenusClientFactory(config, deviceType, app);
await plugin.clients[deviceType].handleSignalKUpdate(pathValue.path, pathValue.value);
activeClientTypes.add(deviceTypeNames[deviceType]);
const enabledDeviceCountText = generateEnabledDeviceCountText(config);
app.setPluginStatus(`Connected to Venus OS, injecting ${enabledDeviceCountText}`);
} catch (err) {
// Clean up connection error messages for better user experience
let cleanMessage = err.message || err.toString();
if (cleanMessage.includes('ENOTFOUND')) {
cleanMessage = `Venus OS device not found at ${config.venusHost} (DNS resolution failed)`;
} else if (cleanMessage.includes('ECONNREFUSED')) {
cleanMessage = `Venus OS device refused connection at ${config.venusHost}:78 (check D-Bus TCP setting)`;
} else if (cleanMessage.includes('timeout')) {
cleanMessage = `Venus OS connection timeout (${config.venusHost}:78)`;
}
app.setPluginError(`Venus OS not reachable: ${cleanMessage}`);
// Mark this client as failed to prevent retries
plugin.clients[deviceType] = null;
// Only log the first connection error per device type to avoid spam
if (!plugin.clients[`${deviceType}_error_logged`]) {
app.error(`Cannot connect to Venus OS for ${deviceTypeNames[deviceType]}: ${cleanMessage}`);
plugin.clients[`${deviceType}_error_logged`] = true;
}
return;
}
} else {
// Client already exists - but check if it's null (failed connection)
if (plugin.clients[deviceType] === null) {
return;
}
try {
await plugin.clients[deviceType].handleSignalKUpdate(pathValue.path, pathValue.value);
// Periodically update status to reflect current enabled device count
// (in case configuration changed or new devices were enabled)
if (Math.random() < 0.01) { // Update status roughly 1% of the time to avoid spam
const enabledDeviceCountText = generateEnabledDeviceCountText(config);
app.setPluginStatus(`Connected to Venus OS, injecting ${enabledDeviceCountText}`);
}
} catch (err) {
// Only log detailed errors if it's not a connection issue
if (err.message && (err.message.includes('ENOTFOUND') || err.message.includes('ECONNREFUSED'))) {
// Suppress frequent connection errors when Venus OS is not available
// The main connection error is already logged during client creation
// Mark client as failed
plugin.clients[deviceType] = null;
} else {
app.error(`Error updating ${deviceType} client for ${pathValue.path}: ${err.message}`);
}
}
}
}
} catch (err) {
// Only log unexpected errors, suppress common connection errors
if (!err.message || (!err.message.includes('ENOTFOUND') && !err.message.includes('ECONNREFUSED'))) {
const pathInfo = pathValue?.path || 'unknown path';
app.error(`Unexpected error processing ${pathInfo}: ${err.message}`);
}
}
});
});
}
} catch (err) {
app.error('Error processing delta:', err);
}
}
// Monitor subscription health - check immediately
if (deltaCount === 0) {
app.setPluginStatus(`No Signal K data received - check server configuration`);
}
// Handle venus client value changes by setting values back to Signal K
Object.values(plugin.clients).forEach(client => {
client.on('valueChanged', async (venusPath, value) => {
try {
const signalKPath = mapVenusToSignalKPath(venusPath);
if (signalKPath) {
// Use Signal K's internal API instead of external PUT
await app.putSelfPath(signalKPath, value, 'venus-bridge');
app.debug(`Updated Signal K path ${signalKPath} with value ${value}`);
}
} catch (err) {
app.error(`Error updating Signal K path from venus ${venusPath}:`, err);
}
});
});
// Set initial status immediately if no data comes in
if (activeClientTypes.size === 0) {
// Check if any devices are enabled
const hasEnabledDevices = ['batteries', 'tanks', 'environment', 'switches'].some(deviceType => {
if (config[deviceType]) {
return Object.values(config[deviceType]).some(enabled => enabled === true);
}
return false;
});
if (!hasEnabledDevices) {
const deviceCountText = generateDeviceCountText();
if (deviceCountText.includes('0 devices')) {
app.setPluginStatus('Discovering Signal K devices');
} else {
app.setPluginStatus(`Device Discovery: Found ${deviceCountText} - configure in settings`);
}
} else if (venusReachable === false) {
const deviceCountText = generateDeviceCountText();
app.setPluginStatus(`Discovery: ${deviceCountText} found - Venus OS not connected at ${config.venusHost}`);
} else {
app.setPluginStatus(`Waiting for Signal K data (${config.venusHost})`);
}
}
},
stop: function() {
app.setPluginStatus('Stopping Signal K to Venus OS bridge');
app.debug('Stopping Signal K to Venus OS bridge');
// Clear connectivity interval
if (plugin.connectivityInterval) {
clearInterval(plugin.connectivityInterval);
}
if (plugin.unsubscribe) {
plugin.unsubscribe();
plugin.unsubscribe = null;
}
if (plugin.clients) {
Object.values(plugin.clients).forEach(async client => {
if (client && client.disconnect) {
await client.disconnect();
}
});
}
app.setPluginStatus('Stopped');
}
};
// Helper function to identify device type from Signal K path
function identifyDeviceType(path) {
// Filter out Cerbo GX relays (venus-0, venus-1) to prevent feedback loops
if (path.match(/electrical\.switches\.venus-[01]\./)) {
return null;
}
if (settings.batteryRegex.test(path)) return 'batteries';
if (settings.tankRegex.test(path)) return 'tanks';
if (settings.temperatureRegex.test(path) || settings.humidityRegex.test(path)) return 'environment';
if (settings.switchRegex.test(path) || settings.dimmerRegex.test(path)) return 'switches';
return null;
}
// Helper function to map Venus D-Bus paths back to Signal K paths
function mapVenusToSignalKPath(venusPath) {
// This would need proper mapping logic based on your venus client implementations
// For switches/dimmers that support bidirectional updates
if (venusPath.includes('/Switches/')) {
const id = venusPath.match(/\/Switches\/([^\/]+)/)?.[1];
// Filter out Cerbo GX relays to prevent feedback loops
if (id === 'venus-0' || id === 'venus-1') {
return null;
}
if (venusPath.endsWith('/State')) {
return `electrical.switches.${id}.state`;
} else if (venusPath.endsWith('/DimLevel')) {
return `electrical.switches.${id}.dimmingLevel`;
}
}
return null;
}
// Helper function to generate device count text by type (all discovered devices)
function generateDeviceCountText() {
const deviceCounts = {
batteries: discoveredPaths.batteries.size,
tanks: discoveredPaths.tanks.size,
environment: discoveredPaths.environment.size,
switches: discoveredPaths.switches.size
};
const deviceCountParts = [];
if (deviceCounts.batteries > 0) {
deviceCountParts.push(`${deviceCounts.batteries} ${deviceCounts.batteries === 1 ? 'battery' : 'batteries'}`);
}
if (deviceCounts.tanks > 0) {
deviceCountParts.push(`${deviceCounts.tanks} ${deviceCounts.tanks === 1 ? 'tank' : 'tanks'}`);
}
if (deviceCounts.environment > 0) {
deviceCountParts.push(`${deviceCounts.environment} environment ${deviceCounts.environment === 1 ? 'sensor' : 'sensors'}`);
}
if (deviceCounts.switches > 0) {
deviceCountParts.push(`${deviceCounts.switches} ${deviceCounts.switches === 1 ? 'switch' : 'switches'}`);
}
if (deviceCountParts.length > 0) {
return deviceCountParts.join(', ');
} else {
const totalPaths = Object.values(discoveredPaths).reduce((sum, map) => sum + map.size, 0);
return `${totalPaths} devices`;
}
}
// Helper function to generate device count text for enabled devices only
function generateEnabledDeviceCountText(config) {
const enabledCounts = {
batteries: 0,
tanks: 0,
environment: 0,
switches: 0
};
// Count enabled devices for each type
Object.keys(enabledCounts).forEach(deviceType => {
if (config[deviceType]) {
// Count how many devices of this type are enabled
enabledCounts[deviceType] = Object.values(config[deviceType]).filter(enabled => enabled === true).length;
}
});
const deviceCountParts = [];
if (enabledCounts.batteries > 0) {
deviceCountParts.push(`${enabledCounts.batteries} ${enabledCounts.batteries === 1 ? 'battery' : 'batteries'}`);
}
if (enabledCounts.tanks > 0) {
deviceCountParts.push(`${enabledCounts.tanks} ${enabledCounts.tanks === 1 ? 'tank' : 'tanks'}`);
}
if (enabledCounts.environment > 0) {
deviceCountParts.push(`${enabledCounts.environment} environment ${enabledCounts.environment === 1 ? 'sensor' : 'sensors'}`);
}
if (enabledCounts.switches > 0) {
deviceCountParts.push(`${enabledCounts.switches} ${enabledCounts.switches === 1 ? 'switch' : 'switches'}`);
}
if (deviceCountParts.length > 0) {
return deviceCountParts.join(', ');
} else {
return '0 devices';
}
}
// Helper function to check if any paths have been discovered
function hasDiscoveredPaths() {
return Object.values(discoveredPaths).some(pathMap => pathMap.size > 0);
}
// Function to add a discovered path to tracking
function addDiscoveredPath(deviceType, path, value, config) {
try {
const pathMap = discoveredPaths[deviceType];
if (!pathMap) return;
// Extract the device/sensor path (one level up from the property)
const devicePath = getDevicePath(deviceType, path);
if (!devicePath) return;
if (!pathMap.has(devicePath)) {
// Generate a human-readable display name
let displayName = generateDisplayName(deviceType, devicePath);
pathMap.set(devicePath, {
displayName: displayName,
firstSeen: new Date().toISOString(),
lastValue: value,
sampleValue: value,
properties: new Set([path]) // Track which properties we've seen
});
// Trigger schema update if enough time has passed
const now = Date.now();
if (now - lastSchemaUpdate > 2000) { // Throttle updates to every 2 seconds
lastSchemaUpdate = now;
// Notify Signal K that the schema has changed (if supported)
if (app.handleMessage && typeof app.handleMessage === 'function') {
try {
app.handleMessage(plugin.id, {
type: 'schema-update',
timestamp: new Date().toISOString()
});
} catch (err) {
// Schema update notification not supported - ignore silently
}
}
}
// Note: Status message is updated in processDelta when devices are actually enabled and injected
} else {
// Update last seen value and add this property to the set
const deviceInfo = pathMap.get(devicePath);
deviceInfo.lastValue = value;
deviceInfo.properties.add(path);
}
} catch (err) {
app.error(`Error in addDiscoveredPath: ${err.message}`);
}
}
// Function to extract device path from full property path
function getDevicePath(deviceType, fullPath) {
switch (deviceType) {
case 'batteries':
// electrical.batteries.0.voltage -> electrical.batteries.0
const batteryMatch = fullPath.match(/^(electrical\.batteries\.[^.]+)/);
return batteryMatch ? batteryMatch[1] : null;
case 'tanks':
// tanks.blackWater.0.currentLevel -> tanks.blackWater.0
const tankMatch = fullPath.match(/^(tanks\.[^.]+\.[^.]+)/);
return tankMatch ? tankMatch[1] : null;
case 'environment':
// environment.water.temperature -> environment.water
// propulsion.main.temperature -> propulsion.main
// Group by sensor location, not individual property
const envMatch = fullPath.match(/^(environment\.[^.]+|propulsion\.[^.]+)/);
return envMatch ? envMatch[1] : null;
case 'switches':
// electrical.switches.nav.state -> electrical.switches.nav
const switchMatch = fullPath.match(/^(electrical\.switches\.[^.]+)/);
return switchMatch ? switchMatch[1] : null;
}
return null;
}
// Function to generate human-readable display names
function generateDisplayName(deviceType, devicePath) {
switch (deviceType) {
case 'batteries':
// electrical.batteries.0 -> Battery (if only one) or Battery 1, Battery 2 (if multiple)
const batteryMatch = devicePath.match(/electrical\.batteries\.(\d+|[^.]+)/);
if (batteryMatch) {
const batteryId = batteryMatch[1];
// Count total batteries to decide if we need numbers
const totalBatteries = discoveredPaths.batteries ? discoveredPaths.batteries.size : 0;
const isGenericId = ['0', 'main', 'primary', 'default'].includes(batteryId.toLowerCase());
if (totalBatteries <= 1 && isGenericId) {
return 'Battery';
}
// Convert numeric IDs to start from 1 instead of 0
let displayId = batteryId;
if (/^\d+$/.test(batteryId)) {
displayId = (parseInt(batteryId) + 1).toString();
}
return `Battery ${displayId}`;
}
break;
case 'tanks':
// tanks.freshWater.starboard -> Freshwater starboard (if specific name)
const tankMatch = devicePath.match(/tanks\.([^.]+)\.([^.]+)/);
if (tankMatch) {
let tankType = tankMatch[1];
const tankId = tankMatch[2]; // Can be any alphanumeric string per Signal K spec
// Remove camel case and capitalize first letter
tankType = tankType.replace(/([A-Z])/g, ' $1').trim();
tankType = tankType.charAt(0).toUpperCase() + tankType.slice(1).toLowerCase();
// Remove spaces for consistency (Fresh Water -> Freshwater)
tankType = tankType.replace(/\s+/g, '');
// Check if we have multiple tanks of this type
const tanksOfThisType = Array.from(discoveredPaths.tanks?.keys() || [])
.filter(path => path.includes(`tanks.${tankMatch[1]}.`)).length;
// Use generic ID detection instead of assuming only '0' is generic
const isGenericId = ['0', 'main', 'primary', 'default'].includes(tankId.toLowerCase());
// If single tank with generic ID, omit the ID
if (tanksOfThisType <= 1 && isGenericId) {
return tankType;
} else {
// Multiple tanks or specific ID - include the ID
// Convert numeric IDs to start from 1 instead of 0
let displayId = tankId;
if (/^\d+$/.test(tankId)) {
displayId = (parseInt(tankId) + 1).toString();
}
return `${tankType} ${displayId}`;
}
}
break;
case 'environment':
// environment.water -> Water
// propulsion.main -> Main
// environment.outside -> Outside
const envMatch = devicePath.match(/environment\.([^.]+)|propulsion\.([^.]+)/);
if (envMatch) {
let sensor = envMatch[1] || envMatch[2];
// Remove camel case and capitalize first letter
sensor = sensor.replace(/([A-Z])/g, ' $1').trim();
sensor = sensor.charAt(0).toUpperCase() + sensor.slice(1).toLowerCase();
return sensor;
}
break;
case 'switches':
// electrical.switches.nav -> Nav (if functional name)
const switchMatch = devicePath.match(/electrical\.switches\.([^.]+)/);
if (switchMatch) {
let switchId = switchMatch[1];
// If it's a functional name (not just a number), use it directly
if (!/^\d+$/.test(switchId)) {
// Remove camel case and capitalize first letter
switchId = switchId.replace(/([A-Z])/g, ' $1').trim();
return switchId.charAt(0).toUpperCase() + switchId.slice(1).toLowerCase();
} else {
// It's a numeric ID - convert to start from 1 instead of 0
const displayId = (parseInt(switchId) + 1).toString();
return `Switch ${displayId}`;
}
}
break;
}
// Fallback to path-based name with camel case removed
const fallback = devicePath.split('.').pop() || devicePath;
return fallback.replace(/([A-Z])/g, ' $1').trim()
.charAt(0).toUpperCase() + fallback.slice(1).toLowerCase();
}
// Function to check if a path is enabled in configuration
function isPathEnabled(deviceType, fullPath, config) {
// Get the device path for checking individual configuration
const devicePath = getDevicePath(deviceType, fullPath);
if (!devicePath) return false; // Default to disabled if we can't parse the path
// Check if this specific device is enabled in the new configuration structure
if (config[deviceType]) {
const safePathKey = devicePath.replace(/[^a-zA-Z0-9]/g, '_');
return config[deviceType][safePathKey] === true;
}
// Default to disabled for newly discovered devices
// Devices will be discovered and appear in the UI but won't be processed until explicitly enabled
return false;
}
return plugin;
}