Read battery, wear, and thermal state from glasses

Updated: Sep 28, 2026

Overview

Device state tells you the physical condition of the wearer’s glasses: how much battery is left, whether the battery is charging, whether the glasses are being worn, whether the arms are folded closed, and how hot the device is. Use it to defer battery-intensive work, pause a feature when the wearer takes the glasses off, or back off before the device throttles.
Unlike camera, microphone, motion, or inputs, device state is not a capability you add to a session. It is metadata carried on the device itself, alongside the device name and connection state. That has two consequences worth designing around:
  • There is nothing to start, stop, or remove, and no per-capability error stream.
  • State is available as soon as the device connects, before you start a session, and it keeps updating for as long as the device stays connected.
FieldTypeUnitNotes
batteryLevel
Integer
Percent, 0 to 100
Reported as unknown until the device sends a reading. See Read battery level safely for the platform difference.
chargingState
Enum
—
UNKNOWN, CHARGING, or NOT_CHARGING.
donState
Enum
—
UNKNOWN, DONNED (being worn), or DOFFED (not being worn).
hingeState
Enum
—
UNKNOWN, OPEN, or CLOSED (arms folded).
thermalLevel
Enum
—
Eight escalating levels. See Respond to thermal level.
For the type of each field, see Device in the API reference for Android and DeviceState for iOS.
Every enum has an UNKNOWN case, and it is the initial value for all of them. UNKNOWN means no usable information is available—for example, the field has not been reported, is not applicable to the device, or the SDK did not recognise the value. It may not eventually become a real value.
Device state is delivered only while the glasses are connected. When the device disconnects, updates stop; the last values you received are not refreshed and become stale. Check the device’s connection state before you act on them.

Use this page with AI

Copy this prompt into your AI coding tool to add or debug a device state integration:
Use https://wearables.developer.meta.com/docs/develop/dat/device-state/, then use the Wearables MCP endpoint https://mcp.developer.meta.com/wearables to call search_dat_docs for current Device Access Toolkit device state guidance. Inspect my code first, then add the smallest working slice: observe the device snapshot and read battery level, charging state, don state, hinge state, and thermal level. Remember device state is metadata on the device, not a capability I add to a session, and that it is available before a session starts. Handle the UNKNOWN case for every enum, handle an unreported battery level, and stop trusting the values once the device disconnects.

Prerequisites

Device state does not require a user-facing permission request. It is device metadata rather than user content, so there is no Permission value to check or request, and you do not call checkPermissionStatus or requestPermission before reading it. See Permissions and registration for the permissions that do require a user prompt.

Read battery level safely

The platforms represent an unavailable battery reading differently:
PlatformTypeUnavailable value
Android
Int
0
iOS
Int?
nil
On Android, the wire value 0 means the battery level is unknown or the battery is empty. The SDK exposes that value directly as 0.
On iOS, nil means no battery reading is available.

Respond to thermal level

Thermal level escalates as the device heats up. It is the same enum on both platforms.
LevelMeaningWhat to do
UNKNOWN
Not reported yet.
Nothing. Wait for a real value.
NONE
Normal operating temperature.
Nothing.
LIGHT
Slightly elevated.
Nothing, but stop increasing load.
MODERATE
Moderately elevated.
Reduce optional work, such as a high sampling rate or a high-resolution stream.
SEVERE
Severely elevated. Performance may be throttled.
Shed load. Expect the device to slow down regardless of what you do.
CRITICAL
Critical. Device performance may be heavily restricted, and sessions may be denied or terminated.
Treat the device as potentially unavailable.
EMERGENCY
Emergency. The device is preparing to shut down.
Treat the device as potentially unavailable.
SHUTDOWN
The device is shutting down due to thermal conditions.
Treat the device as potentially unavailable.
At CRITICAL or above, the device may become unavailable. Stop device-dependent work, and handle session termination or disconnection through Session lifecycle.

Update behavior

The device pushes a new snapshot whenever any field changes; the SDK does not poll. Snapshots are compared by value, so you are only notified when something actually changed. A field you care about may be unchanged in a snapshot you receive, because a different field moved — always read the field you need from the snapshot rather than assuming the notification was about it.

Android

Device state is part of the Device snapshot, which you observe as a StateFlow. There are two entry points to the same underlying flow; pick whichever matches where you are in the session lifecycle.

Before or without a session

Get deviceIdentifier by collecting Wearables.devices, as shown in Launch registration from your app. Wearables.devicesMetadata maps each known device identifier to a flow of its snapshot. Use it to show battery or wear state before the user starts anything.
import com.meta.wearable.dat.core.Wearables
import com.meta.wearable.dat.core.types.ChargingState
import com.meta.wearable.dat.core.types.DonState

val deviceFlow = Wearables.devicesMetadata[deviceIdentifier] ?: return

scope.launch {
  deviceFlow.collect { device ->
    val battery = device.batteryLevel.takeIf { it != 0 }
    showBattery(battery, charging = device.chargingState == ChargingState.CHARGING)
    showWorn(device.donState == DonState.DONNED)
  }
}
devicesMetadata returns null for a device the SDK does not know about, so handle the missing case rather than asserting.

During a session

DeviceSession.deviceInfo exposes the snapshot for the session’s own device, backed by the same flow, so you do not need to hold on to the identifier.
import com.meta.wearable.dat.core.types.ThermalLevel

scope.launch {
  session.deviceInfo.collect { device ->
    if (device.thermalLevel >= ThermalLevel.SEVERE) {
      reduceWorkload()
    }
  }
}
Comparing thermal levels with >= works because the enum is declared in escalating order, which keeps a threshold check from having to list every level above it.
Both flows are state flows, so a new collector immediately receives the current snapshot. Cancel the collecting scope during teardown; there is nothing else to release.

Test without hardware

Mock Device Kit drives the same snapshot flow, so your production observation code runs unchanged. The setters work at the connection level, with or without a session.
mockDevice.setBatteryLevel(42)
mockDevice.setChargingState(ChargingState.CHARGING)
mockDevice.setThermalLevel(ThermalLevel.SEVERE)
mockDevice.don()
mockDevice.doff()
mockGlasses.fold()
mockGlasses.unfold()
setBatteryLevel clamps its argument to 0 through 100. Wear state comes from don() and doff(), and hinge state from fold() and unfold() on the mock glasses. See Android testing for how to create a mock device.

iOS

On iOS device state is exposed both as properties on Device and as an immutable DeviceState snapshot delivered to a listener.

Get a device and read its state

Get identifier from Wearables.shared.devicesStream(), as shown in Launch registration from your app, then resolve the corresponding device.
import MWDATCore

guard let device = Wearables.shared.deviceForIdentifier(identifier) else { return }

let battery = device.batteryLevel        // Int?, nil when not reported
let charging = device.chargingState      // .unknown, .charging, .notCharging
let worn = device.donState == .donned
Inside a session, DeviceSession.device gives you the same object without holding the identifier.

Observe changes

addDeviceStateListener fires immediately with the current state and again on every change. It returns a token you cancel to stop listening.
let token = device.addDeviceStateListener { state in
  Task { @MainActor in
    showBattery(state.batteryLevel, charging: state.chargingState == .charging)
    showWorn(state.donState == .donned)

    if state.thermalLevel == .severe || state.thermalLevel == .critical {
      reduceWorkload()
    }
  }
}
The DeviceState snapshot also carries linkState and compatibility, so a single callback tells you both the device’s physical state and whether it is still connected and usable. Reading linkState from the same snapshot avoids acting on battery or thermal values from a device that has since disconnected.
Cancel the token during teardown:
token.cancel()
To observe raw connection transitions on their own, use addLinkStateListener instead.

Test without hardware

mockDevice.setBatteryLevel(42)
mockDevice.setChargingState(.charging)
mockDevice.setThermalLevel(.severe)
mockDevice.don()
mockDevice.doff()
mockGlasses.fold()
mockGlasses.unfold()
setBatteryLevel takes an optional, so pass nil to simulate a device that has not reported a battery reading. This is the case Android cannot represent directly, which makes it worth an explicit test. See iOS testing for how to create a mock device.

Platform differences

The set of fields and every enum case are identical on both platforms. The surface around them differs:
  • Unknown battery. Android uses Int with 0 meaning unknown. iOS uses Int? with nil meaning unknown.
  • Observation. Android exposes a StateFlow of the whole Device snapshot. iOS delivers an immutable DeviceState snapshot to a listener and also exposes each field as a property on Device.
  • Snapshot contents. The iOS DeviceState snapshot includes linkState and compatibility alongside the physical state. On Android those fields live on the same Device object you are already collecting, so both platforms give you the same information in one place.
  • Teardown. On Android you cancel the coroutine scope collecting the flow. On iOS you cancel the listener token.
  • Mock battery. Only the iOS mock can simulate an unreported battery level, by passing nil.

Next steps