// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. // Copyright (C) LibreHardwareMonitor and Contributors. // All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Text; using Windows.Win32; using Windows.Win32.Devices.DeviceAndDriverInstallation; using Windows.Win32.Foundation; using Windows.Win32.Storage.FileSystem; using Windows.Win32.System.Power; using LibreHardwareMonitor.Interop; using Microsoft.Win32.SafeHandles; namespace LibreHardwareMonitor.Hardware.Battery; internal class BatteryGroup : IGroup { private readonly List _hardware = []; public unsafe BatteryGroup(ISettings settings) { // No implementation for battery information on Unix systems if (Software.OperatingSystem.IsUnix) return; #if WINDOWS SetupDiDestroyDeviceInfoListSafeHandle hdev = PInvoke.SetupDiGetClassDevs(PInvoke.GUID_DEVICE_BATTERY, null, HWND.Null, SETUP_DI_GET_CLASS_DEVS_FLAGS.DIGCF_PRESENT | SETUP_DI_GET_CLASS_DEVS_FLAGS.DIGCF_DEVICEINTERFACE); if (!hdev.IsInvalid) { for (uint i = 0;; i++) { SP_DEVICE_INTERFACE_DATA data = default; data.cbSize = (uint)sizeof(SP_DEVICE_INTERFACE_DATA); if (!PInvoke.SetupDiEnumDeviceInterfaces(hdev, null, PInvoke.GUID_DEVICE_BATTERY, i, ref data)) { if (Marshal.GetLastWin32Error() == (int)WIN32_ERROR.ERROR_NO_MORE_ITEMS) break; } else { uint cbRequired = 0; PInvoke.SetupDiGetDeviceInterfaceDetail((HDEVINFO)hdev.DangerousGetHandle(), &data, null, 0, &cbRequired, null); if (Marshal.GetLastWin32Error() == (int)WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER) { IntPtr buffer = Marshal.AllocHGlobal((int)cbRequired); SP_DEVICE_INTERFACE_DETAIL_DATA_W* pDetailData = (SP_DEVICE_INTERFACE_DETAIL_DATA_W*)buffer; pDetailData->cbSize = (uint)sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W); if (PInvoke.SetupDiGetDeviceInterfaceDetail((HDEVINFO)hdev.DangerousGetHandle(), &data, pDetailData, cbRequired, &cbRequired, null)) { string devicePath; fixed (void* pDevicePath = &pDetailData->DevicePath.e0) devicePath = new string((char*)pDevicePath); SafeFileHandle battery = PInvoke.CreateFile(devicePath, (uint)FileAccess.ReadWrite, FILE_SHARE_MODE.FILE_SHARE_READ | FILE_SHARE_MODE.FILE_SHARE_WRITE, null, FILE_CREATION_DISPOSITION.OPEN_EXISTING, FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL, null); if (!battery.IsInvalid) { BATTERY_QUERY_INFORMATION bqi = default; uint dwWait = 0; uint bytesReturned = 0; if (PInvoke.DeviceIoControl((HANDLE)battery.DangerousGetHandle(), PInvoke.IOCTL_BATTERY_QUERY_TAG, &dwWait, sizeof(uint), &bqi.BatteryTag, sizeof(uint), &bytesReturned, null)) { BATTERY_INFORMATION bi = default; bqi.InformationLevel = BATTERY_QUERY_INFORMATION_LEVEL.BatteryInformation; if (PInvoke.DeviceIoControl((HANDLE)battery.DangerousGetHandle(), PInvoke.IOCTL_BATTERY_QUERY_INFORMATION, &bqi, (uint)sizeof(BATTERY_QUERY_INFORMATION), &bi, (uint)sizeof(BATTERY_INFORMATION), &bytesReturned, null)) { // Only batteries count. if ((bi.Capabilities & PInvoke.BATTERY_SYSTEM_BATTERY) == PInvoke.BATTERY_SYSTEM_BATTERY) { bqi.InformationLevel = BATTERY_QUERY_INFORMATION_LEVEL.BatteryDeviceName; QueryStringFromBatteryInfo(battery, bqi, out string batteryName); bqi.InformationLevel = BATTERY_QUERY_INFORMATION_LEVEL.BatteryManufactureName; QueryStringFromBatteryInfo(battery, bqi, out string manufacturer); _hardware.Add(new Battery(batteryName, manufacturer, battery, bi, bqi.BatteryTag, settings)); } } } } } Marshal.FreeHGlobal(buffer); } } } hdev.Dispose(); } #endif } /// public IReadOnlyList Hardware => _hardware; #if WINDOWS private static unsafe bool QueryStringFromBatteryInfo(SafeFileHandle battery, BATTERY_QUERY_INFORMATION bqi, out string value) { value = null; bool result = false; Span span = stackalloc char[100]; fixed (char* pSpan = span) { uint returnBytes = 0; if (PInvoke.DeviceIoControl((HANDLE)battery.DangerousGetHandle(), PInvoke.IOCTL_BATTERY_QUERY_INFORMATION, &bqi, (uint)sizeof(BATTERY_QUERY_INFORMATION), pSpan, 200, &returnBytes, null)) { value = new string(pSpan); result = true; } } return result; } #endif /// public void Close() { foreach (Battery battery in _hardware) battery.Close(); } /// public string GetReport() { StringBuilder reportBuilder = new(); uint count = 1; foreach (Battery bat in _hardware) { string chemistry = bat.Chemistry switch { BatteryChemistry.LeadAcid => "Lead Acid", BatteryChemistry.NickelCadmium => "Nickel-Cadmium", BatteryChemistry.NickelMetalHydride => "Nickel-Metal Hydride", BatteryChemistry.LithiumIon => "Lithium Ion", BatteryChemistry.NickelZinc => "Nickel-Zinc", BatteryChemistry.AlkalineManganese => "Rechargeable Alkaline-Manganese", _ => "Unknown" }; reportBuilder.Append("Battery #").Append(count).AppendLine(":") .Append(" Name: ").AppendLine(bat.Name) .Append(" Manufacturer: ").AppendLine(bat.Manufacturer) .Append(" Chemistry: ").AppendLine(chemistry); if (bat.DegradationLevel.HasValue) reportBuilder.Append(" Degradation Level: ").AppendFormat("{0:F2}", bat.DegradationLevel).AppendLine(" %"); if (bat.DesignedCapacity.HasValue) reportBuilder.Append(" Designed Capacity: ").Append(bat.DesignedCapacity).AppendLine(" mWh"); if (bat.FullChargedCapacity.HasValue) reportBuilder.Append(" Fully-Charged Capacity: ").Append(bat.FullChargedCapacity).AppendLine(" mWh"); if (bat.RemainingCapacity.HasValue) reportBuilder.Append(" Remaining Capacity: ").Append(bat.RemainingCapacity).AppendLine(" mWh"); if (bat.ChargeLevel.HasValue) reportBuilder.Append(" Charge Level: ").AppendFormat("{0:F2}", bat.ChargeLevel).AppendLine(" %"); if (bat.Voltage.HasValue) reportBuilder.Append(" Voltage: ").AppendFormat("{0:F3}", bat.Voltage).AppendLine(" V"); if (bat.Temperature.HasValue) reportBuilder.Append(" Temperature: ").AppendFormat("{0:F3}", bat.Temperature).AppendLine(" ºC"); if (bat.RemainingTime.HasValue) reportBuilder.Append(" Remaining Time (Estimated): ").AppendFormat("{0:g}", TimeSpan.FromSeconds(bat.RemainingTime.Value)).AppendLine(); string cdRateSensorName; string cdCurrentSensorName; if (bat.ChargeDischargeRate > 0) { cdRateSensorName = " Charge Rate: "; cdCurrentSensorName = " Charge Current: "; } else if (bat.ChargeDischargeRate < 0) { cdRateSensorName = " Discharge Rate: "; cdCurrentSensorName = " Discharge Current: "; } else { cdRateSensorName = " Charge/Discharge Rate: "; cdCurrentSensorName = " Charge/Discharge Current: "; } if (bat.ChargeDischargeRate.HasValue) reportBuilder.Append(cdRateSensorName).AppendFormat("{0:F1}", Math.Abs(bat.ChargeDischargeRate.Value)).AppendLine(" W"); if (bat.ChargeDischargeCurrent.HasValue) reportBuilder.Append(cdCurrentSensorName).AppendFormat("{0:F3}", Math.Abs(bat.ChargeDischargeCurrent.Value)).AppendLine(" A"); reportBuilder.AppendLine(); count++; } return reportBuilder.ToString(); } }