Skip to content

Commit 02403c5

Browse files
committed
feat(sensors): add new sensors and enhanced sensor fields
New sensors: - hostname: system hostname for device name display - motherboard: CPU fan, system fans, CPU voltage from hwmon Enhanced sensors: - cpu: added name, power, voltage fields - amd_gpu: added voltage, clock, memory_clock fields - nvidia_gpu: added clock, memory_clock fields These additions support the AIDA64-style theme with detailed hardware monitoring.
1 parent 0a67f2b commit 02403c5

5 files changed

Lines changed: 397 additions & 5 deletions

File tree

pkg/sensors/amd_gpu_linux.go

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ func (p *AMDGPUProvider) Meta() SensorMeta {
3232
{Name: "MemoryTotal", JSONName: "memory_total", TSName: "memoryTotal", Type: FieldTypeOptionalNumber, Unit: "MB", Description: "VRAM total"},
3333
{Name: "Power", JSONName: "power", TSName: "power", Type: FieldTypeOptionalNumber, Unit: "W", Description: "Power draw"},
3434
{Name: "FanSpeed", JSONName: "fan_speed", TSName: "fanSpeed", Type: FieldTypeOptionalNumber, Unit: "%", Description: "Fan speed"},
35+
{Name: "Voltage", JSONName: "voltage", TSName: "voltage", Type: FieldTypeOptionalNumber, Unit: "V", Description: "GPU core voltage"},
36+
{Name: "Clock", JSONName: "clock", TSName: "clock", Type: FieldTypeOptionalNumber, Unit: "MHz", Description: "GPU clock speed"},
37+
{Name: "MemoryClock", JSONName: "memory_clock", TSName: "memoryClock", Type: FieldTypeOptionalNumber, Unit: "MHz", Description: "Memory clock speed"},
3538
},
3639
}
3740
}
@@ -49,7 +52,7 @@ func (p *AMDGPUProvider) Collect(state *CollectorState) map[string]interface{} {
4952
}
5053

5154
result := map[string]interface{}{
52-
"name": "AMD GPU",
55+
"name": p.getGPUName(cardPath),
5356
}
5457

5558
// Read GPU load
@@ -59,7 +62,7 @@ func (p *AMDGPUProvider) Collect(state *CollectorState) map[string]interface{} {
5962
}
6063
}
6164

62-
// Read temperature from hwmon
65+
// Read data from hwmon
6366
hwmonPaths, _ := filepath.Glob(filepath.Join(cardPath, "hwmon", "hwmon*"))
6467
for _, hwmon := range hwmonPaths {
6568
// Temperature
@@ -69,8 +72,12 @@ func (p *AMDGPUProvider) Collect(state *CollectorState) map[string]interface{} {
6972
}
7073
}
7174

72-
// Power
73-
if data, err := os.ReadFile(filepath.Join(hwmon, "power1_average")); err == nil {
75+
// Power (try power1_input first, then power1_average)
76+
if data, err := os.ReadFile(filepath.Join(hwmon, "power1_input")); err == nil {
77+
if microW, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64); err == nil {
78+
result["power"] = float64(microW) / 1000000.0
79+
}
80+
} else if data, err := os.ReadFile(filepath.Join(hwmon, "power1_average")); err == nil {
7481
if microW, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64); err == nil {
7582
result["power"] = float64(microW) / 1000000.0
7683
}
@@ -86,6 +93,51 @@ func (p *AMDGPUProvider) Collect(state *CollectorState) map[string]interface{} {
8693
}
8794
}
8895
}
96+
97+
// Voltage (vddgfx - in0 or labeled)
98+
// Try labeled voltage first
99+
inLabels, _ := filepath.Glob(filepath.Join(hwmon, "in*_label"))
100+
for _, labelPath := range inLabels {
101+
labelBytes, err := os.ReadFile(labelPath)
102+
if err != nil {
103+
continue
104+
}
105+
label := strings.TrimSpace(string(labelBytes))
106+
if strings.ToLower(label) == "vddgfx" {
107+
inputPath := strings.Replace(labelPath, "_label", "_input", 1)
108+
if data, err := os.ReadFile(inputPath); err == nil {
109+
if milliV, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64); err == nil {
110+
result["voltage"] = float64(milliV) / 1000.0
111+
}
112+
}
113+
break
114+
}
115+
}
116+
117+
// GPU Clock (sclk - freq1 or labeled)
118+
freqLabels, _ := filepath.Glob(filepath.Join(hwmon, "freq*_label"))
119+
for _, labelPath := range freqLabels {
120+
labelBytes, err := os.ReadFile(labelPath)
121+
if err != nil {
122+
continue
123+
}
124+
label := strings.TrimSpace(string(labelBytes))
125+
if strings.ToLower(label) == "sclk" {
126+
inputPath := strings.Replace(labelPath, "_label", "_input", 1)
127+
if data, err := os.ReadFile(inputPath); err == nil {
128+
if hz, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64); err == nil {
129+
result["clock"] = float64(hz) / 1000000.0 // Hz to MHz
130+
}
131+
}
132+
} else if strings.ToLower(label) == "mclk" {
133+
inputPath := strings.Replace(labelPath, "_label", "_input", 1)
134+
if data, err := os.ReadFile(inputPath); err == nil {
135+
if hz, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64); err == nil {
136+
result["memory_clock"] = float64(hz) / 1000000.0 // Hz to MHz
137+
}
138+
}
139+
}
140+
}
89141
}
90142

91143
// VRAM info
@@ -104,6 +156,19 @@ func (p *AMDGPUProvider) Collect(state *CollectorState) map[string]interface{} {
104156
return result
105157
}
106158

159+
func (p *AMDGPUProvider) getGPUName(cardPath string) string {
160+
// Try to get the marketing name from the device
161+
if data, err := os.ReadFile(filepath.Join(cardPath, "product_name")); err == nil {
162+
name := strings.TrimSpace(string(data))
163+
if name != "" {
164+
return name
165+
}
166+
}
167+
168+
// Try device ID lookup or just return generic name
169+
return "AMD GPU"
170+
}
171+
107172
func (p *AMDGPUProvider) findAMDCard() string {
108173
cardPaths, _ := filepath.Glob("/sys/class/drm/card*/device")
109174

pkg/sensors/cpu_linux.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,13 @@ func (p *CPUProvider) Meta() SensorMeta {
2828
Category: "system",
2929
Platforms: []string{"linux"},
3030
Fields: []FieldDef{
31+
{Name: "Name", JSONName: "name", TSName: "name", Type: FieldTypeString, Unit: "", Description: "CPU model name"},
3132
{Name: "Load", JSONName: "load", TSName: "load", Type: FieldTypeNumber, Unit: "%", Description: "CPU load percentage"},
3233
{Name: "Temperature", JSONName: "temperature", TSName: "temperature", Type: FieldTypeOptionalNumber, Unit: "°C", Description: "CPU temperature"},
3334
{Name: "Frequency", JSONName: "frequency", TSName: "frequency", Type: FieldTypeOptionalNumber, Unit: "MHz", Description: "CPU frequency"},
3435
{Name: "Cores", JSONName: "cores", TSName: "cores", Type: FieldTypeNumber, Unit: "", Description: "Number of CPU cores"},
36+
{Name: "Power", JSONName: "power", TSName: "power", Type: FieldTypeOptionalNumber, Unit: "W", Description: "CPU package power"},
37+
{Name: "Voltage", JSONName: "voltage", TSName: "voltage", Type: FieldTypeOptionalNumber, Unit: "V", Description: "CPU core voltage"},
3538
},
3639
}
3740
}
@@ -46,6 +49,9 @@ func (p *CPUProvider) Available() bool {
4649
func (p *CPUProvider) Collect(state *CollectorState) map[string]interface{} {
4750
result := make(map[string]interface{})
4851

52+
// Get CPU name
53+
result["name"] = p.collectName()
54+
4955
// Get CPU load
5056
load := p.collectLoad(state)
5157
result["load"] = load
@@ -63,6 +69,16 @@ func (p *CPUProvider) Collect(state *CollectorState) map[string]interface{} {
6369
// Get core count
6470
result["cores"] = runtime.NumCPU()
6571

72+
// Get power (from RAPL or hwmon)
73+
if power := p.collectPower(); power != nil {
74+
result["power"] = *power
75+
}
76+
77+
// Get voltage (from hwmon)
78+
if voltage := p.collectVoltage(); voltage != nil {
79+
result["voltage"] = *voltage
80+
}
81+
6682
return result
6783
}
6884

@@ -217,3 +233,110 @@ func (p *CPUProvider) collectFrequency() *float64 {
217233

218234
return nil
219235
}
236+
237+
func (p *CPUProvider) collectName() string {
238+
file, err := os.Open("/proc/cpuinfo")
239+
if err != nil {
240+
return "Unknown CPU"
241+
}
242+
defer file.Close()
243+
244+
scanner := bufio.NewScanner(file)
245+
for scanner.Scan() {
246+
line := scanner.Text()
247+
if strings.HasPrefix(line, "model name") {
248+
parts := strings.SplitN(line, ":", 2)
249+
if len(parts) >= 2 {
250+
return strings.TrimSpace(parts[1])
251+
}
252+
}
253+
}
254+
255+
return "Unknown CPU"
256+
}
257+
258+
func (p *CPUProvider) collectPower() *float64 {
259+
// Try RAPL (Running Average Power Limit) for Intel/AMD
260+
raplPaths, _ := filepath.Glob("/sys/class/powercap/intel-rapl:0/energy_uj")
261+
for _, raplPath := range raplPaths {
262+
// RAPL gives cumulative energy, we'd need to track delta
263+
// For now, try hwmon power sensors instead
264+
_ = raplPath
265+
}
266+
267+
// Try hwmon power sensors (zenpower, k10temp with power)
268+
hwmonPaths, _ := filepath.Glob("/sys/class/hwmon/hwmon*/name")
269+
for _, namePath := range hwmonPaths {
270+
nameBytes, err := os.ReadFile(namePath)
271+
if err != nil {
272+
continue
273+
}
274+
name := strings.TrimSpace(string(nameBytes))
275+
276+
// Look for CPU power sensors
277+
if strings.Contains(name, "zenpower") || strings.Contains(name, "k10temp") {
278+
dir := filepath.Dir(namePath)
279+
powerFiles, _ := filepath.Glob(filepath.Join(dir, "power*_input"))
280+
for _, powerFile := range powerFiles {
281+
data, err := os.ReadFile(powerFile)
282+
if err != nil {
283+
continue
284+
}
285+
microW, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
286+
if err != nil {
287+
continue
288+
}
289+
power := float64(microW) / 1000000.0
290+
return &power
291+
}
292+
}
293+
}
294+
295+
return nil
296+
}
297+
298+
func (p *CPUProvider) collectVoltage() *float64 {
299+
// Try hwmon voltage sensors
300+
hwmonPaths, _ := filepath.Glob("/sys/class/hwmon/hwmon*/name")
301+
for _, namePath := range hwmonPaths {
302+
nameBytes, err := os.ReadFile(namePath)
303+
if err != nil {
304+
continue
305+
}
306+
name := strings.TrimSpace(string(nameBytes))
307+
308+
// Look for CPU voltage sensors (zenpower, nct6687, etc.)
309+
if strings.Contains(name, "zenpower") || strings.Contains(name, "nct6") {
310+
dir := filepath.Dir(namePath)
311+
312+
// Try labeled voltage inputs first
313+
inLabels, _ := filepath.Glob(filepath.Join(dir, "in*_label"))
314+
for _, labelPath := range inLabels {
315+
labelBytes, err := os.ReadFile(labelPath)
316+
if err != nil {
317+
continue
318+
}
319+
label := strings.TrimSpace(string(labelBytes))
320+
321+
// Look for CPU core voltage labels
322+
if strings.Contains(strings.ToLower(label), "vcore") ||
323+
strings.Contains(strings.ToLower(label), "cpu") ||
324+
strings.Contains(strings.ToLower(label), "svi2_core") {
325+
inputPath := strings.Replace(labelPath, "_label", "_input", 1)
326+
data, err := os.ReadFile(inputPath)
327+
if err != nil {
328+
continue
329+
}
330+
milliV, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
331+
if err != nil {
332+
continue
333+
}
334+
voltage := float64(milliV) / 1000.0
335+
return &voltage
336+
}
337+
}
338+
}
339+
}
340+
341+
return nil
342+
}

pkg/sensors/hostname.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package sensors
2+
3+
import (
4+
"os"
5+
)
6+
7+
func init() {
8+
Register(&HostnameProvider{})
9+
}
10+
11+
// HostnameProvider provides hostname/device name.
12+
type HostnameProvider struct{}
13+
14+
// Meta returns the sensor metadata.
15+
func (p *HostnameProvider) Meta() SensorMeta {
16+
return SensorMeta{
17+
ID: "hostname",
18+
Name: "Hostname",
19+
Description: "System hostname and device name",
20+
Category: "system",
21+
Platforms: []string{"linux", "darwin", "windows"},
22+
Fields: []FieldDef{
23+
{Name: "Hostname", JSONName: "hostname", TSName: "hostname", Type: FieldTypeString, Unit: "", Description: "System hostname"},
24+
},
25+
}
26+
}
27+
28+
// Available returns true - hostname is always available.
29+
func (p *HostnameProvider) Available() bool {
30+
return true
31+
}
32+
33+
// Collect gathers hostname.
34+
func (p *HostnameProvider) Collect(state *CollectorState) map[string]interface{} {
35+
hostname, err := os.Hostname()
36+
if err != nil {
37+
hostname = "Unknown"
38+
}
39+
40+
return map[string]interface{}{
41+
"hostname": hostname,
42+
}
43+
}

0 commit comments

Comments
 (0)