-
-
-
+
About CoolerDash
Plugin
-
CoolerDash v{{VERSION}} — LCD Temperature Dashboard
+
CoolerDash v{{VERSION}} — LCD Temperature Dashboard
Description
@@ -1183,7 +1046,7 @@
Community & Feedback
@@ -1192,11 +1055,11 @@
System Environment
Kernel
- —
+ —
CoolerControl Version
- —
+ —
@@ -1211,7 +1074,7 @@
System Environment
- Note: Changes are applied immediately — the plugin will be restarted automatically after Save or Reset.
+ Note: Changes are applied immediately — the plugin will be restarted automatically after Save or Reset.
@@ -1265,43 +1128,51 @@
System Environment
size_temp: 100.0,
size_labels: 30.0
},
- cpu: {
- threshold_1: 55.0,
- threshold_2: 65.0,
- threshold_3: 75.0,
- max_scale: 115.0,
- threshold_1_color: { r: 0, g: 255, b: 0 },
- threshold_2_color: { r: 255, g: 140, b: 0 },
- threshold_3_color: { r: 255, g: 70, b: 0 },
- threshold_4_color: { r: 255, g: 0, b: 0 }
- },
- gpu: {
- threshold_1: 55.0,
- threshold_2: 65.0,
- threshold_3: 75.0,
- max_scale: 115.0,
- threshold_1_color: { r: 0, g: 255, b: 0 },
- threshold_2_color: { r: 255, g: 140, b: 0 },
- threshold_3_color: { r: 255, g: 70, b: 0 },
- threshold_4_color: { r: 255, g: 0, b: 0 }
- },
- liquid: {
- max_scale: 50.0,
- threshold_1: 25.0,
- threshold_2: 28.0,
- threshold_3: 31.0,
- threshold_1_color: { r: 0, g: 255, b: 0 },
- threshold_2_color: { r: 255, g: 140, b: 0 },
- threshold_3_color: { r: 255, g: 70, b: 0 },
- threshold_4_color: { r: 255, g: 0, b: 0 }
+ sensors: {
+ cpu: {
+ threshold_1: 55.0,
+ threshold_2: 65.0,
+ threshold_3: 75.0,
+ max_scale: 115.0,
+ font_size_temp: 0,
+ label: '',
+ threshold_1_color: { r: 0, g: 255, b: 0 },
+ threshold_2_color: { r: 255, g: 140, b: 0 },
+ threshold_3_color: { r: 255, g: 70, b: 0 },
+ threshold_4_color: { r: 255, g: 0, b: 0 },
+ offset_x: 0,
+ offset_y: 0
+ },
+ gpu: {
+ threshold_1: 55.0,
+ threshold_2: 65.0,
+ threshold_3: 75.0,
+ max_scale: 115.0,
+ font_size_temp: 0,
+ label: '',
+ threshold_1_color: { r: 0, g: 255, b: 0 },
+ threshold_2_color: { r: 255, g: 140, b: 0 },
+ threshold_3_color: { r: 255, g: 70, b: 0 },
+ threshold_4_color: { r: 255, g: 0, b: 0 },
+ offset_x: 0,
+ offset_y: 0
+ },
+ liquid: {
+ threshold_1: 25.0,
+ threshold_2: 28.0,
+ threshold_3: 31.0,
+ max_scale: 50.0,
+ font_size_temp: 0,
+ label: '',
+ threshold_1_color: { r: 0, g: 255, b: 0 },
+ threshold_2_color: { r: 255, g: 140, b: 0 },
+ threshold_3_color: { r: 255, g: 70, b: 0 },
+ threshold_4_color: { r: 255, g: 0, b: 0 },
+ offset_x: 0,
+ offset_y: 0
+ }
},
positioning: {
- temp_offset_x_cpu: 0,
- temp_offset_x_gpu: 0,
- temp_offset_y_cpu: 0,
- temp_offset_y_gpu: 0,
- temp_offset_x_liquid: 0,
- temp_offset_y_liquid: 0,
degree_spacing: 16,
label_offset_x: 0,
label_offset_y: 0
@@ -1310,6 +1181,21 @@
System Environment
let DEFAULT_CONFIG = null;
let currentTab = 0;
+ let discoveredSensors = [];
+ let currentSensorConfig = {};
+
+ /**
+ * @brief Escape HTML special characters to prevent XSS.
+ */
+ function escapeHtml(str) {
+ if (str === null || str === undefined) return '';
+ return String(str)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
// ===== FALLBACK FUNCTIONS =====
const isInCoolerControl = window.parent !== window;
@@ -1367,75 +1253,536 @@
System Environment
window.runPluginScript = function(mainFunction) { mainFunction(); };
}
+ // ===== SENSOR DISCOVERY =====
+ async function discoverSensors(apiAddress, password) {
+ try {
+ const headers = { 'Content-Type': 'application/json' };
+ if (password) {
+ headers['Authorization'] = 'Basic ' + btoa('admin:' + password);
+ }
+
+ // GET /devices for device names
+ const devRes = await fetch(apiAddress + '/devices', { headers: password ? { 'Authorization': headers['Authorization'] } : {} });
+ const devData = devRes.ok ? await devRes.json() : { devices: [] };
+ const deviceNames = {};
+ for (const dev of (devData.devices || [])) {
+ if (dev.uid) deviceNames[dev.uid] = dev.name || dev.uid;
+ }
+
+ // POST /status for current sensor data
+ const statusRes = await fetch(apiAddress + '/status', { method: 'POST', headers, body: '{}' });
+ if (!statusRes.ok) return;
+ const statusData = await statusRes.json();
+
+ const sensors = [];
+ for (const dev of (statusData.devices || [])) {
+ const uid = dev.uid || '';
+ const devName = deviceNames[uid] || uid;
+ const lastStatus = Array.isArray(dev.status_history) && dev.status_history.length > 0
+ ? dev.status_history[dev.status_history.length - 1]
+ : {};
+
+ // Temperature sensors
+ const temps = lastStatus.temps || [];
+ for (const t of temps) {
+ if (t.temp !== undefined && t.temp > -50 && t.temp < 200) {
+ sensors.push({
+ id: uid + ':' + t.name,
+ label: devName + ' \u2014 ' + t.name,
+ device: devName,
+ name: t.name,
+ category: 'temp'
+ });
+ }
+ }
+
+ // Channel sensors (RPM, Duty, Watts, Freq)
+ const channels = lastStatus.channels || [];
+ for (const ch of channels) {
+ if (ch.rpm !== undefined) {
+ sensors.push({
+ id: uid + ':' + ch.name + ' RPM',
+ label: devName + ' \u2014 ' + ch.name + ' RPM',
+ device: devName,
+ name: ch.name + ' RPM',
+ category: 'rpm'
+ });
+ }
+ if (ch.duty !== undefined) {
+ sensors.push({
+ id: uid + ':' + ch.name + ' Duty',
+ label: devName + ' \u2014 ' + ch.name + ' Duty',
+ device: devName,
+ name: ch.name + ' Duty',
+ category: 'duty'
+ });
+ }
+ if (ch.watts !== undefined) {
+ sensors.push({
+ id: uid + ':' + ch.name + ' Watts',
+ label: devName + ' \u2014 ' + ch.name + ' Watts',
+ device: devName,
+ name: ch.name + ' Watts',
+ category: 'watts'
+ });
+ }
+ if (ch.freq !== undefined) {
+ sensors.push({
+ id: uid + ':' + ch.name + ' Freq',
+ label: devName + ' \u2014 ' + ch.name + ' Freq',
+ device: devName,
+ name: ch.name + ' Freq',
+ category: 'freq'
+ });
+ }
+ }
+ }
+
+ discoveredSensors = sensors;
+ updateSensorSlotOptions();
+ // Re-render sensor configs to update display names
+ if (Object.keys(currentSensorConfig).length > 0) {
+ renderSensorConfigs({ sensors: currentSensorConfig });
+ }
+ } catch (error) {
+ console.warn('Sensor discovery failed:', error);
+ }
+ }
+
+ function updateSensorSlotOptions() {
+ var selects = document.querySelectorAll('.sensor-slot-select');
+ for (var i = 0; i < selects.length; i++) {
+ var select = selects[i];
+ var currentValue = select.value;
+
+ // Remove old dynamic optgroups (keep first "Standard" group)
+ var groups = select.querySelectorAll('optgroup');
+ for (var g = groups.length - 1; g >= 1; g--) {
+ select.removeChild(groups[g]);
+ }
+ // Also remove any loose options outside optgroups
+ var looseOpts = select.querySelectorAll(':scope > option');
+ for (var lo = 0; lo < looseOpts.length; lo++) {
+ select.removeChild(looseOpts[lo]);
+ }
+
+ // Group discovered sensors by device
+ if (discoveredSensors.length > 0) {
+ var byDevice = {};
+ for (var s = 0; s < discoveredSensors.length; s++) {
+ var sensor = discoveredSensors[s];
+ if (!byDevice[sensor.device]) byDevice[sensor.device] = [];
+ byDevice[sensor.device].push(sensor);
+ }
+ for (var device in byDevice) {
+ if (!byDevice.hasOwnProperty(device)) continue;
+ var group = document.createElement('optgroup');
+ group.label = device;
+ var deviceSensors = byDevice[device];
+ deviceSensors.sort(function(a, b) {
+ return a.name.localeCompare(b.name);
+ });
+ for (var ds = 0; ds < deviceSensors.length; ds++) {
+ var opt = document.createElement('option');
+ opt.value = deviceSensors[ds].id;
+ opt.textContent = deviceSensors[ds].name;
+ group.appendChild(opt);
+ }
+ select.appendChild(group);
+ }
+ }
+
+ // Restore selected value
+ if (currentValue) {
+ var exists = false;
+ for (var oi = 0; oi < select.options.length; oi++) {
+ if (select.options[oi].value === currentValue) { exists = true; break; }
+ }
+ if (exists) {
+ select.value = currentValue;
+ } else if (currentValue !== 'none' && currentValue !== '') {
+ // Add as custom option if not found (e.g. sensor not currently connected)
+ var customOpt = document.createElement('option');
+ customOpt.value = currentValue;
+ customOpt.textContent = getSensorDisplayName(currentValue);
+ select.insertBefore(customOpt, select.firstChild);
+ select.value = currentValue;
+ }
+ }
+ }
+ }
+
+ // ===== SENSOR CONFIG DEFAULTS =====
+ function getDefaultSensorConfig(sensorId) {
+ var defaults = {
+ threshold_1: 55.0, threshold_2: 65.0, threshold_3: 75.0, max_scale: 115.0,
+ font_size_temp: 0, label: '',
+ threshold_1_color: { r: 0, g: 255, b: 0 },
+ threshold_2_color: { r: 255, g: 140, b: 0 },
+ threshold_3_color: { r: 255, g: 70, b: 0 },
+ threshold_4_color: { r: 255, g: 0, b: 0 },
+ offset_x: 0, offset_y: 0
+ };
+
+ if (sensorId === 'liquid') {
+ defaults.threshold_1 = 25.0;
+ defaults.threshold_2 = 28.0;
+ defaults.threshold_3 = 31.0;
+ defaults.max_scale = 50.0;
+ } else if (/RPM|rpm/.test(sensorId)) {
+ defaults.threshold_1 = 500;
+ defaults.threshold_2 = 1000;
+ defaults.threshold_3 = 1500;
+ defaults.max_scale = 3000;
+ } else if (/Duty|duty/.test(sensorId)) {
+ defaults.threshold_1 = 25;
+ defaults.threshold_2 = 50;
+ defaults.threshold_3 = 75;
+ defaults.max_scale = 100;
+ } else if (/Watts|watts/.test(sensorId)) {
+ defaults.threshold_1 = 50;
+ defaults.threshold_2 = 100;
+ defaults.threshold_3 = 200;
+ defaults.max_scale = 500;
+ } else if (/Freq|freq/.test(sensorId)) {
+ defaults.threshold_1 = 1000;
+ defaults.threshold_2 = 2000;
+ defaults.threshold_3 = 3000;
+ defaults.max_scale = 6000;
+ }
+
+ return defaults;
+ }
+
+ function getSensorDisplayName(sensorId) {
+ var legacyNames = { cpu: 'CPU', gpu: 'GPU', liquid: 'Liquid' };
+ if (legacyNames[sensorId]) return legacyNames[sensorId];
+ var found = null;
+ for (var i = 0; i < discoveredSensors.length; i++) {
+ if (discoveredSensors[i].id === sensorId) { found = discoveredSensors[i]; break; }
+ }
+ if (found) return found.label;
+ // Fallback: show the raw ID
+ return sensorId;
+ }
+
+ // ===== DYNAMIC SENSOR CONFIG UI =====
+ function renderSensorConfigs(config) {
+ var container = document.getElementById('sensor-config-container');
+ if (!container) return;
+
+ var sensors = config.sensors || {};
+
+ // Ensure all discovered sensors have a config entry
+ for (var d = 0; d < discoveredSensors.length; d++) {
+ var dsId = discoveredSensors[d].id;
+ if (!sensors[dsId]) {
+ sensors[dsId] = getDefaultSensorConfig(dsId);
+ }
+ }
+
+ var sensorIds = Object.keys(sensors);
+
+ // Filter out dynamic sensors that duplicate a legacy sensor
+ // e.g. if "liquid" exists, skip any "
:liquid" entry
+ var legacyNames = { cpu: true, gpu: true, liquid: true };
+ sensorIds = sensorIds.filter(function(id) {
+ if (legacyNames[id]) return true; // keep legacy entries
+ var colonIdx = id.indexOf(':');
+ if (colonIdx < 0) return true; // not dynamic
+ var sensorName = id.substring(colonIdx + 1).toLowerCase();
+ // Skip if a legacy key matches the sensor name
+ if (sensorName === 'liquid' && sensors['liquid']) return false;
+ if (sensorName === 'cpu' && sensors['cpu']) return false;
+ if (sensorName === 'gpu' && sensors['gpu']) return false;
+ return true;
+ });
+
+ // Sort: cpu/gpu/liquid always first, then alphabetically by display name
+ var legacyOrder = { cpu: 0, gpu: 1, liquid: 2 };
+ sensorIds.sort(function(a, b) {
+ var aLegacy = legacyOrder.hasOwnProperty(a) ? legacyOrder[a] : 99;
+ var bLegacy = legacyOrder.hasOwnProperty(b) ? legacyOrder[b] : 99;
+ if (aLegacy !== bLegacy) return aLegacy - bLegacy;
+ return getSensorDisplayName(a).localeCompare(getSensorDisplayName(b));
+ });
+
+ if (sensorIds.length === 0) {
+ container.innerHTML = 'No sensor configurations found. Assign sensors in the Display tab.
';
+ return;
+ }
+
+ container.innerHTML = '';
+
+ for (var si = 0; si < sensorIds.length; si++) {
+ var sensorId = sensorIds[si];
+ var sc = sensors[sensorId];
+ var safeName = sensorId.replace(/[^a-zA-Z0-9]/g, '_');
+ var displayName = escapeHtml(getSensorDisplayName(sensorId));
+ var defs = getDefaultSensorConfig(sensorId);
+ var eSensorId = escapeHtml(sensorId);
+ var eLabel = escapeHtml(sc.label || '');
+
+ // Sanitize all numeric values to break taint chain
+ var eFontSize = Number(sc.font_size_temp) || 0;
+ var eOffsetX = Number(sc.offset_x) || 0;
+ var eOffsetY = Number(sc.offset_y) || 0;
+ var eThresh1 = Number(sc.threshold_1 !== undefined ? sc.threshold_1 : defs.threshold_1);
+ var eThresh2 = Number(sc.threshold_2 !== undefined ? sc.threshold_2 : defs.threshold_2);
+ var eThresh3 = Number(sc.threshold_3 !== undefined ? sc.threshold_3 : defs.threshold_3);
+ var eMaxScale = Number(sc.max_scale !== undefined ? sc.max_scale : defs.max_scale);
+
+ var section = document.createElement('div');
+ section.className = 'sensor-config-section';
+ section.setAttribute('data-sensor-id', sensorId);
+
+ section.innerHTML =
+ '' +
+ '' + displayName + '
' +
+ '' +
+
+ // Display section
+ '
Display
' +
+ '
' +
+
+ // Thresholds section
+ '
Thresholds
' +
+ '
' +
+
+ // Bar Colors section
+ '
Bar Colors
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+
+ '
' +
+ ' ';
+
+ container.appendChild(section);
+
+ // Setup color pickers after DOM insertion
+ setupColorPicker('color_' + safeName + '_1', 'rgb_' + safeName + '_1', sc.threshold_1_color || defs.threshold_1_color);
+ setupColorPicker('color_' + safeName + '_2', 'rgb_' + safeName + '_2', sc.threshold_2_color || defs.threshold_2_color);
+ setupColorPicker('color_' + safeName + '_3', 'rgb_' + safeName + '_3', sc.threshold_3_color || defs.threshold_3_color);
+ setupColorPicker('color_' + safeName + '_4', 'rgb_' + safeName + '_4', sc.threshold_4_color || defs.threshold_4_color);
+ }
+
+ // Store reference for buildConfig
+ currentSensorConfig = sensors;
+ }
+
+ function ensureSensorConfigsForSlots() {
+ var slotIds = ['sensor_slot_up', 'sensor_slot_mid', 'sensor_slot_down'];
+ var activeSensors = [];
+
+ for (var i = 0; i < slotIds.length; i++) {
+ var select = document.getElementById(slotIds[i]);
+ if (select && select.value !== 'none' && select.value !== '') {
+ if (activeSensors.indexOf(select.value) === -1) {
+ activeSensors.push(select.value);
+ }
+ }
+ }
+
+ // Check if any sensor is missing from the config
+ var needsRebuild = false;
+ for (var s = 0; s < activeSensors.length; s++) {
+ if (!currentSensorConfig[activeSensors[s]]) {
+ needsRebuild = true;
+ break;
+ }
+ }
+
+ if (needsRebuild) {
+ // Collect current values from DOM
+ var sensors = collectSensorConfigsFromDOM();
+ // Add defaults for new sensors
+ for (var ns = 0; ns < activeSensors.length; ns++) {
+ if (!sensors[activeSensors[ns]]) {
+ sensors[activeSensors[ns]] = getDefaultSensorConfig(activeSensors[ns]);
+ }
+ }
+ currentSensorConfig = sensors;
+ renderSensorConfigs({ sensors: sensors });
+ }
+ }
+
+ function collectSensorConfigsFromDOM() {
+ var sensors = {};
+ var sections = document.querySelectorAll('.sensor-config-section');
+ for (var i = 0; i < sections.length; i++) {
+ var section = sections[i];
+ var sensorId = section.getAttribute('data-sensor-id');
+ if (!sensorId) continue;
+
+ var safeName = sensorId.replace(/[^a-zA-Z0-9]/g, '_');
+ var sc = {};
+
+ // Threshold/offset fields (sanitize: numbers stay numbers, strings get escaped)
+ var fields = section.querySelectorAll('.sensor-field');
+ for (var f = 0; f < fields.length; f++) {
+ var field = fields[f].getAttribute('data-field');
+ if (field) {
+ if (field === 'label') {
+ // Label is the only string field - store as-is (escaped on render)
+ sc[field] = String(fields[f].value || '');
+ } else {
+ // All other fields are numeric - force to number
+ sc[field] = Number(fields[f].value) || 0;
+ }
+ }
+ }
+
+ // Colors
+ sc.threshold_1_color = getColorFromPicker('color_' + safeName + '_1');
+ sc.threshold_2_color = getColorFromPicker('color_' + safeName + '_2');
+ sc.threshold_3_color = getColorFromPicker('color_' + safeName + '_3');
+ sc.threshold_4_color = getColorFromPicker('color_' + safeName + '_4');
+
+ sensors[sensorId] = sc;
+ }
+ return sensors;
+ }
+
// ===== DEVICE DETECTION =====
- let lastApiAddress = '';
- let lastApiPassword = '';
+ var lastApiAddress = '';
+ var lastApiPassword = '';
async function fetchDeviceInfo(apiAddress, password) {
lastApiAddress = apiAddress;
lastApiPassword = password;
- const loadingEl = document.getElementById('device-loading');
- const contentEl = document.getElementById('device-panel-content');
- const errorEl = document.getElementById('device-error');
+ var loadingEl = document.getElementById('device-loading');
+ var contentEl = document.getElementById('device-panel-content');
+ var errorEl = document.getElementById('device-error');
loadingEl.style.display = 'block';
contentEl.style.display = 'none';
errorEl.style.display = 'none';
try {
- const headers = {};
+ var headers = {};
if (password) {
- headers['Authorization'] = `Basic ${btoa(`admin:${password}`)}`;
+ headers['Authorization'] = 'Basic ' + btoa('admin:' + password);
}
- const response = await fetch(`${apiAddress}/devices`, { headers });
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
- const data = await response.json();
-
- const devices = data.devices || [];
- let found = null;
- for (const dev of devices) {
- const dtype = dev.type || '';
+ var response = await fetch(apiAddress + '/devices', { headers: headers });
+ if (!response.ok) throw new Error('HTTP ' + response.status);
+ var data = await response.json();
+
+ var devices = data.devices || [];
+ var found = null;
+ for (var i = 0; i < devices.length; i++) {
+ var dev = devices[i];
+ var dtype = dev.type || '';
if (dtype === 'Liquidctl') {
- found = dev;
- break;
+ var lcdInfo = dev.info && dev.info.channels && dev.info.channels.lcd && dev.info.channels.lcd.lcd_info;
+ if (lcdInfo && lcdInfo.screen_width > 0 && lcdInfo.screen_height > 0) {
+ found = dev;
+ break;
+ }
}
}
loadingEl.style.display = 'none';
if (found) {
- const name = found.name || 'Unknown Device';
- const uid = found.uid || '—';
+ var name = found.name || 'Unknown Device';
+ var uid = found.uid || '\u2014';
document.getElementById('device-name').textContent = name;
document.getElementById('device-uid').textContent = uid;
- // Firmware from lc_info
- const firmware = found.lc_info?.firmware_version || '—';
+ var firmware = (found.lc_info && found.lc_info.firmware_version) || '\u2014';
document.getElementById('device-firmware').textContent = firmware;
- // Liquidctl version from driver_info
- const liquidctlVer = found.info?.driver_info?.version || '—';
+ var liquidctlVer = (found.info && found.info.driver_info && found.info.driver_info.version) || '\u2014';
document.getElementById('device-liquidctl').textContent = liquidctlVer;
- // LCD info path: info.channels..lcd_info
- let width = 0, height = 0;
- const channels = found.info?.channels;
+ var width = 0, height = 0;
+ var channels = found.info && found.info.channels;
if (channels) {
- for (const ch of Object.values(channels)) {
- if (ch.lcd_info) {
- width = ch.lcd_info.screen_width || 0;
- height = ch.lcd_info.screen_height || 0;
+ for (var chKey in channels) {
+ if (channels.hasOwnProperty(chKey) && channels[chKey].lcd_info) {
+ width = channels[chKey].lcd_info.screen_width || 0;
+ height = channels[chKey].lcd_info.screen_height || 0;
break;
}
}
}
- const resEl = document.getElementById('device-resolution');
- const shapeEl = document.getElementById('device-shape');
+ var resEl = document.getElementById('device-resolution');
+ var shapeEl = document.getElementById('device-shape');
if (width > 0 && height > 0) {
- resEl.textContent = `${width} × ${height} px`;
- const isCircular = name.includes('Kraken') && (width > 240 || height > 240);
+ resEl.textContent = width + ' \u00D7 ' + height + ' px';
+ var isCircular = name.includes('Kraken') && (width > 240 || height > 240);
shapeEl.textContent = isCircular ? 'Circular' : 'Rectangular';
} else {
resEl.textContent = 'Not available';
@@ -1446,12 +1793,12 @@ System Environment
' Connected';
contentEl.style.display = 'block';
} else {
- document.getElementById('device-name').textContent = '—';
- document.getElementById('device-uid').textContent = '—';
- document.getElementById('device-resolution').textContent = '—';
- document.getElementById('device-shape').textContent = '—';
- document.getElementById('device-firmware').textContent = '—';
- document.getElementById('device-liquidctl').textContent = '—';
+ document.getElementById('device-name').textContent = '\u2014';
+ document.getElementById('device-uid').textContent = '\u2014';
+ document.getElementById('device-resolution').textContent = '\u2014';
+ document.getElementById('device-shape').textContent = '\u2014';
+ document.getElementById('device-firmware').textContent = '\u2014';
+ document.getElementById('device-liquidctl').textContent = '\u2014';
document.getElementById('device-status').innerHTML =
' No LCD device found';
contentEl.style.display = 'block';
@@ -1462,7 +1809,7 @@ System Environment
document.getElementById('device-status').innerHTML =
' Detection failed';
document.getElementById('device-error-text').textContent =
- `Could not connect to CoolerControl API (${error.message}).`;
+ 'Could not connect to CoolerControl API (' + error.message + ').';
contentEl.style.display = 'block';
errorEl.style.display = 'block';
}
@@ -1474,28 +1821,26 @@ System Environment
async function fetchSystemInfo(apiAddress, password) {
try {
- const headers = {};
+ var headers = {};
if (password) {
- headers['Authorization'] = `Basic ${btoa(`admin:${password}`)}`;
+ headers['Authorization'] = 'Basic ' + btoa('admin:' + password);
}
- // Fetch /health for CC version
- const healthRes = await fetch(`${apiAddress}/health`, { headers });
+ var healthRes = await fetch(apiAddress + '/health', { headers: headers });
if (healthRes.ok) {
- const health = await healthRes.json();
- const ccVersion = health.details?.version || '—';
+ var health = await healthRes.json();
+ var ccVersion = (health.details && health.details.version) || '\u2014';
document.getElementById('sys-cc-version').textContent = ccVersion;
}
- // Fetch /devices to extract kernel from driver_info (drv_type 'Kernel')
- const devRes = await fetch(`${apiAddress}/devices`, { headers });
+ var devRes = await fetch(apiAddress + '/devices', { headers: headers });
if (devRes.ok) {
- const data = await devRes.json();
- const devices = data.devices || [];
- let kernel = '';
- for (const dev of devices) {
- const drvType = dev.info?.driver_info?.drv_type;
- const ver = dev.info?.driver_info?.version;
+ var data = await devRes.json();
+ var devices = data.devices || [];
+ var kernel = '';
+ for (var i = 0; i < devices.length; i++) {
+ var drvType = devices[i].info && devices[i].info.driver_info && devices[i].info.driver_info.drv_type;
+ var ver = devices[i].info && devices[i].info.driver_info && devices[i].info.driver_info.version;
if (drvType === 'Kernel' && ver) {
kernel = ver;
break;
@@ -1512,53 +1857,63 @@ System Environment
// ===== UI FUNCTIONS =====
function switchTab(index) {
- document.querySelectorAll('.tab').forEach((tab, i) => tab.classList.toggle('active', i === index));
- document.querySelectorAll('.panel').forEach((panel, i) => panel.classList.toggle('active', i === index));
+ var tabs = document.querySelectorAll('.tab');
+ var panels = document.querySelectorAll('.panel');
+ for (var i = 0; i < tabs.length; i++) {
+ tabs[i].classList.toggle('active', i === index);
+ }
+ for (var j = 0; j < panels.length; j++) {
+ panels[j].classList.toggle('active', j === index);
+ }
currentTab = index;
}
function toggleCircleInterval() {
- const mode = document.getElementById('display_mode').value;
+ var mode = document.getElementById('display_mode').value;
document.getElementById('circle_interval_group').style.display = mode === 'circle' ? 'block' : 'none';
- const midSlotGroup = document.getElementById('sensor_slot_mid_group');
- const midBarHeightGroup = document.getElementById('bar_height_mid_group');
+ var midSlotGroup = document.getElementById('sensor_slot_mid_group');
+ var midBarHeightGroup = document.getElementById('bar_height_mid_group');
if (midSlotGroup) midSlotGroup.style.display = mode === 'circle' ? 'block' : 'none';
if (midBarHeightGroup) midBarHeightGroup.style.display = mode === 'circle' ? 'block' : 'none';
if (mode === 'dual') {
- const midSlot = document.getElementById('sensor_slot_mid');
+ var midSlot = document.getElementById('sensor_slot_mid');
if (midSlot) midSlot.value = 'none';
}
validateSensorSlots();
}
function validateSensorSlots() {
- const mode = document.getElementById('display_mode').value;
- const slotUp = document.getElementById('sensor_slot_up').value;
- const slotMid = document.getElementById('sensor_slot_mid').value;
- const slotDown = document.getElementById('sensor_slot_down').value;
+ var mode = document.getElementById('display_mode').value;
+ var slotUp = document.getElementById('sensor_slot_up').value;
+ var slotMid = document.getElementById('sensor_slot_mid').value;
+ var slotDown = document.getElementById('sensor_slot_down').value;
- const warningDiv = document.getElementById('sensor_slot_warning');
- const warningText = document.getElementById('sensor_slot_warning_text');
- let warnings = [];
+ var warningDiv = document.getElementById('sensor_slot_warning');
+ var warningText = document.getElementById('sensor_slot_warning_text');
+ var warnings = [];
- const activeSlots = [];
+ var activeSlots = [];
if (slotUp !== 'none') activeSlots.push({ name: 'Upper', value: slotUp });
if (mode === 'circle' && slotMid !== 'none') activeSlots.push({ name: 'Middle', value: slotMid });
if (slotDown !== 'none') activeSlots.push({ name: 'Lower', value: slotDown });
if (activeSlots.length === 0) warnings.push('At least one sensor slot must be active.');
- const usedSensors = {};
- for (const slot of activeSlots) {
- if (usedSensors[slot.value]) {
- warnings.push(`Duplicate: "${slot.value}" used in ${usedSensors[slot.value]} and ${slot.name}.`);
+ var usedSensors = {};
+ for (var i = 0; i < activeSlots.length; i++) {
+ if (usedSensors[activeSlots[i].value]) {
+ warnings.push('Duplicate: "' + activeSlots[i].value + '" used in ' + usedSensors[activeSlots[i].value] + ' and ' + activeSlots[i].name + '.');
} else {
- usedSensors[slot.value] = slot.name;
+ usedSensors[activeSlots[i].value] = activeSlots[i].name;
}
}
warningDiv.style.display = warnings.length > 0 ? 'block' : 'none';
warningText.textContent = warnings.join(' ');
+
+ // Ensure sensor configs exist for all active sensors
+ ensureSensorConfigsForSlots();
+
return warnings.length === 0;
}
@@ -1567,40 +1922,40 @@ System Environment
}
function rgbToHex(rgb) {
- const r = Math.round(rgb.r).toString(16).padStart(2, '0');
- const g = Math.round(rgb.g).toString(16).padStart(2, '0');
- const b = Math.round(rgb.b).toString(16).padStart(2, '0');
- return `#${r}${g}${b}`;
+ var r = Math.round(rgb.r).toString(16).padStart(2, '0');
+ var g = Math.round(rgb.g).toString(16).padStart(2, '0');
+ var b = Math.round(rgb.b).toString(16).padStart(2, '0');
+ return '#' + r + g + b;
}
function hexToRgb(hex) {
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null;
}
function updateColorRGB(colorId, rgbId) {
- const rgb = hexToRgb(document.getElementById(colorId).value);
- if (rgb) document.getElementById(rgbId).textContent = `RGB(${rgb.r}, ${rgb.g}, ${rgb.b})`;
+ var rgb = hexToRgb(document.getElementById(colorId).value);
+ if (rgb) document.getElementById(rgbId).textContent = 'RGB(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ')';
}
function setupColorPicker(colorId, rgbId, rgbObj) {
- const colorInput = document.getElementById(colorId);
+ var colorInput = document.getElementById(colorId);
if (rgbObj && colorInput) {
colorInput.value = rgbToHex(rgbObj);
- const rgbEl = document.getElementById(rgbId);
- if (rgbEl) rgbEl.textContent = `RGB(${Math.round(rgbObj.r)}, ${Math.round(rgbObj.g)}, ${Math.round(rgbObj.b)})`;
+ var rgbEl = document.getElementById(rgbId);
+ if (rgbEl) rgbEl.textContent = 'RGB(' + Math.round(rgbObj.r) + ', ' + Math.round(rgbObj.g) + ', ' + Math.round(rgbObj.b) + ')';
}
}
function getColorFromPicker(colorId) {
- const el = document.getElementById(colorId);
+ var el = document.getElementById(colorId);
return el ? hexToRgb(el.value) : null;
}
async function loadDefaultConfig() {
try {
- const response = await fetch('config.json');
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ var response = await fetch('config.json');
+ if (!response.ok) throw new Error('HTTP ' + response.status);
DEFAULT_CONFIG = await response.json();
return DEFAULT_CONFIG;
} catch (error) {
@@ -1610,64 +1965,99 @@ System Environment
}
}
+ // ===== BUILD CONFIG FROM FORM =====
function buildConfig() {
- const form = document.getElementById('configForm');
- const formData = new FormData(form);
- const config = {
+ var form = document.getElementById('configForm');
+ var formData = new FormData(form);
+ var config = {
daemon: {}, paths: {}, display: {}, layout: {}, colors: {},
- font: {}, cpu: {}, gpu: {}, liquid: {}, positioning: {}
+ font: {}, sensors: {}, positioning: {}
};
- for (const [key, value] of formData.entries()) {
- const [section, field] = key.split('.');
- if (section && field && config[section]) {
- const numValue = parseFloat(value);
- config[section][field] = isNaN(numValue) ? value : numValue;
+ var entries = formData.entries();
+ var entry;
+ while (!(entry = entries.next()).done) {
+ var key = entry.value[0];
+ var value = entry.value[1];
+ var parts = key.split('.');
+ if (parts.length === 2 && config[parts[0]] !== undefined) {
+ // Sensor slot values must always be strings
+ var isSlotField = (parts[1] === 'sensor_slot_up' || parts[1] === 'sensor_slot_mid' || parts[1] === 'sensor_slot_down');
+ if (isSlotField) {
+ config[parts[0]][parts[1]] = value;
+ } else {
+ // Strict number check: only convert pure numeric strings
+ var trimmed = value.trim();
+ var isNumeric = trimmed !== '' && /^-?\d+(\.\d+)?$/.test(trimmed);
+ config[parts[0]][parts[1]] = isNumeric ? parseFloat(trimmed) : value;
+ }
}
}
- // Colors
+ // Global colors
config.colors.display_background = getColorFromPicker('color_display_background');
config.colors.bar_background = getColorFromPicker('color_bar_background');
config.colors.bar_border = getColorFromPicker('color_bar_border');
config.colors.font_temp = getColorFromPicker('color_font_temp');
config.colors.font_label = getColorFromPicker('color_font_label');
- // CPU colors
- config.cpu.threshold_1_color = getColorFromPicker('color_cpu_1');
- config.cpu.threshold_2_color = getColorFromPicker('color_cpu_2');
- config.cpu.threshold_3_color = getColorFromPicker('color_cpu_3');
- config.cpu.threshold_4_color = getColorFromPicker('color_cpu_4');
-
- // GPU colors
- config.gpu.threshold_1_color = getColorFromPicker('color_gpu_1');
- config.gpu.threshold_2_color = getColorFromPicker('color_gpu_2');
- config.gpu.threshold_3_color = getColorFromPicker('color_gpu_3');
- config.gpu.threshold_4_color = getColorFromPicker('color_gpu_4');
-
- // Liquid colors
- config.liquid.threshold_1_color = getColorFromPicker('color_liquid_1');
- config.liquid.threshold_2_color = getColorFromPicker('color_liquid_2');
- config.liquid.threshold_3_color = getColorFromPicker('color_liquid_3');
- config.liquid.threshold_4_color = getColorFromPicker('color_liquid_4');
+ // Sensor configs from dynamic form sections
+ config.sensors = collectSensorConfigsFromDOM();
return config;
}
+ // ===== POPULATE FORM FROM CONFIG =====
function populateForm(config) {
- for (const [section, fields] of Object.entries(config)) {
+ // Standard form fields (non-sensor, non-color)
+ for (var section in config) {
+ if (!config.hasOwnProperty(section)) continue;
+ if (section === 'sensors' || section === 'colors') continue;
+ var fields = config[section];
if (typeof fields === 'object' && fields !== null) {
- for (const [field, value] of Object.entries(fields)) {
- const input = document.querySelector(`[name="${section}.${field}"]`);
+ for (var field in fields) {
+ if (!fields.hasOwnProperty(field)) continue;
+ var value = fields[field];
+ var input = document.querySelector('[name="' + section + '.' + field + '"]');
if (input && typeof value !== 'object') {
- input.value = typeof value === 'boolean' ? (value ? "1" : "0") : value;
+ input.value = (typeof value === 'boolean') ? (value ? "1" : "0") : value;
if (field === 'brightness') updateRangeValue('brightness', value);
}
}
}
}
- // Colors
+ // Sensor slot selects: ensure custom values are available as options
+ var slotFields = ['sensor_slot_up', 'sensor_slot_mid', 'sensor_slot_down'];
+ var slotDefaults = { sensor_slot_up: 'cpu', sensor_slot_mid: 'liquid', sensor_slot_down: 'gpu' };
+ for (var sf = 0; sf < slotFields.length; sf++) {
+ var slotValue = config.display && config.display[slotFields[sf]];
+ if (slotValue) {
+ // Ensure value is a string (fix legacy numeric corruption)
+ slotValue = String(slotValue);
+ // Reject pure numeric values (corrupt from old parseFloat bug)
+ if (/^\d+$/.test(slotValue)) {
+ slotValue = slotDefaults[slotFields[sf]] || 'cpu';
+ config.display[slotFields[sf]] = slotValue;
+ }
+ var select = document.getElementById(slotFields[sf]);
+ if (select) {
+ var exists = false;
+ for (var oi = 0; oi < select.options.length; oi++) {
+ if (select.options[oi].value === slotValue) { exists = true; break; }
+ }
+ if (!exists && slotValue !== 'none') {
+ var opt = document.createElement('option');
+ opt.value = slotValue;
+ opt.textContent = getSensorDisplayName(slotValue);
+ select.insertBefore(opt, select.firstChild);
+ }
+ select.value = slotValue;
+ }
+ }
+ }
+
+ // Global colors
if (config.colors) {
setupColorPicker('color_display_background', 'rgb_display_background', config.colors.display_background);
setupColorPicker('color_bar_background', 'rgb_bar_background', config.colors.bar_background);
@@ -1676,47 +2066,28 @@ System Environment
setupColorPicker('color_font_label', 'rgb_font_label', config.colors.font_label);
}
- // CPU colors
- if (config.cpu) {
- setupColorPicker('color_cpu_1', 'rgb_cpu_1', config.cpu.threshold_1_color);
- setupColorPicker('color_cpu_2', 'rgb_cpu_2', config.cpu.threshold_2_color);
- setupColorPicker('color_cpu_3', 'rgb_cpu_3', config.cpu.threshold_3_color);
- setupColorPicker('color_cpu_4', 'rgb_cpu_4', config.cpu.threshold_4_color);
- }
-
- // GPU colors
- if (config.gpu) {
- setupColorPicker('color_gpu_1', 'rgb_gpu_1', config.gpu.threshold_1_color);
- setupColorPicker('color_gpu_2', 'rgb_gpu_2', config.gpu.threshold_2_color);
- setupColorPicker('color_gpu_3', 'rgb_gpu_3', config.gpu.threshold_3_color);
- setupColorPicker('color_gpu_4', 'rgb_gpu_4', config.gpu.threshold_4_color);
- }
-
- // Liquid colors
- if (config.liquid) {
- setupColorPicker('color_liquid_1', 'rgb_liquid_1', config.liquid.threshold_1_color);
- setupColorPicker('color_liquid_2', 'rgb_liquid_2', config.liquid.threshold_2_color);
- setupColorPicker('color_liquid_3', 'rgb_liquid_3', config.liquid.threshold_3_color);
- setupColorPicker('color_liquid_4', 'rgb_liquid_4', config.liquid.threshold_4_color);
- }
+ // Render dynamic sensor configs
+ currentSensorConfig = config.sensors || {};
+ renderSensorConfigs(config);
toggleCircleInterval();
validateSensorSlots();
}
+ // ===== SAVE / RESET / EXPORT =====
async function saveConfig() {
if (!validateSensorSlots()) {
alert("Please fix sensor slot warnings before saving.");
return;
}
- const config = buildConfig();
+ var config = buildConfig();
try {
await writeConfigToFile(config);
requestRestartAfterSave(1000);
await savePluginConfig(config);
alert("Configuration saved! Plugin will restart.");
- setTimeout(() => window.close(), 300);
+ setTimeout(function() { window.close(); }, 300);
} catch (error) {
console.error("Save failed:", error);
alert("Save failed: " + error.message);
@@ -1725,14 +2096,14 @@ System Environment
function exportConfig() {
try {
- const config = buildConfig();
- const jsonStr = JSON.stringify(config, null, 2);
- const blob = new Blob([jsonStr], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
+ var config = buildConfig();
+ var jsonStr = JSON.stringify(config, null, 2);
+ var blob = new Blob([jsonStr], { type: 'application/json' });
+ var url = URL.createObjectURL(blob);
+ var a = document.createElement('a');
a.href = url;
- const date = new Date().toISOString().slice(0, 10);
- a.download = `coolerdash-config-backup-${date}.json`;
+ var date = new Date().toISOString().slice(0, 10);
+ a.download = 'coolerdash-config-backup-' + date + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
@@ -1745,20 +2116,21 @@ System Environment
async function restartPluginDaemon(config) {
try {
- const apiAddress = config.daemon?.address || 'http://localhost:11987';
- const password = config.daemon?.password || '';
- const headers = { 'Content-Type': 'application/json' };
- if (password) headers['Authorization'] = `Basic ${btoa(`admin:${password}`)}`;
+ var apiAddress = (config.daemon && config.daemon.address) || 'http://localhost:11987';
+ var password = (config.daemon && config.daemon.password) || '';
+ var headers = { 'Content-Type': 'application/json' };
+ if (password) headers['Authorization'] = 'Basic ' + btoa('admin:' + password);
- const response = await fetch(`${apiAddress}/plugins/coolerdash/restart`, { method: 'POST', headers });
+ var response = await fetch(apiAddress + '/plugins/coolerdash/restart', { method: 'POST', headers: headers });
if (!response.ok) console.warn("Could not restart plugin automatically");
} catch (error) {
console.error("Plugin restart failed:", error);
}
}
- function requestRestartAfterSave(delayMs = 800) {
- const doRestart = () => {
+ function requestRestartAfterSave(delayMs) {
+ delayMs = delayMs || 800;
+ var doRestart = function() {
try {
if (typeof restart === 'function') restart();
else restartPluginDaemon(buildConfig());
@@ -1766,10 +2138,10 @@ System Environment
};
if (typeof successfulConfigSaveCallback === 'function') {
- successfulConfigSaveCallback(() => setTimeout(doRestart, delayMs));
+ successfulConfigSaveCallback(function() { setTimeout(doRestart, delayMs); });
} else {
- const handler = (event) => {
- if (event.data?.type === 'configSaved') {
+ var handler = function(event) {
+ if (event.data && event.data.type === 'configSaved') {
window.removeEventListener('message', handler);
setTimeout(doRestart, delayMs);
}
@@ -1780,20 +2152,20 @@ System Environment
async function writeConfigToFile(config) {
try {
- const apiAddress = config.daemon?.address || 'http://localhost:11987';
- const password = config.daemon?.password || '';
+ var apiAddress = (config.daemon && config.daemon.address) || 'http://localhost:11987';
+ var password = (config.daemon && config.daemon.password) || '';
if (password) {
- const response = await fetch(`${apiAddress}/plugins/coolerdash/config`, {
+ var response = await fetch(apiAddress + '/plugins/coolerdash/config', {
method: 'PUT',
- headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${btoa(`admin:${password}`)}` },
+ headers: { 'Content-Type': 'application/json', 'Authorization': 'Basic ' + btoa('admin:' + password) },
body: JSON.stringify(config, null, 2)
});
if (response.ok) return true;
}
- const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
- const a = document.createElement('a');
+ var blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
+ var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'config.json';
a.click();
@@ -1812,43 +2184,119 @@ System Environment
requestRestartAfterSave(1000);
await savePluginConfig(FACTORY_DEFAULTS);
alert('Reset to factory defaults! Plugin will restart.');
- setTimeout(() => window.close(), 300);
+ setTimeout(function() { window.close(); }, 300);
} catch (error) {
console.error("Reset failed:", error);
alert('Reset failed: ' + error.message);
}
}
- // Initialize
- runPluginScript(async () => {
+ // ===== LEGACY CONFIG MIGRATION =====
+ function migrateConfig(config) {
+ var sensors = config.sensors || {};
+ var needsMigration = false;
+
+ // Detect legacy format: top-level cpu/gpu/liquid sections
+ if (!config.sensors && (config.cpu || config.gpu || config.liquid)) {
+ needsMigration = true;
+ if (config.cpu) sensors.cpu = config.cpu;
+ if (config.gpu) sensors.gpu = config.gpu;
+ if (config.liquid) sensors.liquid = config.liquid;
+
+ // Migrate per-sensor positioning offsets
+ if (config.positioning) {
+ if (sensors.cpu) {
+ sensors.cpu.offset_x = config.positioning.temp_offset_x_cpu || 0;
+ sensors.cpu.offset_y = config.positioning.temp_offset_y_cpu || 0;
+ }
+ if (sensors.gpu) {
+ sensors.gpu.offset_x = config.positioning.temp_offset_x_gpu || 0;
+ sensors.gpu.offset_y = config.positioning.temp_offset_y_gpu || 0;
+ }
+ if (sensors.liquid) {
+ sensors.liquid.offset_x = config.positioning.temp_offset_x_liquid || 0;
+ sensors.liquid.offset_y = config.positioning.temp_offset_y_liquid || 0;
+ }
+ }
+ }
+
+ // Build merged config
+ var merged = {
+ daemon: Object.assign({}, DEFAULT_CONFIG.daemon, config.daemon || {}),
+ paths: Object.assign({}, DEFAULT_CONFIG.paths, config.paths || {}),
+ display: Object.assign({}, DEFAULT_CONFIG.display, config.display || {}),
+ layout: Object.assign({}, DEFAULT_CONFIG.layout, config.layout || {}),
+ colors: Object.assign({}, DEFAULT_CONFIG.colors, config.colors || {}),
+ font: Object.assign({}, DEFAULT_CONFIG.font, config.font || {}),
+ sensors: Object.assign({}, DEFAULT_CONFIG.sensors || {}),
+ positioning: Object.assign({}, DEFAULT_CONFIG.positioning, config.positioning || {})
+ };
+
+ // Deep merge sensor configs (skip corrupt numeric keys)
+ for (var id in sensors) {
+ if (!sensors.hasOwnProperty(id)) continue;
+ // Remove corrupt numeric sensor IDs from old parseFloat bug
+ if (/^\d+$/.test(id)) continue;
+ var defaults = merged.sensors[id] || getDefaultSensorConfig(id);
+ merged.sensors[id] = Object.assign({}, defaults, sensors[id]);
+ // Deep merge color objects
+ var colorKeys = ['threshold_1_color', 'threshold_2_color', 'threshold_3_color', 'threshold_4_color'];
+ for (var c = 0; c < colorKeys.length; c++) {
+ if (sensors[id][colorKeys[c]]) {
+ merged.sensors[id][colorKeys[c]] = Object.assign({}, defaults[colorKeys[c]] || {}, sensors[id][colorKeys[c]]);
+ }
+ }
+ }
+
+ // Sanitize corrupt numeric slot values (from old parseFloat bug)
+ var slotDefaults = { sensor_slot_up: 'cpu', sensor_slot_mid: 'liquid', sensor_slot_down: 'gpu' };
+ for (var slotKey in slotDefaults) {
+ if (merged.display && merged.display[slotKey] !== undefined) {
+ var sv = String(merged.display[slotKey]);
+ if (/^\d+$/.test(sv)) {
+ merged.display[slotKey] = slotDefaults[slotKey];
+ } else {
+ merged.display[slotKey] = sv;
+ }
+ }
+ }
+
+ // Clean up legacy positioning keys
+ delete merged.positioning.temp_offset_x_cpu;
+ delete merged.positioning.temp_offset_x_gpu;
+ delete merged.positioning.temp_offset_y_cpu;
+ delete merged.positioning.temp_offset_y_gpu;
+ delete merged.positioning.temp_offset_x_liquid;
+ delete merged.positioning.temp_offset_y_liquid;
+
+ // Remove legacy top-level sensor sections
+ delete merged.cpu;
+ delete merged.gpu;
+ delete merged.liquid;
+
+ return merged;
+ }
+
+ // ===== INITIALIZATION =====
+ runPluginScript(async function() {
try {
await loadDefaultConfig();
- let config = await getPluginConfig();
+ var config = await getPluginConfig();
if (!config || Object.keys(config).length === 0) {
- config = DEFAULT_CONFIG;
+ config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
} else {
- config = {
- daemon: { ...DEFAULT_CONFIG.daemon, ...(config.daemon || {}) },
- paths: { ...DEFAULT_CONFIG.paths, ...(config.paths || {}) },
- display: { ...DEFAULT_CONFIG.display, ...(config.display || {}) },
- layout: { ...DEFAULT_CONFIG.layout, ...(config.layout || {}) },
- colors: { ...DEFAULT_CONFIG.colors, ...(config.colors || {}) },
- font: { ...DEFAULT_CONFIG.font, ...(config.font || {}) },
- cpu: { ...DEFAULT_CONFIG.cpu, ...(config.cpu || {}) },
- gpu: { ...DEFAULT_CONFIG.gpu, ...(config.gpu || {}) },
- liquid: { ...DEFAULT_CONFIG.liquid, ...(config.liquid || {}) },
- positioning: { ...DEFAULT_CONFIG.positioning, ...(config.positioning || {}) }
- };
+ config = migrateConfig(config);
}
populateForm(config);
- // Detect device from CoolerControl API
- const apiAddr = config.daemon?.address || 'http://localhost:11987';
- const apiPass = config.daemon?.password || '';
+ // Detect device and discover sensors from CoolerControl API
+ var apiAddr = (config.daemon && config.daemon.address) || 'http://localhost:11987';
+ var apiPass = (config.daemon && config.daemon.password) || '';
fetchDeviceInfo(apiAddr, apiPass);
fetchSystemInfo(apiAddr, apiPass);
+ discoverSensors(apiAddr, apiPass);
} catch (error) {
console.error("Init failed:", error);
if (DEFAULT_CONFIG) populateForm(DEFAULT_CONFIG);
diff --git a/etc/udev/rules.d/99-coolerdash.rules b/etc/udev/rules.d/99-coolerdash.rules
index 2043616..b1d32f0 100644
--- a/etc/udev/rules.d/99-coolerdash.rules
+++ b/etc/udev/rules.d/99-coolerdash.rules
@@ -4,10 +4,10 @@
# NZXT Vendor ID: 1e71
# Disable autosuspend and enable persist for all NZXT devices (Kraken, etc.)
# Disable USB autosuspend for NZXT devices to prevent "bucket switch" errors
-ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/control}="on"
-ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/autosuspend_delay_ms}="-1"
-ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/persist}="1"
-ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/wakeup}="disabled"
-ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{avoid_reset_quirk}="1"
+#ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/control}="on"
+#ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/autosuspend_delay_ms}="-1"
+#ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/persist}="1"
+#ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{power/wakeup}="disabled"
+#ACTION=="add|bind|change", SUBSYSTEM=="usb", ATTR{idVendor}=="1e71", ATTR{avoid_reset_quirk}="1"
# Placeholder for additional rules if needed in the future
diff --git a/src/device/config.c b/src/device/config.c
index 5fcb627..464615d 100644
--- a/src/device/config.c
+++ b/src/device/config.c
@@ -237,39 +237,144 @@ static void set_font_defaults(Config *config)
}
/**
- * @brief Set temperature defaults.
+ * @brief Find or create a SensorConfig entry by sensor_id.
+ * @return Pointer to entry, or NULL if array is full
*/
-static void set_temperature_defaults(Config *config)
+static SensorConfig *ensure_sensor_config(Config *config, const char *sensor_id)
{
- // CPU temperature defaults
- if (config->temp_cpu_threshold_1 == 0.0f)
- config->temp_cpu_threshold_1 = 55.0f;
- if (config->temp_cpu_threshold_2 == 0.0f)
- config->temp_cpu_threshold_2 = 65.0f;
- if (config->temp_cpu_threshold_3 == 0.0f)
- config->temp_cpu_threshold_3 = 75.0f;
- if (config->temp_cpu_max_scale == 0.0f)
- config->temp_cpu_max_scale = 115.0f;
-
- // GPU temperature defaults (same as CPU)
- if (config->temp_gpu_threshold_1 == 0.0f)
- config->temp_gpu_threshold_1 = 55.0f;
- if (config->temp_gpu_threshold_2 == 0.0f)
- config->temp_gpu_threshold_2 = 65.0f;
- if (config->temp_gpu_threshold_3 == 0.0f)
- config->temp_gpu_threshold_3 = 75.0f;
- if (config->temp_gpu_max_scale == 0.0f)
- config->temp_gpu_max_scale = 115.0f;
-
- // Liquid temperature defaults
- if (config->temp_liquid_threshold_1 == 0.0f)
- config->temp_liquid_threshold_1 = 25.0f;
- if (config->temp_liquid_threshold_2 == 0.0f)
- config->temp_liquid_threshold_2 = 28.0f;
- if (config->temp_liquid_threshold_3 == 0.0f)
- config->temp_liquid_threshold_3 = 31.0f;
- if (config->temp_liquid_max_scale == 0.0f)
- config->temp_liquid_max_scale = 50.0f;
+ for (int i = 0; i < config->sensor_config_count; i++)
+ {
+ if (strcmp(config->sensor_configs[i].sensor_id, sensor_id) == 0)
+ return &config->sensor_configs[i];
+ }
+ if (config->sensor_config_count >= MAX_SENSOR_CONFIGS)
+ return NULL;
+ SensorConfig *sc = &config->sensor_configs[config->sensor_config_count++];
+ memset(sc, 0, sizeof(SensorConfig));
+ cc_safe_strcpy(sc->sensor_id, sizeof(sc->sensor_id), sensor_id);
+ return sc;
+}
+
+/**
+ * @brief Initialize a SensorConfig with defaults for a given category.
+ */
+void init_default_sensor_config(SensorConfig *sc, const char *sensor_id,
+ int category)
+{
+ if (!sc || !sensor_id)
+ return;
+ memset(sc, 0, sizeof(SensorConfig));
+ cc_safe_strcpy(sc->sensor_id, sizeof(sc->sensor_id), sensor_id);
+
+ switch (category)
+ {
+ case 0: /* SENSOR_CATEGORY_TEMP */
+ sc->threshold_1 = 55.0f;
+ sc->threshold_2 = 65.0f;
+ sc->threshold_3 = 75.0f;
+ sc->max_scale = 115.0f;
+ break;
+ case 1: /* SENSOR_CATEGORY_RPM */
+ sc->threshold_1 = 500.0f;
+ sc->threshold_2 = 1000.0f;
+ sc->threshold_3 = 1500.0f;
+ sc->max_scale = 3000.0f;
+ break;
+ case 2: /* SENSOR_CATEGORY_DUTY */
+ sc->threshold_1 = 25.0f;
+ sc->threshold_2 = 50.0f;
+ sc->threshold_3 = 75.0f;
+ sc->max_scale = 100.0f;
+ break;
+ case 3: /* SENSOR_CATEGORY_WATTS */
+ sc->threshold_1 = 50.0f;
+ sc->threshold_2 = 100.0f;
+ sc->threshold_3 = 200.0f;
+ sc->max_scale = 500.0f;
+ break;
+ case 4: /* SENSOR_CATEGORY_FREQ */
+ sc->threshold_1 = 1000.0f;
+ sc->threshold_2 = 2000.0f;
+ sc->threshold_3 = 3000.0f;
+ sc->max_scale = 6000.0f;
+ break;
+ default:
+ sc->threshold_1 = 55.0f;
+ sc->threshold_2 = 65.0f;
+ sc->threshold_3 = 75.0f;
+ sc->max_scale = 115.0f;
+ break;
+ }
+
+ sc->threshold_1_bar = (Color){0, 255, 0, 1};
+ sc->threshold_2_bar = (Color){255, 140, 0, 1};
+ sc->threshold_3_bar = (Color){255, 70, 0, 1};
+ sc->threshold_4_bar = (Color){255, 0, 0, 1};
+}
+
+/**
+ * @brief Find sensor configuration by sensor ID (public).
+ */
+const SensorConfig *get_sensor_config(const Config *config,
+ const char *sensor_id)
+{
+ if (!config || !sensor_id)
+ return NULL;
+ for (int i = 0; i < config->sensor_config_count; i++)
+ {
+ if (strcmp(config->sensor_configs[i].sensor_id, sensor_id) == 0)
+ return &config->sensor_configs[i];
+ }
+ return NULL;
+}
+
+/**
+ * @brief Set default sensor configurations.
+ * @details Ensures cpu, gpu, liquid configs exist with proper threshold defaults.
+ */
+static void set_default_sensor_configs(Config *config)
+{
+ /* Ensure CPU config */
+ SensorConfig *cpu = ensure_sensor_config(config, "cpu");
+ if (cpu)
+ {
+ if (cpu->threshold_1 == 0.0f)
+ cpu->threshold_1 = 55.0f;
+ if (cpu->threshold_2 == 0.0f)
+ cpu->threshold_2 = 65.0f;
+ if (cpu->threshold_3 == 0.0f)
+ cpu->threshold_3 = 75.0f;
+ if (cpu->max_scale == 0.0f)
+ cpu->max_scale = 115.0f;
+ }
+
+ /* Ensure GPU config */
+ SensorConfig *gpu = ensure_sensor_config(config, "gpu");
+ if (gpu)
+ {
+ if (gpu->threshold_1 == 0.0f)
+ gpu->threshold_1 = 55.0f;
+ if (gpu->threshold_2 == 0.0f)
+ gpu->threshold_2 = 65.0f;
+ if (gpu->threshold_3 == 0.0f)
+ gpu->threshold_3 = 75.0f;
+ if (gpu->max_scale == 0.0f)
+ gpu->max_scale = 115.0f;
+ }
+
+ /* Ensure Liquid config (lower thresholds) */
+ SensorConfig *liquid = ensure_sensor_config(config, "liquid");
+ if (liquid)
+ {
+ if (liquid->threshold_1 == 0.0f)
+ liquid->threshold_1 = 25.0f;
+ if (liquid->threshold_2 == 0.0f)
+ liquid->threshold_2 = 28.0f;
+ if (liquid->threshold_3 == 0.0f)
+ liquid->threshold_3 = 31.0f;
+ if (liquid->max_scale == 0.0f)
+ liquid->max_scale = 50.0f;
+ }
}
/**
@@ -295,27 +400,13 @@ typedef struct
*/
static void set_color_defaults(Config *config)
{
+ /* Global color defaults */
ColorDefault color_defaults[] = {
- {&config->display_background_color, 0, 0, 0}, // Main background (black)
+ {&config->display_background_color, 0, 0, 0},
{&config->layout_bar_color_background, 52, 52, 52},
{&config->layout_bar_color_border, 192, 192, 192},
{&config->font_color_temp, 255, 255, 255},
- {&config->font_color_label, 200, 200, 200},
- // CPU temperature colors
- {&config->temp_cpu_threshold_1_bar, 0, 255, 0},
- {&config->temp_cpu_threshold_2_bar, 255, 140, 0},
- {&config->temp_cpu_threshold_3_bar, 255, 70, 0},
- {&config->temp_cpu_threshold_4_bar, 255, 0, 0},
- // GPU temperature colors (same as CPU)
- {&config->temp_gpu_threshold_1_bar, 0, 255, 0},
- {&config->temp_gpu_threshold_2_bar, 255, 140, 0},
- {&config->temp_gpu_threshold_3_bar, 255, 70, 0},
- {&config->temp_gpu_threshold_4_bar, 255, 0, 0},
- // Liquid temperature colors
- {&config->temp_liquid_threshold_1_bar, 0, 255, 0},
- {&config->temp_liquid_threshold_2_bar, 255, 140, 0},
- {&config->temp_liquid_threshold_3_bar, 255, 70, 0},
- {&config->temp_liquid_threshold_4_bar, 255, 0, 0}};
+ {&config->font_color_label, 200, 200, 200}};
const size_t color_count = sizeof(color_defaults) / sizeof(color_defaults[0]);
for (size_t i = 0; i < color_count; i++)
@@ -327,19 +418,44 @@ static void set_color_defaults(Config *config)
color_defaults[i].color_ptr->b = color_defaults[i].b;
}
}
+
+ /* Per-sensor color defaults (apply to all sensor configs) */
+ for (int i = 0; i < config->sensor_config_count; i++)
+ {
+ SensorConfig *sc = &config->sensor_configs[i];
+ if (is_color_unset(&sc->threshold_1_bar))
+ sc->threshold_1_bar = (Color){0, 255, 0, 1};
+ if (is_color_unset(&sc->threshold_2_bar))
+ sc->threshold_2_bar = (Color){255, 140, 0, 1};
+ if (is_color_unset(&sc->threshold_3_bar))
+ sc->threshold_3_bar = (Color){255, 70, 0, 1};
+ if (is_color_unset(&sc->threshold_4_bar))
+ sc->threshold_4_bar = (Color){255, 0, 0, 1};
+ }
}
/**
* @brief Check if a sensor slot value is valid.
+ * @details Accepts legacy ("cpu","gpu","liquid","none") and dynamic ("uid:name").
*/
static int is_valid_sensor_slot(const char *slot)
{
if (!slot || slot[0] == '\0')
return 0;
- return (strcmp(slot, "cpu") == 0 ||
- strcmp(slot, "gpu") == 0 ||
- strcmp(slot, "liquid") == 0 ||
- strcmp(slot, "none") == 0);
+
+ /* Legacy values */
+ if (strcmp(slot, "cpu") == 0 ||
+ strcmp(slot, "gpu") == 0 ||
+ strcmp(slot, "liquid") == 0 ||
+ strcmp(slot, "none") == 0)
+ return 1;
+
+ /* Dynamic format: "uid:sensor_name" */
+ const char *sep = strchr(slot, ':');
+ if (sep && sep != slot && *(sep + 1) != '\0')
+ return 1;
+
+ return 0;
}
/**
@@ -439,7 +555,7 @@ static void apply_system_defaults(Config *config)
set_display_defaults(config);
set_layout_defaults(config);
set_font_defaults(config);
- set_temperature_defaults(config);
+ set_default_sensor_configs(config);
set_color_defaults(config);
validate_sensor_slots(config);
}
@@ -677,6 +793,11 @@ static void load_display_from_json(json_t *root, Config *config)
if (value)
cc_safe_strcpy(config->sensor_slot_up, sizeof(config->sensor_slot_up), value);
}
+ else if (slot_up && !json_is_string(slot_up))
+ {
+ log_message(LOG_WARNING, "sensor_slot_up has non-string type, using default '%s'",
+ config->sensor_slot_up);
+ }
json_t *slot_mid = json_object_get(display, "sensor_slot_mid");
if (slot_mid && json_is_string(slot_mid))
@@ -685,6 +806,11 @@ static void load_display_from_json(json_t *root, Config *config)
if (value)
cc_safe_strcpy(config->sensor_slot_mid, sizeof(config->sensor_slot_mid), value);
}
+ else if (slot_mid && !json_is_string(slot_mid))
+ {
+ log_message(LOG_WARNING, "sensor_slot_mid has non-string type, using default '%s'",
+ config->sensor_slot_mid);
+ }
json_t *slot_down = json_object_get(display, "sensor_slot_down");
if (slot_down && json_is_string(slot_down))
@@ -693,6 +819,11 @@ static void load_display_from_json(json_t *root, Config *config)
if (value)
cc_safe_strcpy(config->sensor_slot_down, sizeof(config->sensor_slot_down), value);
}
+ else if (slot_down && !json_is_string(slot_down))
+ {
+ log_message(LOG_WARNING, "sensor_slot_down has non-string type, using default '%s'",
+ config->sensor_slot_down);
+ }
}
/**
@@ -837,124 +968,136 @@ static void load_font_from_json(json_t *root, Config *config)
}
/**
- * @brief Load CPU temperature settings from JSON.
+ * @brief Load a single sensor config entry from a JSON object.
+ * @details Generic loader for thresholds, max_scale, colors, and offsets.
*/
-static void load_cpu_temperature_from_json(json_t *root, Config *config)
+static void load_sensor_config_from_json(json_t *obj, SensorConfig *sc)
{
- json_t *cpu = json_object_get(root, "cpu");
- if (!cpu || !json_is_object(cpu))
+ if (!obj || !json_is_object(obj) || !sc)
return;
- json_t *threshold_1 = json_object_get(cpu, "threshold_1");
- if (threshold_1 && json_is_number(threshold_1))
- {
- config->temp_cpu_threshold_1 = (float)json_number_value(threshold_1);
- }
+ json_t *val;
- json_t *threshold_2 = json_object_get(cpu, "threshold_2");
- if (threshold_2 && json_is_number(threshold_2))
- {
- config->temp_cpu_threshold_2 = (float)json_number_value(threshold_2);
- }
+ val = json_object_get(obj, "threshold_1");
+ if (val && json_is_number(val))
+ sc->threshold_1 = (float)json_number_value(val);
- json_t *threshold_3 = json_object_get(cpu, "threshold_3");
- if (threshold_3 && json_is_number(threshold_3))
- {
- config->temp_cpu_threshold_3 = (float)json_number_value(threshold_3);
- }
+ val = json_object_get(obj, "threshold_2");
+ if (val && json_is_number(val))
+ sc->threshold_2 = (float)json_number_value(val);
- json_t *max_scale = json_object_get(cpu, "max_scale");
- if (max_scale && json_is_number(max_scale))
- {
- config->temp_cpu_max_scale = (float)json_number_value(max_scale);
- }
+ val = json_object_get(obj, "threshold_3");
+ if (val && json_is_number(val))
+ sc->threshold_3 = (float)json_number_value(val);
- read_color_from_json(json_object_get(cpu, "threshold_1_color"), &config->temp_cpu_threshold_1_bar);
- read_color_from_json(json_object_get(cpu, "threshold_2_color"), &config->temp_cpu_threshold_2_bar);
- read_color_from_json(json_object_get(cpu, "threshold_3_color"), &config->temp_cpu_threshold_3_bar);
- read_color_from_json(json_object_get(cpu, "threshold_4_color"), &config->temp_cpu_threshold_4_bar);
-}
+ val = json_object_get(obj, "max_scale");
+ if (val && json_is_number(val))
+ sc->max_scale = (float)json_number_value(val);
-/**
- * @brief Load GPU temperature settings from JSON.
- */
-static void load_gpu_temperature_from_json(json_t *root, Config *config)
-{
- json_t *gpu = json_object_get(root, "gpu");
- if (!gpu || !json_is_object(gpu))
- return;
+ read_color_from_json(json_object_get(obj, "threshold_1_color"), &sc->threshold_1_bar);
+ read_color_from_json(json_object_get(obj, "threshold_2_color"), &sc->threshold_2_bar);
+ read_color_from_json(json_object_get(obj, "threshold_3_color"), &sc->threshold_3_bar);
+ read_color_from_json(json_object_get(obj, "threshold_4_color"), &sc->threshold_4_bar);
- json_t *threshold_1 = json_object_get(gpu, "threshold_1");
- if (threshold_1 && json_is_number(threshold_1))
- {
- config->temp_gpu_threshold_1 = (float)json_number_value(threshold_1);
- }
+ val = json_object_get(obj, "offset_x");
+ if (val && json_is_integer(val))
+ sc->offset_x = (int)json_integer_value(val);
- json_t *threshold_2 = json_object_get(gpu, "threshold_2");
- if (threshold_2 && json_is_number(threshold_2))
- {
- config->temp_gpu_threshold_2 = (float)json_number_value(threshold_2);
- }
+ val = json_object_get(obj, "offset_y");
+ if (val && json_is_integer(val))
+ sc->offset_y = (int)json_integer_value(val);
- json_t *threshold_3 = json_object_get(gpu, "threshold_3");
- if (threshold_3 && json_is_number(threshold_3))
- {
- config->temp_gpu_threshold_3 = (float)json_number_value(threshold_3);
- }
+ val = json_object_get(obj, "font_size_temp");
+ if (val && json_is_number(val))
+ sc->font_size_temp = (float)json_number_value(val);
- json_t *max_scale = json_object_get(gpu, "max_scale");
- if (max_scale && json_is_number(max_scale))
+ val = json_object_get(obj, "label");
+ if (val && json_is_string(val))
{
- config->temp_gpu_max_scale = (float)json_number_value(max_scale);
+ const char *label_str = json_string_value(val);
+ if (label_str)
+ cc_safe_strcpy(sc->label, sizeof(sc->label), label_str);
}
-
- read_color_from_json(json_object_get(gpu, "threshold_1_color"), &config->temp_gpu_threshold_1_bar);
- read_color_from_json(json_object_get(gpu, "threshold_2_color"), &config->temp_gpu_threshold_2_bar);
- read_color_from_json(json_object_get(gpu, "threshold_3_color"), &config->temp_gpu_threshold_3_bar);
- read_color_from_json(json_object_get(gpu, "threshold_4_color"), &config->temp_gpu_threshold_4_bar);
}
/**
- * @brief Load liquid temperature settings from JSON.
+ * @brief Load sensor configurations from JSON "sensors" map.
+ * @details New format: "sensors": { "cpu": {...}, "gpu": {...}, "uid:name": {...} }
+ * Also handles legacy format with top-level "cpu", "gpu", "liquid" objects.
*/
-static void load_liquid_from_json(json_t *root, Config *config)
+static void load_sensors_from_json(json_t *root, Config *config)
{
- json_t *liquid = json_object_get(root, "liquid");
- if (!liquid || !json_is_object(liquid))
- return;
-
- json_t *max_scale = json_object_get(liquid, "max_scale");
- if (max_scale && json_is_number(max_scale))
+ /* New format: "sensors" map */
+ json_t *sensors = json_object_get(root, "sensors");
+ if (sensors && json_is_object(sensors))
{
- config->temp_liquid_max_scale = (float)json_number_value(max_scale);
+ const char *key;
+ json_t *value;
+ json_object_foreach(sensors, key, value)
+ {
+ if (!json_is_object(value))
+ continue;
+ SensorConfig *sc = ensure_sensor_config(config, key);
+ if (sc)
+ load_sensor_config_from_json(value, sc);
+ }
+ return; /* New format takes priority */
}
- json_t *threshold_1 = json_object_get(liquid, "threshold_1");
- if (threshold_1 && json_is_number(threshold_1))
+ /* Legacy backward compatibility: top-level "cpu", "gpu", "liquid" */
+ json_t *cpu = json_object_get(root, "cpu");
+ if (cpu && json_is_object(cpu))
{
- config->temp_liquid_threshold_1 = (float)json_number_value(threshold_1);
+ SensorConfig *sc = ensure_sensor_config(config, "cpu");
+ if (sc)
+ load_sensor_config_from_json(cpu, sc);
}
- json_t *threshold_2 = json_object_get(liquid, "threshold_2");
- if (threshold_2 && json_is_number(threshold_2))
+ json_t *gpu = json_object_get(root, "gpu");
+ if (gpu && json_is_object(gpu))
{
- config->temp_liquid_threshold_2 = (float)json_number_value(threshold_2);
+ SensorConfig *sc = ensure_sensor_config(config, "gpu");
+ if (sc)
+ load_sensor_config_from_json(gpu, sc);
}
- json_t *threshold_3 = json_object_get(liquid, "threshold_3");
- if (threshold_3 && json_is_number(threshold_3))
+ json_t *liquid = json_object_get(root, "liquid");
+ if (liquid && json_is_object(liquid))
{
- config->temp_liquid_threshold_3 = (float)json_number_value(threshold_3);
+ SensorConfig *sc = ensure_sensor_config(config, "liquid");
+ if (sc)
+ load_sensor_config_from_json(liquid, sc);
}
- read_color_from_json(json_object_get(liquid, "threshold_1_color"), &config->temp_liquid_threshold_1_bar);
- read_color_from_json(json_object_get(liquid, "threshold_2_color"), &config->temp_liquid_threshold_2_bar);
- read_color_from_json(json_object_get(liquid, "threshold_3_color"), &config->temp_liquid_threshold_3_bar);
- read_color_from_json(json_object_get(liquid, "threshold_4_color"), &config->temp_liquid_threshold_4_bar);
+ /* Legacy positioning offsets → migrate to SensorConfig */
+ json_t *positioning = json_object_get(root, "positioning");
+ if (positioning && json_is_object(positioning))
+ {
+ const char *offset_keys[][3] = {
+ {"temp_offset_x_cpu", "temp_offset_y_cpu", "cpu"},
+ {"temp_offset_x_gpu", "temp_offset_y_gpu", "gpu"},
+ {"temp_offset_x_liquid", "temp_offset_y_liquid", "liquid"}};
+
+ for (int i = 0; i < 3; i++)
+ {
+ SensorConfig *sc = ensure_sensor_config(config, offset_keys[i][2]);
+ if (!sc)
+ continue;
+
+ json_t *ox = json_object_get(positioning, offset_keys[i][0]);
+ if (ox && json_is_integer(ox))
+ sc->offset_x = (int)json_integer_value(ox);
+
+ json_t *oy = json_object_get(positioning, offset_keys[i][1]);
+ if (oy && json_is_integer(oy))
+ sc->offset_y = (int)json_integer_value(oy);
+ }
+ }
}
/**
- * @brief Load positioning settings from JSON.
+ * @brief Load global positioning settings from JSON.
+ * @details Per-sensor offsets are now handled in load_sensors_from_json().
*/
static void load_positioning_from_json(json_t *root, Config *config)
{
@@ -962,42 +1105,6 @@ static void load_positioning_from_json(json_t *root, Config *config)
if (!positioning || !json_is_object(positioning))
return;
- json_t *temp_offset_x_cpu = json_object_get(positioning, "temp_offset_x_cpu");
- if (temp_offset_x_cpu && json_is_integer(temp_offset_x_cpu))
- {
- config->display_temp_offset_x_cpu = (int)json_integer_value(temp_offset_x_cpu);
- }
-
- json_t *temp_offset_x_gpu = json_object_get(positioning, "temp_offset_x_gpu");
- if (temp_offset_x_gpu && json_is_integer(temp_offset_x_gpu))
- {
- config->display_temp_offset_x_gpu = (int)json_integer_value(temp_offset_x_gpu);
- }
-
- json_t *temp_offset_y_cpu = json_object_get(positioning, "temp_offset_y_cpu");
- if (temp_offset_y_cpu && json_is_integer(temp_offset_y_cpu))
- {
- config->display_temp_offset_y_cpu = (int)json_integer_value(temp_offset_y_cpu);
- }
-
- json_t *temp_offset_y_gpu = json_object_get(positioning, "temp_offset_y_gpu");
- if (temp_offset_y_gpu && json_is_integer(temp_offset_y_gpu))
- {
- config->display_temp_offset_y_gpu = (int)json_integer_value(temp_offset_y_gpu);
- }
-
- json_t *temp_offset_x_liquid = json_object_get(positioning, "temp_offset_x_liquid");
- if (temp_offset_x_liquid && json_is_integer(temp_offset_x_liquid))
- {
- config->display_temp_offset_x_liquid = (int)json_integer_value(temp_offset_x_liquid);
- }
-
- json_t *temp_offset_y_liquid = json_object_get(positioning, "temp_offset_y_liquid");
- if (temp_offset_y_liquid && json_is_integer(temp_offset_y_liquid))
- {
- config->display_temp_offset_y_liquid = (int)json_integer_value(temp_offset_y_liquid);
- }
-
json_t *degree_spacing = json_object_get(positioning, "degree_spacing");
if (degree_spacing && json_is_integer(degree_spacing))
{
@@ -1057,9 +1164,7 @@ int load_plugin_config(Config *config, const char *config_path)
load_layout_from_json(root, config);
load_colors_from_json(root, config);
load_font_from_json(root, config);
- load_cpu_temperature_from_json(root, config);
- load_gpu_temperature_from_json(root, config);
- load_liquid_from_json(root, config);
+ load_sensors_from_json(root, config);
load_positioning_from_json(root, config);
json_decref(root);
diff --git a/src/device/config.h b/src/device/config.h
index 494e8be..0f8e6b7 100644
--- a/src/device/config.h
+++ b/src/device/config.h
@@ -25,7 +25,7 @@
#define CONFIG_MAX_PASSWORD_LEN 128
#define CONFIG_MAX_PATH_LEN 512
#define CONFIG_MAX_FONT_NAME_LEN 64
-#define CONFIG_MAX_SENSOR_SLOT_LEN 32
+#define CONFIG_MAX_SENSOR_SLOT_LEN 256
/**
* @brief Simple color structure.
@@ -53,6 +53,36 @@ typedef enum
LOG_ERROR
} log_level_t;
+// ============================================================================
+// Sensor Configuration (per-sensor thresholds, colors, offsets)
+// ============================================================================
+
+#define MAX_SENSOR_CONFIGS 32
+#define SENSOR_CONFIG_ID_LEN 256
+
+/**
+ * @brief Per-sensor display configuration.
+ * @details Each configured sensor has its own thresholds, bar colors, and
+ * display offsets. Sensor ID format: legacy "cpu"/"gpu"/"liquid" or
+ * dynamic "device_uid:sensor_name".
+ */
+typedef struct
+{
+ char sensor_id[SENSOR_CONFIG_ID_LEN]; /**< Sensor identifier */
+ float threshold_1; /**< Low threshold */
+ float threshold_2; /**< Medium threshold */
+ float threshold_3; /**< High threshold */
+ float max_scale; /**< Maximum bar scale value */
+ Color threshold_1_bar; /**< Bar color below threshold_1 */
+ Color threshold_2_bar; /**< Bar color at threshold_1..2 */
+ Color threshold_3_bar; /**< Bar color at threshold_2..3 */
+ Color threshold_4_bar; /**< Bar color above threshold_3 */
+ int offset_x; /**< Display X offset for value text */
+ int offset_y; /**< Display Y offset for value text */
+ float font_size_temp; /**< Per-sensor temp font size (0 = use global) */
+ char label[32]; /**< Custom display label (empty = auto) */
+} SensorConfig;
+
/**
* @brief Configuration structure.
* @details Contains all settings for the CoolerDash system, including paths,
@@ -109,46 +139,14 @@ typedef struct Config
Color font_color_temp;
Color font_color_label;
- // Display positioning overrides
- int display_temp_offset_x_cpu;
- int display_temp_offset_x_gpu;
- int display_temp_offset_y_cpu;
- int display_temp_offset_y_gpu;
- int display_temp_offset_x_liquid;
- int display_temp_offset_y_liquid;
+ // Display positioning overrides (global)
int display_degree_spacing;
int display_label_offset_x;
int display_label_offset_y;
- // CPU temperature configuration
- float temp_cpu_threshold_1;
- float temp_cpu_threshold_2;
- float temp_cpu_threshold_3;
- float temp_cpu_max_scale;
- Color temp_cpu_threshold_1_bar;
- Color temp_cpu_threshold_2_bar;
- Color temp_cpu_threshold_3_bar;
- Color temp_cpu_threshold_4_bar;
-
- // GPU temperature configuration
- float temp_gpu_threshold_1;
- float temp_gpu_threshold_2;
- float temp_gpu_threshold_3;
- float temp_gpu_max_scale;
- Color temp_gpu_threshold_1_bar;
- Color temp_gpu_threshold_2_bar;
- Color temp_gpu_threshold_3_bar;
- Color temp_gpu_threshold_4_bar;
-
- // Liquid temperature configuration
- float temp_liquid_threshold_1;
- float temp_liquid_threshold_2;
- float temp_liquid_threshold_3;
- float temp_liquid_max_scale;
- Color temp_liquid_threshold_1_bar;
- Color temp_liquid_threshold_2_bar;
- Color temp_liquid_threshold_3_bar;
- Color temp_liquid_threshold_4_bar;
+ // Per-sensor configuration (thresholds, colors, offsets)
+ SensorConfig sensor_configs[MAX_SENSOR_CONFIGS]; /**< Sensor-specific configs */
+ int sensor_config_count; /**< Number of valid entries */
} Config;
/**
@@ -208,4 +206,21 @@ static inline int is_valid_orientation(int orientation)
*/
int load_plugin_config(Config *config, const char *config_path);
+/**
+ * @brief Find sensor configuration by sensor ID.
+ * @details Searches sensor_configs[] for a matching sensor_id.
+ * @param config Configuration struct
+ * @param sensor_id Sensor identifier ("cpu", "gpu", "liquid", or "uid:name")
+ * @return Pointer to matching SensorConfig, or NULL if not found
+ */
+const SensorConfig *get_sensor_config(const Config *config, const char *sensor_id);
+
+/**
+ * @brief Initialize a SensorConfig with default values for a given category.
+ * @param sc SensorConfig to initialize
+ * @param sensor_id Sensor identifier to set
+ * @param category 0=temp, 1=rpm, 2=duty, 3=watts, 4=freq
+ */
+void init_default_sensor_config(SensorConfig *sc, const char *sensor_id, int category);
+
#endif // CONFIG_H
diff --git a/src/main.c b/src/main.c
index 03dfa51..5e5de32 100644
--- a/src/main.c
+++ b/src/main.c
@@ -706,16 +706,17 @@ static void initialize_device_info(Config *config)
log_message(LOG_STATUS, "Device: %s [%s]", name_display, uid_display);
- if (get_temperature_monitor_data(config, &temp_data))
+ if (get_sensor_monitor_data(config, &temp_data))
{
- if (temp_data.temp_cpu > 0.0f || temp_data.temp_gpu > 0.0f)
+ if (temp_data.sensor_count > 0)
{
- log_message(LOG_STATUS, "Sensor values successfully detected");
+ log_message(LOG_STATUS, "Sensor values successfully detected (%d sensors)",
+ temp_data.sensor_count);
}
else
{
log_message(LOG_WARNING,
- "Sensor detection issues - temperature values not available");
+ "Sensor detection issues - no sensor values available");
}
}
else
diff --git a/src/mods/circle.c b/src/mods/circle.c
index 2315770..b07a5fa 100644
--- a/src/mods/circle.c
+++ b/src/mods/circle.c
@@ -129,7 +129,7 @@ static void update_sensor_mode(const struct Config *config)
if (verbose_logging)
{
const char *slot_value = get_slot_value_by_index(config, current_slot_index);
- const char *label = get_slot_label(slot_value);
+ const char *label = get_slot_label(config, NULL, slot_value);
log_message(LOG_INFO,
"Circle mode: switched to %s display (slot: %s, interval: %.0fs)",
label ? label : "unknown",
@@ -161,7 +161,7 @@ static void draw_single_sensor(cairo_t *cr, const struct Config *config,
// Get temperature and label for current slot
const float temp_value = get_slot_temperature(data, slot_value);
- const char *label_text = get_slot_label(slot_value);
+ const char *label_text = get_slot_label(config, data, slot_value);
const float max_temp = get_slot_max_scale(config, slot_value);
const int effective_bar_width = params->safe_bar_width;
@@ -180,67 +180,61 @@ static void draw_single_sensor(cairo_t *cr, const struct Config *config,
// Draw temperature value (centered horizontally INCLUDING degree symbol)
char temp_str[16];
- // Use 1 decimal for liquid, integer for CPU/GPU
- if (strcmp(slot_value, "liquid") == 0)
+ // Use decimal based on sensor type
+ if (get_slot_use_decimal(data, slot_value))
snprintf(temp_str, sizeof(temp_str), "%.1f", temp_value);
else
snprintf(temp_str, sizeof(temp_str), "%d", (int)temp_value);
const Color *value_color = &config->font_color_temp;
+ // Use per-slot font size
+ float slot_font_size = get_slot_font_size(config, slot_value);
+
cairo_select_font_face(cr, config->font_face, CAIRO_FONT_SLANT_NORMAL,
CAIRO_FONT_WEIGHT_BOLD);
- cairo_set_font_size(cr, config->font_size_temp);
+ cairo_set_font_size(cr, slot_font_size);
set_cairo_color(cr, value_color);
cairo_text_extents_t temp_ext;
cairo_text_extents(cr, temp_str, &temp_ext);
// Calculate degree symbol width for proper centering
- cairo_set_font_size(cr, config->font_size_temp / 1.66);
+ cairo_set_font_size(cr, slot_font_size / 1.66);
cairo_text_extents_t degree_ext;
cairo_text_extents(cr, "°", °ree_ext);
- cairo_set_font_size(cr, config->font_size_temp);
+ cairo_set_font_size(cr, slot_font_size);
// Center temperature + degree symbol as a unit
const double total_width = temp_ext.width - 4 + degree_ext.width;
double temp_x = (config->display_width - total_width) / 2.0;
double final_temp_y = temp_y;
- // Apply user-defined offsets based on sensor type
- if (strcmp(slot_value, "cpu") == 0)
- {
- if (config->display_temp_offset_x_cpu != -9999)
- temp_x += config->display_temp_offset_x_cpu;
- if (config->display_temp_offset_y_cpu != -9999)
- final_temp_y += config->display_temp_offset_y_cpu;
- }
- else if (strcmp(slot_value, "gpu") == 0)
- {
- if (config->display_temp_offset_x_gpu != -9999)
- temp_x += config->display_temp_offset_x_gpu;
- if (config->display_temp_offset_y_gpu != -9999)
- final_temp_y += config->display_temp_offset_y_gpu;
- }
- else if (strcmp(slot_value, "liquid") == 0)
- {
- if (config->display_temp_offset_x_liquid != -9999)
- temp_x += config->display_temp_offset_x_liquid;
- if (config->display_temp_offset_y_liquid != -9999)
- final_temp_y += config->display_temp_offset_y_liquid;
- }
+ // Apply user-defined offsets from sensor config
+ int offset_x = get_slot_offset_x(config, slot_value);
+ int offset_y = get_slot_offset_y(config, slot_value);
+ if (offset_x != 0)
+ temp_x += offset_x;
+ if (offset_y != 0)
+ final_temp_y += offset_y;
cairo_move_to(cr, temp_x, final_temp_y);
cairo_show_text(cr, temp_str);
- // Draw degree symbol
- const int degree_spacing = (config->display_degree_spacing > 0)
- ? config->display_degree_spacing
- : 16;
- double degree_x = temp_x + temp_ext.width + degree_spacing;
- double degree_y = final_temp_y - config->font_size_temp * 0.25;
-
- draw_degree_symbol(cr, degree_x, degree_y, config);
+ // Draw degree symbol only for temperature sensors
+ if (get_slot_is_temp(data, slot_value))
+ {
+ const int degree_spacing = (config->display_degree_spacing > 0)
+ ? config->display_degree_spacing
+ : 16;
+ double degree_x = temp_x + temp_ext.width + degree_spacing;
+ double degree_y = final_temp_y - slot_font_size * 0.25;
+
+ cairo_set_font_size(cr, slot_font_size / 1.66);
+ cairo_move_to(cr, degree_x, degree_y);
+ cairo_show_text(cr, "\xC2\xB0");
+ cairo_set_font_size(cr, slot_font_size);
+ }
// Draw temperature bar (centered reference point)
@@ -350,9 +344,9 @@ static int render_circle_display(const struct Config *config,
if (verbose_logging)
{
const char *slot_value = get_slot_value_by_index(config, current_slot_index);
- const char *label = get_slot_label(slot_value);
+ const char *label = get_slot_label(config, data, slot_value);
float temp = get_slot_temperature(data, slot_value);
- log_message(LOG_INFO, "Circle mode: rendering %s (%.1f°C)",
+ log_message(LOG_INFO, "Circle mode: rendering %s (%.1f)",
label ? label : "unknown", temp);
}
@@ -411,11 +405,11 @@ void draw_circle_image(const struct Config *config)
get_liquidctl_data(config, device_uid, sizeof(device_uid), device_name,
sizeof(device_name), &screen_width, &screen_height);
- // Get temperature data
+ // Get sensor data
monitor_sensor_data_t data = {0};
- if (!get_temperature_monitor_data(config, &data))
+ if (!get_sensor_monitor_data(config, &data))
{
- log_message(LOG_WARNING, "Circle mode: Failed to get temperature data");
+ log_message(LOG_WARNING, "Circle mode: Failed to get sensor data");
return;
}
diff --git a/src/mods/display.c b/src/mods/display.c
index 369239e..30959b5 100644
--- a/src/mods/display.c
+++ b/src/mods/display.c
@@ -254,90 +254,83 @@ int slot_is_active(const char *slot_value)
}
/**
- * @brief Get temperature value for a sensor slot.
+ * @brief Get sensor value for a slot.
*/
float get_slot_temperature(const monitor_sensor_data_t *data, const char *slot_value)
{
if (!data || !slot_value)
return 0.0f;
- if (strcmp(slot_value, "cpu") == 0)
- return data->temp_cpu;
- else if (strcmp(slot_value, "gpu") == 0)
- return data->temp_gpu;
- else if (strcmp(slot_value, "liquid") == 0)
- return data->temp_liquid;
+ const sensor_entry_t *entry = find_sensor_for_slot(data, slot_value);
+ if (entry)
+ return entry->value;
return 0.0f;
}
/**
* @brief Get display label for a sensor slot.
+ * @details Returns custom label from SensorConfig if set, otherwise
+ * uses legacy names or sensor name from data.
*/
-const char *get_slot_label(const char *slot_value)
+const char *get_slot_label(const struct Config *config,
+ const monitor_sensor_data_t *data,
+ const char *slot_value)
{
- if (!slot_value || slot_value[0] == '\0')
+ if (!slot_value || slot_value[0] == '\0' || strcmp(slot_value, "none") == 0)
return NULL;
+ /* Check for custom label override in SensorConfig */
+ if (config)
+ {
+ const SensorConfig *sc = get_sensor_config(config, slot_value);
+ if (sc && sc->label[0] != '\0')
+ return sc->label;
+ }
+
+ /* Legacy labels */
if (strcmp(slot_value, "cpu") == 0)
return "CPU";
- else if (strcmp(slot_value, "gpu") == 0)
+ if (strcmp(slot_value, "gpu") == 0)
return "GPU";
- else if (strcmp(slot_value, "liquid") == 0)
+ if (strcmp(slot_value, "liquid") == 0)
return "LIQ";
- else if (strcmp(slot_value, "none") == 0)
- return NULL;
- return NULL;
+ /* Dynamic: use sensor name */
+ if (data)
+ {
+ const sensor_entry_t *entry = find_sensor_for_slot(data, slot_value);
+ if (entry)
+ return entry->name;
+ }
+
+ return "???";
}
/**
- * @brief Get bar color for a sensor slot based on temperature.
+ * @brief Get bar color for a sensor slot based on value.
+ * @details Uses SensorConfig thresholds via get_sensor_config().
*/
-Color get_slot_bar_color(const struct Config *config, const char *slot_value, float temperature)
+Color get_slot_bar_color(const struct Config *config, const char *slot_value,
+ float value)
{
+ Color default_color = {0, 255, 0, 1};
+
if (!config || !slot_value)
- {
- // Return default green color
- Color default_color = {0, 255, 0, 1};
return default_color;
- }
-
- // Liquid uses separate thresholds
- if (strcmp(slot_value, "liquid") == 0)
- {
- if (temperature < config->temp_liquid_threshold_1)
- return config->temp_liquid_threshold_1_bar;
- else if (temperature < config->temp_liquid_threshold_2)
- return config->temp_liquid_threshold_2_bar;
- else if (temperature < config->temp_liquid_threshold_3)
- return config->temp_liquid_threshold_3_bar;
- else
- return config->temp_liquid_threshold_4_bar;
- }
- // GPU uses separate thresholds
- if (strcmp(slot_value, "gpu") == 0)
- {
- if (temperature < config->temp_gpu_threshold_1)
- return config->temp_gpu_threshold_1_bar;
- else if (temperature < config->temp_gpu_threshold_2)
- return config->temp_gpu_threshold_2_bar;
- else if (temperature < config->temp_gpu_threshold_3)
- return config->temp_gpu_threshold_3_bar;
- else
- return config->temp_gpu_threshold_4_bar;
- }
+ const SensorConfig *sc = get_sensor_config(config, slot_value);
+ if (!sc)
+ return default_color;
- // CPU (default) uses separate thresholds
- if (temperature < config->temp_cpu_threshold_1)
- return config->temp_cpu_threshold_1_bar;
- else if (temperature < config->temp_cpu_threshold_2)
- return config->temp_cpu_threshold_2_bar;
- else if (temperature < config->temp_cpu_threshold_3)
- return config->temp_cpu_threshold_3_bar;
+ if (value < sc->threshold_1)
+ return sc->threshold_1_bar;
+ else if (value < sc->threshold_2)
+ return sc->threshold_2_bar;
+ else if (value < sc->threshold_3)
+ return sc->threshold_3_bar;
else
- return config->temp_cpu_threshold_4_bar;
+ return sc->threshold_4_bar;
}
/**
@@ -348,16 +341,11 @@ float get_slot_max_scale(const struct Config *config, const char *slot_value)
if (!config)
return 115.0f;
- // Liquid has its own max scale (typically lower, e.g., 50°C)
- if (slot_value && strcmp(slot_value, "liquid") == 0)
- return config->temp_liquid_max_scale;
+ const SensorConfig *sc = get_sensor_config(config, slot_value);
+ if (sc && sc->max_scale > 0.0f)
+ return sc->max_scale;
- // GPU has its own max scale
- if (slot_value && strcmp(slot_value, "gpu") == 0)
- return config->temp_gpu_max_scale;
-
- // CPU (default) max scale
- return config->temp_cpu_max_scale;
+ return 115.0f;
}
/**
@@ -366,7 +354,7 @@ float get_slot_max_scale(const struct Config *config, const char *slot_value)
uint16_t get_slot_bar_height(const struct Config *config, const char *slot_name)
{
if (!config || !slot_name)
- return 24; // Default fallback
+ return 24;
if (strcmp(slot_name, "up") == 0)
return config->layout_bar_height_up;
@@ -375,7 +363,101 @@ uint16_t get_slot_bar_height(const struct Config *config, const char *slot_name)
else if (strcmp(slot_name, "down") == 0)
return config->layout_bar_height_down;
- return config->layout_bar_height; // Fallback to global
+ return config->layout_bar_height;
+}
+
+/**
+ * @brief Get display unit string for a sensor slot.
+ */
+const char *get_slot_unit(const monitor_sensor_data_t *data,
+ const char *slot_value)
+{
+ if (!data || !slot_value)
+ return "\xC2\xB0"
+ "C";
+
+ const sensor_entry_t *entry = find_sensor_for_slot(data, slot_value);
+ if (entry)
+ return entry->unit;
+
+ return "\xC2\xB0"
+ "C";
+}
+
+/**
+ * @brief Check if sensor should display decimal values.
+ */
+int get_slot_use_decimal(const monitor_sensor_data_t *data,
+ const char *slot_value)
+{
+ if (!data || !slot_value)
+ return 0;
+
+ const sensor_entry_t *entry = find_sensor_for_slot(data, slot_value);
+ if (entry)
+ return entry->use_decimal;
+
+ return 0;
+}
+
+/**
+ * @brief Get display X offset for a sensor slot.
+ */
+int get_slot_offset_x(const struct Config *config, const char *slot_value)
+{
+ if (!config || !slot_value)
+ return 0;
+
+ const SensorConfig *sc = get_sensor_config(config, slot_value);
+ return sc ? sc->offset_x : 0;
+}
+
+/**
+ * @brief Get display Y offset for a sensor slot.
+ */
+int get_slot_offset_y(const struct Config *config, const char *slot_value)
+{
+ if (!config || !slot_value)
+ return 0;
+
+ const SensorConfig *sc = get_sensor_config(config, slot_value);
+ return sc ? sc->offset_y : 0;
+}
+
+/**
+ * @brief Check if a sensor slot is a temperature sensor.
+ */
+int get_slot_is_temp(const monitor_sensor_data_t *data, const char *slot_value)
+{
+ if (!data || !slot_value)
+ return 1; /* Default to temp */
+
+ /* Legacy slots are always temperature */
+ if (strcmp(slot_value, "cpu") == 0 || strcmp(slot_value, "gpu") == 0 ||
+ strcmp(slot_value, "liquid") == 0)
+ return 1;
+
+ const sensor_entry_t *entry = find_sensor_for_slot(data, slot_value);
+ if (entry)
+ return (entry->category == SENSOR_CATEGORY_TEMP) ? 1 : 0;
+
+ return 1;
+}
+
+/**
+ * @brief Get font size for a sensor slot.
+ * @details Returns per-sensor font size if configured (> 0), otherwise global.
+ */
+float get_slot_font_size(const struct Config *config, const char *slot_value)
+{
+ if (!config)
+ return 100.0f;
+
+ const SensorConfig *sc = get_sensor_config(config, slot_value);
+ if (sc && sc->font_size_temp > 0.0f)
+ return sc->font_size_temp;
+
+ return config->font_size_temp;
}
/**
diff --git a/src/mods/display.h b/src/mods/display.h
index b8c1d46..09cf6ab 100644
--- a/src/mods/display.h
+++ b/src/mods/display.h
@@ -138,34 +138,39 @@ void draw_degree_symbol(cairo_t *cr, double x, double y,
int slot_is_active(const char *slot_value);
/**
- * @brief Get temperature value for a sensor slot.
- * @param data Sensor data structure with CPU, GPU, and liquid temperatures
- * @param slot_value Slot configuration value ("cpu", "gpu", "liquid")
- * @return Temperature in Celsius, or 0.0 if slot is "none" or invalid
+ * @brief Get sensor value for a slot.
+ * @param data Sensor data collection
+ * @param slot_value Slot configuration value ("cpu","gpu","liquid" or "uid:name")
+ * @return Sensor value, or 0.0 if slot is "none" or sensor not found
*/
float get_slot_temperature(const monitor_sensor_data_t *data, const char *slot_value);
/**
* @brief Get display label for a sensor slot.
- * @param slot_value Slot configuration value ("cpu", "gpu", "liquid", "none")
- * @return Label string ("CPU", "GPU", "LIQ") or NULL if "none"
+ * @param config Configuration with sensor configs (for custom label override)
+ * @param data Sensor data collection (used for dynamic sensor names)
+ * @param slot_value Slot configuration value
+ * @return Label string (custom, "CPU","GPU","LIQ" or sensor name) or NULL
*/
-const char *get_slot_label(const char *slot_value);
+const char *get_slot_label(const struct Config *config,
+ const monitor_sensor_data_t *data,
+ const char *slot_value);
/**
- * @brief Get bar color for a sensor slot based on temperature.
- * @param config Configuration with threshold colors
- * @param slot_value Slot configuration value (determines which thresholds to use)
- * @param temperature Current temperature value
- * @return Color based on temperature thresholds
+ * @brief Get bar color for a sensor slot based on value.
+ * @param config Configuration with sensor threshold configs
+ * @param slot_value Slot configuration value
+ * @param value Current sensor value
+ * @return Color based on thresholds
*/
-Color get_slot_bar_color(const struct Config *config, const char *slot_value, float temperature);
+Color get_slot_bar_color(const struct Config *config, const char *slot_value,
+ float value);
/**
* @brief Get maximum scale for a sensor slot.
- * @param config Configuration with max scale values
+ * @param config Configuration with sensor configs
* @param slot_value Slot configuration value
- * @return Maximum temperature scale (liquid uses different max)
+ * @return Maximum scale value
*/
float get_slot_max_scale(const struct Config *config, const char *slot_value);
@@ -177,4 +182,54 @@ float get_slot_max_scale(const struct Config *config, const char *slot_value);
*/
uint16_t get_slot_bar_height(const struct Config *config, const char *slot_name);
+/**
+ * @brief Get display unit string for a sensor slot.
+ * @param data Sensor data collection
+ * @param slot_value Slot configuration value
+ * @return Unit string ("°C","RPM","%","W","MHz") or "°C" as default
+ */
+const char *get_slot_unit(const monitor_sensor_data_t *data,
+ const char *slot_value);
+
+/**
+ * @brief Check if sensor should display decimal values.
+ * @param data Sensor data collection
+ * @param slot_value Slot configuration value
+ * @return 1 for decimal display, 0 for integer
+ */
+int get_slot_use_decimal(const monitor_sensor_data_t *data,
+ const char *slot_value);
+
+/**
+ * @brief Get display X offset for a sensor slot.
+ * @param config Configuration with sensor configs
+ * @param slot_value Slot configuration value
+ * @return X offset in pixels
+ */
+int get_slot_offset_x(const struct Config *config, const char *slot_value);
+
+/**
+ * @brief Get display Y offset for a sensor slot.
+ * @param config Configuration with sensor configs
+ * @param slot_value Slot configuration value
+ * @return Y offset in pixels
+ */
+int get_slot_offset_y(const struct Config *config, const char *slot_value);
+
+/**
+ * @brief Check if a sensor slot is a temperature sensor.
+ * @param data Sensor data collection
+ * @param slot_value Slot configuration value
+ * @return 1 if temperature sensor (show degree symbol), 0 otherwise
+ */
+int get_slot_is_temp(const monitor_sensor_data_t *data, const char *slot_value);
+
+/**
+ * @brief Get font size for a sensor slot.
+ * @param config Configuration with sensor configs and global font size
+ * @param slot_value Slot configuration value
+ * @return Per-sensor font size if set, otherwise global font_size_temp
+ */
+float get_slot_font_size(const struct Config *config, const char *slot_value);
+
#endif // DISPLAY_DISPATCHER_H
diff --git a/src/mods/dual.c b/src/mods/dual.c
index 198a9c3..d3ad658 100644
--- a/src/mods/dual.c
+++ b/src/mods/dual.c
@@ -100,77 +100,114 @@ static void draw_temperature_displays(cairo_t *cr,
if (!up_active && down_active)
down_bar_y = start_y;
- cairo_font_extents_t font_ext;
- cairo_font_extents(cr, &font_ext);
-
- // Calculate reference width (widest 2-digit number) for sub-100 alignment
- cairo_text_extents_t ref_width_ext;
- cairo_text_extents(cr, "88", &ref_width_ext);
-
// Draw upper slot temperature
if (up_active)
{
+ // Set per-slot font size
+ float up_font_size = get_slot_font_size(config, slot_up);
+ cairo_set_font_size(cr, up_font_size);
+
+ cairo_font_extents_t up_font_ext;
+ cairo_font_extents(cr, &up_font_ext);
+
+ // Calculate reference width (widest 2-digit number) for sub-100 alignment
+ cairo_text_extents_t up_ref_ext;
+ cairo_text_extents(cr, "88", &up_ref_ext);
+
char up_num_str[16];
- snprintf(up_num_str, sizeof(up_num_str), "%d", (int)temp_up);
+ if (get_slot_use_decimal(data, slot_up))
+ snprintf(up_num_str, sizeof(up_num_str), "%.1f", temp_up);
+ else
+ snprintf(up_num_str, sizeof(up_num_str), "%d", (int)temp_up);
cairo_text_extents_t up_num_ext;
cairo_text_extents(cr, up_num_str, &up_num_ext);
- double up_width = (temp_up >= 100.0f) ? up_num_ext.width : ref_width_ext.width;
+ double up_width = (temp_up >= 100.0f) ? up_num_ext.width : up_ref_ext.width;
double up_temp_x = bar_x + (effective_bar_width - up_width) / 2.0;
if (config->display_width == 240 && config->display_height == 240)
up_temp_x += 20;
- if (config->display_temp_offset_x_cpu != 0)
- up_temp_x += config->display_temp_offset_x_cpu;
+ int offset_x_up = get_slot_offset_x(config, slot_up);
+ if (offset_x_up != 0)
+ up_temp_x += offset_x_up;
- double up_temp_y = up_bar_y + 8 - font_ext.descent;
- if (config->display_temp_offset_y_cpu != 0)
- up_temp_y += config->display_temp_offset_y_cpu;
+ double up_temp_y = up_bar_y + 8 - up_font_ext.descent;
+ int offset_y_up = get_slot_offset_y(config, slot_up);
+ if (offset_y_up != 0)
+ up_temp_y += offset_y_up;
cairo_move_to(cr, up_temp_x, up_temp_y);
cairo_show_text(cr, up_num_str);
- // Degree symbol
- const int degree_spacing = (config->display_degree_spacing > 0) ? config->display_degree_spacing : 16;
- double degree_up_x = up_temp_x + up_width + degree_spacing;
- double degree_up_y = up_temp_y - up_num_ext.height * 0.40;
- draw_degree_symbol(cr, degree_up_x, degree_up_y, config);
+ // Draw degree symbol or unit
+ if (get_slot_is_temp(data, slot_up))
+ {
+ const int degree_spacing = (config->display_degree_spacing > 0) ? config->display_degree_spacing : 16;
+ double degree_up_x = up_temp_x + up_width + degree_spacing;
+ double degree_up_y = up_temp_y - up_num_ext.height * 0.40;
+ cairo_set_font_size(cr, up_font_size / 1.66);
+ cairo_move_to(cr, degree_up_x, degree_up_y);
+ cairo_show_text(cr, "\xC2\xB0");
+ cairo_set_font_size(cr, up_font_size);
+ }
}
// Draw lower slot temperature
if (down_active)
{
+ // Set per-slot font size
+ float down_font_size = get_slot_font_size(config, slot_down);
+ cairo_set_font_size(cr, down_font_size);
+
+ cairo_font_extents_t down_font_ext;
+ cairo_font_extents(cr, &down_font_ext);
+
+ // Calculate reference width for sub-100 alignment
+ cairo_text_extents_t down_ref_ext;
+ cairo_text_extents(cr, "88", &down_ref_ext);
+
char down_num_str[16];
- snprintf(down_num_str, sizeof(down_num_str), "%d", (int)temp_down);
+ if (get_slot_use_decimal(data, slot_down))
+ snprintf(down_num_str, sizeof(down_num_str), "%.1f", temp_down);
+ else
+ snprintf(down_num_str, sizeof(down_num_str), "%d", (int)temp_down);
cairo_text_extents_t down_num_ext;
cairo_text_extents(cr, down_num_str, &down_num_ext);
- double down_width = (temp_down >= 100.0f) ? down_num_ext.width : ref_width_ext.width;
+ double down_width = (temp_down >= 100.0f) ? down_num_ext.width : down_ref_ext.width;
double down_temp_x = bar_x + (effective_bar_width - down_width) / 2.0;
if (config->display_width == 240 && config->display_height == 240)
down_temp_x += 20;
- if (config->display_temp_offset_x_gpu != 0)
- down_temp_x += config->display_temp_offset_x_gpu;
+ int offset_x_down = get_slot_offset_x(config, slot_down);
+ if (offset_x_down != 0)
+ down_temp_x += offset_x_down;
// Use the actual bar height for positioning
uint16_t effective_down_height = down_active ? bar_height_down : 0;
- double down_temp_y = down_bar_y + effective_down_height - 4 + font_ext.ascent;
- if (config->display_temp_offset_y_gpu != 0)
- down_temp_y += config->display_temp_offset_y_gpu;
+ double down_temp_y = down_bar_y + effective_down_height - 4 + down_font_ext.ascent;
+ int offset_y_down = get_slot_offset_y(config, slot_down);
+ if (offset_y_down != 0)
+ down_temp_y += offset_y_down;
cairo_move_to(cr, down_temp_x, down_temp_y);
cairo_show_text(cr, down_num_str);
- // Degree symbol
- const int degree_spacing = (config->display_degree_spacing > 0) ? config->display_degree_spacing : 16;
- double degree_down_x = down_temp_x + down_width + degree_spacing;
- double degree_down_y = down_temp_y - down_num_ext.height * 0.40;
- draw_degree_symbol(cr, degree_down_x, degree_down_y, config);
+ // Draw degree symbol or unit
+ if (get_slot_is_temp(data, slot_down))
+ {
+ const int degree_spacing = (config->display_degree_spacing > 0) ? config->display_degree_spacing : 16;
+ double degree_down_x = down_temp_x + down_width + degree_spacing;
+ double degree_down_y = down_temp_y - down_num_ext.height * 0.40;
+ cairo_set_font_size(cr, down_font_size / 1.66);
+ cairo_move_to(cr, degree_down_x, degree_down_y);
+ cairo_show_text(cr, "\xC2\xB0");
+ cairo_set_font_size(cr, down_font_size);
+ }
}
}
@@ -294,7 +331,7 @@ static void draw_labels(cairo_t *cr, const struct Config *config,
const monitor_sensor_data_t *data,
const ScalingParams *params)
{
- (void)data; // Reserved for future use (e.g., dynamic labels based on values)
+ (void)data;
if (!cr || !config || !params)
return;
@@ -306,8 +343,8 @@ static void draw_labels(cairo_t *cr, const struct Config *config,
const int down_active = slot_is_active(slot_down);
// Get labels from slots (NULL if "none")
- const char *label_up = get_slot_label(slot_up);
- const char *label_down = get_slot_label(slot_down);
+ const char *label_up = get_slot_label(config, data, slot_up);
+ const char *label_down = get_slot_label(config, data, slot_down);
// Get bar heights
const uint16_t bar_height_up = get_slot_bar_height(config, "up");
@@ -401,8 +438,17 @@ static void render_display_content(cairo_t *cr, const struct Config *config,
float temp_up = get_slot_temperature(data, config->sensor_slot_up);
float temp_down = get_slot_temperature(data, config->sensor_slot_down);
- // Labels only if both temps < 99°C (to avoid overlap with large numbers)
- if (temp_up < 99.0f && temp_down < 99.0f)
+ // Labels only if temperature sensors < 99°C (to avoid overlap with large numbers)
+ // Non-temperature sensors always show labels (RPM, Watts etc. have different scales)
+ int up_is_temp = get_slot_is_temp(data, config->sensor_slot_up);
+ int down_is_temp = get_slot_is_temp(data, config->sensor_slot_down);
+ int show_labels = 1;
+ if (up_is_temp && temp_up >= 99.0f)
+ show_labels = 0;
+ if (down_is_temp && temp_down >= 99.0f)
+ show_labels = 0;
+
+ if (show_labels)
{
cairo_set_font_size(cr, config->font_size_labels);
set_cairo_color(cr, &config->font_color_label);
@@ -488,10 +534,10 @@ void draw_dual_image(const struct Config *config)
}
// Get sensor data
- monitor_sensor_data_t sensor_data = {.temp_cpu = 0.0f, .temp_gpu = 0.0f};
- if (!get_temperature_monitor_data(config, &sensor_data))
+ monitor_sensor_data_t sensor_data = {0};
+ if (!get_sensor_monitor_data(config, &sensor_data))
{
- log_message(LOG_WARNING, "Failed to retrieve temperature data");
+ log_message(LOG_WARNING, "Failed to retrieve sensor data");
return;
}
diff --git a/src/srv/cc_conf.c b/src/srv/cc_conf.c
index a6211c1..7ff25ec 100644
--- a/src/srv/cc_conf.c
+++ b/src/srv/cc_conf.c
@@ -69,10 +69,52 @@ static struct
int is_circular;
} device_cache = {0};
+/**
+ * @brief Cache for all device names and types (populated from /devices).
+ * @details Maps device UID to display name and type string.
+ */
+static struct
+{
+ char uid[128];
+ char name[CC_NAME_SIZE];
+ char type[16];
+} device_name_cache[MAX_DEVICE_NAME_CACHE];
+static int device_name_cache_count = 0;
+
+/**
+ * @brief Get device display name by UID.
+ */
+const char *get_device_name_by_uid(const char *device_uid)
+{
+ if (!device_uid)
+ return "";
+ for (int i = 0; i < device_name_cache_count; i++)
+ {
+ if (strcmp(device_name_cache[i].uid, device_uid) == 0)
+ return device_name_cache[i].name;
+ }
+ return "";
+}
+
+/**
+ * @brief Get device type string by UID.
+ */
+const char *get_device_type_by_uid(const char *device_uid)
+{
+ if (!device_uid)
+ return NULL;
+ for (int i = 0; i < device_name_cache_count; i++)
+ {
+ if (strcmp(device_name_cache[i].uid, device_uid) == 0)
+ return device_name_cache[i].type;
+ }
+ return NULL;
+}
+
/**
* @brief Extract device type from JSON device object.
* @details Common helper function to extract device type string from JSON
- * device object.
+ * device object. Checks "type" first (/devices), falls back to "d_type" (/status).
*/
const char *extract_device_type_from_json(const json_t *dev)
{
@@ -81,7 +123,12 @@ const char *extract_device_type_from_json(const json_t *dev)
const json_t *type_val = json_object_get(dev, "type");
if (!type_val || !json_is_string(type_val))
- return NULL;
+ {
+ /* Fallback: /status endpoint uses "d_type" instead of "type" */
+ type_val = json_object_get(dev, "d_type");
+ if (!type_val || !json_is_string(type_val))
+ return NULL;
+ }
return json_string_value(type_val);
}
@@ -246,7 +293,26 @@ static void extract_liquidctl_device_info(const json_t *dev, char *lcd_uid,
}
/**
- * @brief Search for Liquidctl device in devices array.
+ * @brief Check if a Liquidctl device has an LCD display.
+ * @details Verifies that lcd_info exists in info.channels.lcd path.
+ */
+static int has_lcd_display(const json_t *dev)
+{
+ const json_t *lcd_info = get_lcd_info_from_device(dev);
+ if (!lcd_info)
+ return 0;
+
+ const json_t *w = json_object_get(lcd_info, "screen_width");
+ const json_t *h = json_object_get(lcd_info, "screen_height");
+ if (!w || !h || !json_is_integer(w) || !json_is_integer(h))
+ return 0;
+
+ return (json_integer_value(w) > 0 && json_integer_value(h) > 0);
+}
+
+/**
+ * @brief Search for Liquidctl device with LCD in devices array.
+ * @details Only selects Liquidctl devices that have a valid LCD display.
*/
static int search_liquidctl_device(const json_t *devices, char *lcd_uid,
size_t uid_size, int *found_liquidctl,
@@ -264,6 +330,15 @@ static int search_liquidctl_device(const json_t *devices, char *lcd_uid,
if (!is_liquidctl_device(type_str))
continue;
+ if (!has_lcd_display(dev))
+ {
+ const json_t *name_val = json_object_get(dev, "name");
+ const char *name = name_val ? json_string_value(name_val) : "unknown";
+ log_message(LOG_INFO, "Skipping Liquidctl device without LCD: %s",
+ name ? name : "unknown");
+ continue;
+ }
+
extract_liquidctl_device_info(dev, lcd_uid, uid_size, found_liquidctl,
screen_width, screen_height, device_name,
name_size);
@@ -328,11 +403,77 @@ static void configure_device_cache_curl(CURL *curl, const char *url,
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, *headers);
}
+/**
+ * @brief Populate device name cache from parsed JSON devices array.
+ * @details Caches UID, name, and type for all devices (not just Liquidctl).
+ */
+static void populate_device_name_cache(const char *json_data)
+{
+ if (!json_data)
+ return;
+
+ json_error_t error;
+ json_t *root = json_loads(json_data, 0, &error);
+ if (!root)
+ return;
+
+ const json_t *devices = json_object_get(root, "devices");
+ if (!devices || !json_is_array(devices))
+ {
+ json_decref(root);
+ return;
+ }
+
+ device_name_cache_count = 0;
+ const size_t count = json_array_size(devices);
+ for (size_t i = 0; i < count && device_name_cache_count < MAX_DEVICE_NAME_CACHE; i++)
+ {
+ const json_t *dev = json_array_get(devices, i);
+ if (!dev)
+ continue;
+
+ const json_t *uid_val = json_object_get(dev, "uid");
+ const json_t *name_val = json_object_get(dev, "name");
+ const char *type_str = extract_device_type_from_json(dev);
+
+ if (!uid_val || !json_is_string(uid_val))
+ continue;
+
+ int idx = device_name_cache_count;
+ cc_safe_strcpy(device_name_cache[idx].uid,
+ sizeof(device_name_cache[idx].uid),
+ json_string_value(uid_val));
+
+ if (name_val && json_is_string(name_val))
+ cc_safe_strcpy(device_name_cache[idx].name,
+ sizeof(device_name_cache[idx].name),
+ json_string_value(name_val));
+ else
+ device_name_cache[idx].name[0] = '\0';
+
+ if (type_str)
+ cc_safe_strcpy(device_name_cache[idx].type,
+ sizeof(device_name_cache[idx].type),
+ type_str);
+ else
+ device_name_cache[idx].type[0] = '\0';
+
+ device_name_cache_count++;
+ }
+
+ log_message(LOG_INFO, "Device name cache: %d devices cached",
+ device_name_cache_count);
+ json_decref(root);
+}
+
/**
* @brief Process device cache API response and populate cache.
*/
static int process_device_cache_response(const http_response *chunk)
{
+ /* Populate device name cache for ALL devices (used by sensor system) */
+ populate_device_name_cache(chunk->data);
+
int found_liquidctl = 0;
int result = parse_liquidctl_data(
chunk->data, device_cache.device_uid, sizeof(device_cache.device_uid),
diff --git a/src/srv/cc_conf.h b/src/srv/cc_conf.h
index 37ad27f..678621c 100644
--- a/src/srv/cc_conf.h
+++ b/src/srv/cc_conf.h
@@ -18,6 +18,7 @@
// Include necessary headers
// cppcheck-suppress-begin missingIncludeSystem
#include
+#include
// cppcheck-suppress-end missingIncludeSystem
// Include project headers
@@ -25,6 +26,7 @@
// Basic constants
#define CC_NAME_SIZE 128
+#define MAX_DEVICE_NAME_CACHE 32
// Forward declarations
struct Config;
@@ -71,4 +73,29 @@ int update_config_from_device(struct Config *config);
int is_circular_display_device(const char *device_name, int screen_width,
int screen_height);
+/**
+ * @brief Get device display name by UID.
+ * @details Retrieves the cached device name for a given UID.
+ * Cache is populated during init_device_cache().
+ * @param device_uid Device UID to look up
+ * @return Device name string, or empty string if not found
+ */
+const char *get_device_name_by_uid(const char *device_uid);
+
+/**
+ * @brief Get device type string by UID.
+ * @param device_uid Device UID to look up
+ * @return Device type string ("CPU","GPU","Liquidctl","Hwmon","CustomSensors"),
+ * or NULL if not found
+ */
+const char *get_device_type_by_uid(const char *device_uid);
+
+/**
+ * @brief Extract device type from JSON device object.
+ * @details Checks "type" field first, falls back to "d_type" (used in /status).
+ * @param dev JSON device object
+ * @return Device type string, or NULL if not found
+ */
+const char *extract_device_type_from_json(const json_t *dev);
+
#endif // CC_CONF_H
diff --git a/src/srv/cc_sensor.c b/src/srv/cc_sensor.c
index 8739db9..e9f9603 100644
--- a/src/srv/cc_sensor.c
+++ b/src/srv/cc_sensor.c
@@ -8,8 +8,9 @@
*/
/**
- * @brief CPU/GPU temperature monitoring via CoolerControl API.
- * @details Reads sensor data from /status endpoint.
+ * @brief Sensor monitoring via CoolerControl API.
+ * @details Reads all sensor data (temps, RPM, duty, watts, freq) from
+ * /status endpoint for all device types.
*/
// Include necessary headers
@@ -27,12 +28,6 @@
#include "cc_main.h"
#include "cc_sensor.h"
-/**
- * @brief Extract device type from JSON object.
- * @details Helper function to extract device type from JSON object.
- */
-extern const char *extract_device_type_from_json(const json_t *dev);
-
/** @brief Cached CURL handle for reuse across polling cycles. */
static CURL *sensor_curl_handle = NULL;
@@ -65,32 +60,60 @@ void cleanup_sensor_curl_handle(void)
}
/**
- * @brief Extract temperature from device status history.
- * @details Helper function to get temperature from the latest status entry.
+ * @brief Add a sensor entry to the monitor data collection.
+ * @details Helper to append a new sensor entry with bounds checking.
+ * @return 1 if added, 0 if array full
*/
-static float extract_device_temperature(const json_t *device,
- const char *device_type)
+static int add_sensor_entry(monitor_sensor_data_t *data,
+ const char *name, const char *device_uid,
+ const char *device_type, sensor_category_t category,
+ float value, const char *unit, int use_decimal)
+{
+ if (data->sensor_count >= MAX_SENSORS)
+ return 0;
+
+ sensor_entry_t *entry = &data->sensors[data->sensor_count];
+ cc_safe_strcpy(entry->name, sizeof(entry->name), name);
+ cc_safe_strcpy(entry->device_uid, sizeof(entry->device_uid), device_uid);
+ cc_safe_strcpy(entry->device_type, sizeof(entry->device_type), device_type);
+ cc_safe_strcpy(entry->unit, sizeof(entry->unit), unit);
+
+ /* Device name from cache */
+ const char *dev_name = get_device_name_by_uid(device_uid);
+ cc_safe_strcpy(entry->device_name, sizeof(entry->device_name), dev_name);
+
+ entry->category = category;
+ entry->value = value;
+ entry->use_decimal = use_decimal;
+
+ data->sensor_count++;
+ return 1;
+}
+
+/**
+ * @brief Collect all temperature sensors from a device's latest status.
+ * @details Iterates temps[] array in the last status_history entry.
+ */
+static void collect_device_temps(const json_t *device, const char *device_uid,
+ const char *device_type,
+ monitor_sensor_data_t *data)
{
- // Get status history
const json_t *status_history = json_object_get(device, "status_history");
if (!status_history || !json_is_array(status_history))
- return 0.0f;
+ return;
size_t history_count = json_array_size(status_history);
if (history_count == 0)
- return 0.0f;
+ return;
- // Get latest status
const json_t *last_status = json_array_get(status_history, history_count - 1);
if (!last_status)
- return 0.0f;
+ return;
- // Get temperatures array
const json_t *temps = json_object_get(last_status, "temps");
if (!temps || !json_is_array(temps))
- return 0.0f;
+ return;
- // Search for appropriate temperature sensor
size_t temp_count = json_array_size(temps);
for (size_t i = 0; i < temp_count; i++)
{
@@ -101,65 +124,139 @@ static float extract_device_temperature(const json_t *device,
const json_t *name_val = json_object_get(temp_entry, "name");
const json_t *temp_val = json_object_get(temp_entry, "temp");
- if (!name_val || !json_is_string(name_val) || !temp_val ||
- !json_is_number(temp_val))
+ if (!name_val || !json_is_string(name_val) ||
+ !temp_val || !json_is_number(temp_val))
continue;
- const char *sensor_name = json_string_value(name_val);
float temperature = (float)json_number_value(temp_val);
- // Validate temperature range
- if (temperature < -50.0f || temperature > 150.0f)
+ /* Skip invalid readings */
+ if (temperature < -50.0f || temperature > 200.0f)
continue;
- // Check sensor name based on device type
- if (strcmp(device_type, "CPU") == 0 && strcmp(sensor_name, "temp1") == 0)
+ /* Decimal only for Liquidctl (coolant) sensors */
+ int use_dec = (strcmp(device_type, "Liquidctl") == 0) ? 1 : 0;
+
+ add_sensor_entry(data, json_string_value(name_val), device_uid,
+ device_type, SENSOR_CATEGORY_TEMP, temperature,
+ "\xC2\xB0"
+ "C",
+ use_dec);
+ }
+}
+
+/**
+ * @brief Collect all channel sensors from a device's latest status.
+ * @details Iterates channels[] array and creates entries for RPM, duty,
+ * watts, and freq values.
+ */
+static void collect_device_channels(const json_t *device,
+ const char *device_uid,
+ const char *device_type,
+ monitor_sensor_data_t *data)
+{
+ const json_t *status_history = json_object_get(device, "status_history");
+ if (!status_history || !json_is_array(status_history))
+ return;
+
+ size_t history_count = json_array_size(status_history);
+ if (history_count == 0)
+ return;
+
+ const json_t *last_status = json_array_get(status_history, history_count - 1);
+ if (!last_status)
+ return;
+
+ const json_t *channels = json_object_get(last_status, "channels");
+ if (!channels || !json_is_array(channels))
+ return;
+
+ size_t channel_count = json_array_size(channels);
+ for (size_t i = 0; i < channel_count; i++)
+ {
+ const json_t *ch = json_array_get(channels, i);
+ if (!ch)
+ continue;
+
+ const json_t *name_val = json_object_get(ch, "name");
+ if (!name_val || !json_is_string(name_val))
+ continue;
+
+ const char *ch_name = json_string_value(name_val);
+ char sensor_name[SENSOR_NAME_LEN];
+
+ /* RPM */
+ const json_t *rpm_val = json_object_get(ch, "rpm");
+ if (rpm_val && json_is_number(rpm_val))
{
- return temperature;
+ float rpm = (float)json_number_value(rpm_val);
+ if (rpm >= 0.0f)
+ {
+ int n = snprintf(sensor_name, sizeof(sensor_name),
+ "%s RPM", ch_name);
+ if (n > 0 && (size_t)n < sizeof(sensor_name))
+ add_sensor_entry(data, sensor_name, device_uid,
+ device_type, SENSOR_CATEGORY_RPM,
+ rpm, "RPM", 0);
+ }
}
- else if (strcmp(device_type, "GPU") == 0 &&
- (strstr(sensor_name, "GPU") || strstr(sensor_name, "gpu") ||
- strstr(sensor_name, "temp1")))
+
+ /* Duty cycle */
+ const json_t *duty_val = json_object_get(ch, "duty");
+ if (duty_val && json_is_number(duty_val))
{
- return temperature;
+ float duty = (float)json_number_value(duty_val);
+ int n = snprintf(sensor_name, sizeof(sensor_name),
+ "%s Duty", ch_name);
+ if (n > 0 && (size_t)n < sizeof(sensor_name))
+ add_sensor_entry(data, sensor_name, device_uid,
+ device_type, SENSOR_CATEGORY_DUTY,
+ duty, "%", 1);
}
- else if (strcmp(device_type, "Liquidctl") == 0 &&
- (strstr(sensor_name, "Liquid") ||
- strstr(sensor_name, "liquid") ||
- strstr(sensor_name, "Coolant") ||
- strstr(sensor_name, "coolant")))
+
+ /* Watts */
+ const json_t *watts_val = json_object_get(ch, "watts");
+ if (watts_val && json_is_number(watts_val))
{
- return temperature;
+ float watts = (float)json_number_value(watts_val);
+ int n = snprintf(sensor_name, sizeof(sensor_name),
+ "%s Power", ch_name);
+ if (n > 0 && (size_t)n < sizeof(sensor_name))
+ add_sensor_entry(data, sensor_name, device_uid,
+ device_type, SENSOR_CATEGORY_WATTS,
+ watts, "W", 1);
}
- }
- return 0.0f;
+ /* Frequency */
+ const json_t *freq_val = json_object_get(ch, "freq");
+ if (freq_val && json_is_number(freq_val))
+ {
+ float freq = (float)json_number_value(freq_val);
+ int n = snprintf(sensor_name, sizeof(sensor_name),
+ "%s Freq", ch_name);
+ if (n > 0 && (size_t)n < sizeof(sensor_name))
+ add_sensor_entry(data, sensor_name, device_uid,
+ device_type, SENSOR_CATEGORY_FREQ,
+ freq, "MHz", 0);
+ }
+ }
}
/**
- * @brief Parse sensor JSON and extract temperatures from CPU, GPU, and
- * Liquidctl devices.
- * @details Simplified JSON parsing to extract CPU, GPU, and Liquid temperature
- * values.
+ * @brief Parse /status JSON and collect all sensors from all devices.
+ * @details Iterates all devices and collects temperature and channel data
+ * into the monitor_sensor_data_t structure.
*/
-static int parse_temperature_data(const char *json, float *temp_cpu,
- float *temp_gpu, float *temp_liquid)
+static int parse_all_sensor_data(const char *json, monitor_sensor_data_t *data)
{
- if (!json || json[0] == '\0')
+ if (!json || json[0] == '\0' || !data)
{
- log_message(LOG_ERROR, "Invalid JSON input");
+ log_message(LOG_ERROR, "Invalid input for sensor parsing");
return 0;
}
- // Initialize outputs
- if (temp_cpu)
- *temp_cpu = 0.0f;
- if (temp_gpu)
- *temp_gpu = 0.0f;
- if (temp_liquid)
- *temp_liquid = 0.0f;
+ data->sensor_count = 0;
- // Parse JSON
json_error_t json_error;
json_t *root = json_loads(json, 0, &json_error);
if (!root)
@@ -168,7 +265,6 @@ static int parse_temperature_data(const char *json, float *temp_cpu,
return 0;
}
- // Get devices array
const json_t *devices = json_object_get(root, "devices");
if (!devices || !json_is_array(devices))
{
@@ -176,12 +272,8 @@ static int parse_temperature_data(const char *json, float *temp_cpu,
return 0;
}
- // Search for CPU, GPU, and Liquidctl devices
size_t device_count = json_array_size(devices);
- int cpu_found = 0, gpu_found = 0, liquid_found = 0;
-
- for (size_t i = 0;
- i < device_count && (!cpu_found || !gpu_found || !liquid_found); i++)
+ for (size_t i = 0; i < device_count; i++)
{
const json_t *device = json_array_get(devices, i);
if (!device)
@@ -191,36 +283,21 @@ static int parse_temperature_data(const char *json, float *temp_cpu,
if (!device_type)
continue;
- if (!cpu_found && strcmp(device_type, "CPU") == 0)
- {
- if (temp_cpu)
- {
- *temp_cpu = extract_device_temperature(device, "CPU");
- cpu_found = 1;
- }
- }
- else if (!gpu_found && strcmp(device_type, "GPU") == 0)
- {
- if (temp_gpu)
- {
- *temp_gpu = extract_device_temperature(device, "GPU");
- gpu_found = 1;
- }
- }
- else if (!liquid_found && strcmp(device_type, "Liquidctl") == 0)
- {
- if (temp_liquid)
- {
- *temp_liquid = extract_device_temperature(device, "Liquidctl");
- if (*temp_liquid > 0.0f)
- {
- liquid_found = 1;
- }
- }
- }
+ /* Extract device UID */
+ const json_t *uid_val = json_object_get(device, "uid");
+ if (!uid_val || !json_is_string(uid_val))
+ continue;
+
+ const char *device_uid = json_string_value(uid_val);
+
+ /* Collect all temps and channels from this device */
+ collect_device_temps(device, device_uid, device_type, data);
+ collect_device_channels(device, device_uid, device_type, data);
}
json_decref(root);
+ log_message(LOG_INFO, "Parsed %d sensors from %zu devices",
+ data->sensor_count, device_count);
return 1;
}
@@ -246,20 +323,16 @@ static void configure_status_request(CURL *curl, const char *url,
}
/**
- * @brief Get CPU, GPU, and Liquid temperature data from CoolerControl API.
- * @details Simplified HTTP request to get temperature data from status
- * endpoint.
+ * @brief Get all sensor data from CoolerControl /status API.
+ * @details Polls the API and collects all sensors into the data structure.
*/
-static int get_temperature_data(const Config *config, float *temp_cpu,
- float *temp_gpu, float *temp_liquid)
+static int get_sensor_data_from_api(const Config *config,
+ monitor_sensor_data_t *data)
{
- if (!config || !temp_cpu || !temp_gpu || !temp_liquid)
+ if (!config || !data)
return 0;
- // Initialize outputs
- *temp_cpu = 0.0f;
- *temp_gpu = 0.0f;
- *temp_liquid = 0.0f;
+ data->sensor_count = 0;
if (config->daemon_address[0] == '\0')
{
@@ -267,21 +340,17 @@ static int get_temperature_data(const Config *config, float *temp_cpu,
return 0;
}
- // Get cached CURL handle for sensor polling
CURL *curl = get_sensor_curl_handle();
if (!curl)
return 0;
- // Reset handle state for clean request
curl_easy_reset(curl);
- // Build URL
char url[256];
int url_len = snprintf(url, sizeof(url), "%s/status", config->daemon_address);
if (url_len < 0 || url_len >= (int)sizeof(url))
return 0;
- // Initialize response buffer
struct http_response response = {0};
if (!cc_init_response_buffer(&response, 8192))
{
@@ -289,23 +358,19 @@ static int get_temperature_data(const Config *config, float *temp_cpu,
return 0;
}
- // Configure request
configure_status_request(curl, url, &response);
- // Enable SSL verification for HTTPS
if (strncmp(config->daemon_address, "https://", 8) == 0)
{
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
}
- // Set headers
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "accept: application/json");
headers = curl_slist_append(headers, "content-type: application/json");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
- // Perform request and parse response
int result = 0;
CURLcode curl_result = curl_easy_perform(curl);
if (curl_result == CURLE_OK)
@@ -315,12 +380,11 @@ static int get_temperature_data(const Config *config, float *temp_cpu,
if (response_code == 200)
{
- result = parse_temperature_data(response.data, temp_cpu, temp_gpu,
- temp_liquid);
+ result = parse_all_sensor_data(response.data, data);
}
else
{
- log_message(LOG_ERROR, "HTTP error: %ld when fetching temperature data",
+ log_message(LOG_ERROR, "HTTP error: %ld when fetching sensor data",
response_code);
}
}
@@ -329,7 +393,6 @@ static int get_temperature_data(const Config *config, float *temp_cpu,
log_message(LOG_ERROR, "CURL error: %s", curl_easy_strerror(curl_result));
}
- // Cleanup request-specific resources
cc_cleanup_response_buffer(&response);
if (headers)
curl_slist_free_all(headers);
@@ -337,18 +400,104 @@ static int get_temperature_data(const Config *config, float *temp_cpu,
return result;
}
+// ============================================================================
+// Slot Resolution Functions
+// ============================================================================
+
/**
- * @brief Get all relevant sensor data (CPU/GPU/Liquid temperature and LCD UID).
- * @details Reads the current CPU, GPU, and Liquid temperatures via API.
+ * @brief Check if a slot value is a legacy type.
*/
-int get_temperature_monitor_data(const Config *config,
- monitor_sensor_data_t *data)
+int is_legacy_sensor_slot(const char *slot_value)
+{
+ if (!slot_value)
+ return 0;
+ return (strcmp(slot_value, "cpu") == 0 ||
+ strcmp(slot_value, "gpu") == 0 ||
+ strcmp(slot_value, "liquid") == 0 ||
+ strcmp(slot_value, "none") == 0);
+}
+
+/**
+ * @brief Resolve a legacy slot value to matching sensor entry.
+ * @details Maps "cpu"→first CPU temp, "gpu"→first GPU temp,
+ * "liquid"→first Liquidctl temp.
+ */
+static const sensor_entry_t *resolve_legacy_slot(
+ const monitor_sensor_data_t *data, const char *slot_value)
+{
+ if (!data || !slot_value)
+ return NULL;
+
+ const char *target_type = NULL;
+ if (strcmp(slot_value, "cpu") == 0)
+ target_type = "CPU";
+ else if (strcmp(slot_value, "gpu") == 0)
+ target_type = "GPU";
+ else if (strcmp(slot_value, "liquid") == 0)
+ target_type = "Liquidctl";
+ else
+ return NULL;
+
+ /* Find first temperature sensor matching the device type */
+ for (int i = 0; i < data->sensor_count; i++)
+ {
+ if (data->sensors[i].category == SENSOR_CATEGORY_TEMP &&
+ strcmp(data->sensors[i].device_type, target_type) == 0)
+ {
+ return &data->sensors[i];
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * @brief Find sensor entry matching a slot value.
+ * @details Handles both legacy ("cpu","gpu","liquid") and dynamic
+ * ("uid:sensor_name") slot resolution.
+ */
+const sensor_entry_t *find_sensor_for_slot(const monitor_sensor_data_t *data,
+ const char *slot_value)
+{
+ if (!data || !slot_value || strcmp(slot_value, "none") == 0)
+ return NULL;
+
+ /* Legacy slot resolution */
+ if (is_legacy_sensor_slot(slot_value))
+ return resolve_legacy_slot(data, slot_value);
+
+ /* Dynamic slot: "device_uid:sensor_name" */
+ const char *separator = strchr(slot_value, ':');
+ if (!separator || separator == slot_value)
+ return NULL;
+
+ size_t uid_len = (size_t)(separator - slot_value);
+ const char *sensor_name = separator + 1;
+
+ for (int i = 0; i < data->sensor_count; i++)
+ {
+ if (strlen(data->sensors[i].device_uid) == uid_len &&
+ strncmp(data->sensors[i].device_uid, slot_value, uid_len) == 0 &&
+ strcmp(data->sensors[i].name, sensor_name) == 0)
+ {
+ return &data->sensors[i];
+ }
+ }
+
+ return NULL;
+}
+
+// ============================================================================
+// Public API
+// ============================================================================
+
+/**
+ * @brief Get all sensor data from CoolerControl API.
+ */
+int get_sensor_monitor_data(const Config *config, monitor_sensor_data_t *data)
{
- // Check if config and data pointers are valid
if (!config || !data)
return 0;
- // Get temperature data from monitor module
- return get_temperature_data(config, &data->temp_cpu, &data->temp_gpu,
- &data->temp_liquid);
+ return get_sensor_data_from_api(config, data);
}
diff --git a/src/srv/cc_sensor.h b/src/srv/cc_sensor.h
index 9158602..6545875 100644
--- a/src/srv/cc_sensor.h
+++ b/src/srv/cc_sensor.h
@@ -8,15 +8,15 @@
*/
/**
- * @brief CPU/GPU temperature monitoring via CoolerControl API.
- * @details Reads sensor data from /status endpoint.
+ * @brief Sensor monitoring via CoolerControl API.
+ * @details Reads all sensor data (temperatures, RPM, duty, watts, frequency)
+ * from /status endpoint for all device types (CPU, GPU, Liquidctl, Hwmon,
+ * CustomSensors).
*/
-// Include necessary headers
#ifndef CC_SENSOR_H
#define CC_SENSOR_H
-// Include necessary headers
// cppcheck-suppress-begin missingIncludeSystem
#include
// cppcheck-suppress-end missingIncludeSystem
@@ -24,25 +24,105 @@
// Forward declaration
struct Config;
+// ============================================================================
+// Sensor Data Model Constants
+// ============================================================================
+
+#define MAX_SENSORS 64
+#define SENSOR_NAME_LEN 48
+#define SENSOR_UID_LEN 96
+#define SENSOR_DEVICE_NAME_LEN 64
+#define SENSOR_DEVICE_TYPE_LEN 16
+#define SENSOR_UNIT_LEN 8
+
+// ============================================================================
+// Sensor Category Enum
+// ============================================================================
+
+/**
+ * @brief Category of a sensor value.
+ * @details Determines default thresholds, display formatting, and unit.
+ */
+typedef enum
+{
+ SENSOR_CATEGORY_TEMP = 0, /**< Temperature in °C */
+ SENSOR_CATEGORY_RPM, /**< Fan/Pump speed in RPM */
+ SENSOR_CATEGORY_DUTY, /**< Duty cycle in % */
+ SENSOR_CATEGORY_WATTS, /**< Power consumption in W */
+ SENSOR_CATEGORY_FREQ /**< Frequency in MHz */
+} sensor_category_t;
+
+// ============================================================================
+// Sensor Entry
+// ============================================================================
+
+/**
+ * @brief Single sensor data entry from CoolerControl API.
+ * @details Represents one sensor value with its metadata. Names match
+ * CoolerControl's display names for maximum UI synchronicity.
+ */
+typedef struct
+{
+ char name[SENSOR_NAME_LEN]; /**< CC sensor name (e.g. "temp1", "Liquid Temperature") */
+ char device_uid[SENSOR_UID_LEN]; /**< CC device UID */
+ char device_name[SENSOR_DEVICE_NAME_LEN]; /**< CC device name (e.g. "NZXT Kraken Z73") */
+ char device_type[SENSOR_DEVICE_TYPE_LEN]; /**< CC device type ("CPU","GPU","Liquidctl","Hwmon","CustomSensors") */
+ sensor_category_t category; /**< Sensor value category */
+ float value; /**< Current sensor value */
+ char unit[SENSOR_UNIT_LEN]; /**< Display unit ("°C","RPM","%","W","MHz") */
+ int use_decimal; /**< 1=show decimal (e.g. 31.5), 0=integer (e.g. 1200) */
+} sensor_entry_t;
+
+// ============================================================================
+// Monitor Sensor Data (runtime collection)
+// ============================================================================
+
/**
- * @brief Structure to hold temperature sensor data.
- * @details Contains temperature values (CPU, GPU, and Liquid/Coolant)
- * representing temperatures in degrees Celsius.
+ * @brief Collection of all sensor values from one API poll.
+ * @details Contains all discovered sensors across all CoolerControl devices.
*/
typedef struct
{
- float temp_cpu;
- float temp_gpu;
- float temp_liquid; // Liquid/Coolant temperature from Liquidctl device
+ sensor_entry_t sensors[MAX_SENSORS]; /**< Array of all discovered sensors */
+ int sensor_count; /**< Number of valid entries in sensors[] */
} monitor_sensor_data_t;
+// ============================================================================
+// Sensor Slot Resolution Functions
+// ============================================================================
+
+/**
+ * @brief Find sensor entry matching a slot value.
+ * @details Resolves legacy ("cpu","gpu","liquid") and dynamic ("uid:name")
+ * slot values to the corresponding sensor_entry_t in the data array.
+ * @param data Current sensor data collection
+ * @param slot_value Slot configuration value
+ * @return Pointer to matching sensor entry, or NULL if not found
+ */
+const sensor_entry_t *find_sensor_for_slot(const monitor_sensor_data_t *data,
+ const char *slot_value);
+
+/**
+ * @brief Check if a slot value is a legacy type.
+ * @param slot_value Slot configuration value
+ * @return 1 if "cpu", "gpu", "liquid", or "none"; 0 otherwise
+ */
+int is_legacy_sensor_slot(const char *slot_value);
+
+// ============================================================================
+// Data Retrieval
+// ============================================================================
+
/**
- * @brief Get temperature data into structure.
- * @details High-level convenience function that retrieves temperature data and
- * populates a monitor_sensor_data_t structure with the values.
+ * @brief Get all sensor data from CoolerControl API.
+ * @details Polls /status endpoint and collects all sensors (temps + channels)
+ * from all device types into the monitor_sensor_data_t structure.
+ * @param config Configuration with daemon address
+ * @param data Output sensor data structure
+ * @return 1 on success, 0 on failure
*/
-int get_temperature_monitor_data(const struct Config *config,
- monitor_sensor_data_t *data);
+int get_sensor_monitor_data(const struct Config *config,
+ monitor_sensor_data_t *data);
/**
* @brief Cleanup cached sensor CURL handle.