- wear: Compose for Wear UI, polls api.anthropic.com, shows 5h/7d utilization - mobile: companion app — token input + Data Layer API + WiFi ADB install - ADB pairing via libadb-android (SPAKE2 + TLS), pm install on watch - Build: AGP 8.7.3, Gradle 8.13, Kotlin 1.9.22, compileSdk 35
62 lines
2.7 KiB
Kotlin
62 lines
2.7 KiB
Kotlin
package com.eps.claudemeter
|
||
|
||
import kotlinx.coroutines.Dispatchers
|
||
import kotlinx.coroutines.withContext
|
||
import java.net.HttpURLConnection
|
||
import java.net.URL
|
||
import kotlin.math.roundToInt
|
||
|
||
// ponytail: тот же запрос, что у Clawdmeter — POST с max_tokens=1 ради rate-limit заголовков.
|
||
// HttpURLConnection (stdlib), без OkHttp. Один запрос = один Usage.
|
||
object ApiPoller {
|
||
private const val URL_STR = "https://api.anthropic.com/v1/messages"
|
||
// ponytail: haiku — самый дешёвый, нужен только заголовок
|
||
private const val BODY =
|
||
"""{"model":"claude-haiku-4-5-20251001","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}"""
|
||
|
||
suspend fun poll(token: String): Usage = withContext(Dispatchers.IO) {
|
||
val conn = (URL(URL_STR).openConnection() as HttpURLConnection).apply {
|
||
requestMethod = "POST"
|
||
setRequestProperty("Authorization", "Bearer $token")
|
||
setRequestProperty("anthropic-version", "2023-06-01")
|
||
setRequestProperty("Content-Type", "application/json")
|
||
setRequestProperty("User-Agent", "claude-code/2.1.5")
|
||
doOutput = true
|
||
connectTimeout = 20000
|
||
readTimeout = 20000
|
||
}
|
||
try {
|
||
conn.outputStream.use { it.write(BODY.toByteArray()) }
|
||
val code = conn.responseCode
|
||
if (code >= 400) throw Exception("HTTP $code: ${conn.errorStream?.bufferedReader()?.use { it.readText() }?.take(200)}")
|
||
val h: (String) -> String? = { name -> conn.getHeaderField(name) }
|
||
Usage(
|
||
short = pct(h("anthropic-ratelimit-unified-5h-utilization")),
|
||
shortReset = resetMin(h("anthropic-ratelimit-unified-5h-reset")),
|
||
weekly = pct(h("anthropic-ratelimit-unified-7d-utilization")),
|
||
weeklyReset = resetMin(h("anthropic-ratelimit-unified-7d-reset")),
|
||
status = h("anthropic-ratelimit-unified-5h-status") ?: "unknown",
|
||
)
|
||
} finally {
|
||
conn.disconnect()
|
||
}
|
||
}
|
||
|
||
private fun pct(s: String?): Int =
|
||
s?.toFloatOrNull()?.let { (it * 100).roundToInt() } ?: 0
|
||
|
||
private fun resetMin(s: String?): Int {
|
||
val ts = s?.toFloatOrNull()?.toLong() ?: return 0
|
||
val mins = (ts - System.currentTimeMillis() / 1000) / 60
|
||
return if (mins > 0) mins.toInt() else 0
|
||
}
|
||
}
|
||
|
||
data class Usage(
|
||
val short: Int, // % загрузки 5-часового окна
|
||
val shortReset: Int, // минуты до сброса
|
||
val weekly: Int, // % недельного окна
|
||
val weeklyReset: Int, // минуты до сброса
|
||
val status: String, // normal / banned / unknown
|
||
)
|