Skip to content
This repository was archived by the owner on Jul 24, 2025. It is now read-only.

Commit 80c8ac0

Browse files
authored
Add battery min/max discharge rate and wear level (#1660)
1 parent 1462460 commit 80c8ac0

File tree

12 files changed

+329
-1
lines changed

12 files changed

+329
-1
lines changed

LenovoLegionToolkit.Lib/IoCModule.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using LenovoLegionToolkit.Lib.Integrations;
1616
using LenovoLegionToolkit.Lib.Listeners;
1717
using LenovoLegionToolkit.Lib.PackageDownloader;
18+
using LenovoLegionToolkit.Lib.Services;
1819
using LenovoLegionToolkit.Lib.Settings;
1920
using LenovoLegionToolkit.Lib.SoftwareDisabler;
2021
using LenovoLegionToolkit.Lib.Utils;
@@ -133,5 +134,7 @@ protected override void Load(ContainerBuilder builder)
133134
builder.Register<HWiNFOIntegration>();
134135

135136
builder.Register<SunriseSunset>();
137+
138+
builder.Register<BatteryDischargeRateMonitorService>();
136139
}
137140
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using LenovoLegionToolkit.Lib.System;
5+
using LenovoLegionToolkit.Lib.Utils;
6+
7+
namespace LenovoLegionToolkit.Lib.Services;
8+
9+
public class BatteryDischargeRateMonitorService
10+
{
11+
private CancellationTokenSource? _cts;
12+
private Task? _refreshTask;
13+
14+
public async Task StartStopIfNeededAsync()
15+
{
16+
await StopAsync().ConfigureAwait(false);
17+
18+
if (_refreshTask != null)
19+
return;
20+
21+
_cts?.Cancel();
22+
_cts = new CancellationTokenSource();
23+
24+
var token = _cts.Token;
25+
26+
_refreshTask = Task.Run(async () =>
27+
{
28+
if (Log.Instance.IsTraceEnabled)
29+
Log.Instance.Trace($"Battery monitoring service started...");
30+
31+
while (!token.IsCancellationRequested)
32+
{
33+
try
34+
{
35+
Battery.SetMinMaxDischargeRate();
36+
37+
await Task.Delay(TimeSpan.FromSeconds(3), token);
38+
}
39+
catch (OperationCanceledException) { }
40+
catch (Exception ex)
41+
{
42+
if (Log.Instance.IsTraceEnabled)
43+
Log.Instance.Trace($"Battery monitoring service failed.", ex);
44+
}
45+
}
46+
47+
if (Log.Instance.IsTraceEnabled)
48+
Log.Instance.Trace($"Battery monitoring service stopped.");
49+
}, token);
50+
}
51+
52+
public async Task StopAsync()
53+
{
54+
if (Log.Instance.IsTraceEnabled)
55+
Log.Instance.Trace($"Stopping...");
56+
57+
if (_cts is not null)
58+
await _cts.CancelAsync().ConfigureAwait(false);
59+
60+
_cts = null;
61+
62+
if (_refreshTask is not null)
63+
await _refreshTask.ConfigureAwait(false);
64+
65+
_refreshTask = null;
66+
67+
if (Log.Instance.IsTraceEnabled)
68+
Log.Instance.Trace($"Stopped.");
69+
}
70+
}

LenovoLegionToolkit.Lib/Structs.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public readonly struct BatteryInformation(
1818
int batteryLifeRemaining,
1919
int fullBatteryLifeRemaining,
2020
int dischargeRate,
21+
int minDischargeRate,
22+
int maxDischargeRate,
2123
int estimateChargeRemaining,
2224
int designCapacity,
2325
int fullChargeCapacity,
@@ -32,6 +34,8 @@ public readonly struct BatteryInformation(
3234
public int BatteryLifeRemaining { get; } = batteryLifeRemaining;
3335
public int FullBatteryLifeRemaining { get; init; } = fullBatteryLifeRemaining;
3436
public int DischargeRate { get; } = dischargeRate;
37+
public int MinDischargeRate { get; } = minDischargeRate;
38+
public int MaxDischargeRate { get; } = maxDischargeRate;
3539
public int EstimateChargeRemaining { get; } = estimateChargeRemaining;
3640
public int DesignCapacity { get; } = designCapacity;
3741
public int FullChargeCapacity { get; } = fullChargeCapacity;
@@ -40,6 +44,10 @@ public readonly struct BatteryInformation(
4044
public double? BatteryTemperatureC { get; } = batteryTemperatureC;
4145
public DateTime? ManufactureDate { get; } = manufactureDate;
4246
public DateTime? FirstUseDate { get; } = firstUseDate;
47+
public double BatteryHealth =>
48+
DesignCapacity > 0
49+
? Math.Round((double)FullChargeCapacity / DesignCapacity * 100.0, 2, MidpointRounding.AwayFromZero)
50+
: 0.0;
4351
}
4452

4553
public readonly struct BiosVersion(string prefix, int? version)

LenovoLegionToolkit.Lib/System/Battery.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,33 @@ namespace LenovoLegionToolkit.Lib.System;
1313
public static class Battery
1414
{
1515
private static readonly ApplicationSettings Settings = IoCContainer.Resolve<ApplicationSettings>();
16+
private static int MinDischargeRate { get; set; } = int.MaxValue;
17+
private static int MaxDischargeRate { get; set; } = 0;
18+
19+
public static void SetMinMaxDischargeRate(BATTERY_STATUS? status = null)
20+
{
21+
if (!status.HasValue)
22+
{
23+
var batteryTag = GetBatteryTag();
24+
status = GetBatteryStatus(batteryTag);
25+
}
26+
27+
if (status.Value.Rate == 0
28+
|| (status.Value.Rate > 0 && (MinDischargeRate < 0 || MaxDischargeRate < 0))
29+
|| (status.Value.Rate < 0 && (MinDischargeRate > 0 || MaxDischargeRate > 0)))
30+
{
31+
MinDischargeRate = int.MaxValue;
32+
MaxDischargeRate = 0;
33+
}
34+
35+
if (status.Value.Rate != 0)
36+
{
37+
if (Math.Abs(status.Value.Rate) < Math.Abs(MinDischargeRate))
38+
MinDischargeRate = status.Value.Rate;
39+
if (Math.Abs(status.Value.Rate) > Math.Abs(MaxDischargeRate))
40+
MaxDischargeRate = status.Value.Rate;
41+
}
42+
}
1643

1744
public static BatteryInformation GetBatteryInformation()
1845
{
@@ -25,6 +52,9 @@ public static BatteryInformation GetBatteryInformation()
2552
double? temperatureC = null;
2653
DateTime? manufactureDate = null;
2754
DateTime? firstUseDate = null;
55+
56+
SetMinMaxDischargeRate(status);
57+
2858
try
2959
{
3060
var lenovoBatteryInformation = FindLenovoBatteryInformation();
@@ -46,6 +76,8 @@ public static BatteryInformation GetBatteryInformation()
4676
(int)powerStatus.BatteryLifeTime,
4777
(int)powerStatus.BatteryFullLifeTime,
4878
status.Rate,
79+
(status.Rate == 0) ? 0 : MinDischargeRate,
80+
MaxDischargeRate,
4981
(int)status.Capacity,
5082
(int)information.DesignedCapacity,
5183
(int)information.FullChargedCapacity,

LenovoLegionToolkit.WPF/App.xaml.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using LenovoLegionToolkit.Lib.Integrations;
2424
using LenovoLegionToolkit.Lib.Listeners;
2525
using LenovoLegionToolkit.Lib.Macro;
26+
using LenovoLegionToolkit.Lib.Services;
2627
using LenovoLegionToolkit.Lib.SoftwareDisabler;
2728
using LenovoLegionToolkit.Lib.Utils;
2829
using LenovoLegionToolkit.WPF.CLI;
@@ -137,6 +138,7 @@ private async void Application_Startup(object sender, StartupEventArgs e)
137138
await IoCContainer.Resolve<AIController>().StartIfNeededAsync();
138139
await IoCContainer.Resolve<HWiNFOIntegration>().StartStopIfNeededAsync();
139140
await IoCContainer.Resolve<IpcServer>().StartStopIfNeededAsync();
141+
await IoCContainer.Resolve<BatteryDischargeRateMonitorService>().StartStopIfNeededAsync();
140142

141143
#if !DEBUG
142144
Autorun.Validate();
@@ -259,6 +261,15 @@ public async Task ShutdownAsync()
259261
}
260262
catch { /* Ignored. */ }
261263

264+
try
265+
{
266+
if (IoCContainer.TryResolve<BatteryDischargeRateMonitorService>() is { } batteryDischargeMon)
267+
{
268+
await batteryDischargeMon.StopAsync();
269+
}
270+
}
271+
catch { /* Ignored. */ }
272+
262273
Shutdown();
263274
}
264275

LenovoLegionToolkit.WPF/Pages/BatteryPage.xaml

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,32 @@
9191
FontSize="14" />
9292
</custom:CardControl>
9393

94+
<custom:CardControl Margin="0,0,0,8">
95+
<custom:CardControl.Header>
96+
<controls:CardHeaderControl Title="{x:Static resources:Resource.BatteryPage_MinDischargeRate_Title}" Subtitle="{x:Static resources:Resource.BatteryPage_MinDischargeRate_Message}" />
97+
</custom:CardControl.Header>
98+
<TextBlock
99+
x:Name="_batteryMinDischargeRateText"
100+
AutomationProperties.HelpText="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Text}"
101+
AutomationProperties.Name="{x:Static resources:Resource.BatteryPage_MinDischargeRate_Title}"
102+
FlowDirection="LeftToRight"
103+
Focusable="True"
104+
FontSize="14" />
105+
</custom:CardControl>
106+
107+
<custom:CardControl Margin="0,0,0,8">
108+
<custom:CardControl.Header>
109+
<controls:CardHeaderControl Title="{x:Static resources:Resource.BatteryPage_MaxDischargeRate_Title}" Subtitle="{x:Static resources:Resource.BatteryPage_MaxDischargeRate_Message}" />
110+
</custom:CardControl.Header>
111+
<TextBlock
112+
x:Name="_batteryMaxDischargeRateText"
113+
AutomationProperties.HelpText="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Text}"
114+
AutomationProperties.Name="{x:Static resources:Resource.BatteryPage_MaxDischargeRate_Title}"
115+
FlowDirection="LeftToRight"
116+
Focusable="True"
117+
FontSize="14" />
118+
</custom:CardControl>
119+
94120
<custom:CardControl Margin="0,0,0,8">
95121
<custom:CardControl.Header>
96122
<controls:CardHeaderControl Title="{x:Static resources:Resource.BatteryPage_CurrentCapacity_Title}" Subtitle="{x:Static resources:Resource.BatteryPage_CurrentCapacity_Message}" />
@@ -117,7 +143,7 @@
117143
FontSize="14" />
118144
</custom:CardControl>
119145

120-
<custom:CardControl Margin="0,0,0,24">
146+
<custom:CardControl Margin="0,0,0,8">
121147
<custom:CardControl.Header>
122148
<controls:CardHeaderControl Title="{x:Static resources:Resource.BatteryPage_DesignCapacity_Title}" Subtitle="{x:Static resources:Resource.BatteryPage_DesignCapacity_Message}" />
123149
</custom:CardControl.Header>
@@ -130,6 +156,19 @@
130156
FontSize="14" />
131157
</custom:CardControl>
132158

159+
<custom:CardControl Margin="0,0,0,24">
160+
<custom:CardControl.Header>
161+
<controls:CardHeaderControl Title="{x:Static resources:Resource.BatteryPage_BatteryHealth_Title}" Subtitle="{x:Static resources:Resource.BatteryPage_BatteryHealth_Message}" />
162+
</custom:CardControl.Header>
163+
<TextBlock
164+
x:Name="_batteryHealthText"
165+
AutomationProperties.HelpText="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Text}"
166+
AutomationProperties.Name="{x:Static resources:Resource.BatteryPage_BatteryHealth_Title}"
167+
FlowDirection="LeftToRight"
168+
Focusable="True"
169+
FontSize="14" />
170+
</custom:CardControl>
171+
133172
<custom:CardControl Margin="0,0,0,8">
134173
<custom:CardControl.Header>
135174
<controls:CardHeaderControl Title="{x:Static resources:Resource.BatteryPage_OnBatterySince_Title}" Subtitle="{x:Static resources:Resource.BatteryPage_OnBatterySince_Message}" />

LenovoLegionToolkit.WPF/Pages/BatteryPage.xaml.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,12 @@ private void Set(BatteryInformation batteryInfo, PowerAdapterStatus powerAdapter
124124
}
125125

126126
_batteryDischargeRateText.Text = $"{batteryInfo.DischargeRate / 1000.0:+0.00;-0.00;0.00} W";
127+
_batteryMinDischargeRateText.Text = $"{batteryInfo.MinDischargeRate / 1000.0:+0.00;-0.00;0.00} W";
128+
_batteryMaxDischargeRateText.Text = $"{batteryInfo.MaxDischargeRate / 1000.0:+0.00;-0.00;0.00} W";
127129
_batteryCapacityText.Text = $"{batteryInfo.EstimateChargeRemaining / 1000.0:0.00} Wh";
128130
_batteryFullChargeCapacityText.Text = $"{batteryInfo.FullChargeCapacity / 1000.0:0.00} Wh";
129131
_batteryDesignCapacityText.Text = $"{batteryInfo.DesignCapacity / 1000.0:0.00} Wh";
132+
_batteryHealthText.Text = $"{batteryInfo.BatteryHealth:0.00} %";
130133

131134
if (batteryInfo.ManufactureDate is not null)
132135
_batteryManufactureDateText.Text = batteryInfo.ManufactureDate?.ToString(LocalizationHelper.ShortDateFormat) ?? "-";

LenovoLegionToolkit.WPF/Resources/Resource.Designer.cs

Lines changed: 72 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)