Skip to content

Preferences: "Sound with the low battery alert" (off by default), repeats every 5 min while low, for full-screen games (#66), unit tests - #137

Merged
HeyOkay merged 2 commits into
HeyOkay:mainfrom
ahmedkhursheed23:low-battery-sound
Oct 5, 2026
Merged

HeyOkay merged 2 commits into
HeyOkay:mainfrom
ahmedkhursheed23:low-battery-sound

Conversation

@ahmedkhursheed23

Copy link
Copy Markdown
Contributor

This change adds an optional sound to the low battery alert (#66). The option is Preferences > Sound with the low battery alert. It is off by default, so the app does not change for current users. When the option is on, the low battery alert also plays a Windows sound. The sound plays again every 5 minutes while the device stays low.

1. Why

Users in full-screen games do not see the low battery notification. Example: a user plays a full-screen game with a wireless headset at 25 %. The level drops to 20 %. The app shows the "Low battery" notification, but the game hides it. The user does not know about the low battery until the headset turns off.

2. Change

  • halo_battery.pyw:
    • New setting low_sound in DEFAULTS (default False). load_config() accepts only a boolean for it, as for the other switches.
    • New toggle "Sound with the low battery alert" in Preferences, below "Alert when fully charged". The Fluent menu and the classic menu both show it, because both use the same pystray menu.
    • New pure function low_battery_sound(level, charging, online, low, last, now). It returns "low", "critical" or None. It returns a sound when the device is at or below the alert level, online and not charging, and when no sound played in the last LOW_SOUND_REPEAT (300 s). At CRITICAL_LEVEL (5 %) or below, it returns "critical".
    • New function play_low_sound(kind). It plays %WINDIR%\Media\Windows Battery Low.wav or Windows Battery Critical.wav with SND_FILENAME | SND_ASYNC | SND_NODEFAULT. If the file is missing, it plays the SystemExclamation or SystemHand alias with SND_ALIAS | SND_ASYNC. SND_ASYNC makes sure that the poll thread does not wait. An error from winsound goes to the log only.
    • App.check_alert() calls the new App.check_low_sound() after the existing notification code. The first sound plays together with the notification. When the device charges or goes above the alert level + 5 % (the existing re-arm rule), the repeat timer resets. The sound stops when the device charges, goes above the alert level, goes offline or goes away, or when the user turns the option off.
  • README.md: the new item is in the Preferences list of "Tray menu".
  • CHANGELOG.md: entry under [Unreleased] > ### Added.

Not in this change (possible follow-up): a custom sound file, and the "3 times at a 1 minute interval" repeat for the critical level from the issue. The repeat interval is a constant, not a setting.

Overlap with #107 (open): #107 changes check_alert() (per-device alert level low_for(st), and self.notify() holds notifications while a game is full screen). #107 does not add a sound. A trial merge of this branch with #107 merges check_alert() without a conflict, and check_low_sound() then uses the per-device level. The sound does not go through self.notify(), so "Quiet while gaming" does not hold it. This is the purpose of #66. The only new conflict is in the Preferences item list, next to "Alert when fully charged", where #107 adds "Estimated time left" and "Quiet while gaming". main and #107 already conflict in the same block.

3. Tests

New file tests/test_low_sound.py:

  • LowSoundDecisionTests (the pure helper): test_repeat_is_five_minutes, test_first_alert_plays_the_low_sound, test_repeats_after_five_minutes, test_not_before_five_minutes, test_critical_sound_at_5_percent_or_below, test_no_sound_while_charging, test_no_sound_above_the_threshold, test_no_sound_when_offline_or_level_unknown, test_no_sound_when_the_low_battery_alert_is_off.
  • LowSoundWiringTests (winsound replaced by a mock): test_off_by_default, test_alert_plays_windows_battery_low_async, test_critical_level_plays_windows_battery_critical, test_missing_file_falls_back_to_a_system_sound, test_repeats_every_five_minutes_while_low, test_stops_when_charging_and_starts_again_after, test_stops_above_the_threshold, test_stops_while_offline, test_stops_when_the_option_is_turned_off, test_a_sound_error_does_not_break_the_poll, test_setting_is_in_preferences_and_toggles, test_setting_is_read_from_the_settings_file.

tests/test_hide_rename.py: the test harness sets low_sound_at, and the pinned Preferences list has the new item.

Suite: python -m unittest discover -s tests: 262 tests, OK.

Mutation check: 19 mutations of the new code (default on, each condition of the helper, the 300 s and 5 % constants, the flags, the fallback, the try/except, the timer reset, the call in check_alert(), the option check, the timestamp, the menu item). The new tests catch all 19.

Manual check on Windows 11: play_low_sound("low") returns in 0.002 s and plays the sound.

Base and other PRs

Preferences > "Sound with the low battery alert", off by default. When
the low battery alert fires, also play Windows' own "Windows Battery
Low.wav" ("Windows Battery Critical.wav" at 5 % or less) from
%WINDIR%\Media, asynchronously, with a system sound as fallback. It
repeats every 5 minutes while the device stays at or below the alert
level, awake and off the charger. The decision is a pure helper,
low_battery_sound(), covered by unit tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QmGsnjgXs8VbKmmxLkPsf6
@HeyOkay

HeyOkay commented Sep 29, 2026

Copy link
Copy Markdown
Owner

Thanks for this! PR #107 and a batch of other PRs were just merged into main (the listen-only PlayStation handling from e80dfd1 is kept, and make_providers() in halo_battery.pyw is now the single provider list), so this PR now conflicts with main. Could you merge main into it or rebase it? A new provider also needs an entry in make_providers() and PROVIDER_LABELS (the Device types menu) — test_every_provider_has_a_label checks that. Once it is green on top of main I'll merge it for 1.13.0.

@casparas123

Copy link
Copy Markdown
Contributor

Trial-merged against today's main: stops in CHANGELOG.md, README.md, halo_battery.pyw and tests/test_hide_rename.py - all from main gaining two newer menu items ("Estimated time left", "Quiet while gaming") after this branched.

Suite passes with that (443 tests, including the new sound test).

@HeyOkay HeyOkay added this to the v1.14.0 milestone Sep 29, 2026
Resolved CHANGELOG.md (bullet under Unreleased/Added), README.md, the
Preferences menu and its order test: "Sound with the low battery alert" goes
after main's "Estimated time left" and "Quiet while gaming". The sound uses
main's per-device level (low_for), is reset with the alert when a device is
hidden, its type turned off or its level changed, and still plays while Quiet
while gaming holds the notification (new tests for both).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QmGsnjgXs8VbKmmxLkPsf6
@ahmedkhursheed23

Copy link
Copy Markdown
Contributor Author

Merged main (1.13.0) into this branch; it is mergeable again. "Sound with the low battery alert" comes after "Estimated time left" and "Quiet while gaming" (menu, test and README). The sound uses the per-device alert level (low_for(st)), and it plays also while Quiet while gaming holds the notification, which is the point of #66. Its timer is reset where main resets alerted (hide, device type off, a new alert level). Full suite: OK.

@HeyOkay
HeyOkay merged commit a392d2a into HeyOkay:main Oct 5, 2026
HeyOkay added a commit that referenced this pull request Oct 5, 2026
casparas123 pushed a commit to casparas123/HaloBattery that referenced this pull request Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Empty Battery Warning Sound Alert with a Repeat and Interval Feature

3 participants