BatteryManager fields satisfy the spec's mutual-exclusion invariants
Check id battery-spec-invariants
What an ordinary browser yields
Pass — charging/discharging times and level derive from one real power state, so all four invariants hold (a no-battery desktop reporting charging:true, level:1, dischargingTime:Infinity passes).
What a detector infers
The probe calls navigator.getBattery() once and records four fields together: level, charging, chargingTime, dischargingTime. The Battery Status spec makes three of them mutually dependent rather than free — if charging is true, dischargingTime must be Infinity; if charging is false, chargingTime must be Infinity; and level is a ratio confined to 0–1. The check asserts exactly those three, and nothing else. The mechanism a detector exploits is that these fields are derived from one underlying power state in a real implementation, but a configuration layer that writes each property independently has no shared state to derive them from — so it can emit charging:true alongside a finite dischargingTime, a combination the spec forbids and no real BatteryManager produces. That inference is reliable because it needs no baseline and no population data: the contradiction is internal to the four values and self-evident from the spec text. Note what this check deliberately does not treat as suspicious: a desktop with no battery reporting charging:true, level:1, chargingTime:0, dischargingTime:Infinity is fully coherent — every invariant holds — and it passes. The probe also omits the `!level` guard that CreepJS uses, because that guard misreports a genuine 0% battery as "unsupported"; a real 0 is a reading, not an absence.
How to resolve it
Derive the battery fields from a single consistent state instead of setting them independently: charging:true implies dischargingTime === Infinity, charging:false implies chargingTime === Infinity, and level must stay within 0–1. If there is no reason to synthesize a power state, leave BatteryManager native. On engines without navigator.getBattery (Firefox, Safari), or when the call throws or exceeds the probe's 1500ms timeout, status.battery is null and this row is not emitted at all — absence of the row is not a failure.
Read in the wild by
CreepJS
CreepJS, an open-source fingerprinting research demo rather than a production detector, calls navigator.getBattery() in its getStatus() collector and takes all four BatteryManager fields together — charging, chargingTime, dischargingTime and level — returning them in its status object and rendering them in the battery panel, with the readout gated on a truthy level.
src/status/index.ts (master): L85-88 `async function getBattery(): Promise<BatteryManager | null> { if (!('getBattery' in navigator)) return null; return navigator.getBattery() }`; L160-165 `const { charging, chargingTime, dischargingTime, level, } = batteryInfo || {}` inside getStatus() (getBattery() invoked in the Promise.all at L150, fields returned at L191-195); L275-276 render guard `<div class="block-text-large unblurred help" title="Navigator.getBattery()">${!level || isNaN(Number(level)) ? HTMLNote.UNSUPPORTED : …}`
Akamai
Lists the Battery API — charging status and level — as a hardware signal.
signal_categories.md - 2. Hardware - Battery API
Edioff/akamai-analysis — signal_categories.mdEvery attribution traces to a published artifact. See the sources and their limits.
Nearby checks in Environment & locale
- the analyser byte spectrum is the defined quantisation of its float spectrum
audio-analyser-float-vs-byteAn AnalyserNode publishes the same spectrum twice. getFloatFrequencyData returns decibels, and getByteFrequencyData returns that spectrum… - speech synthesis voices (host OS speech engine)
speech-voicesspeechSynthesis.getVoices() enumerates the host operating system's speech engine — a resource stealth stacks essentially never touch, which… - the voice list arrives the way Chrome delivers it
speech-voices-async-pathspeech-voices reads WHAT is in the voice list. This reads HOW the list arrived, which is a separate fact and survives a roster whose every… - an OS that ships a speech synthesiser enumerates at least one voice
speech-voices-vs-platformThe plain speech-voices readout above refuses to score an empty list, and as written it is right to: Chrome does not build the voice list in… - a live audio destination corresponds to an audio device that exists
audio-sink-vs-device-listA resource oracle, and deliberately a different resource from the voice list. - WebSQL (openDatabase) is absent for Chrome ≥ 119
opendatabase-removedWebSQL was removed from Chrome in version 119, so window.openDatabase should not exist on any newer Chromium. - enumerateDevices exposes audiooutput on a Chromium claim
media-kinds-vs-engineThe probe calls navigator.mediaDevices.enumerateDevices() and keeps only each entry's .kind, sorted — never labels or deviceIds, which is… - mediaCapabilities and canPlayType agree about the same codec
mediacapabilities-vs-canplaytypeTwo different APIs answer from the same platform decoder registry.
Clearcote is a browser built for fingerprint coherence
It is a Chromium fork, maintained by the same people who wrote this reference. It ships as a compiled browser rather than as a stealth script injected into someone else's — which is a description of how it is built, and is not an argument about how it behaves on this check.
This audit takes no position on how Clearcote scores on Environment & locale checks, on this one, or anywhere else. It has no baseline corpus of other people's fingerprints to rank you against and no vendor scoreboard — nearly every check is self-referential, asking one browser the same question through two independent APIs and reporting whether both answers can be true at once. It runs identically on any browser, including ours. Run it on yours and read the result yourself.
See the other checks in Environment & locale — the family battery-spec-invariants belongs to.