
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build
import android.util.Log
import org.json.JSONObject
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.abs
private val hasPostedBatteryHealth = AtomicBoolean(false)
/***
* 发送电池健康度(App启动后发送一次)
*/
private fun postBatteryHealthContent() {
// 防止重复发送
if (!hasPostedBatteryHealth.compareAndSet(false, true)) return
handler.postDelayed({
runCatching {
val healthJson = buildBatteryHealthJson(applicationContext)
mBinder?.sendData(healthJson)
Log.i("BatteryHealth", "battery health sent: $healthJson")
}.onFailure {
Log.e("BatteryHealth", "postBatteryHealthContent failed", it)
}
}, 500)
}
private fun buildBatteryHealthJson(context: Context): String {
val bm = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val manufacturer = Build.MANUFACTURER.orEmpty()
val model = Build.MODEL.orEmpty()
val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
val scale = intent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
val batteryPct = if (level >= 0 && scale > 0) level.toDouble() / scale.toDouble() else -1.0
val tempDeci = intent?.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, Int.MIN_VALUE) ?: Int.MIN_VALUE
val tempC = if (tempDeci != Int.MIN_VALUE) tempDeci / 10.0 else 25.0
val chargeCounterUah = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER)
val chargeCounterMah = if (chargeCounterUah > 0) chargeCounterUah / 1000.0 else -1.0
// --- 尝试读取sysfs ---
val fullMah = readSysfsMah("/sys/class/power_supply/battery/charge_full")
val designMahSys = readSysfsMah("/sys/class/power_supply/battery/charge_full_design")
val cycleCount = readSysfsInt("/sys/class/power_supply/battery/cycle_count")
val asoc = readSysfsDouble("/sys/class/power_supply/battery/fg_asoc")
?: readSysfsDouble("/sys/class/power_supply/battery/batt_asoc")
val samsungFccMah = readSysfsMah("/sys/class/power_supply/battery/batt_full_capacity")
// 设计容量优先sysfs,再按机型兜底
val designMah = when {
designMahSys > 0 -> designMahSys
else -> getFallbackDesignCapacityMah(model)
}
// 满充容量优先sysfs;若无,则在已知电量时用chargeCounter反推(粗估)
val fullChargeMah = when {
fullMah > 0 -> fullMah
chargeCounterMah > 0 && batteryPct in 0.05..1.0 -> chargeCounterMah / batteryPct
else -> -1.0
}
// ---- 子分 ----
val subScores = mutableMapOf<String, Double>()
// 容量
if (fullChargeMah > 0 && designMah > 0) {
subScores["capacity"] = clip(fullChargeMah / designMah * 100.0)
}
// 循环(默认寿命800)
if (cycleCount != null && cycleCount >= 0) {
subScores["cycle"] = clip(100.0 - cycleCount.toDouble() / 800.0 * 100.0)
}
// 热行为(单点温度简化)
subScores["thermal"] = clip(
when {
tempC <= 35 -> 95.0
tempC <= 40 -> 85.0
tempC <= 45 -> 70.0
else -> 50.0
}
)
// 内阻代理(当前版本没有短窗ΔV/I,给中性分)
subScores["resistance"] = 70.0
// 效率(暂无完整充放电周期,给中性偏上)
subScores["efficiency"] = 80.0
// 通用动态加权
val weights = mapOf(
"capacity" to 0.45,
"resistance" to 0.15,
"cycle" to 0.15,
"thermal" to 0.15,
"efficiency" to 0.10
)
val genericScore = weightedScore(subScores, weights)
// 三星增强链路
val isSamsung = manufacturer.equals("samsung", ignoreCase = true)
val finalScore = if (isSamsung) {
val samsungSubs = mutableMapOf<String, Double>()
if (asoc != null) samsungSubs["asoc"] = clip(asoc)
if (samsungFccMah > 0 && designMah > 0) samsungSubs["fcc"] = clip(samsungFccMah / designMah * 100.0)
subScores["cycle"]?.let { samsungSubs["cycle"] = it }
subScores["resistance"]?.let { samsungSubs["resistance"] = it }
subScores["thermal"]?.let { samsungSubs["thermal"] = it }
samsungSubs["anomaly"] = 90.0 // 先给默认值,后续可接异常事件惩罚
val samsungWeights = mapOf(
"asoc" to 0.35,
"fcc" to 0.25,
"cycle" to 0.15,
"resistance" to 0.10,
"thermal" to 0.10,
"anomaly" to 0.05
)
weightedScore(samsungSubs, samsungWeights)
} else genericScore
val score = clip(finalScore)
val levelText = levelByScore(score)
// 置信度:数据源质量 * 覆盖率 * 稳定性(这里先固定0.85)
val qSource = when {
isSamsung && (asoc != null || samsungFccMah > 0) -> 0.96
fullMah > 0 || designMahSys > 0 || cycleCount != null -> 0.90
else -> 0.70
}
val qCoverage = subScores.size / 5.0
val confidence = clip01(qSource * qCoverage * 0.85)
val json = JSONObject().apply {
put("type", "battery_health")
put("timestamp", System.currentTimeMillis())
put("manufacturer", manufacturer)
put("model", model)
put("healthScore", round1(score))
put("healthLevel", levelText)
put("confidence", round2(confidence))
put("explain", "generic+dynamicWeight${if (isSamsung) "+samsungEnhanced" else ""}")
put("subScores", JSONObject().apply {
subScores.forEach { (k, v) -> put(k, round1(v)) }
})
put("raw", JSONObject().apply {
put("batteryPct", if (batteryPct >= 0) round2(batteryPct * 100) else -1)
put("tempC", round1(tempC))
put("fullChargeMah", if (fullChargeMah > 0) round1(fullChargeMah) else -1)
put("designMah", if (designMah > 0) round1(designMah) else -1)
put("cycleCount", cycleCount ?: -1)
put("asoc", asoc ?: -1)
put("samsungFccMah", if (samsungFccMah > 0) round1(samsungFccMah) else -1)
})
}
return json.toString()
}
// ------------------ helpers ------------------
private fun weightedScore(scores: Map<String, Double>, weights: Map<String, Double>): Double {
val keys = weights.keys.filter { scores[it] != null }
if (keys.isEmpty()) return 0.0
val w = keys.sumOf { weights[it] ?: 0.0 }
if (w <= 0.0) return 0.0
return keys.sumOf { (weights[it] ?: 0.0) * (scores[it] ?: 0.0) } / w
}
private fun levelByScore(score: Double): String = when {
score >= 90 -> "A"
score >= 80 -> "B"
score >= 70 -> "C"
score >= 60 -> "D"
else -> "E"
}
private fun clip(v: Double): Double = v.coerceIn(0.0, 100.0)
private fun clip01(v: Double): Double = v.coerceIn(0.0, 1.0)
private fun round1(v: Double): Double = String.format("%.1f", v).toDouble()
private fun round2(v: Double): Double = String.format("%.2f", v).toDouble()
private fun readText(path: String): String? = runCatching {
val f = File(path)
if (f.exists() && f.canRead()) f.readText().trim() else null
}.getOrNull()
private fun readSysfsInt(path: String): Int? = readText(path)?.toIntOrNull()
private fun readSysfsDouble(path: String): Double? = readText(path)?.toDoubleOrNull()
private fun readSysfsMah(path: String): Double {
val raw = readSysfsDouble(path) ?: return -1.0
return if (raw > 100_000) raw / 1000.0 else raw // µAh -> mAh
}
private fun getFallbackDesignCapacityMah(model: String): Double {
// 你后续可以替换为更完整机型表/服务端配置
return when {
model.contains("SM-S9", true) -> 3900.0
model.contains("SM-S91", true) -> 4700.0
model.contains("SM-S92", true) -> 5000.0
model.contains("SM-G99", true) -> 5000.0
else -> 4500.0
}
}
{"type":"battery_health","timestamp":1780571802331,"manufacturer":"HONOR","model":"LGE-AN00","healthScore":89.7,"healthLevel":"B","confidence":0.48,"explain":"generic+dynamicWeight","subScores":{"capacity":100,"thermal":85,"resistance":70,"efficiency":80},"raw":{"batteryPct":76,"tempC":37,"fullChargeMah":4605.3,"designMah":4500,"cycleCount":-1,"asoc":-1,"samsungFccMah":-1}}总体健康评估
| 指标 | 值 | 说明 |
|---|---|---|
| 健康评分 | 89.7 | 百分制,整体良好 |
| 健康等级 | B | 中上水平,有一定老化 |
| 置信度 | 0.48 | 偏低,诊断结果不够确定 |
子项评分分析
| 维度 | 得分 | 评价 |
|---|---|---|
| 容量 (capacity) | 100 | 满分,电池实际容量 4605.3mAh > 设计容量 4500mAh,无衰减 |
| 温度 (thermal) | 85 | 当前 37°C,属于正常偏高 |
| 内阻 (resistance) | 70 | ⚠️ 中等偏低,内阻增大说明电池有一定老化 |
| 效率 (efficiency) | 80 | 一般,充放电效率有损耗 |
关键原始数据
- 电量:当前 76%
- 实际容量:4605.3mAh(超设计容量约 2.3%,可能是新电池或标定偏差)
- 设计容量:4500mAh
- 循环次数:-1(无法获取,HONOR 未上报此数据)
- ASOC:-1(无法获取绝对荷电状态)
阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/22935,转载请注明出处。
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/22935,转载请注明出处。


评论0