-
Notifications
You must be signed in to change notification settings - Fork 188
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduces a plugin to display information about Galaxy Buds devices …
…using (#860) the earbuds tool from https://github.com/JojiiOfficial/LiveBudsCli * Displays per bud battery level * Displays case battery level * Displays placement status * Displays and sets both Ambient sound (AMB) and Active noise control (ANC) * Has commands to connect/disconnect * Dynamic coloring based on battery level * Equalizer setting * Touchpad lock
- Loading branch information
1 parent
c899ec1
commit f3c539a
Showing
3 changed files
with
827 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
from enum import Enum, IntEnum | ||
from json import JSONDecodeError, loads | ||
from i3pystatus import IntervalModule | ||
from i3pystatus.core.command import run_through_shell | ||
from i3pystatus.core.color import ColorRangeModule | ||
|
||
|
||
class BudsEqualizer(Enum): | ||
off = 0 | ||
bass = 1 | ||
soft = 2 | ||
dynamic = 3 | ||
clear = 4 | ||
treble = 5 | ||
|
||
|
||
class BudsPlacementStatus(IntEnum): | ||
wearing = 1 | ||
idle = 2 | ||
case = 3 | ||
|
||
|
||
class Buds(IntervalModule, ColorRangeModule): | ||
earbuds_binary = "earbuds" | ||
|
||
""" | ||
Displays information about Galaxy Buds devices | ||
Requires the earbuds tool from https://github.com/JojiiOfficial/LiveBudsCli | ||
.. rubric :: Available formatters | ||
* {amb} Displays the current ambient sound control status. | ||
* {anc} Displays the current active noise control status. | ||
* {battery} Displays combined battery level for left and right. | ||
If both are at the same level, it simply returns the battery level. | ||
If they have different levels and the drift threshold is enabled, provided | ||
they do not exceed the threshold, display the smaller level. | ||
If they have different battery levels, it returns both levels, if the threshold | ||
is exceeded. | ||
* `{left_battery}` Displays the left bud battery level. | ||
* `{right_battery}` Displays the right bud battery level. | ||
* `{battery_case} Displays the case battery level, if one of the buds is on the case. | ||
* `{device_model}` The model of the device. | ||
* `{equalizer} Displays current equalizer setting, only if the equalizer is on. | ||
* `{placement_left}` A placement indicator for the left bud, if it's on the (C)ase, (I)dle or being (W)ear. | ||
* `{placement_right}` A placement indicator for the right bud, if it's on the (C)ase, (I)dle or being (W)ear. | ||
* `{touchpad}` Displays if the touchpad is locked, and only if it is locked. A T(ouchpad)L(ocked) string indicates | ||
the touchpad is locked. | ||
""" | ||
|
||
settings = ( | ||
("format", "Format string used for output"), | ||
("interval", "Interval to run the module"), | ||
("hide_no_device", "Hide the output if no device is connected"), | ||
("battery_drift_threshold", "Drift threshold."), | ||
("use_battery_drift_threshold", "Whether to display combined or separate levels, based on drift"), | ||
("connected_color", "Output color for when the device is connected"), | ||
("disconnected_color", "Output color for when the device is disconnected"), | ||
("dynamic_color", "Output color based on battery level. Overrides connected_color"), | ||
("start_color", "Hex or English name for start of color range, eg '#00FF00' or 'green'"), | ||
("end_color", "Hex or English name for end of color range, eg '#FF0000' or 'red'") | ||
) | ||
|
||
format = ( | ||
"{device_model} " | ||
"L{placement_left}" | ||
"{battery}" | ||
"R{placement_right}" | ||
"{battery_case}" | ||
"{amb}" | ||
"{anc}" | ||
"{equalizer}" | ||
"{touchpad}" | ||
) | ||
hide_no_device = False | ||
battery_limit = 100 | ||
battery_drift_threshold = 3 | ||
use_battery_drift_threshold = True | ||
|
||
connected_color = "#00FF00" | ||
disconnected_color = "#FFFFFF" | ||
dynamic_color = True | ||
colors = [] | ||
|
||
on_leftclick = 'toggle_anc' | ||
on_rightclick = 'toggle_amb' | ||
on_doubleleftclick = 'connect' | ||
on_doublerightclick = 'disconnect' | ||
on_middleclick = ['equalizer_set', BudsEqualizer.off] | ||
on_downscroll = ['equalizer_set', -1] | ||
on_upscroll = ['equalizer_set', +1] | ||
on_doublemiddleclick = 'restart_daemon' | ||
on_doubleupscroll = ['touchpad_set', 'true'] | ||
on_doubledownscroll = ['touchpad_set', 'false'] | ||
|
||
def init(self): | ||
if not self.dynamic_color: | ||
self.end_color = self.start_color = self.connected_color | ||
# battery discharges from battery_limit to 0 | ||
self.colors = self.get_hex_color_range(self.end_color, self.start_color, self.battery_limit) | ||
|
||
def run(self): | ||
try: | ||
status = loads(run_through_shell(f"{self.earbuds_binary} status -o json -q").out) | ||
except JSONDecodeError: | ||
self.output = None | ||
else: | ||
payload = status.get("payload") | ||
if payload: | ||
amb = payload.get("ambient_sound_enabled") | ||
anc = payload.get("noise_reduction") | ||
left_battery = payload.get("batt_left") | ||
right_battery = payload.get("batt_right") | ||
equalizer_type = BudsEqualizer(payload.get("equalizer_type", 0)) | ||
placement_left = payload.get("placement_left") | ||
placement_right = payload.get("placement_right") | ||
tab_lock_status = payload.get("tab_lock_status") | ||
# determine touchpad lock status | ||
touchpad = "" | ||
if tab_lock_status: | ||
touch_an_hold_on = tab_lock_status.get("touch_an_hold_on") | ||
tap_on = tab_lock_status.get("tap_on") | ||
if not touch_an_hold_on and not tap_on: | ||
touchpad = " TL" | ||
|
||
# determine battery level and color to display | ||
battery_display = f"{left_battery} {right_battery}" | ||
color = self.connected_color | ||
combined_level = min(left_battery, right_battery) | ||
# if one bud has battery depleted, invert the logic. | ||
if left_battery == 0 or right_battery == 0: | ||
combined_level = max(left_battery, right_battery) | ||
if self.use_battery_drift_threshold: | ||
drift = abs(left_battery - right_battery) | ||
# only use drift if buds aren't on case, otherwise show both. | ||
if drift <= self.battery_drift_threshold and not ( | ||
placement_left == BudsPlacementStatus.case or | ||
placement_right == BudsPlacementStatus.case | ||
): | ||
|
||
battery_display = f"{combined_level}" | ||
|
||
if self.dynamic_color: | ||
color = self.get_gradient( | ||
combined_level, | ||
self.colors, | ||
self.battery_limit | ||
) | ||
|
||
fdict = { | ||
"amb": " AMB" if amb else "", | ||
"anc": " ANC" if anc else "", | ||
"battery": battery_display, | ||
"left_battery": left_battery, | ||
"right_battery": right_battery, | ||
"battery_case": | ||
f' {payload.get("batt_case")}C' if placement_left == BudsPlacementStatus.case | ||
or placement_right == BudsPlacementStatus.case else "", | ||
"device_model": payload.get("model"), | ||
"equalizer": "" if equalizer_type == BudsEqualizer.off else f" {equalizer_type.name.capitalize()}", | ||
"placement_left": self.translate_placement(placement_left), | ||
"placement_right": self.translate_placement(placement_right), | ||
"touchpad": touchpad | ||
} | ||
|
||
self.output = { | ||
"full_text": self.format.format(**fdict), | ||
"color": color | ||
} | ||
|
||
return payload | ||
else: | ||
if not self.hide_no_device: | ||
self.output = { | ||
"full_text": "Disconnected", | ||
"color": self.disconnected_color | ||
} | ||
else: | ||
self.output = None | ||
|
||
return | ||
|
||
def connect(self): | ||
run_through_shell(f"{self.earbuds_binary} connect") | ||
|
||
def disconnect(self): | ||
run_through_shell(f"{self.earbuds_binary} disconnect") | ||
|
||
def equalizer_set(self, adjustment): | ||
payload = self.run() | ||
if payload: | ||
current_eq = int(payload.get("equalizer_type", 0)) # Default to 0 if not found | ||
|
||
if isinstance(adjustment, BudsEqualizer): | ||
new_eq_value = adjustment.value | ||
else: # Adjustment is -1 or +1 | ||
# Calculate new equalizer setting, ensuring it wraps correctly within bounds | ||
new_eq_value = (current_eq + adjustment) % len(BudsEqualizer) | ||
|
||
# Find the enum member corresponding to the new equalizer value | ||
new_eq_setting = BudsEqualizer(new_eq_value) | ||
|
||
# Execute the command with the new equalizer setting | ||
run_through_shell(f"{self.earbuds_binary} set equalizer {new_eq_setting.name}") | ||
|
||
def restart_daemon(self): | ||
run_through_shell(f"{self.earbuds_binary} -kd") | ||
|
||
def toggle_amb(self): | ||
payload = self.run() | ||
if payload: | ||
amb = payload.get("ambient_sound_enabled") | ||
if amb: | ||
run_through_shell(f"{self.earbuds_binary} set ambientsound 0") | ||
else: | ||
run_through_shell(f"{self.earbuds_binary} set ambientsound 1") | ||
|
||
def toggle_anc(self): | ||
payload = self.run() | ||
if payload: | ||
anc = payload.get("noise_reduction") | ||
if anc: | ||
run_through_shell(f"{self.earbuds_binary} set anc false") | ||
else: | ||
run_through_shell(f"{self.earbuds_binary} set anc true") | ||
|
||
def touchpad_set(self, setting): | ||
run_through_shell(f"{self.earbuds_binary} set touchpad {setting}") | ||
|
||
@staticmethod | ||
def translate_placement(placement): | ||
mapping = { | ||
BudsPlacementStatus.wearing.value: "W", | ||
BudsPlacementStatus.idle.value: "I", | ||
BudsPlacementStatus.case.value: "C", | ||
} | ||
return mapping.get(placement, "?") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
{ | ||
"connected_payload": { | ||
"status": "success", | ||
"device": "00:00:00:00:00:00", | ||
"status_message": null, | ||
"payload": { | ||
"address": "00:00:00:00:00:00", | ||
"ready": true, | ||
"batt_left": 53, | ||
"batt_right": 48, | ||
"batt_case": 88, | ||
"placement_left": 1, | ||
"placement_right": 1, | ||
"equalizer_type": 0, | ||
"touchpads_blocked": false, | ||
"noise_reduction": false, | ||
"did_battery_notify": false, | ||
"touchpad_option_left": 2, | ||
"touchpad_option_right": 2, | ||
"paused_music_earlier": false, | ||
"debug": { | ||
"voltage_left": 0.0, | ||
"voltage_right": 0.0, | ||
"temperature_left": 36.0, | ||
"temperature_right": 37.0, | ||
"current_left": 0.0, | ||
"current_right": 0.0 | ||
}, | ||
"model": "Buds2", | ||
"ambient_sound_enabled": false, | ||
"ambient_sound_volume": 0, | ||
"extra_high_ambient_volume": false, | ||
"tab_lock_status": { | ||
"touch_an_hold_on": true, | ||
"triple_tap_on": true, | ||
"double_tap_on": true, | ||
"tap_on": true, | ||
"touch_controls_on": true | ||
} | ||
} | ||
}, | ||
"disconnected_payload": { | ||
"status": "error", | ||
"device": "", | ||
"status_message": "No connected device found", | ||
"payload": null | ||
} | ||
} |
Oops, something went wrong.