diff --git a/bin/analyze-go b/bin/analyze-go index 9f6f6b9..7224aa4 100755 Binary files a/bin/analyze-go and b/bin/analyze-go differ diff --git a/bin/status-go b/bin/status-go index f5c0a5f..2a1b897 100755 Binary files a/bin/status-go and b/bin/status-go differ diff --git a/cmd/status/metrics.go b/cmd/status/metrics.go index d9d67dc..8d783a6 100644 --- a/cmd/status/metrics.go +++ b/cmd/status/metrics.go @@ -118,10 +118,13 @@ type BatteryStatus struct { } type ThermalStatus struct { - CPUTemp float64 - GPUTemp float64 - FanSpeed int - FanCount int + CPUTemp float64 + GPUTemp float64 + FanSpeed int + FanCount int + SystemPower float64 // System power consumption in Watts + AdapterPower float64 // AC adapter max power in Watts + BatteryPower float64 // Battery charge/discharge power in Watts (positive = discharging) } type SensorReading struct { diff --git a/cmd/status/metrics_battery.go b/cmd/status/metrics_battery.go index 2fc4ec2..104a9a5 100644 --- a/cmd/status/metrics_battery.go +++ b/cmd/status/metrics_battery.go @@ -132,15 +132,13 @@ func getCachedPowerData() (health string, cycles int) { for _, line := range lines { lower := strings.ToLower(line) if strings.Contains(lower, "cycle count") { - parts := strings.Split(line, ":") - if len(parts) == 2 { - cycles, _ = strconv.Atoi(strings.TrimSpace(parts[1])) + if _, after, found := strings.Cut(line, ":"); found { + cycles, _ = strconv.Atoi(strings.TrimSpace(after)) } } if strings.Contains(lower, "condition") { - parts := strings.Split(line, ":") - if len(parts) == 2 { - health = strings.TrimSpace(parts[1]) + if _, after, found := strings.Cut(line, ":"); found { + health = strings.TrimSpace(after) } } } @@ -175,44 +173,93 @@ func collectThermal() ThermalStatus { var thermal ThermalStatus - // Get fan info from cached system_profiler + // Get fan info and adapter power from cached system_profiler out := getSystemPowerOutput() if out != "" { lines := strings.Split(out, "\n") for _, line := range lines { lower := strings.ToLower(line) if strings.Contains(lower, "fan") && strings.Contains(lower, "speed") { - parts := strings.Split(line, ":") - if len(parts) == 2 { + if _, after, found := strings.Cut(line, ":"); found { // Extract number from string like "1200 RPM" - numStr := strings.TrimSpace(parts[1]) - numStr = strings.Split(numStr, " ")[0] + numStr := strings.TrimSpace(after) + numStr, _, _ = strings.Cut(numStr, " ") thermal.FanSpeed, _ = strconv.Atoi(numStr) } } } } - // 1. Try ioreg battery temperature (simple, no sudo needed) - ctxIoreg, cancelIoreg := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancelIoreg() - if out, err := runCmd(ctxIoreg, "sh", "-c", "ioreg -rn AppleSmartBattery | awk '/\"Temperature\"/ {print $3}'"); err == nil { - valStr := strings.TrimSpace(out) - if tempRaw, err := strconv.Atoi(valStr); err == nil && tempRaw > 0 { - thermal.CPUTemp = float64(tempRaw) / 100.0 - return thermal + // Get power metrics from ioreg (fast, real-time data) + ctxPower, cancelPower := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancelPower() + if out, err := runCmd(ctxPower, "ioreg", "-rn", "AppleSmartBattery"); err == nil { + lines := strings.Split(out, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + + // Get battery temperature + // Matches: "Temperature" = 3055 (note: space before =) + if _, after, found := strings.Cut(line, "\"Temperature\" = "); found { + valStr := strings.TrimSpace(after) + if tempRaw, err := strconv.Atoi(valStr); err == nil && tempRaw > 0 { + thermal.CPUTemp = float64(tempRaw) / 100.0 + } + } + + // Get adapter power (Watts) + // Read from current adapter: "AdapterDetails" = {"Watts"=140...} + // Skip historical data: "AppleRawAdapterDetails" = ({Watts=90}, {Watts=140}) + if strings.Contains(line, "\"AdapterDetails\" = {") && !strings.Contains(line, "AppleRaw") { + if _, after, found := strings.Cut(line, "\"Watts\"="); found { + valStr := strings.TrimSpace(after) + // Remove trailing characters like , or } + valStr, _, _ = strings.Cut(valStr, ",") + valStr, _, _ = strings.Cut(valStr, "}") + valStr = strings.TrimSpace(valStr) + if watts, err := strconv.ParseFloat(valStr, 64); err == nil && watts > 0 { + thermal.AdapterPower = watts + } + } + } + + // Get system power consumption (mW -> W) + // Matches: "SystemPowerIn"=12345 + if _, after, found := strings.Cut(line, "\"SystemPowerIn\"="); found { + valStr := strings.TrimSpace(after) + valStr, _, _ = strings.Cut(valStr, ",") + valStr, _, _ = strings.Cut(valStr, "}") + valStr = strings.TrimSpace(valStr) + if powerMW, err := strconv.ParseFloat(valStr, 64); err == nil && powerMW > 0 { + thermal.SystemPower = powerMW / 1000.0 + } + } + + // Get battery power (mW -> W, positive = discharging) + // Matches: "BatteryPower"=12345 + if _, after, found := strings.Cut(line, "\"BatteryPower\"="); found { + valStr := strings.TrimSpace(after) + valStr, _, _ = strings.Cut(valStr, ",") + valStr, _, _ = strings.Cut(valStr, "}") + valStr = strings.TrimSpace(valStr) + if powerMW, err := strconv.ParseFloat(valStr, 64); err == nil { + thermal.BatteryPower = powerMW / 1000.0 + } + } } } - // 2. Try thermal level as a proxy (fallback) - ctx2, cancel2 := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel2() - out2, err := runCmd(ctx2, "sysctl", "-n", "machdep.xcpm.cpu_thermal_level") - if err == nil { - level, _ := strconv.Atoi(strings.TrimSpace(out2)) - // Estimate temp: level 0-100 roughly maps to 40-100°C - if level >= 0 { - thermal.CPUTemp = 45 + float64(level)*0.5 + // Fallback: Try thermal level as a proxy if temperature not found + if thermal.CPUTemp == 0 { + ctx2, cancel2 := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel2() + out2, err := runCmd(ctx2, "sysctl", "-n", "machdep.xcpm.cpu_thermal_level") + if err == nil { + level, _ := strconv.Atoi(strings.TrimSpace(out2)) + // Estimate temp: level 0-100 roughly maps to 40-100°C + if level >= 0 { + thermal.CPUTemp = 45 + float64(level)*0.5 + } } } diff --git a/cmd/status/view.go b/cmd/status/view.go index 5d48557..bb3c177 100644 --- a/cmd/status/view.go +++ b/cmd/status/view.go @@ -520,7 +520,7 @@ func renderBatteryCard(batts []BatteryStatus, thermal ThermalStatus) cardData { } lines = append(lines, fmt.Sprintf("Level %s %s", batteryProgressBar(b.Percent), percentText)) - // Line 2: status + // Line 2: status with power info statusIcon := "" statusStyle := subtleStyle if statusLower == "charging" || statusLower == "charged" { @@ -537,6 +537,18 @@ func renderBatteryCard(batts []BatteryStatus, thermal ThermalStatus) cardData { if b.TimeLeft != "" { statusText += " · " + b.TimeLeft } + // Add power information + if statusLower == "charging" || statusLower == "charged" { + // AC powered - show system power consumption + if thermal.SystemPower > 0 { + statusText += fmt.Sprintf(" · %.0fW", thermal.SystemPower) + } else if thermal.AdapterPower > 0 { + statusText += fmt.Sprintf(" · %.0fW Adapter", thermal.AdapterPower) + } + } else if thermal.BatteryPower > 0 { + // Battery powered - show discharge rate + statusText += fmt.Sprintf(" · %.0fW", thermal.BatteryPower) + } lines = append(lines, statusStyle.Render(statusText+statusIcon)) // Line 3: Health + cycles + temp diff --git a/mole b/mole index 1c14f32..94de8c6 100755 --- a/mole +++ b/mole @@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/core/common.sh" # Version info -VERSION="1.14.1" +VERSION="1.14.2" MOLE_TAGLINE="Deep clean and optimize your Mac." # Check TouchID configuration