Skip to content

Instantly share code, notes, and snippets.

@GenoZhou
Created December 26, 2025 06:48
Show Gist options
  • Select an option

  • Save GenoZhou/a6ba51fba9296889b6d6ca9ea39fe9e6 to your computer and use it in GitHub Desktop.

Select an option

Save GenoZhou/a6ba51fba9296889b6d6ca9ea39fe9e6 to your computer and use it in GitHub Desktop.
Mac Power Monitor
#!/usr/bin/env python3
"""
Mac Power Monitor - Real-time power consumption dashboard for macOS
Features:
- Dynamic power detection (supports any charger wattage)
- Hardware telemetry: CPU/GPU/ANE (measured), Memory/Storage/Network/Screen (estimated)
- Application power analysis with proportional allocation
- Smart battery status detection (optimized charging, insufficient power warnings)
- Cross-validation using multiple data sources (ioreg, powermetrics, system_profiler)
Usage: sudo python3 main.py
Requirements: macOS, Python 3.x, sudo privileges
Compatibility: Intel Macs and Apple Silicon (M1/M2/M3)
"""
import os
import subprocess
import time
import re
def get_output(command):
try:
return subprocess.check_output(command, shell=True, timeout=5).decode('utf-8')
except:
return ""
def is_apple_silicon():
cpu_info = get_output("sysctl -n machdep.cpu.brand_string")
return "Apple" in cpu_info
def parse_power_data():
# 1. Get Charger Information (Hub Input)
charger_raw = get_output("system_profiler SPPowerDataType")
wattage_match = re.search(r"Wattage \(W\): (\d+)", charger_raw)
in_watts = wattage_match.group(1) if wattage_match else "0"
# 2. Get Battery Drain/Charge Rate (Net System Balance)
battery_raw = get_output("ioreg -rw0 -c AppleSmartBattery")
amps = re.search(r"\"Amperage\" = (-?\d+)", battery_raw)
volts = re.search(r"\"Voltage\" = (\d+)", battery_raw)
live_net_watts = 0.0
if amps and volts:
try:
amp_value = int(amps.group(1))
volt_value = int(volts.group(1))
# Handle signed integer overflow: if value is very large (close to 2^64),
# it's likely a negative value represented as unsigned int64
# Convert to signed int64 if value > 2^63
if amp_value > 2**63:
amp_value = amp_value - 2**64
# Validate values are in reasonable range
# Amperage: -10000 to 10000 mA (typical range for MacBook)
# Voltage: 10000 to 20000 mV (typical range for MacBook battery)
if -10000 <= amp_value <= 10000 and 10000 <= volt_value <= 20000:
# (mA * mV) / 1,000,000 = Watts
live_net_watts = (amp_value * volt_value) / 1000000.0
else:
# Values out of range, set to 0
live_net_watts = 0.0
except (ValueError, OverflowError):
# If conversion fails, set to 0
live_net_watts = 0.0
# 2a. Get Battery Status Information
is_charging_match = re.search(r"\"IsCharging\" = (Yes|No)", battery_raw)
fully_charged_match = re.search(r"\"FullyCharged\" = (Yes|No)", battery_raw)
external_connected_match = re.search(r"\"ExternalConnected\" = (Yes|No)", battery_raw)
max_capacity_match = re.search(r"\"MaxCapacity\" = (\d+)", battery_raw)
current_capacity_match = re.search(r"\"CurrentCapacity\" = (\d+)", battery_raw)
# Extract PowerTelemetryData (more accurate system power)
system_power_in_match = re.search(r"\"SystemPowerIn\"\s*=\s*(\d+)", battery_raw)
system_power_in = 0.0
if system_power_in_match:
try:
system_power_in = int(system_power_in_match.group(1))
# SystemPowerIn is in watts, but verify (if > 1000, might be mW)
if system_power_in > 1000:
system_power_in = system_power_in / 1000.0
except (ValueError, OverflowError):
pass
# Extract AdapterDetails (more accurate adapter power)
adapter_watts_match = re.search(r"\"AdapterDetails\".*?\"Watts\"\s*=\s*(\d+)", battery_raw)
adapter_watts = 0
if adapter_watts_match:
try:
adapter_watts = int(adapter_watts_match.group(1))
except ValueError:
pass
# Fallback to system_profiler value if AdapterDetails not found
if adapter_watts == 0:
adapter_watts = int(in_watts) if in_watts.isdigit() else 0
# Determine if battery is fully charged
battery_full = False
if fully_charged_match and fully_charged_match.group(1) == "Yes":
battery_full = True
elif max_capacity_match and current_capacity_match:
try:
max_cap = int(max_capacity_match.group(1))
curr_cap = int(current_capacity_match.group(1))
if curr_cap >= max_cap * 0.99: # 99%以上认为充满
battery_full = True
except (ValueError, ZeroDivisionError):
pass
# Calculate battery percentage
battery_percent = 0
if max_capacity_match and current_capacity_match:
try:
max_cap = int(max_capacity_match.group(1))
curr_cap = int(current_capacity_match.group(1))
if max_cap > 0:
battery_percent = int((curr_cap / max_cap) * 100)
except (ValueError, ZeroDivisionError):
pass
# 3. Get Hardware Breakdown (CPU/GPU/System)
# Apple Silicon uses different samplers than Intel
apple_silicon = is_apple_silicon()
sampler = "apple_gpu,cpu_power" if apple_silicon else "cpu_power,gpu_power"
# Add ANE sampler for Apple Silicon if available
if apple_silicon:
sampler += ",ane"
pm_raw = get_output(f"sudo powermetrics -n 1 -i 1 --samplers {sampler}")
# Regex for both Intel and Apple Silicon patterns
cpu_match = re.search(r"(?:CPU|Combined) Power: (\d+) mW", pm_raw)
gpu_match = re.search(r"(?:GPU|GPU) Power: (\d+) mW", pm_raw)
ane_match = re.search(r"ANE Power: (\d+) mW", pm_raw)
# Extract Combined Power (CPU + GPU + ANE) - more accurate hardware total
combined_power_match = re.search(r"Combined Power \(CPU \+ GPU \+ ANE\):\s+(\d+)\s+mW", pm_raw)
hardware_combined_power = 0.0
if combined_power_match:
try:
hardware_combined_power = int(combined_power_match.group(1)) / 1000.0
except ValueError:
pass
cpu_w = int(cpu_match.group(1))/1000.0 if cpu_match else 0.0
gpu_w = int(gpu_match.group(1))/1000.0 if gpu_match else 0.0
ane_w = int(ane_match.group(1))/1000.0 if ane_match else 0.0
# Use Combined Power if available, otherwise sum components
if hardware_combined_power > 0:
# Adjust individual components proportionally if Combined Power is available
component_sum = cpu_w + gpu_w + ane_w
if component_sum > 0:
scale_factor = hardware_combined_power / component_sum
cpu_w = cpu_w * scale_factor
gpu_w = gpu_w * scale_factor
ane_w = ane_w * scale_factor
# 4. Get Memory Power Estimate (ESTIMATED)
# Formula: Memory Power = Base Power (0.5W) + Usage-based Power (0-1.5W)
# Calculation: Based on active and wired pages from vm_stat
# Note: This is an estimation. Actual memory power varies by:
# - RAM capacity and type (DDR4/DDR5)
# - Memory frequency
# - Activity level
vm_stat_raw = get_output("vm_stat")
pages_free = re.search(r"Pages free:\s+(\d+)", vm_stat_raw)
pages_active = re.search(r"Pages active:\s+(\d+)", vm_stat_raw)
pages_inactive = re.search(r"Pages inactive:\s+(\d+)", vm_stat_raw)
pages_wired = re.search(r"Pages wired down:\s+(\d+)", vm_stat_raw)
memory_w = 0.5 # Base power (idle)
memory_calc_note = "Base: 0.5W"
if pages_active and pages_wired:
try:
active_pages = int(pages_active.group(1).replace('.', ''))
wired_pages = int(pages_wired.group(1).replace('.', ''))
total_used = active_pages + wired_pages
# Estimation: 0.5W base + usage-based (max 1.5W additional)
# Assuming ~8GB RAM, each page is 4KB
# Normalization: 2M pages ≈ 8GB RAM
memory_usage_ratio = min(1.0, total_used / 2000000.0)
usage_power = memory_usage_ratio * 1.5
memory_w = 0.5 + usage_power
memory_calc_note = f"Base: 0.5W + Usage: {usage_power:.2f}W (ratio: {memory_usage_ratio:.2f})"
except:
pass
# 5. Get Storage/SSD Power Estimate (ESTIMATED)
# Formula: SSD Power = Idle Power (0.5W) or Active Power (1.5W)
# Calculation: Based on disk activity from iostat
# Note: This is a rough estimation. Actual SSD power varies by:
# - SSD type (SATA/NVMe)
# - Read/write operations
# - Drive capacity and technology
disk_w = 0.5 # Base idle power
disk_calc_note = "Idle: 0.5W"
try:
iostat_raw = get_output("iostat -d 1 1 2>/dev/null")
# Check for disk activity indicators
if iostat_raw and ('%util' in iostat_raw or 'KB/t' in iostat_raw):
# If there's any activity, estimate moderate activity power
disk_w = 1.5
disk_calc_note = "Active: 1.5W (estimated)"
except:
pass
# 6. Get Network Power Estimate (ESTIMATED)
# Formula: Network Power = WiFi Power (1.5W if active) + Ethernet Power (0.5W if active)
# Calculation: Based on network interface status
# Note: This is an estimation. Actual network power varies by:
# - WiFi/Ethernet chip type
# - Data transfer rate
# - Signal strength (WiFi)
network_w = 0.0
network_calc_note = "Inactive: 0W"
# Check WiFi status
wifi_raw = get_output("networksetup -getairportpower en0 2>/dev/null || networksetup -getairportpower en1 2>/dev/null")
wifi_active = False
if wifi_raw and "On" in wifi_raw:
network_w += 1.5 # WiFi active power
wifi_active = True
# Check Ethernet status
ethernet_active = False
ethernet_raw = get_output("ifconfig | grep -E '^en[0-9]' | grep -v 'inet' | head -1")
if ethernet_raw:
# Check if ethernet interface is up
ifconfig_raw = get_output("ifconfig | grep -A 1 '^en[0-9]' | grep 'status: active'")
if ifconfig_raw:
network_w += 0.5 # Ethernet active power
ethernet_active = True
if wifi_active and ethernet_active:
network_calc_note = "WiFi: 1.5W + Ethernet: 0.5W"
elif wifi_active:
network_calc_note = "WiFi: 1.5W"
elif ethernet_active:
network_calc_note = "Ethernet: 0.5W"
# 7. Calculate Screen/Display Power (ESTIMATED - residual calculation)
# Formula: Screen Power = Total System Power - (CPU + GPU + ANE + Memory + Disk + Network)
# Note: This is calculated as the residual power after accounting for other components.
# Actual screen power varies by:
# - Display brightness
# - Screen size and resolution
# - Display technology (LCD/OLED)
total_accounted = cpu_w + gpu_w + ane_w + memory_w + disk_w + network_w
# Calculate total system power using more reasonable method
hub_val_int = int(in_watts) if in_watts.isdigit() else 0
if hub_val_int > 0:
# Has external power source
if live_net_watts > 0:
# Charging: total power = input power - charging power
total_system_watts = hub_val_int - live_net_watts
else:
# Discharging: total power = input power + discharge power
total_system_watts = hub_val_int + abs(live_net_watts)
else:
# No external power: use battery discharge power or fallback
if live_net_watts != 0:
total_system_watts = abs(live_net_watts)
else:
# Fallback: estimate based on hardware components
total_system_watts = cpu_w + gpu_w + ane_w + memory_w + disk_w + network_w + 10.0
# Sanity check: ensure total system power is in reasonable range (5W to 200W)
total_system_watts = max(5.0, min(200.0, total_system_watts))
screen_w = max(0.0, total_system_watts - total_accounted)
# 8. Get Top 5 Apps with calculated Estimated Wattage
app_data = []
top_raw = get_output("top -l 1 -n 5 -o power -stats command,power")
lines = top_raw.strip().split('\n')
for line in lines:
if line and not any(x in line for x in ['COMMAND', 'Processes', 'Load']):
parts = line.split()
if len(parts) >= 2:
name = parts[0][:15]
score_str = parts[-1].replace(',', '')
try:
score = float(score_str)
# Improved conversion: consider system load and actual hardware power
# Base conversion: 100 units ≈ 1W, but adjust based on actual CPU+GPU power
hardware_total = cpu_w + gpu_w
# If hardware is active, scale conversion factor
if hardware_total > 0:
# More accurate: scale based on actual hardware utilization
conversion_factor = max(80.0, min(120.0, 100.0 * (hardware_total / max(1.0, abs(live_net_watts)))))
else:
conversion_factor = 100.0
est_w = score / conversion_factor
app_data.append((name, score, est_w))
except ValueError:
continue
# 9. Proportional allocation of hardware power to apps
total_energy_impact = sum(score for _, score, _ in app_data)
hardware_total = cpu_w + gpu_w
# Update app_data with allocated power
updated_app_data = []
for name, score, est_w in app_data:
if total_energy_impact > 0 and hardware_total > 0:
# Allocate hardware power proportionally
proportion = score / total_energy_impact
allocated_w = hardware_total * proportion
updated_app_data.append((name, score, est_w, allocated_w))
else:
# Fallback to original conversion
updated_app_data.append((name, score, est_w, est_w))
app_data = updated_app_data
return {
"in_watts": in_watts,
"live_net_watts": live_net_watts,
"cpu_w": cpu_w,
"gpu_w": gpu_w,
"ane_w": ane_w,
"memory_w": memory_w,
"disk_w": disk_w,
"network_w": network_w,
"screen_w": screen_w,
"apps": app_data,
"is_as": apple_silicon,
"battery_full": battery_full,
"battery_percent": battery_percent,
"external_connected": external_connected_match.group(1) == "Yes" if external_connected_match else False,
"is_charging": is_charging_match.group(1) == "Yes" if is_charging_match else False,
"system_power_in": system_power_in,
"adapter_watts": adapter_watts,
"hardware_combined_power": hardware_combined_power,
"memory_calc_note": memory_calc_note,
"disk_calc_note": disk_calc_note,
"network_calc_note": network_calc_note,
"total_accounted": total_accounted
}
def main():
print("Starting Universal Mac Power Dashboard...")
print("(Sudo password required for hardware telemetry)")
try:
while True:
data = parse_power_data()
os.system('clear')
arch_label = "APPLE SILICON" if data['is_as'] else "INTEL i9"
# Header
print("┌────────────────────────────────────────────────┐")
print(f"│ {arch_label} POWER DASHBOARD │")
print("└────────────────────────────────────────────────┘")
# Section 1: Supply
# Use adapter_watts if available (more accurate), otherwise fallback to in_watts
adapter_watts = data.get('adapter_watts', 0)
hub_val = adapter_watts if adapter_watts > 0 else int(data['in_watts'])
system_power_in = data.get('system_power_in', 0)
print(f"\n[ POWER SUPPLY ]")
print(f" Input Source: {hub_val}W")
if system_power_in > 0:
print(f" System Power: {system_power_in:.1f}W")
# Display battery percentage if available
battery_percent = data.get('battery_percent', 0)
if battery_percent > 0:
if battery_percent >= 99:
battery_display = "FULL (100%)"
else:
battery_display = f"{battery_percent}%"
print(f" Battery Level: {battery_display}")
# Improved status logic with cross-validation
external_connected = data.get('external_connected', False)
is_charging = data.get('is_charging', False)
battery_full = data.get('battery_full', False)
live_net_watts = data.get('live_net_watts', 0)
# Determine if actually using battery
actually_using_battery = False
if external_connected:
# Has external power
if system_power_in > 0 and hub_val > 0:
# Compare system power with adapter power
# If system power significantly exceeds adapter power, likely using battery
if system_power_in > hub_val * 1.3: # 30% tolerance
if abs(live_net_watts) > 1.0: # More than 1W from battery
actually_using_battery = True
elif abs(live_net_watts) > 1.0: # Fallback: use Amperage if > 1W
actually_using_battery = True
else:
# No external power, definitely using battery
actually_using_battery = True
# Determine status display
if external_connected:
if battery_full:
# Battery full and has external power
if abs(live_net_watts) < 1.0:
status = "🔋 FULLY CHARGED (Charging Paused)"
else:
battery_draw = abs(live_net_watts)
status = f"⚠️ FULLY CHARGED, INSUFFICIENT POWER ({hub_val}W input, {battery_draw:.1f}W from battery)"
elif is_charging:
# Charging
status = "🔋 CHARGING"
elif not is_charging and battery_percent > 80:
# Not charging but battery > 80% - likely paused charging (optimized charging)
if abs(live_net_watts) < 1.0:
status = "⏸️ CHARGING PAUSED (Optimized)"
elif actually_using_battery:
battery_draw = abs(live_net_watts)
status = f"⚠️ INSUFFICIENT POWER ({hub_val}W input, {battery_draw:.1f}W from battery)"
else:
status = "⚖️ BALANCED"
elif actually_using_battery:
# Using battery due to insufficient power
battery_draw = abs(live_net_watts)
status = f"⚠️ INSUFFICIENT POWER ({hub_val}W input, {battery_draw:.1f}W from battery)"
else:
# Balanced or small measurement error
status = "⚖️ BALANCED"
else:
# No external power
if battery_full:
status = "🔋 FULLY CHARGED (On Battery)"
elif live_net_watts < 0:
status = "🔻 DISCHARGING"
else:
status = "⚖️ BALANCED"
print(f" Supply Status: {status}")
# Section 2: Real-time Balance
print(f"\n[ TOTAL SYSTEM CONSUMPTION ]")
# Use SystemPowerIn if available (more accurate), otherwise calculate
if system_power_in > 0:
total_usage = system_power_in
elif hub_val > 0:
# Has external power: system consumption = input power - charging power
if data['live_net_watts'] > 0:
total_usage = hub_val - data['live_net_watts']
else:
total_usage = hub_val + abs(data['live_net_watts'])
# Sanity check
if total_usage < 0 or total_usage > 200:
total_usage = hub_val * 0.8 # Fallback: assume 80% efficiency
total_usage = max(0, total_usage)
else:
# No external power: use battery discharge
total_usage = abs(data['live_net_watts']) if data['live_net_watts'] < 0 else 0.0
if total_usage > 200:
total_usage = 0.0
print(f" Total Usage: {total_usage:.2f}W")
# Energy state display
if external_connected:
if is_charging:
print(f" Energy State: 🔋 CHARGING (+{data['live_net_watts']:.1f}W)")
elif actually_using_battery:
battery_draw = abs(data['live_net_watts'])
print(f" Energy State: ⚠️ USING BATTERY ({battery_draw:.1f}W from battery)")
else:
print(f" Energy State: ⚖️ BALANCED (Adapter Power Sufficient)")
else:
if data['live_net_watts'] < 0:
print(f" Energy State: 🔻 DISCHARGING ({abs(data['live_net_watts']):.1f}W)")
else:
print(f" Energy State: ⚖️ BALANCED")
# Section 3: Hardware Breakdown
print(f"\n[ HARDWARE TELEMETRY ]")
print(f" Processor (CPU): {data['cpu_w']:.2f}W (measured)")
print(f" Graphics (GPU): {data['gpu_w']:.2f}W (measured)")
if data['is_as'] and data['ane_w'] > 0:
print(f" Neural Engine (ANE): {data['ane_w']:.2f}W (measured)")
# Format calculation notes more concisely
memory_note = data.get('memory_calc_note', '')
if 'Usage:' in memory_note:
# Extract usage value
usage_match = re.search(r'Usage: ([\d.]+)W', memory_note)
if usage_match:
memory_note = f"est: 0.5W+{usage_match.group(1)}W"
else:
memory_note = "est"
else:
memory_note = "est"
print(f" Memory (RAM): {data['memory_w']:.2f}W ({memory_note})")
# Format disk note
disk_note = "est: active" if 'Active' in data.get('disk_calc_note', '') else "est: idle"
print(f" Storage (SSD): {data['disk_w']:.2f}W ({disk_note})")
# Format network note
network_calc = data.get('network_calc_note', '')
if 'WiFi' in network_calc and 'Ethernet' in network_calc:
network_note = "est: WiFi+Eth"
elif 'WiFi' in network_calc:
network_note = "est: WiFi"
elif 'Ethernet' in network_calc:
network_note = "est: Eth"
else:
network_note = "est"
print(f" Network: {data['network_w']:.2f}W ({network_note})")
print(f" Display (Screen): {data['screen_w']:.2f}W (est: residual)")
# Show component sum vs system total
total_accounted = data.get('total_accounted', 0) + data['screen_w']
system_total = data.get('system_power_in', 0) if data.get('system_power_in', 0) > 0 else total_usage
if system_total > 0:
difference = system_total - total_accounted
difference_pct = (difference / system_total * 100) if system_total > 0 else 0
print(f"\n Component Sum: {total_accounted:.2f}W")
print(f" System Total: {system_total:.2f}W")
if abs(difference) > 0.5: # Only show if significant difference
print(f" Difference: {difference:+.2f}W ({difference_pct:+.1f}%)")
print(f" Note: Difference may include unmeasured components, measurement")
print(f" errors, or system overhead (power conversion, cooling, etc.)")
# Section 4: App Breakdown
print(f"\n[ TOP APPS BY WATTAGE (EST.) ]")
print(f" {'APP NAME':<18} {'ENERGY IMPACT':<15} {'EST. WATTS':<12} {'ALLOCATED W'}")
print(f" ─────────────────────────────────────────────────────────────")
for app_item in data['apps']:
if len(app_item) == 4:
name, score, est_w, allocated_w = app_item
print(f" {name:<18} {score:<15.1f} {est_w:<12.2f} {allocated_w:.2f}W")
else:
# Fallback for old format
name, score, est_w = app_item
print(f" {name:<18} {score:<15.1f} {est_w:<12.2f} {est_w:.2f}W")
print("\n" + "─"*50)
print(" Press Ctrl+C to Exit")
time.sleep(3)
except KeyboardInterrupt:
print("\nExiting...")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment