Skip to content

Battery level of WH-1000XM4 headphones and other series models, based on the WMI wrapper for Plug-and-Play devices.

License

Notifications You must be signed in to change notification settings

nikvoronin/Xm4Battery

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Xm4Battery

Battery level of WH-1000XM4 headphones and other series models, based on the WMI wrapper for Plug-and-Play devices.

Note

WMI = Windows Management Interface.

The primary goal of the project is to get battery level of WH-1000XM4 headphones. Perhaps Xm4Battery might also works with similar models of headphones such as WH-1000XM4, WF-1000XM4 or WH-1000XM3-5-6-etc.

emoji_flash_bullet_battery_level_v23-5-2

Desktop Application

This Windows Forms application runs as a tray icon with no main window.
The ready-to-run version is available in the Latest Release section.

System requirements: Windows 10 x64, .NET Desktop Runtime 10.0 LTS

Important

Before starting the application, pair your headphones with your laptop.

Headphones Win 10 Win 11
WH-1000_XM6 ? ?
WH-1000_XM5 ? ?
WH-1000_XM4 Yes Yes
WH-1000_XM3 Yes ?

User interface

  • F - 100% fully charged
  • 4..9 - 40..90%
  • 3 yellow - 30%
  • 2 orange - 20%
  • ! red - 10%
  • X - headphones disconnected.
  • % - headphones disconnected, the last known battery level was low (30% or lower).

When the headphones are disconnected, a tooltip displays their last known battery level and the date and time of their most recent connection.

🐭 Right Mouse Button opens a context menu:

  • Connect - tries connect already paired headphones. ⚠
  • Disconnect - tries disconnect headphones (not unpair, just disconnect). ⚠
  • Launch at Startup - toggles whether the application automatically starts when Windows boots up. When ✅enabled, the application will launch automatically upon system startup.
  • About - leads to this page.
  • Quit - closes and unloads application at all.

Warning

Connect / Disconnect items appear if the app is run as an administrator.
* These functions may cause system artefacts or unusual behavior of Volume Control, Sound Mixer, Bluetooth Device Manager, etc.
** Especially the Disconnect item. Connect is a law-abiding one.

Tray icon mods

The real icon size is 20x20 pixels. It is automatically scaled by system depend on display scaling factor.

Note

The app icon is currently adjusted to 125% display scaling. Other scaling factors may lead to uglifying tray icon.

Icon text color and background are defined in the Program.TrayIcon.csCreateXmIcon method:

// icon background color
var iconBackgroundBrush =
    uiBatteryLevel switch {
        <= DisconnectedLevel => Brushes.Transparent,
        <= CriticalPowerLevel => Brushes.Red,
        <= LowPowerLevel => Brushes.Orange,
        <= WarningPowerLevel => Brushes.Yellow,
        _ => Brushes.White // 40..100(F)
    };

// icon text color
var iconTextBrush =
    uiBatteryLevel switch {
        <= DisconnectedLevel => Brushes.WhiteSmoke,
        <= CriticalPowerLevel => Brushes.White,
        //<= LowPowerLevel => Brushes.Magenta,
        //<= WarningLevel => Brushes.Cyan,
        _ => Brushes.Black
    };

Font of the notification icon text (battery level or headphones status):

static readonly Font _notifyIconFont =
    new( "Segoe UI", 12.5f, FontStyle.Regular );

Xm4Poller

Automatically updates status of headphones.

Start and stop device polling

var xm4result = Xm4Entity.Create();
if ( xm4result.IsFailed ) return 1;

Xm4Entity xm4 = xm4result.Value;

var statePoller = new Xm4Poller ( 
    xm4,
    ( previousState, newState ) => {
        // this handler is called when xm4 state changed:
        // connection status or/and battery charge level.
        // previousState <> newState - always unequal!
        UpdateUi_ForExample(newState);
    } );

statePoller.Start();

// starts main loop of window-less WinForms app
Application.Run();

// application was closed, quit
statePoller.Stop();

Xm4State

namespace WmiPnp.Xm4;

public record Xm4State
{
    public bool Connected   // true if connected, false - otherwise.
    public int BatteryLevel // battery charge level

Xm4Entity

Create XM4 instance

var xm4result = Xm4Entity.CreateDefault();
if ( xm4result.IsFailed ) return; // headphones did not found at all

Xm4Entity _xm4 = xm4result.Value;

Is connected or not?

...
bool connected = _xm4.IsConnected;

What was the last connected time?

We cannot determine the last connection time while the headphones are online and already connected. This property is only valid when the headphones are DISconnected.

Result<DateTime> dt = _xm4.LastConnectedTime;
bool disconnected = !_xm4.IsConnected;
if ( disconnected )
    Console.WriteLine( $"Last connected time: {_xm4.LastConnectedTime.Value}.\n" );
else
    var it_is_true = _xm4.LastConnectedTime.IsFailed; // can not get the last connected time

Headphones battery level

It retrieves the current battery level when the headphones are connected; otherwise, when they are disconnected, it returns the last known level.

int level = _xm4.BatteryLevel;

Re/Connect already paired

When headphones are used with multiple devices - such as a laptop, PC, or smartphone - you may need to reconnect them periodically. In this scenario, the headphones are already paired but currently disconnected.

For such cases, the WmiPnp module includes experimental methods: Xm4Entity.TryConnect() and the highly unstable Xm4Entity.TryDisconnect(). Both require the application to run with administrator privileges; otherwise, they are silently ignored.

If you're curious about completely turning off Bluetooth, see the section on Windows Radio.

PnpEntity

First, we need to know either the name or the device id of the target device - or at least a partial match of its name.

  • ByFriendlyName - exact a friendly name.
  • ByDeviceId - exact a device id, like {GUID} pid.
  • FindByFriendlyName - a part of a friendly name. Returns a list of founded devices IEnumerable<PnpEntity> or empty list otherwise.
  • FindByNameForExactClass - same as FindByFriendlyName but with exact class name equality.
  • EntityOrNone - a where part of WQL request to retrieve exact a single device only.
  • EntitiesOrNone - a where part of WQL request to retrieve zero, one or several devices at once.

All of these methods return either an instance of PnpEntity or a Result.Fail if the specified device is not found.

How To find PNP-device?

Result<PnpEntity> result =
    PnpEntity.ByFriendlyName( "The Bluetooth Device #42" );

if ( result.IsSuccess ) { // device found
    PnpEntity btDevice = result.Value;
    ...
}

Get and update a specific property of a device

...
PnpEntity btDevice = result.Value;

while ( !Console.KeyAvailable ) {
    Result<DeviceProperty> propertyResult =
        btDevice.GetDeviceProperty(
            Xm4Entity.DeviceProperty_IsConnected );

    if ( propertyResult.IsSuccess ) {
        DeviceProperty dp = propertyResult.Value;
        bool connected = (bool)(dp.Data ?? false);

        Console.WriteLine(
            $"{btDevice.Name} is {(connected ? "connected" : "disconnected")}" );
    }

    // wait a little before the next attempt
    Thread.Sleep( TimeSpan.FromSeconds( 1 ) );
}

Enumerate all properties of device

...
PnpEntity btDevice = result.Value;

IEnumerable<DeviceProperty> properties = btDevice.GetProperties();

foreach( var p in properties ) {
    Console.WriteLine( $"{p.KeyName}: {p.Data}" );
    ...
}

Enable or disable device

Some devices can be enabled or disabled.

...
PnpEntity btDevice = result.Value;

btDevice.Disable();
btDevice.Enable();

Device specific properties

Key = {GUID} pid

Battery level

  • Key = {104EA319-6EE2-4701-BD47-8DDBF425BBE5} 2
  • Type = 3 (Byte)

Data is in percents

Is connected or not

  • Key = {83DA6326-97A6-4088-9453-A1923F573B29} 15
  • Type = 17 (Boolean)

Data = False → device is disconnected

Last arrival date

  • Key = {83DA6326-97A6-4088-9453-A1923F573B29} 102
  • KeyName = DEVPKEY_Device_LastArrivalDate
  • Type = 16 (FileTime)

Data = 20230131090906.098359+180 → 2023 Jan 31, 9:09:06 GMT+3

Last removal date

Key = {83da6326-97a6-4088-9453-a1923f573b29} 103

XM4 related properties

  • WH-1000XM4 Hands-Free AG - exact name for PnpEntity to get a BATTERY LEVEL only.
  • WH-1000XM4 - exact name for PnpEntity to get a STATE of the xm4.

Note

The app actually uses naming templates such as W_-1000XM_ to abstract and match various headphone models (e.g., WH-1000XM3, WF-1000XM4, etc.).

DEVPKEY_Device_DevNodeStatus

Tip

Instead of using these bit flags, we can use the Is Connected property to retrieve the connection status of the XM4.

  • Key = {4340A6C5-93FA-4706-972C-7B648008A5A7} 2

  • KeyName = DEVPKEY_Device_DevNodeStatus

  • Type = 7 (Uint32)

  • Connected = 25190410 (fall bit#25): value & 0x20000 == 0

  • Disconnected = 58744842 (set bit#25): value & 0x20000 == 0x20000

DEVPKEY_Bluetooth_LastConnectedTime

This is the only property that provides the last connection date and time of the headphones, and it is available only when the headphones are DISconnected.

  • Key = {2BD67D8B-8BEB-48D5-87E0-6CDA3428040A} 11
  • KeyName = DEVPKEY_Bluetooth_LastConnectedTime
  • Type = 16 (FileTime)

For ex.: Data = 20230131090906.098359+180 → 2023 Jan 31, 9:09:06, GMT+3

?Last connected time

Contains the same data as the DEVPKEY_Bluetooth_LastConnectedTime property and behaves identically.

  • Key = {2BD67D8B-8BEB-48D5-87E0-6CDA3428040A} 5
  • Type = 16 (FileTime)

Windows Radio

Preparation

It is possible to use UWP APIs from a desktop application by setting the TargetFramework in your YourProject.csproj file to a Windows-specific .NET version, such as netX.x-windows10.0.xxxxx.x.

For example:

<PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net6.0-windows10.0.17763.0</TargetFramework>
    ...

Switch system bluetooth on and off

With this setup, we can now use the Windows.Devices.Radios namespace:

using Windows.Devices.Radios;

Warning

Be aware: this can completely turn off the system Bluetooth radio - not just enable or disable it.
Use at your own risk!

public static async Task OsEnableBluetooth() =>
    InternalBluetoothState( enable: true );

public static async Task OsDisableBluetooth() =>
    InternalBluetoothState( enable: false );

private async Task InternalBluetoothState( bool enable )
{
    var result = await Radio.RequestAccessAsync();
    if (result != RadioAccessStatus.Allowed) return;

    var bluetooth =
        (await Radio.GetRadiosAsync())
        .FirstOrDefault(
            radio => radio.Kind == RadioKind.Bluetooth );

    await bluetooth?.SetStateAsync(
        enable ? RadioState.On
        : RadioState.Off );
}

Tip

We can also use Windows.Devices.Bluetooth namespace or even Windows.Devices.*** for other peripheral devices.

References