Skip to main content

crabshell_services/
power.rs

1//! Batteries and power supplies from UPower, and the power profile from
2//! power-profiles-daemon, both on the system bus.
3
4#[cfg(test)]
5mod tests;
6
7use std::cell::RefCell;
8use std::rc::Rc;
9use std::sync::Once;
10use std::time::Duration;
11
12use crabshell_reactive::{ReadSignal, Signal};
13use crabshell_ui::Cx;
14use zbus::{Array, Connection, Message, OwnedObjectPath, OwnedVariant};
15
16use crate::Shared;
17use crate::dbus::{self, CALL_TIMEOUT, Daemon, Props, set_changed};
18
19const UPOWER: &str = "org.freedesktop.UPower";
20const UPOWER_PATH: &str = "/org/freedesktop/UPower";
21const DEVICE: &str = "org.freedesktop.UPower.Device";
22/// The daemon's first name, which every version since still owns.
23const PROFILES: &str = "net.hadess.PowerProfiles";
24const PROFILES_PATH: &str = "/net/hadess/PowerProfiles";
25
26/// The most devices kept: UPower lists one per battery, charger and
27/// wireless gadget.
28const MAX_DEVICES: usize = 64;
29
30/// What a power device is, from UPower's `Type`.
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
32pub enum DeviceKind {
33  /// Not known, or a kind this list leaves out.
34  #[default]
35  Unknown,
36  /// A charger or other mains supply.
37  LinePower,
38  /// A laptop's battery.
39  Battery,
40  /// An uninterruptible power supply.
41  Ups,
42  /// A monitor.
43  Monitor,
44  /// A wireless mouse.
45  Mouse,
46  /// A wireless keyboard.
47  Keyboard,
48  /// A phone.
49  Phone,
50  /// A tablet.
51  Tablet,
52  /// A game controller.
53  GamingInput,
54  /// A headset.
55  Headset,
56  /// Headphones.
57  Headphones,
58  /// Speakers.
59  Speakers,
60}
61
62/// How a battery is doing, from UPower's `State`.
63#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
64pub enum BatteryState {
65  /// Not known, or not a battery.
66  #[default]
67  Unknown,
68  /// Charging.
69  Charging,
70  /// Running the machine.
71  Discharging,
72  /// Empty.
73  Empty,
74  /// Full, and on the charger.
75  FullyCharged,
76  /// Waiting to charge.
77  PendingCharge,
78  /// Waiting to run the machine.
79  PendingDischarge,
80}
81
82/// A battery or power supply UPower knows.
83#[derive(Clone, Debug, Default, PartialEq)]
84pub struct PowerDevice {
85  /// UPower's object path for it, which names it while it exists.
86  pub path: String,
87  /// What it is.
88  pub kind: DeviceKind,
89  /// Its model name, or `""`.
90  pub model: String,
91  /// The icon UPower suggests, such as `battery-good-charging-symbolic`.
92  pub icon_name: String,
93  /// Whether a battery is in its bay.
94  pub present: bool,
95  /// Whether it powers this machine, rather than a gadget.
96  pub power_supply: bool,
97  /// How it is doing.
98  pub state: BatteryState,
99  /// How full it is, from 0 to 100.
100  pub percent: f64,
101  /// How long until it runs out, if UPower knows.
102  pub time_to_empty: Option<Duration>,
103  /// How long until it is full, if UPower knows.
104  pub time_to_full: Option<Duration>,
105  /// The power going in or out, in watts.
106  pub energy_rate: f64,
107}
108
109impl PowerDevice {
110  fn from_props(path: String, props: &Props) -> PowerDevice {
111    let seconds = |name| {
112      dbus::get::<i64>(props, name)
113        .filter(|seconds| *seconds > 0)
114        .map(|seconds| Duration::from_secs(seconds as u64))
115    };
116    PowerDevice {
117      path,
118      kind: match dbus::get::<u32>(props, "Type") {
119        Some(1) => DeviceKind::LinePower,
120        Some(2) => DeviceKind::Battery,
121        Some(3) => DeviceKind::Ups,
122        Some(4) => DeviceKind::Monitor,
123        Some(5) => DeviceKind::Mouse,
124        Some(6) => DeviceKind::Keyboard,
125        Some(8) => DeviceKind::Phone,
126        Some(10) => DeviceKind::Tablet,
127        Some(12) => DeviceKind::GamingInput,
128        Some(17) => DeviceKind::Headset,
129        Some(18) => DeviceKind::Speakers,
130        Some(19) => DeviceKind::Headphones,
131        _ => DeviceKind::Unknown,
132      },
133      model: dbus::text(props, "Model"),
134      icon_name: dbus::text(props, "IconName"),
135      present: dbus::get(props, "IsPresent").unwrap_or(false),
136      power_supply: dbus::get(props, "PowerSupply").unwrap_or(false),
137      state: match dbus::get::<u32>(props, "State") {
138        Some(1) => BatteryState::Charging,
139        Some(2) => BatteryState::Discharging,
140        Some(3) => BatteryState::Empty,
141        Some(4) => BatteryState::FullyCharged,
142        Some(5) => BatteryState::PendingCharge,
143        Some(6) => BatteryState::PendingDischarge,
144        _ => BatteryState::Unknown,
145      },
146      percent: dbus::get::<f64>(props, "Percentage")
147        .filter(|percent| percent.is_finite())
148        .map_or(0.0, |percent| percent.clamp(0.0, 100.0)),
149      time_to_empty: seconds("TimeToEmpty"),
150      time_to_full: seconds("TimeToFull"),
151      energy_rate: dbus::get::<f64>(props, "EnergyRate")
152        .filter(|rate| rate.is_finite())
153        .unwrap_or(0.0),
154    }
155  }
156}
157
158/// UPower's batteries and power supplies, from `cx.upower()`. Cloning it
159/// is cheap.
160#[derive(Clone)]
161pub struct UPower {
162  devices: ReadSignal<Vec<PowerDevice>>,
163  display: ReadSignal<Option<PowerDevice>>,
164  on_battery: ReadSignal<bool>,
165}
166
167impl UPower {
168  /// Every device, in the order UPower lists them. Empty while UPower
169  /// isn't running.
170  pub fn devices(&self) -> ReadSignal<Vec<PowerDevice>> {
171    self.devices
172  }
173
174  /// The one device that sums up the machine's batteries, as a battery
175  /// icon in a bar shows. On a machine with none, it isn't `present`.
176  /// `None` while UPower isn't running.
177  pub fn display_device(&self) -> ReadSignal<Option<PowerDevice>> {
178    self.display
179  }
180
181  /// Whether the machine runs on battery.
182  pub fn on_battery(&self) -> ReadSignal<bool> {
183    self.on_battery
184  }
185}
186
187/// `cx.upower()`.
188pub(crate) fn upower(cx: &Cx) -> UPower {
189  let Some(service) = cx.service::<UPowerService>() else {
190    static LOGGED: Once = Once::new();
191    LOGGED.call_once(|| {
192      tracing::warn!("cx.upower() needs the services the shell provides; it stays empty");
193    });
194    return UPower {
195      devices: cx.signal(Vec::new()).read_only(),
196      display: cx.signal(None).read_only(),
197      on_battery: cx.signal(false).read_only(),
198    };
199  };
200  service.start()
201}
202
203/// UPower for one shell, followed from the first ask.
204pub(crate) struct UPowerService {
205  shared: Rc<Shared>,
206  state: RefCell<Option<UPowerState>>,
207}
208
209struct UPowerState {
210  devices: Signal<Vec<PowerDevice>>,
211  display: Signal<Option<PowerDevice>>,
212  on_battery: Signal<bool>,
213  /// Each device's properties, in UPower's order.
214  objects: Vec<(String, Props)>,
215  /// The display device's path and properties.
216  display_device: Option<(String, Props)>,
217  generation: u64,
218}
219
220impl UPowerService {
221  pub(crate) fn new(shared: Rc<Shared>) -> UPowerService {
222    UPowerService {
223      shared,
224      state: RefCell::new(None),
225    }
226  }
227
228  fn start(self: &Rc<Self>) -> UPower {
229    if let Some(state) = &*self.state.borrow() {
230      return UPower {
231        devices: state.devices.read_only(),
232        display: state.display.read_only(),
233        on_battery: state.on_battery.read_only(),
234      };
235    }
236    let root = &self.shared.root;
237    let state = UPowerState {
238      devices: root.signal(Vec::new()),
239      display: root.signal(None),
240      on_battery: root.signal(false),
241      objects: Vec::new(),
242      display_device: None,
243      generation: 0,
244    };
245    let upower = UPower {
246      devices: state.devices.read_only(),
247      display: state.display.read_only(),
248      on_battery: state.on_battery.read_only(),
249    };
250    *self.state.borrow_mut() = Some(state);
251    match self.shared.config.system_bus.clone() {
252      Some(address) => {
253        let follow = dbus::follow(Rc::clone(&self.shared), Rc::clone(self), address);
254        self.shared.spawn(follow);
255      }
256      None => tracing::info!("no system bus, so UPower's devices stay empty"),
257    }
258    upower
259  }
260
261  /// Shows the devices as they are now.
262  fn publish(&self) {
263    // Set apart from the borrow, since effects may ask for more.
264    let shown = self.state.borrow().as_ref().map(|state| {
265      let devices = state
266        .objects
267        .iter()
268        .map(|(path, props)| PowerDevice::from_props(path.clone(), props))
269        .collect::<Vec<_>>();
270      let display = state
271        .display_device
272        .as_ref()
273        .map(|(path, props)| PowerDevice::from_props(path.clone(), props));
274      (state.devices, devices, state.display, display)
275    });
276    if let Some((devices_signal, devices, display_signal, display)) = shown {
277      set_changed(devices_signal, devices);
278      set_changed(display_signal, display);
279    }
280  }
281}
282
283impl Daemon for UPowerService {
284  const NAME: &'static str = UPOWER;
285
286  fn alive(&self) -> bool {
287    let state = self.state.borrow();
288    state
289      .as_ref()
290      .is_some_and(|state| state.devices.try_with(|_| ()).is_some())
291  }
292
293  fn clear(&self) -> u64 {
294    let (devices, display, on_battery, generation) = {
295      let mut state = self.state.borrow_mut();
296      let Some(state) = state.as_mut() else {
297        return 0;
298      };
299      state.objects.clear();
300      state.display_device = None;
301      state.generation += 1;
302      (
303        state.devices,
304        state.display,
305        state.on_battery,
306        state.generation,
307      )
308    };
309    set_changed(devices, Vec::new());
310    set_changed(display, None);
311    set_changed(on_battery, false);
312    generation
313  }
314
315  async fn read(self: Rc<Self>, connection: Connection, generation: u64) {
316    let shared = &self.shared;
317    let read = async {
318      let root = dbus::get_all(shared, &connection, UPOWER, UPOWER_PATH, UPOWER).await?;
319      let call = connection.call_method(
320        Some(UPOWER),
321        UPOWER_PATH,
322        Some(UPOWER),
323        "EnumerateDevices",
324        &(),
325      );
326      let paths: Vec<OwnedObjectPath> = shared
327        .timeout(CALL_TIMEOUT, call)
328        .await?
329        .body()
330        .deserialize()?;
331      let call = connection.call_method(
332        Some(UPOWER),
333        UPOWER_PATH,
334        Some(UPOWER),
335        "GetDisplayDevice",
336        &(),
337      );
338      let display: OwnedObjectPath = shared
339        .timeout(CALL_TIMEOUT, call)
340        .await?
341        .body()
342        .deserialize()?;
343      let mut objects = Vec::new();
344      for path in paths.into_iter().take(MAX_DEVICES) {
345        let path = path.as_str().to_owned();
346        match dbus::get_all(shared, &connection, UPOWER, &path, DEVICE).await {
347          Ok(props) => objects.push((path, props)),
348          // One device gone between the list and its read.
349          Err(error) => tracing::info!(%error, path, "couldn't read a power device"),
350        }
351      }
352      let display_path = display.as_str().to_owned();
353      let display = dbus::get_all(shared, &connection, UPOWER, &display_path, DEVICE)
354        .await
355        .ok()
356        .map(|props| (display_path, props));
357      zbus::Result::Ok((root, objects, display))
358    };
359    let (root, objects, display) = match read.await {
360      Ok(read) => read,
361      Err(error) => return tracing::warn!(%error, "couldn't read UPower"),
362    };
363    let on_battery = {
364      let mut state = self.state.borrow_mut();
365      let Some(state) = state
366        .as_mut()
367        .filter(|state| state.generation == generation)
368      else {
369        return;
370      };
371      state.objects = objects;
372      state.display_device = display;
373      state.on_battery
374    };
375    set_changed(on_battery, dbus::get(&root, "OnBattery").unwrap_or(false));
376    self.publish();
377  }
378
379  /// A device added or removed: UPower names it without its properties.
380  fn reads_again(&self, message: &Message) -> bool {
381    let header = message.header();
382    header.interface().map(|interface| interface.as_str()) == Some(UPOWER)
383      && matches!(
384        header.member().map(|member| member.as_str()),
385        Some("DeviceAdded" | "DeviceRemoved")
386      )
387  }
388
389  fn signal(self: &Rc<Self>, _: &Connection, message: &Message) {
390    let Some((path, interface, changed, dropped)) = dbus::properties_changed(message) else {
391      return;
392    };
393    {
394      if path == UPOWER_PATH && interface == UPOWER {
395        let on_battery = self.state.borrow().as_ref().map(|state| state.on_battery);
396        if let (Some(signal), Some(value)) = (on_battery, changed.get("OnBattery")) {
397          set_changed(signal, bool::try_from(value).unwrap_or(false));
398        }
399        return;
400      }
401      if interface != DEVICE {
402        return;
403      }
404      {
405        let mut state = self.state.borrow_mut();
406        let Some(state) = state.as_mut() else {
407          return;
408        };
409        let display = state.display_device.iter_mut();
410        let object = state
411          .objects
412          .iter_mut()
413          .chain(display)
414          .find(|(at, _)| *at == path);
415        let Some((_, props)) = object else {
416          return;
417        };
418        dbus::apply(props, changed, dropped);
419      }
420    }
421    self.publish();
422  }
423}
424
425/// A power profile power-profiles-daemon can set.
426#[derive(Clone, Copy, Debug, PartialEq, Eq)]
427pub enum PowerProfile {
428  /// Saves battery at the cost of speed.
429  PowerSaver,
430  /// The default.
431  Balanced,
432  /// Fast, at the cost of battery and heat.
433  Performance,
434}
435
436impl PowerProfile {
437  fn parse(name: &str) -> Option<PowerProfile> {
438    match name {
439      "power-saver" => Some(PowerProfile::PowerSaver),
440      "balanced" => Some(PowerProfile::Balanced),
441      "performance" => Some(PowerProfile::Performance),
442      _ => None,
443    }
444  }
445
446  fn name(self) -> &'static str {
447    match self {
448      PowerProfile::PowerSaver => "power-saver",
449      PowerProfile::Balanced => "balanced",
450      PowerProfile::Performance => "performance",
451    }
452  }
453}
454
455/// Why the performance profile runs slower than it could, from the
456/// daemon's `PerformanceDegraded`.
457#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
458pub enum PerformanceDegradation {
459  /// It isn't, as far as the daemon can tell, or the daemon isn't
460  /// running.
461  #[default]
462  None,
463  /// The machine is on someone's lap, so it is kept cooler.
464  LapDetected,
465  /// The machine is close to overheating.
466  HighTemperature,
467  /// For a reason this list doesn't know. The daemon's docs say new ones
468  /// may come, and to show a generic reason for them.
469  Other,
470}
471
472impl PerformanceDegradation {
473  fn parse(reason: &str) -> PerformanceDegradation {
474    match reason {
475      "" => PerformanceDegradation::None,
476      "lap-detected" => PerformanceDegradation::LapDetected,
477      "high-operating-temperature" => PerformanceDegradation::HighTemperature,
478      other => {
479        tracing::debug!(
480          reason = other,
481          "power-profiles-daemon names an unknown reason for degraded performance"
482        );
483        PerformanceDegradation::Other
484      }
485    }
486  }
487}
488
489/// The power profile, from `cx.power_profiles()`. Cloning it is cheap.
490#[derive(Clone)]
491pub struct PowerProfiles {
492  service: Option<Rc<ProfilesService>>,
493  active: ReadSignal<Option<PowerProfile>>,
494  available: ReadSignal<Vec<PowerProfile>>,
495  degradation: ReadSignal<PerformanceDegradation>,
496}
497
498impl PowerProfiles {
499  /// The profile in effect. `None` while power-profiles-daemon isn't
500  /// running, or in a profile this list doesn't know.
501  pub fn active(&self) -> ReadSignal<Option<PowerProfile>> {
502    self.active
503  }
504
505  /// The profiles this machine offers, in the daemon's order. Laptops
506  /// without a performance mode leave it out.
507  pub fn available(&self) -> ReadSignal<Vec<PowerProfile>> {
508    self.available
509  }
510
511  /// Why the performance profile runs slower than it could, which shows
512  /// whichever profile is active.
513  pub fn degradation(&self) -> ReadSignal<PerformanceDegradation> {
514    self.degradation
515  }
516
517  /// Asks the daemon to switch to `profile`. It shows in [`active`]
518  /// once the daemon says it did. A refusal, such as one polkit makes,
519  /// is logged.
520  ///
521  /// [`active`]: Self::active
522  pub fn set(&self, profile: PowerProfile) {
523    let Some(service) = &self.service else {
524      return;
525    };
526    let Some(connection) = service
527      .state
528      .borrow()
529      .as_ref()
530      .and_then(|s| s.connection.clone())
531    else {
532      return tracing::info!("power-profiles-daemon isn't running; no profile set");
533    };
534    let shared = Rc::clone(&service.shared);
535    service.shared.spawn(async move {
536      let value = zbus::Variant::from(profile.name());
537      let at = (PROFILES_PATH, PROFILES);
538      let set = dbus::set(&shared, &connection, PROFILES, at, "ActiveProfile", value);
539      if let Err(error) = set.await {
540        tracing::warn!(%error, profile = profile.name(), "the power profile wasn't set");
541      }
542    });
543  }
544}
545
546/// `cx.power_profiles()`.
547pub(crate) fn power_profiles(cx: &Cx) -> PowerProfiles {
548  let Some(service) = cx.service::<ProfilesService>() else {
549    static LOGGED: Once = Once::new();
550    LOGGED.call_once(|| {
551      tracing::warn!("cx.power_profiles() needs the services the shell provides; it stays empty");
552    });
553    return PowerProfiles {
554      service: None,
555      active: cx.signal(None).read_only(),
556      available: cx.signal(Vec::new()).read_only(),
557      degradation: cx.signal(PerformanceDegradation::None).read_only(),
558    };
559  };
560  service.start()
561}
562
563/// power-profiles-daemon for one shell, followed from the first ask.
564pub(crate) struct ProfilesService {
565  shared: Rc<Shared>,
566  state: RefCell<Option<ProfilesState>>,
567}
568
569struct ProfilesState {
570  active: Signal<Option<PowerProfile>>,
571  available: Signal<Vec<PowerProfile>>,
572  degradation: Signal<PerformanceDegradation>,
573  props: Props,
574  /// The bus, while the daemon runs, for `set`.
575  connection: Option<Connection>,
576  generation: u64,
577}
578
579impl ProfilesService {
580  pub(crate) fn new(shared: Rc<Shared>) -> ProfilesService {
581    ProfilesService {
582      shared,
583      state: RefCell::new(None),
584    }
585  }
586
587  fn start(self: &Rc<Self>) -> PowerProfiles {
588    if let Some(state) = &*self.state.borrow() {
589      return PowerProfiles {
590        service: Some(Rc::clone(self)),
591        active: state.active.read_only(),
592        available: state.available.read_only(),
593        degradation: state.degradation.read_only(),
594      };
595    }
596    let root = &self.shared.root;
597    let (active, available) = (root.signal(None), root.signal(Vec::new()));
598    let degradation = root.signal(PerformanceDegradation::None);
599    *self.state.borrow_mut() = Some(ProfilesState {
600      active,
601      available,
602      degradation,
603      props: Props::new(),
604      connection: None,
605      generation: 0,
606    });
607    match self.shared.config.system_bus.clone() {
608      Some(address) => {
609        let follow = dbus::follow(Rc::clone(&self.shared), Rc::clone(self), address);
610        self.shared.spawn(follow);
611      }
612      None => tracing::info!("no system bus, so there is no power profile"),
613    }
614    PowerProfiles {
615      service: Some(Rc::clone(self)),
616      active: active.read_only(),
617      available: available.read_only(),
618      degradation: degradation.read_only(),
619    }
620  }
621
622  fn publish(&self) {
623    let shown = self.state.borrow().as_ref().map(|state| {
624      let active = dbus::get::<&str>(&state.props, "ActiveProfile").and_then(PowerProfile::parse);
625      let available = state.props.get("Profiles").map(profiles);
626      let degradation = dbus::get::<&str>(&state.props, "PerformanceDegraded")
627        .map_or(PerformanceDegradation::None, PerformanceDegradation::parse);
628      (
629        (state.active, active),
630        (state.available, available.unwrap_or_default()),
631        (state.degradation, degradation),
632      )
633    });
634    if let Some((active, available, degradation)) = shown {
635      set_changed(active.0, active.1);
636      set_changed(available.0, available.1);
637      set_changed(degradation.0, degradation.1);
638    }
639  }
640}
641
642/// The profiles in the daemon's `Profiles`, an `aa{sv}` whose dicts each
643/// name one in `Profile`.
644fn profiles(value: &OwnedVariant) -> Vec<PowerProfile> {
645  let Ok(list) = <&Array>::try_from(value) else {
646    return Vec::new();
647  };
648  list
649    .inner()
650    .iter()
651    .filter_map(|entry| {
652      let zbus::Variant::Dict(dict) = entry else {
653        return None;
654      };
655      let name = dict.iter().find_map(|(key, value)| {
656        if <&str>::try_from(key).ok()? != "Profile" {
657          return None;
658        }
659        let value = match value {
660          zbus::Variant::Variant(inner) => &**inner,
661          other => other,
662        };
663        <&str>::try_from(value).ok()
664      })?;
665      PowerProfile::parse(name)
666    })
667    .collect()
668}
669
670impl Daemon for ProfilesService {
671  const NAME: &'static str = PROFILES;
672
673  fn alive(&self) -> bool {
674    let state = self.state.borrow();
675    state
676      .as_ref()
677      .is_some_and(|state| state.active.try_with(|_| ()).is_some())
678  }
679
680  fn clear(&self) -> u64 {
681    let generation = {
682      let mut state = self.state.borrow_mut();
683      let Some(state) = state.as_mut() else {
684        return 0;
685      };
686      state.props.clear();
687      state.connection = None;
688      state.generation += 1;
689      state.generation
690    };
691    self.publish();
692    generation
693  }
694
695  async fn read(self: Rc<Self>, connection: Connection, generation: u64) {
696    let read = dbus::get_all(&self.shared, &connection, PROFILES, PROFILES_PATH, PROFILES).await;
697    let props = match read {
698      Ok(props) => props,
699      Err(error) => return tracing::warn!(%error, "couldn't read power-profiles-daemon"),
700    };
701    {
702      let mut state = self.state.borrow_mut();
703      let Some(state) = state
704        .as_mut()
705        .filter(|state| state.generation == generation)
706      else {
707        return;
708      };
709      state.props = props;
710      state.connection = Some(connection);
711    }
712    self.publish();
713  }
714
715  fn signal(self: &Rc<Self>, _: &Connection, message: &Message) {
716    let Some((path, interface, changed, dropped)) = dbus::properties_changed(message) else {
717      return;
718    };
719    if path != PROFILES_PATH || interface != PROFILES {
720      return;
721    }
722    if let Some(state) = self.state.borrow_mut().as_mut() {
723      dbus::apply(&mut state.props, changed, dropped);
724    }
725    self.publish();
726  }
727}