Add support for PowerBeats4

Thanks to @docgalaxyblock
This commit is contained in:
darken
2024-02-25 13:29:13 +01:00
committed by Matthias Urhahn
parent a86093f617
commit 34bf2311b4
5 changed files with 168 additions and 0 deletions
@@ -114,6 +114,9 @@ interface PodDevice {
@Json(name = "beats.powerbeats.3") POWERBEATS_3(
"Power Beats 3"
),
@Json(name = "beats.powerbeats.4") POWERBEATS_4(
"Power Beats 4"
),
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
"Power Beats Pro"
),
@@ -27,6 +27,7 @@ abstract class AppleFactoryModule {
@Binds @IntoSet abstract fun beatsStudio3(factory: BeatsStudio3.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun powerBeats4(factory: PowerBeats4.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun beatsFitPro(factory: BeatsFitPro.Factory): ApplePodsFactory<out ApplePods>
@@ -0,0 +1,60 @@
package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.airpods.HasStateDetectionAirPods
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant
import javax.inject.Inject
data class PowerBeats4(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
) : SingleApplePods, HasStateDetectionAirPods {
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_4
override val rssi: Int
get() = rssiAverage ?: super<SingleApplePods>.rssi
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = PowerBeats4(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
if (result == null) return basic
return basic.copy(
identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenLastAt = scanResult.receivedAt,
seenCounter = result.seenCounter,
reliability = result.reliability,
rssiAverage = result.rssiSmoothed(basic.rssi),
)
}
}
companion object {
private val DEVICE_CODE = 0x0D20.toUShort()
private val TAG = logTag("PodDevice", "Beats", "PowerBeats", "4")
}
}
@@ -40,4 +40,33 @@ class BeatsFitProTest : BaseAirPodsTest() {
model shouldBe PodDevice.Model.BEATS_FIT_PRO
}
}
@Test
fun `extra rl test case`() = runTest {
create<BeatsFitPro>("07 19 01 12 20 04 FA 92 54 11 24 CE B1 DF 9D 8D F5 E3 37 60 B1 23 8B 90 3B 63 3F") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x1220.toUShort()
rawStatus shouldBe 0x04.toUByte()
rawPodsBattery shouldBe 0xFA.toUByte()
rawFlags shouldBe 0x9.toUShort()
rawCaseBattery shouldBe 0x2.toUShort()
rawCaseLidState shouldBe 0x54.toUByte()
rawDeviceColor shouldBe 0x11.toUByte()
rawSuffix shouldBe 0x24.toUByte()
isLeftPodMicrophone shouldBe false
isRightPodMicrophone shouldBe true
batteryLeftPodPercent shouldBe null
batteryRightPodPercent shouldBe 1.0f
isCaseCharging shouldBe false
isRightPodCharging shouldBe true
isLeftPodCharging shouldBe false
batteryCasePercent shouldBe 0.2f
podStyle.identifier shouldBe HasAppleColor.DeviceColor.UNKNOWN.name
model shouldBe PodDevice.Model.BEATS_FIT_PRO
}
}
}
@@ -0,0 +1,75 @@
package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.airpods.HasStateDetectionAirPods
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
class PowerBeats4Test : BaseAirPodsTest() {
@Test
fun `playing music`() = runTest {
create<PowerBeats4>("07 19 01 0D 20 00 02 80 01 00 05 07 C5 E1 9C BD 9A 05 0E AE 9E 56 53 2F F9 75 4A") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0D20.toUShort()
rawStatus shouldBe 0x0.toUByte()
rawPodsBattery shouldBe 0x02.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0x0.toUShort()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x05.toUByte()
batteryHeadsetPercent shouldBe 0.2f
model shouldBe PodDevice.Model.POWERBEATS_4
state shouldBe HasStateDetectionAirPods.ConnectionState.MUSIC
}
}
@Test
fun `extra test case`() = runTest {
create<PowerBeats4>("07 19 01 0D 20 00 02 80 02 00 04 80 90 60 77 32 C2 0C 75 7A 3D 7E D7 1C 0E B8 43") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0D20.toUShort()
rawStatus shouldBe 0x0.toUByte()
rawPodsBattery shouldBe 0x02.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0x0.toUShort()
rawCaseLidState shouldBe 0x02.toUByte()
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x04.toUByte()
batteryHeadsetPercent shouldBe 0.2f
model shouldBe PodDevice.Model.POWERBEATS_4
state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE
}
}
@Test
fun `disconnected state`() = runTest {
create<PowerBeats4>("07 19 01 0D 20 00 02 80 01 00 00 25 DF 66 40 AF 44 7B 77 95 8F D1 92 50 26 11 74") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0D20.toUShort()
rawStatus shouldBe 0x0.toUByte()
rawPodsBattery shouldBe 0x02.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0x0.toUShort()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x00.toUByte()
batteryHeadsetPercent shouldBe 0.2f
model shouldBe PodDevice.Model.POWERBEATS_4
state shouldBe HasStateDetectionAirPods.ConnectionState.DISCONNECTED
}
}
}