diff --git a/.gitignore b/.gitignore index 5b90e79..2d5b2eb 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ go.work.sum # env file .env +!lhm/* diff --git a/go.mod b/go.mod index da40bf8..838913e 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,9 @@ module rpi5-health go 1.26.4 -require golang.org/x/sys v0.48.0 +require ( + github.com/yusufpapurcu/wmi v1.2.4 + golang.org/x/sys v0.48.0 +) + +require github.com/go-ole/go-ole v1.2.6 // indirect diff --git a/go.sum b/go.sum index e3bf0e4..83404e3 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,7 @@ +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/internal/cli/cli.go b/internal/cli/cli.go index e50ad89..aee74ba 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -8,6 +8,7 @@ import ( loadaverage "rpi5-health/internal/load_average" "rpi5-health/internal/memory" "rpi5-health/internal/processor" + "rpi5-health/internal/temperature" "rpi5-health/internal/uptime" "strings" ) @@ -15,10 +16,10 @@ import ( func Listen() { reader := bufio.NewScanner(os.Stdin) - fmt.Print("> ") - for { + fmt.Print("\n> ") + if !reader.Scan() { log.Println("Program terminated (SIGINT)") return @@ -34,6 +35,14 @@ func Listen() { switch text { case "exit": return + case "help": + fmt.Print(` +exit - Exit the program +meminfo - Print memory information +cpuinfo - Print cpu information +uptime - Print system uptime +loadavg - Print cpu load average information`) + fmt.Println() case "meminfo": memInfo, _ := memory.GetRamInformation() @@ -56,6 +65,14 @@ func Listen() { loadAvg, _ := loadaverage.GetLoadAverage() fmt.Printf("Load average: L1: %.2f | L5: %.2f | L15: %.2f\n", loadAvg.Load1, loadAvg.Load5, loadAvg.Load15) + case "temperature": + temp, _ := temperature.GetTemperatureInformation() + + if temp.Supported { + fmt.Printf("Current temperature: %.2fC°", temp.Celsius) + } else { + fmt.Printf("Temperature reading is unsupported. Try installing Libre Hardware Monitor, or running as Administrator.") + } } } diff --git a/internal/temperature/linux_temp.go b/internal/temperature/linux_temp.go new file mode 100644 index 0000000..7f1d248 --- /dev/null +++ b/internal/temperature/linux_temp.go @@ -0,0 +1,43 @@ +//go:build linux + +package temperature + +import ( + "bufio" + "fmt" + "os" + "strconv" +) + +func getLinuxCPUTemperature() (TemperatureInformation, error) { + tempFile, err := os.Open("/sys/class/thermal/thermal_zone0/temp") + if err != nil { + fmt.Printf("Could not find /sys/class/thermal/thermal_zone0/temp. Trying /sys/class/hwmon/hwmon0/temp1_input...") + tempFile, err = os.Open("/sys/class/hwmon/hwmon0/temp1_input") + if err != nil { + return TemperatureInformation{0, false}, fmt.Errorf("Couldn't find temperature information. System is unsupported.") + } + } + defer tempFile.Close() + + fileReader := bufio.NewScanner(tempFile) + + var celsius float64 + + fileReader.Scan() + if fileReader.Err() != nil { + return TemperatureInformation{0, false}, fmt.Errorf("Could not read temperature.") + } + + data := fileReader.Text() + celsius, _ = strconv.ParseFloat(data, 64) + + return TemperatureInformation{ + Celsius: celsius / 1000.0, + Supported: true, + }, nil +} + +func GetTemperatureInformation() (TemperatureInformation, error) { + return getLinuxCPUTemperature() +} diff --git a/internal/temperature/temp.go b/internal/temperature/temp.go new file mode 100644 index 0000000..c5bbb2a --- /dev/null +++ b/internal/temperature/temp.go @@ -0,0 +1,7 @@ +package temperature + +type TemperatureInformation struct { + Celsius float64 + + Supported bool +} diff --git a/internal/temperature/win_temp.go b/internal/temperature/win_temp.go new file mode 100644 index 0000000..b8e98c5 --- /dev/null +++ b/internal/temperature/win_temp.go @@ -0,0 +1,87 @@ +//go:build windows + +package temperature + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/yusufpapurcu/wmi" +) + +type LHMSensor struct { + Value float32 +} + +type LHMNode struct { + Text string `json:"Text"` + Value string `json:"Value"` + Children []LHMNode `json:"Children"` +} + +type MSAcpiThermalZone struct { + CurrentTemperature uint32 +} + +func findCPUTemp(node LHMNode) string { + if strings.Contains(node.Text, "CPU Package") && strings.Contains(node.Value, "°C") { + return node.Value + } + + for _, child := range node.Children { + if val := findCPUTemp(child); val != "" { + return val + } + } + return "" +} + +func getWinCPUTemperature() TemperatureInformation { + // check for preexisting monitoring tools + var lhm []LHMSensor + lhmQuery := "SELECT Value FROM Sensor WHERE SensorType = 'Temperature' AND Name LIKE '%CPU Package%'" + if err := wmi.QueryNamespace(lhmQuery, &lhm, `root\OpenHardwareMonitor`); err == nil && len(lhm) > 0 { + return TemperatureInformation{Celsius: float64(lhm[0].Value), Supported: true} + } else { + // Try LHM local webserver + client := http.Client{Timeout: 1 * time.Second} + resp, err := client.Get("http://localhost:8085/data.json") + if err == nil { + defer resp.Body.Close() + var root LHMNode + if json.NewDecoder(resp.Body).Decode(&root) == nil { + tempStr := findCPUTemp(root) + if tempStr != "" { + var temp float64 + fmt.Sscanf(tempStr, "%f", &temp) + return TemperatureInformation{Celsius: temp, Supported: true} + } + } + } + } + + // check native ACPI + var acpi []MSAcpiThermalZone + acpiQuery := "SELECT CurrentTemperature FROM MSAcpi_ThermalZoneTemperature" + if err := wmi.QueryNamespace(acpiQuery, &acpi, `root\wmi`); err == nil && len(acpi) > 0 { + kelvinTenths := float64(acpi[0].CurrentTemperature) + celsius := (kelvinTenths / 10.0) - 273.15 + + // Discard known fake motherboard values + if celsius != 27.8 && celsius != 29.8 && celsius != 24.05 && celsius > 0 && celsius < 150 { + return TemperatureInformation{Celsius: celsius, Supported: true} + } + } + + return TemperatureInformation{0, false} // unsupported +} + +// This is really finicky and is rarely working well due to windows +// temperature information being a RING0 feature. Maybe later this +// can be improved upon, but for now it's good enough as it is now. +func GetTemperatureInformation() (TemperatureInformation, error) { + return getWinCPUTemperature(), nil +}