chromium/tools/mac/power/power_sampler/battery_sampler.mm

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "tools/mac/power/power_sampler/battery_sampler.h"

#import <Foundation/Foundation.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/ps/IOPSKeys.h>
#include <cstdint>

#include "base/apple/foundation_util.h"
#include "base/apple/mach_logging.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/logging.h"
#include "base/mac/scoped_ioobject.h"
#include "base/memory/ptr_util.h"
#include "base/time/time.h"

namespace power_sampler {
namespace {

// Returns the value corresponding to |key| in the dictionary |description|.
// Returns |default_value| if the dictionary does not contain |key|, the
// corresponding value is nullptr or it could not be converted to SInt64.
std::optional<SInt64> GetValueAsSInt64(CFDictionaryRef description,
                                       CFStringRef key) {
  CFNumberRef number_ref =
      base::apple::GetValueFromDictionary<CFNumberRef>(description, key);

  SInt64 value;
  if (number_ref && CFNumberGetValue(number_ref, kCFNumberSInt64Type, &value))
    return value;

  return std::nullopt;
}

std::optional<bool> GetValueAsBoolean(CFDictionaryRef description,
                                      CFStringRef key) {
  CFBooleanRef boolean =
      base::apple::GetValueFromDictionary<CFBooleanRef>(description, key);
  if (!boolean)
    return std::nullopt;
  return CFBooleanGetValue(boolean);
}

}  // namespace

BatterySampler::~BatterySampler() = default;

// static
std::unique_ptr<BatterySampler> BatterySampler::Create() {
  // Retrieve the IOPMPowerSource service.
  base::mac::ScopedIOObject<io_service_t> power_source(
      IOServiceGetMatchingService(kIOMasterPortDefault,
                                  IOServiceMatching("IOPMPowerSource")));
  if (!power_source) {
    return nullptr;
  }

  auto get_seconds_since_epoch_fn = []() -> int64_t {
    return (base::Time::Now() - base::Time::UnixEpoch()).InSeconds();
  };

  return CreateImpl(MaybeGetBatteryData, get_seconds_since_epoch_fn,
                    std::move(power_source));
}

std::string BatterySampler::GetName() {
  return kSamplerName;
}

Sampler::DatumNameUnits BatterySampler::GetDatumNameUnits() {
  DatumNameUnits ret;
  ret.emplace("external_connected", "bool");
  ret.emplace("voltage", "V");
  ret.emplace("current_capacity", "Ah");
  ret.emplace("max_capacity", "Ah");
  // https://en.wikipedia.org/wiki/Power_(physics)
  ret.emplace("avg_power", "W");
  // https://en.wikipedia.org/wiki/Electric_charge
  ret.emplace("electric_charge_delta", "mAh");
  ret.emplace("sample_age", "s");

  return ret;
}

Sampler::Sample BatterySampler::GetSample(base::TimeTicks sample_time) {
  std::optional<BatteryData> new_battery_data =
      maybe_get_battery_data_fn_(power_source_.get());
  if (!new_battery_data.has_value())
    return Sample();

  Sample sample;
  const BatteryData& new_data = new_battery_data.value();
  if (prev_battery_data_.has_value()) {
    // There's a previous sample to refer to. Compute the average power if
    // there's been any reported current consumption since that sample.
    // Note that the current consumption is reported in integral units of mAh,
    // and that the underlying sampling when on battery is once a minute.
    auto avg_consumption =
        MaybeComputeAvgConsumption(sample_time - prev_battery_sample_time_,
                                   prev_battery_data_.value(), new_data);

    if (avg_consumption.has_value()) {
      sample.emplace("avg_power", avg_consumption->watts);
      sample.emplace("electric_charge_delta", avg_consumption->mah);

      // The previous sample is consumed, store the new one.
      StoreBatteryData(sample_time, new_data);
    }
  }

  sample.emplace("external_connected", new_data.external_connected);
  sample.emplace("voltage", new_data.voltage_mv / 1000.0);
  sample.emplace("current_capacity", new_data.current_capacity_mah / 1000.0);
  sample.emplace("max_capacity", new_data.max_capacity_mah / 1000.0);
  sample.emplace("sample_age", get_seconds_since_epoch_fn_() -
                                   new_data.update_time_seconds_since_epoch);

  if (!prev_battery_data_.has_value()) {
    // Store an initial sample.
    StoreBatteryData(sample_time, new_data);
  }

  return sample;
}

// static
std::optional<BatterySampler::BatteryData> BatterySampler::MaybeGetBatteryData(
    io_service_t power_source) {
  base::apple::ScopedCFTypeRef<CFMutableDictionaryRef> dict;
  kern_return_t result = IORegistryEntryCreateCFProperties(
      power_source, dict.InitializeInto(), 0, 0);
  if (result != KERN_SUCCESS) {
    MACH_LOG(ERROR, result) << "IORegistryEntryCreateCFProperties";
    return std::nullopt;
  }

  std::optional<bool> external_connected =
      GetValueAsBoolean(dict.get(), CFSTR("ExternalConnected"));
  std::optional<SInt64> voltage_mv =
      GetValueAsSInt64(dict.get(), CFSTR(kIOPSVoltageKey));
  std::optional<SInt64> current_capacity_mah =
      GetValueAsSInt64(dict.get(), CFSTR("AppleRawCurrentCapacity"));
  std::optional<SInt64> max_capacity_mah =
      GetValueAsSInt64(dict.get(), CFSTR("AppleRawMaxCapacity"));
  std::optional<SInt64> update_time =
      GetValueAsSInt64(dict.get(), CFSTR("UpdateTime"));

  if (!external_connected.has_value() || !voltage_mv.has_value() ||
      !current_capacity_mah.has_value() || !max_capacity_mah.has_value()) {
    return std::nullopt;
  }

  BatteryData data{.external_connected = external_connected.value(),
                   .voltage_mv = voltage_mv.value(),
                   .current_capacity_mah = current_capacity_mah.value(),
                   .max_capacity_mah = max_capacity_mah.value(),
                   .update_time_seconds_since_epoch = update_time.value()};

  return data;
}

//  static
std::optional<BatterySampler::AvgConsumption>
BatterySampler::MaybeComputeAvgConsumption(base::TimeDelta duration,
                                           const BatteryData& prev_data,
                                           const BatteryData& new_data) {
  // The gauging hardware measures current consumed (or charged), but reports
  // the remaining capacity with respect to a load-dependent max capacity.
  // Here, however, we care about the delta capacity consumed rather than the
  // capacity remaining. To get to capacity consumed, we flip the capacity
  // remaining estimates to capacity consumed and work from there. It's been
  // experimentally determined that this backs out the effects of any
  // load-dependent max capacity estimates to yield the capacity consumed.
  int64_t prev_current_consumed_mah =
      prev_data.max_capacity_mah - prev_data.current_capacity_mah;
  int64_t new_current_consumed_mah =
      new_data.max_capacity_mah - new_data.current_capacity_mah;

  // Compute the consumed capacity delta.
  int64_t delta_current_consumed_mah =
      prev_current_consumed_mah - new_current_consumed_mah;
  if (delta_current_consumed_mah == 0)
    return std::nullopt;

  double avg_voltage_v =
      (new_data.voltage_mv + prev_data.voltage_mv) / (2.0 * 1000.0);

  constexpr double kAsPerMAh =
      static_cast<double>(base::TimeTicks::kSecondsPerHour) / 1000.0;
  double avg_current_a =
      delta_current_consumed_mah * kAsPerMAh / duration.InSecondsF();

  // Arbitrarily use positive values to represent energy being consumed
  // (charging the battery will produce negative values).
  return AvgConsumption{.watts = avg_voltage_v * -avg_current_a,
                        .mah = -delta_current_consumed_mah};
}

// static
std::unique_ptr<BatterySampler> BatterySampler::CreateImpl(
    MaybeGetBatteryDataFn maybe_get_battery_data_fn,
    GetSecondsSinceEpochFn get_seconds_since_epoch_fn,
    base::mac::ScopedIOObject<io_service_t> power_source) {
  // Validate that we can work with this source.
  auto battery_data = maybe_get_battery_data_fn(power_source.get());
  if (!battery_data.has_value())
    return nullptr;

  return base::WrapUnique(
      new BatterySampler(maybe_get_battery_data_fn, get_seconds_since_epoch_fn,
                         std::move(power_source), *battery_data));
}

BatterySampler::BatterySampler(
    MaybeGetBatteryDataFn maybe_get_battery_data_fn,
    GetSecondsSinceEpochFn get_seconds_since_epoch_fn,
    base::mac::ScopedIOObject<io_service_t> power_source,
    BatteryData initial_battery_data)
    : maybe_get_battery_data_fn_(maybe_get_battery_data_fn),
      get_seconds_since_epoch_fn_(get_seconds_since_epoch_fn),
      power_source_(std::move(power_source)) {}

void BatterySampler::StoreBatteryData(base::TimeTicks sample_time,
                                      const BatteryData& battery_data) {
  prev_battery_sample_time_ = sample_time;
  prev_battery_data_ = battery_data;
}

}  // namespace power_sampler