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
This commit is contained in:
parent
c899ec1c5a
commit
f3c539ad78
237
i3pystatus/buds.py
Normal file
237
i3pystatus/buds.py
Normal file
@ -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, "?")
|
48
tests/test_buds.json
Normal file
48
tests/test_buds.json
Normal file
@ -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
|
||||
}
|
||||
}
|
542
tests/test_buds.py
Normal file
542
tests/test_buds.py
Normal file
@ -0,0 +1,542 @@
|
||||
import json
|
||||
import unittest
|
||||
from copy import deepcopy
|
||||
from unittest.mock import patch
|
||||
from i3pystatus.core.color import ColorRangeModule
|
||||
from i3pystatus.buds import Buds, BudsEqualizer, BudsPlacementStatus
|
||||
|
||||
|
||||
class TestBuds(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.buds = Buds()
|
||||
with open('test_buds.json', 'rb') as file:
|
||||
self.payload = json.load(file)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_run_device_connected(self, mock_run):
|
||||
# Setup: Use json.dumps as we expect JSON
|
||||
payload = self.payload.get('connected_payload')
|
||||
mock_run.return_value.out = json.dumps(payload)
|
||||
|
||||
# Action: Call run() and save return for comparison
|
||||
buds_run_return = self.buds.run()
|
||||
|
||||
# Verify: Assert called with right params
|
||||
mock_run.assert_called_with(f"{self.buds.earbuds_binary} status -o json -q")
|
||||
|
||||
expected_output = {
|
||||
"full_text": "Buds2 LW53 48RW",
|
||||
"color": self.buds.get_gradient(
|
||||
48,
|
||||
self.buds.colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
|
||||
# Verify: Assert correct output
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
# Verify: run() return is equal to payload
|
||||
self.assertDictEqual(payload.get('payload'), buds_run_return)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_run_device_disconnected(self, mock_run):
|
||||
# Setup: Use json.dumps as we expect JSON
|
||||
mock_run.return_value.out = json.dumps(self.payload.get('disconnected_payload'))
|
||||
|
||||
# Action: Call run() and save return for comparison
|
||||
buds_run_return = self.buds.run()
|
||||
|
||||
# Verify: Assert called with right params
|
||||
mock_run.assert_called_with(f"{self.buds.earbuds_binary} status -o json -q")
|
||||
|
||||
expected_output = {
|
||||
"full_text": "Disconnected",
|
||||
"color": self.buds.disconnected_color
|
||||
}
|
||||
|
||||
# Verify: Assert correct output
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
# Verify: run() return should be none
|
||||
self.assertIsNone(buds_run_return)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_toggle_amb(self, mock_run):
|
||||
# Setup: AMB is initially disabled
|
||||
modified_payload = deepcopy(self.payload.get('connected_payload'))
|
||||
modified_payload['payload']['ambient_sound_enabled'] = False
|
||||
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Toggle AMB
|
||||
self.buds.toggle_amb()
|
||||
|
||||
# Verify: The correct command is sent to enable AMB
|
||||
mock_run.assert_called_with(f"{self.buds.earbuds_binary} set ambientsound 1")
|
||||
|
||||
# Setup: Change the payload again to update the AMB status
|
||||
modified_payload['payload']['ambient_sound_enabled'] = True
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call run again to update output
|
||||
self.buds.run()
|
||||
|
||||
# Verify: The output correctly displays AMB is enabled
|
||||
expected_output = {
|
||||
"full_text": "Buds2 LW53 48RW AMB",
|
||||
"color": self.buds.get_gradient(
|
||||
48,
|
||||
self.buds.colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
|
||||
# Action: Toggle AMB again
|
||||
self.buds.toggle_amb()
|
||||
|
||||
# Verify: The correct command is sent to disable AMB this time
|
||||
mock_run.assert_called_with(f"{self.buds.earbuds_binary} set ambientsound 0")
|
||||
|
||||
# Setup: Change the payload one last time to update the AMB status
|
||||
modified_payload['payload']['ambient_sound_enabled'] = False
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call run again to update output
|
||||
self.buds.run()
|
||||
|
||||
# Verify: The output correctly displays AMB is disabled
|
||||
expected_output = {
|
||||
"full_text": "Buds2 LW53 48RW",
|
||||
"color": self.buds.get_gradient(
|
||||
48,
|
||||
self.buds.colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_toggle_anc(self, mock_run):
|
||||
# Setup: ANC is initially disabled
|
||||
modified_payload = deepcopy(self.payload.get('connected_payload'))
|
||||
modified_payload['payload']['noise_reduction'] = False
|
||||
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Toggle ANC
|
||||
self.buds.toggle_anc()
|
||||
|
||||
# Verify: The correct command is sent to enable ANC
|
||||
mock_run.assert_called_with(f"{self.buds.earbuds_binary} set anc true")
|
||||
|
||||
# Setup: Change the payload again to update the ANC status
|
||||
modified_payload['payload']['noise_reduction'] = True
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call run again to update output
|
||||
self.buds.run()
|
||||
|
||||
# Verify: The output correctly displays ANC is enabled
|
||||
expected_output = {
|
||||
"full_text": "Buds2 LW53 48RW ANC",
|
||||
"color": self.buds.get_gradient(
|
||||
48,
|
||||
self.buds.colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
|
||||
# Action: Toggle ANC again
|
||||
self.buds.toggle_anc()
|
||||
|
||||
# Verify: The correct command is sent to disable ANC this time
|
||||
mock_run.assert_called_with(f"{self.buds.earbuds_binary} set anc false")
|
||||
|
||||
# Setup: Change the payload one last time to update the ANC status
|
||||
modified_payload['payload']['noise_reduction'] = False
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call run again to update output
|
||||
self.buds.run()
|
||||
|
||||
# Verify: The output correctly displays ANC is disabled
|
||||
expected_output = {
|
||||
"full_text": "Buds2 LW53 48RW",
|
||||
"color": self.buds.get_gradient(
|
||||
48,
|
||||
self.buds.colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_combined_battery(self, mock_run):
|
||||
# Setup: Equal left and right battery value
|
||||
modified_payload = deepcopy(self.payload.get('connected_payload'))
|
||||
modified_payload['payload']['batt_left'] = modified_payload['payload']['batt_right']
|
||||
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call run() to update the output
|
||||
self.buds.run()
|
||||
|
||||
expected_output = {
|
||||
"full_text": "Buds2 LW48RW",
|
||||
"color": self.buds.get_gradient(
|
||||
48,
|
||||
self.buds.colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
|
||||
# Verify: The output correctly displays combined battery status
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
|
||||
# Setup: Different left and right battery value
|
||||
mock_run.return_value.out = json.dumps(self.payload.get('connected_payload'))
|
||||
|
||||
# Action: Call run() again to update the output
|
||||
self.buds.run()
|
||||
|
||||
expected_output = {
|
||||
"full_text": "Buds2 LW53 48RW",
|
||||
"color": self.buds.get_gradient(
|
||||
48,
|
||||
self.buds.colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
|
||||
# Verify: The output correctly displays combined battery status
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_combined_battery_drift(self, mock_run):
|
||||
# Setup: Different battery level, should show smaller
|
||||
modified_payload = deepcopy(self.payload.get('connected_payload'))
|
||||
modified_payload['payload']['batt_left'] = modified_payload['payload']['batt_right']
|
||||
modified_payload['payload']['batt_left'] -= self.buds.battery_drift_threshold
|
||||
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call run() to update the output
|
||||
self.buds.run()
|
||||
|
||||
expected_level = min(modified_payload['payload']['batt_left'], modified_payload['payload']['batt_right'])
|
||||
expected_output = {
|
||||
# Verify: The level should be the smallest one
|
||||
"full_text":
|
||||
f"Buds2 LW{expected_level}RW",
|
||||
"color": self.buds.get_gradient(
|
||||
expected_level,
|
||||
self.buds.colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
|
||||
# Verify: The output correctly displays combined battery status
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
|
||||
# Setup: One battery is at level 0, should show the other
|
||||
modified_payload = deepcopy(self.payload.get('connected_payload'))
|
||||
modified_payload['payload']['batt_left'] = 0
|
||||
modified_payload['payload']['batt_right'] = 0 + self.buds.battery_drift_threshold
|
||||
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call run() again to update the output
|
||||
self.buds.run()
|
||||
|
||||
expected_level = max(modified_payload['payload']['batt_left'], modified_payload['payload']['batt_right'])
|
||||
expected_output = {
|
||||
# Verify: The level should be the biggest one
|
||||
"full_text":
|
||||
f"Buds2 LW{expected_level}RW",
|
||||
"color": self.buds.get_gradient(
|
||||
expected_level,
|
||||
self.buds.colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
|
||||
# Verify: The output correctly displays combined battery status
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_combined_battery_drift_case(self, mock_run):
|
||||
# Setup: Change status of one buds to be on the case
|
||||
modified_payload = deepcopy(self.payload.get('connected_payload'))
|
||||
modified_payload['payload']['placement_left'] = BudsPlacementStatus.case
|
||||
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call run() to update the output
|
||||
self.buds.run()
|
||||
|
||||
expected_output = {
|
||||
"full_text": f"Buds2 LC53 48RW 88C",
|
||||
"color": self.buds.get_gradient(
|
||||
48,
|
||||
self.buds.colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
|
||||
# Verify: The output correctly displays combined battery status
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_connect(self, mock_run):
|
||||
# Action: Call connect
|
||||
self.buds.connect()
|
||||
|
||||
# Verify: The correct command is sent to connect
|
||||
mock_run.assert_called_with(f"{self.buds.earbuds_binary} connect")
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_disconnect(self, mock_run):
|
||||
# Action: Call disconnect
|
||||
self.buds.disconnect()
|
||||
|
||||
# Verify: The correct command is sent to disconnect
|
||||
mock_run.assert_called_with(f"{self.buds.earbuds_binary} disconnect")
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_restart_daemin(self, mock_run):
|
||||
# Action: Call restart_daemon
|
||||
self.buds.restart_daemon()
|
||||
|
||||
# Verify: The correct command is sent to restart the daemon
|
||||
mock_run.assert_called_with(f"{self.buds.earbuds_binary} -kd")
|
||||
|
||||
def run_placement_helper(self, mock_run, placement_left, placement_right, case_battery, expected_display):
|
||||
modified_payload = deepcopy(self.payload.get('connected_payload'))
|
||||
modified_payload['payload']['placement_left'] = placement_left
|
||||
modified_payload['payload']['placement_right'] = placement_right
|
||||
if case_battery is not None:
|
||||
modified_payload['payload']['batt_case'] = case_battery
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
self.buds.run()
|
||||
|
||||
expected_output = {
|
||||
"full_text": expected_display,
|
||||
"color": self.buds.get_gradient(
|
||||
48,
|
||||
self.buds.colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_placement_wearing(self, mock_run):
|
||||
self.run_placement_helper(
|
||||
mock_run,
|
||||
BudsPlacementStatus.wearing.value,
|
||||
BudsPlacementStatus.wearing.value,
|
||||
None,
|
||||
"Buds2 LW53 48RW"
|
||||
)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_placement_idle(self, mock_run):
|
||||
self.run_placement_helper(
|
||||
mock_run,
|
||||
BudsPlacementStatus.idle.value,
|
||||
BudsPlacementStatus.idle.value,
|
||||
None,
|
||||
"Buds2 LI53 48RI"
|
||||
)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_placement_case_with_battery(self, mock_run):
|
||||
# Verify: Case battery is returned if a bud is on the case
|
||||
self.run_placement_helper(
|
||||
mock_run,
|
||||
BudsPlacementStatus.case.value,
|
||||
BudsPlacementStatus.case.value,
|
||||
88,
|
||||
"Buds2 LC53 48RC 88C"
|
||||
)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_battery_level_dynamic_color(self, mock_run):
|
||||
# Setup: Build the colors array independently of our class
|
||||
colors = ColorRangeModule.get_hex_color_range(
|
||||
self.buds.end_color,
|
||||
self.buds.start_color,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
modified_payload = deepcopy(self.payload.get('connected_payload'))
|
||||
|
||||
for battery_level in range(0, self.buds.battery_limit + 1):
|
||||
# Setup: Make both levels equal
|
||||
modified_payload['payload']['batt_left'] = battery_level
|
||||
modified_payload['payload']['batt_right'] = battery_level
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call run() again to update the output
|
||||
self.buds.run()
|
||||
|
||||
expected_output = {
|
||||
"full_text": f"Buds2 LW{battery_level}RW",
|
||||
"color": self.buds.get_gradient(
|
||||
battery_level,
|
||||
colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_set_equalizer_direct(self, mock_run):
|
||||
for eq_setting in BudsEqualizer:
|
||||
with self.subTest(msg=f"Failed testing equalizer {eq_setting.name}", eq_setting=eq_setting):
|
||||
# Setup: Create a copy of the payload
|
||||
modified_payload = deepcopy(self.payload.get('connected_payload'))
|
||||
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call the set function with each equalizer setting
|
||||
self.buds.equalizer_set(eq_setting)
|
||||
|
||||
expected_command = f"{self.buds.earbuds_binary} set equalizer {eq_setting.name}"
|
||||
|
||||
# Verify: Correct equalizer command is used
|
||||
mock_run.assert_called_with(expected_command)
|
||||
|
||||
# Setup: Modify payload to verify output
|
||||
modified_payload['payload']['equalizer_type'] = eq_setting.value
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call run() again to update the output
|
||||
self.buds.run()
|
||||
|
||||
expected_equalizer = f" {eq_setting.name.capitalize()}" if eq_setting.name != "off" else ""
|
||||
expected_output = {
|
||||
"full_text": f"Buds2 LW53 48RW{expected_equalizer}",
|
||||
"color": self.buds.get_gradient(
|
||||
48,
|
||||
self.buds.colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
|
||||
# Verify: Output was updated with equalizer
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_increment_equalizer(self, mock_run):
|
||||
# Setup: Create a copy of the payload
|
||||
modified_payload = deepcopy(self.payload.get('connected_payload'))
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call the set to increment by one the equalizer setting
|
||||
self.buds.equalizer_set(+1)
|
||||
|
||||
# Verify: Correct equalizer command is used
|
||||
expected_command = f"{self.buds.earbuds_binary} set equalizer {BudsEqualizer.bass.name}"
|
||||
mock_run.assert_called_with(expected_command)
|
||||
|
||||
# Setup: Modify payload to verify output
|
||||
modified_payload['payload']['equalizer_type'] = BudsEqualizer.bass.value
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call run() again to update the output
|
||||
self.buds.run()
|
||||
|
||||
expected_equalizer = f" {BudsEqualizer.bass.name.capitalize()}"
|
||||
expected_output = {
|
||||
"full_text": f"Buds2 LW53 48RW{expected_equalizer}",
|
||||
"color": self.buds.get_gradient(
|
||||
48,
|
||||
self.buds.colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
|
||||
# Verify: Output was updated with equalizer
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_decrement_equalizer_from_off(self, mock_run):
|
||||
# Setup: Create a copy of the payload
|
||||
modified_payload = deepcopy(self.payload.get('connected_payload'))
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call the set to decrement by one the equalizer setting
|
||||
self.buds.equalizer_set(-1)
|
||||
|
||||
# Verify: Correct equalizer command is used
|
||||
expected_command = f"{self.buds.earbuds_binary} set equalizer {BudsEqualizer.treble.name}"
|
||||
mock_run.assert_called_with(expected_command)
|
||||
|
||||
# Setup: Modify payload to verify output
|
||||
modified_payload['payload']['equalizer_type'] = BudsEqualizer.treble.value
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call run() again to update the output
|
||||
self.buds.run()
|
||||
|
||||
expected_equalizer = f" {BudsEqualizer.treble.name.capitalize()}"
|
||||
expected_output = {
|
||||
"full_text": f"Buds2 LW53 48RW{expected_equalizer}",
|
||||
"color": self.buds.get_gradient(
|
||||
48,
|
||||
self.buds.colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
|
||||
# Verify: Output was updated with equalizer
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
|
||||
def run_touchpad_set(self, mock_run, setting_value):
|
||||
# Setup: Create a copy of the payload
|
||||
modified_payload = deepcopy(self.payload.get('connected_payload'))
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call the set with the appropriate setting
|
||||
self.buds.touchpad_set(f'{setting_value}')
|
||||
|
||||
# Verify: Correct command to disable the touchpad is called
|
||||
expected_command = f"{self.buds.earbuds_binary} set touchpad {setting_value}"
|
||||
mock_run.assert_called_with(expected_command)
|
||||
|
||||
# Setup: Modify the payload if we are disabling the touchpad
|
||||
if setting_value == 'false':
|
||||
modified_payload['payload']['tab_lock_status']['touch_an_hold_on'] = False
|
||||
modified_payload['payload']['tab_lock_status']['tap_on'] = False
|
||||
mock_run.return_value.out = json.dumps(modified_payload)
|
||||
|
||||
# Action: Call run() again to update the output
|
||||
self.buds.run()
|
||||
|
||||
# Setup:
|
||||
expected_output = {
|
||||
"full_text": f"Buds2 LW53 48RW{' TL' if setting_value == 'false' else ''}",
|
||||
"color": self.buds.get_gradient(
|
||||
48,
|
||||
self.buds.colors,
|
||||
self.buds.battery_limit
|
||||
)
|
||||
}
|
||||
|
||||
# Verify: Output was updated with equalizer
|
||||
self.assertEqual(expected_output, self.buds.output)
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_touchpad_disable(self, mock_run):
|
||||
self.run_touchpad_set(mock_run, "false")
|
||||
|
||||
@patch('i3pystatus.buds.run_through_shell')
|
||||
def test_touchpad_enable(self, mock_run):
|
||||
self.run_touchpad_set(mock_run, "true")
|
Loading…
Reference in New Issue
Block a user