1use serde::Serialize;
2
3#[derive(Debug, Clone, Serialize)]
4#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
5pub struct Power {
6 pub source: Source,
7 #[cfg_attr(feature = "schema", schemars(range(min = 0, max = 100)))]
10 pub battery_percent: Option<u8>,
11 pub low_power_mode: bool,
15 pub energy_mode: EnergyMode,
16}
17
18#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
19#[serde(rename_all = "lowercase")]
20#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21#[cfg_attr(feature = "schema", schemars(rename_all = "lowercase"))]
22#[cfg_attr(
23 feature = "schema",
24 schemars(description = "Whether the system is currently drawing from AC or battery (IOPSGetProvidingPowerSourceType).")
25)]
26pub enum Source {
27 Ac,
28 Battery,
29}
30
31#[derive(Debug, Clone, Copy, Serialize)]
32#[serde(rename_all = "lowercase")]
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34#[cfg_attr(feature = "schema", schemars(rename_all = "lowercase"))]
35#[cfg_attr(
36 feature = "schema",
37 schemars(description = "Configured energy preference for the current power source, read from IOPM active preferences (the same data source pmset(8) uses). `high` is only available on Apple Silicon Pro/Max. `unknown` is returned when the preference is unreadable or carries an unrecognized value.")
38)]
39pub enum EnergyMode {
40 Automatic,
41 Low,
42 High,
43 Unknown,
45}
46
47#[cfg(target_os = "macos")]
48impl Power {
49 pub fn collect() -> Self {
50 let (source, battery_percent) = read_power_source();
51 let low_power_mode = macstate_sys::objc::is_low_power_mode_enabled();
52 let energy_mode = read_energy_mode(source);
53 Self {
54 source,
55 battery_percent,
56 low_power_mode,
57 energy_mode,
58 }
59 }
60}
61
62#[cfg(not(target_os = "macos"))]
63impl Power {
64 pub fn collect() -> Self {
65 Self {
66 source: Source::Ac,
67 battery_percent: None,
68 low_power_mode: false,
69 energy_mode: EnergyMode::Unknown,
70 }
71 }
72}
73
74#[cfg(target_os = "macos")]
75fn read_power_source() -> (Source, Option<u8>) {
76 use macstate_sys::cf::{
77 cfstring_to_string, dict_get_i32, CFArrayGetCount, CFArrayGetValueAtIndex, CFOwned,
78 };
79 use macstate_sys::iokit::{
80 kIOPSACPowerValue, kIOPSCurrentCapacityKey, kIOPSMaxCapacityKey,
81 IOPSCopyPowerSourcesInfo, IOPSCopyPowerSourcesList, IOPSGetPowerSourceDescription,
82 IOPSGetProvidingPowerSourceType,
83 };
84
85 unsafe {
86 let snapshot = match CFOwned::from_create(IOPSCopyPowerSourcesInfo()) {
87 Some(s) => s,
88 None => return (Source::Ac, None),
89 };
90
91 let provider = IOPSGetProvidingPowerSourceType(snapshot.as_ptr());
92 let source = match cfstring_to_string(provider) {
93 Some(s) if s == kIOPSACPowerValue => Source::Ac,
94 Some(_) => Source::Battery,
95 None => Source::Ac,
96 };
97
98 let list = match CFOwned::from_create(IOPSCopyPowerSourcesList(snapshot.as_ptr())) {
99 Some(l) => l,
100 None => return (source, None),
101 };
102
103 let count = CFArrayGetCount(list.as_ptr());
104 let mut percent: Option<u8> = None;
105 for i in 0..count {
106 let ps = CFArrayGetValueAtIndex(list.as_ptr(), i);
107 if ps.is_null() {
108 continue;
109 }
110 let desc = IOPSGetPowerSourceDescription(snapshot.as_ptr(), ps);
111 if desc.is_null() {
112 continue;
113 }
114 let cur = dict_get_i32(desc, kIOPSCurrentCapacityKey);
115 let max = dict_get_i32(desc, kIOPSMaxCapacityKey);
116 if let (Some(cur), Some(max)) = (cur, max) {
117 if max > 0 {
118 let pct = ((cur as f64 / max as f64) * 100.0).round();
119 percent = Some(pct.clamp(0.0, 100.0) as u8);
120 break;
121 }
122 }
123 }
124
125 (source, percent)
126 }
127}
128
129#[cfg(target_os = "macos")]
130fn read_energy_mode(source: Source) -> EnergyMode {
131 use macstate_sys::cf::{dict_get_dict, dict_get_i32, CFOwned};
132 use macstate_sys::iokit::{
133 kIOPMLowPowerModeKey, kIOPSACPowerValue, kIOPSBatteryPowerValue,
134 IOPMCopyActivePMPreferences,
135 };
136
137 unsafe {
138 let prefs = match CFOwned::from_create(IOPMCopyActivePMPreferences()) {
139 Some(p) => p,
140 None => return EnergyMode::Unknown,
141 };
142 let key = match source {
143 Source::Ac => kIOPSACPowerValue,
144 Source::Battery => kIOPSBatteryPowerValue,
145 };
146 let sub = dict_get_dict(prefs.as_ptr(), key);
147 if sub.is_null() {
148 return EnergyMode::Unknown;
149 }
150 match dict_get_i32(sub, kIOPMLowPowerModeKey) {
154 Some(0) => EnergyMode::Automatic,
155 Some(1) => EnergyMode::Low,
156 Some(2) => EnergyMode::High,
157 _ => EnergyMode::Unknown,
158 }
159 }
160}