mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 10:46:12 -04:00
Revert "feat(battery): Add experimental case charge ETA and case battery health"
This reverts commit 87a9881024.
This commit is contained in:
-1
@@ -23,7 +23,6 @@ class DeviceInfoDetailItemsTest : BaseTest() {
|
||||
batteryHealth = "Battery Health",
|
||||
leftBatteryHealth = "Left Battery Health",
|
||||
rightBatteryHealth = "Right Battery Health",
|
||||
caseBatteryHealth = "Case Battery Health",
|
||||
)
|
||||
|
||||
private val formatter: (Instant) -> String = { "fmt:${it.epochSecond}" }
|
||||
|
||||
@@ -39,9 +39,6 @@ class BatteryEstimatorTest : BaseTest() {
|
||||
estimateEnabled: Boolean = true,
|
||||
worn: Boolean = false,
|
||||
systemConnected: Boolean = false,
|
||||
case: Float? = null,
|
||||
caseCharging: Boolean = false,
|
||||
caseLive: Boolean = true,
|
||||
): PodDevice {
|
||||
val state = when {
|
||||
optimized -> ChargingState.CHARGING_OPTIMIZED
|
||||
@@ -51,10 +48,6 @@ class BatteryEstimatorTest : BaseTest() {
|
||||
val batteries = buildMap {
|
||||
if (left != null) put(BatteryType.LEFT, Battery(BatteryType.LEFT, left, state))
|
||||
if (right != null) put(BatteryType.RIGHT, Battery(BatteryType.RIGHT, right, state))
|
||||
if (case != null) put(
|
||||
BatteryType.CASE,
|
||||
Battery(BatteryType.CASE, case, if (caseCharging) ChargingState.CHARGING else ChargingState.NOT_CHARGING),
|
||||
)
|
||||
}
|
||||
val settings = if (worn) {
|
||||
mapOf<kotlin.reflect.KClass<out AapSetting>, AapSetting>(
|
||||
@@ -67,7 +60,7 @@ class BatteryEstimatorTest : BaseTest() {
|
||||
return PodDevice(
|
||||
profileId = profileId,
|
||||
ble = null,
|
||||
aap = AapPodState(batteries = batteries, settings = settings, caseIsLive = case != null && caseLive),
|
||||
aap = AapPodState(batteries = batteries, settings = settings),
|
||||
profileModel = model,
|
||||
batteryEstimateEnabled = estimateEnabled,
|
||||
isSystemConnected = systemConnected,
|
||||
@@ -561,94 +554,6 @@ class BatteryEstimatorTest : BaseTest() {
|
||||
result["p1"].shouldNotBeNull().left.shouldNotBeNull().source shouldBe BatteryEstimate.Source.SPEC
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a rising case charge yields a case ETA`() = runTest(UnconfinedTestDispatcher()) {
|
||||
// 2%/min case rise -> 1.2/hr; at 44% that's (1 - 0.44) / 1.2 * 60 == 28 min. No spec
|
||||
// seed exists for cases, so the live fit is the only source on a first charge.
|
||||
val emissions = (0 until 4).map { i ->
|
||||
listOf(device("p1", left = null, right = null, case = 0.20f + i * 0.08f, caseCharging = true))
|
||||
}
|
||||
val result = collectEstimate(estimator(emissions))
|
||||
result["p1"].shouldNotBeNull().caseMinutesUntilCharged shouldBe 28
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a frozen case reading is never sampled`() = runTest(UnconfinedTestDispatcher()) {
|
||||
// caseIsLive == false means the merged CASE value is a stale echo (pods undocked).
|
||||
val emissions = (0 until 4).map { i ->
|
||||
listOf(device("p1", left = null, right = null, case = 0.20f + i * 0.08f, caseCharging = true, caseLive = false))
|
||||
}
|
||||
collectEstimate(estimator(emissions)) shouldBe emptyMap()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `docked charging pods draw down the case into a transfer ratio`() = runTest(UnconfinedTestDispatcher()) {
|
||||
// Unplugged case feeding both docked pods: pods gain 0.64 summed while the case drops
|
||||
// 0.08 -> ratio 8.0. The window closes when the pods stop charging, then persists.
|
||||
val docked = (0 until 5).map { i ->
|
||||
listOf(
|
||||
device(
|
||||
"p1",
|
||||
left = 0.20f + i * 0.08f, right = 0.20f + i * 0.08f, charging = true,
|
||||
case = 0.50f - i * 0.02f, caseCharging = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
val closed = listOf(
|
||||
listOf(device("p1", left = 0.52f, right = 0.52f, case = 0.42f, caseCharging = false))
|
||||
)
|
||||
val emissions = docked + closed
|
||||
val drainStore = mockk<BatteryDrainStore> {
|
||||
every { profiles } returns MutableStateFlow(emptyMap())
|
||||
coEvery { save(any(), any()) } returns Unit
|
||||
}
|
||||
val deviceMonitor = mockk<DeviceMonitor> { every { devices } returns flowOf(*emissions.toTypedArray()) }
|
||||
val timeSource = mockk<TimeSource> {
|
||||
every { elapsedRealtime() } returnsMany emissions.indices.map { it * 4 * 60_000L }
|
||||
every { now() } returns now
|
||||
}
|
||||
val audioManager = mockk<android.media.AudioManager> { every { isMusicActive } returns false }
|
||||
BatteryEstimator(deviceMonitor, drainStore, timeSource, audioManager).monitor().collect {}
|
||||
|
||||
coVerify {
|
||||
drainStore.save("p1", match { profile ->
|
||||
val transfer = profile.caseTransfer
|
||||
transfer != null && transfer.ratio > 7.9f && transfer.ratio < 8.1f &&
|
||||
// The case never learns hourly drain rates.
|
||||
profile.rates.keys.none { it.endsWith("/CASE") }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a plugged-in case opens no transfer window`() = runTest(UnconfinedTestDispatcher()) {
|
||||
// Case charging from cable while pods also charge — nothing here measures transfer.
|
||||
val emissions = (0 until 5).map { i ->
|
||||
listOf(
|
||||
device(
|
||||
"p1",
|
||||
left = 0.20f + i * 0.08f, right = 0.20f + i * 0.08f, charging = true,
|
||||
case = 0.80f, caseCharging = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
val drainStore = mockk<BatteryDrainStore> {
|
||||
every { profiles } returns MutableStateFlow(emptyMap())
|
||||
coEvery { save(any(), any()) } returns Unit
|
||||
}
|
||||
val deviceMonitor = mockk<DeviceMonitor> { every { devices } returns flowOf(*emissions.toTypedArray()) }
|
||||
val timeSource = mockk<TimeSource> {
|
||||
every { elapsedRealtime() } returnsMany emissions.indices.map { it * 4 * 60_000L }
|
||||
every { now() } returns now
|
||||
}
|
||||
val audioManager = mockk<android.media.AudioManager> { every { isMusicActive } returns false }
|
||||
BatteryEstimator(deviceMonitor, drainStore, timeSource, audioManager).monitor().collect {}
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
drainStore.save(any(), match { it.caseTransfer != null })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reset deletes persisted data and drops the estimate`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val drainStore = mockk<BatteryDrainStore> {
|
||||
|
||||
@@ -116,34 +116,6 @@ class BatteryHealthTest : BaseTest() {
|
||||
BatteryHealth.estimate(profile, PodModel.AIRPODS_GEN4_ANC).shouldNotBeNull().left shouldBe 50
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `case health is the observed transfer ratio vs the nominal`() {
|
||||
// Pro 2: nominal = (30 - 6) / 6 * 2 = 8.0 summed pod-fraction per case fraction.
|
||||
// An observed 4.0 means the case delivers half its rated recharges -> 50%.
|
||||
val profile = DrainProfile(
|
||||
caseTransfer = DrainProfile.TransferRatio(ratio = 4f, updateCount = 3, updatedAt = Instant.EPOCH),
|
||||
)
|
||||
val health = BatteryHealth.estimate(profile, PodModel.AIRPODS_PRO2).shouldNotBeNull()
|
||||
health.case shouldBe 50
|
||||
health.left shouldBe null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `case health needs enough observed sessions`() {
|
||||
val profile = DrainProfile(
|
||||
caseTransfer = DrainProfile.TransferRatio(ratio = 4f, updateCount = 2, updatedAt = Instant.EPOCH),
|
||||
)
|
||||
BatteryHealth.estimate(profile, PodModel.AIRPODS_PRO2).shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `case health is capped at 100`() {
|
||||
val profile = DrainProfile(
|
||||
caseTransfer = DrainProfile.TransferRatio(ratio = 12f, updateCount = 3, updatedAt = Instant.EPOCH),
|
||||
)
|
||||
BatteryHealth.estimate(profile, PodModel.AIRPODS_PRO2).shouldNotBeNull().case shouldBe 100
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `headset slot yields a headset figure`() {
|
||||
// AirPods Max rated 20h; managing only 10h -> 50%.
|
||||
|
||||
-6
@@ -30,7 +30,6 @@ class DrainProfileSerializationTest : BaseTest() {
|
||||
profile.chargeRates shouldBe emptyMap()
|
||||
profile.chargeBands shouldBe emptyMap()
|
||||
profile.listeningRates shouldBe emptyMap()
|
||||
profile.caseTransfer shouldBe null
|
||||
profile.rates.getValue("UNKNOWN/LEFT").updateCount shouldBe 1
|
||||
}
|
||||
|
||||
@@ -72,11 +71,6 @@ class DrainProfileSerializationTest : BaseTest() {
|
||||
updatedAt = Instant.ofEpochMilli(1700000000000L),
|
||||
)
|
||||
),
|
||||
caseTransfer = DrainProfile.TransferRatio(
|
||||
ratio = 6.5f,
|
||||
updateCount = 4,
|
||||
updatedAt = Instant.ofEpochMilli(1700000000000L),
|
||||
),
|
||||
)
|
||||
|
||||
json.decodeFromString<DrainProfile>(json.encodeToString(DrainProfile.serializer(), profile)) shouldBe profile
|
||||
|
||||
@@ -155,22 +155,6 @@ class ModelFeaturesTest : BaseTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `with-case totals exist for cased models and exceed the single-charge rating`() {
|
||||
PodModel.entries.forEach { model ->
|
||||
val spec = model.batterySpec ?: return@forEach
|
||||
withClue(model.name) {
|
||||
if (model.features.hasCase) {
|
||||
val withCase = spec.listeningHoursWithCase
|
||||
val single = spec.listeningHoursAncOn ?: spec.listeningHoursAncOff
|
||||
(withCase != null && single != null && withCase > single) shouldBe true
|
||||
} else {
|
||||
spec.listeningHoursWithCase shouldBe null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `every battery spec carries a plausible quick-charge rate`() {
|
||||
// Derived from Apple's published quick-charge claims; must sit inside the band the live
|
||||
|
||||
Reference in New Issue
Block a user