Skip to content

Commit c4e830c

Browse files
committed
Change to use asyncio rather than threading
This allows event-driven things to be far more easily driven (ie. pulsectl) Also, Rather than defining a source / sync for the mute buttons, use the current default one - how often do you need to immediately mute a device that's not in use?
1 parent d96a162 commit c4e830c

17 files changed

+257
-177
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
__pycache__/
55
htmlcov/
66
venv/
7+
.vscode

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.10.0

devdeck/controls/clock_control.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
1+
import asyncio
2+
from asyncio.events import get_event_loop
13
import threading
24
from datetime import datetime
3-
from time import sleep
5+
from asyncio import sleep
46

57
from devdeck_core.controls.deck_control import DeckControl
68

79

810
class ClockControl(DeckControl):
911

10-
def __init__(self, key_no, **kwargs):
12+
def __init__(self, key_no: int, **kwargs):
13+
self.loop = get_event_loop()
1114
super().__init__(key_no, **kwargs)
12-
self.thread = None
13-
self.running = False
1415

1516
def initialize(self):
16-
self.thread = threading.Thread(target=self._update_display)
17-
self.running = True
18-
self.thread.start()
17+
self.loop.create_task(self._update_display())
1918

20-
def _update_display(self):
21-
while self.running is True:
19+
async def _update_display(self):
20+
while True:
2221
with self.deck_context() as context:
2322
now = datetime.now()
2423

@@ -33,10 +32,4 @@ def _update_display(self):
3332
.center_vertically(100) \
3433
.font_size(75) \
3534
.end()
36-
sleep(1)
37-
38-
def dispose(self):
39-
self.running = False
40-
if self.thread:
41-
self.thread.join()
42-
35+
await sleep(1)

devdeck/controls/command_control.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import logging
23
import os
34
from subprocess import Popen, DEVNULL
@@ -19,4 +20,4 @@ def pressed(self):
1920
try:
2021
Popen(self.settings['command'], stdout=DEVNULL, stderr=DEVNULL)
2122
except Exception as ex:
22-
self.__logger.error("Error executing command %s: %s", self.settings['command'], str(ex))
23+
self.__logger.error("Error executing command %s: %s", self.settings['command'], str(ex))
Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,70 @@
1+
from asyncio import sleep
2+
from asyncio.events import get_event_loop
13
import logging
24
import os
35

4-
from pulsectl import pulsectl
6+
import pulsectl_asyncio
57

68
from devdeck_core.controls.deck_control import DeckControl
79

810

911
class MicMuteControl(DeckControl):
1012

1113
def __init__(self, key_no, **kwargs):
14+
self.loop = get_event_loop()
1215
self.pulse = None
1316
self.__logger = logging.getLogger('devdeck')
1417
super().__init__(key_no, **kwargs)
1518

16-
def initialize(self):
19+
async def _init(self):
1720
if self.pulse is None:
18-
self.pulse = pulsectl.Pulse('MicMuteControl')
19-
self.__render_icon()
21+
self.pulse = pulsectl_asyncio.PulseAsync('MicMuteControl')
22+
await self.pulse.connect()
23+
self.loop.create_task(self._update_display())
24+
25+
def initialize(self):
26+
self.loop.create_task(self._init())
2027

2128
def pressed(self):
22-
mic = self.__get_mic()
23-
if mic is None:
24-
return
25-
self.pulse.source_mute(mic.index, mute=(not mic.mute))
26-
self.__render_icon()
29+
self.loop.create_task(self._handle_mute())
2730

28-
def __get_mic(self):
29-
sources = self.pulse.source_list()
31+
async def _handle_mute(self):
32+
mic = await self._get_source()
33+
self.loop.create_task(self.pulse.source_mute(mic.index, mute=(not mic.mute)))
3034

31-
selected_mic = [mic for mic in sources if mic.description == self.settings['microphone']]
32-
if len(selected_mic) == 0:
33-
possible_mics = [output.description for output in sources]
34-
self.__logger.warning("Microphone '%s' not found in list of possible inputs:\n%s",
35-
self.settings['microphone'],
36-
'\n'.join(possible_mics))
37-
return None
38-
return selected_mic[0]
35+
async def _get_source(self):
36+
sources = await self.pulse.source_list()
37+
server_info = await self.pulse.server_info()
38+
default_source_name = server_info.default_source_name
39+
return next((source for source in sources if source.name == default_source_name), None)
3940

40-
def __render_icon(self):
41+
async def _update_display(self):
4142
with self.deck_context() as context:
42-
mic = self.__get_mic()
43-
if mic is None:
44-
with context.renderer() as r:
45-
r \
46-
.text('MIC \nNOT FOUND') \
47-
.color('red') \
48-
.center_vertically() \
49-
.center_horizontally() \
50-
.font_size(85) \
51-
.text_align('center') \
52-
.end()
53-
return
54-
if mic.mute == 0:
55-
with context.renderer() as r:
56-
r.image(os.path.join(os.path.dirname(__file__), "../assets/font-awesome", 'microphone.png')).end()
57-
else:
43+
while True:
44+
mic = await self._get_source()
5845
with context.renderer() as r:
59-
r.image(os.path.join(os.path.dirname(__file__), "../assets/font-awesome", 'microphone-mute.png')).end()
46+
try:
47+
match mic.mute:
48+
case 0:
49+
r.image(os.path.join(os.path.dirname(__file__),
50+
"../assets/font-awesome", 'microphone.png')).end()
51+
case 1:
52+
r.image(os.path.join(os.path.dirname(__file__),
53+
"../assets/font-awesome", 'microphone-mute.png')).end()
54+
except AttributeError:
55+
r \
56+
.text('MIC \nNOT FOUND') \
57+
.color('red') \
58+
.center_vertically() \
59+
.center_horizontally() \
60+
.font_size(85) \
61+
.text_align('center') \
62+
.end()
63+
await sleep(0.1)
6064

6165
def settings_schema(self):
6266
return {
6367
'microphone': {
6468
'type': 'string'
6569
}
66-
}
70+
}

devdeck/controls/name_list_control.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import asyncio
12
import os
23

34
from devdeck_core.controls.deck_control import DeckControl
45

5-
66
class NameListControl(DeckControl):
77

88
def __init__(self, key_no, **kwargs):

devdeck/controls/timer_control.py

Lines changed: 58 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,80 @@
1-
import datetime
1+
from asyncio.events import get_event_loop
2+
from datetime import datetime
23
import os
3-
import threading
4+
import enum
5+
import asyncio
46
from time import sleep
57

68
from devdeck_core.controls.deck_control import DeckControl
79

810

11+
class TimerState(enum.Enum):
12+
RUNNING = 1
13+
STOPPED = 2
14+
RESET = 3
15+
16+
917
class TimerControl(DeckControl):
1018

1119
def __init__(self, key_no, **kwargs):
12-
self.start_time = None
13-
self.end_time = None
14-
self.thread = None
15-
super().__init__(key_no, **kwargs)
20+
self.loop = get_event_loop()
21+
self.start_time: datetime = None
22+
self.end_time: datetime = None
23+
self.state = TimerState.RESET
24+
super().__init__(key_no, ** kwargs)
1625

1726
def initialize(self):
27+
self.loop.create_task(self._update_display())
1828
with self.deck_context() as context:
1929
with context.renderer() as r:
20-
r.image(os.path.join(os.path.dirname(__file__), "../assets/font-awesome", 'stopwatch.png')).end()
30+
r.image(os.path.join(os.path.dirname(__file__),
31+
"../assets/font-awesome", 'stopwatch.png')).end()
2132

2233
def pressed(self):
23-
if self.start_time is None:
24-
self.start_time = datetime.datetime.now()
25-
self.thread = threading.Thread(target=self._update_display)
26-
self.thread.start()
27-
elif self.end_time is None:
28-
self.end_time = datetime.datetime.now()
29-
self.thread.join()
30-
with self.deck_context() as context:
31-
with context.renderer() as r:
32-
r.text(TimerControl.time_diff_to_str(self.end_time - self.start_time))\
33-
.font_size(120)\
34-
.color('red')\
35-
.center_vertically().center_horizontally().end()
36-
else:
37-
self.start_time = None
38-
self.end_time = None
39-
with self.deck_context() as context:
40-
with context.renderer() as r:
41-
r.image(os.path.join(
42-
os.path.join(os.path.dirname(__file__), "../assets/font-awesome", 'stopwatch.png'))).end()
43-
44-
def _update_display(self):
45-
while self.end_time is None:
46-
if self.start_time is None:
47-
sleep(1)
48-
continue
49-
cutoff = datetime.datetime.now() if self.end_time is None else self.end_time
34+
match self.state:
35+
case TimerState.RESET:
36+
self.start_time = datetime.now()
37+
self.end_time = None
38+
self.state = TimerState.RUNNING
39+
case TimerState.RUNNING:
40+
if not self.start_time:
41+
raise Exception("how did you get here?")
42+
self.end_time = datetime.now()
43+
self.state = TimerState.STOPPED
44+
case TimerState.STOPPED:
45+
self.start_time = self.end_time = None
46+
self.state = TimerState.RESET
47+
48+
async def _update_display(self, repeat=True):
49+
while True:
5050
with self.deck_context() as context:
5151
with context.renderer() as r:
52-
r.text(TimerControl.time_diff_to_str(cutoff - self.start_time)) \
53-
.font_size(120) \
54-
.center_vertically().center_horizontally().end()
55-
sleep(1)
52+
match self.state:
53+
case TimerState.RUNNING:
54+
r.text(TimerControl.time_diff_to_str(datetime.now() - self.start_time))\
55+
.font_size(120)\
56+
.color('red')\
57+
.center_vertically().center_horizontally().end()
58+
case TimerState.STOPPED:
59+
r.text(TimerControl.time_diff_to_str(self.end_time - self.start_time))\
60+
.font_size(120)\
61+
.color('yellow')\
62+
.center_vertically().center_horizontally().end()
63+
case _:
64+
r.image(os.path.join(
65+
os.path.join(os.path.dirname(__file__), "../assets/font-awesome", 'stopwatch.png'))).end()
66+
if repeat:
67+
await asyncio.sleep(0.1)
68+
else:
69+
return
5670

5771
@staticmethod
5872
def time_diff_to_str(diff):
59-
seconds = diff.total_seconds()
60-
minutes, seconds = divmod(seconds, 60)
73+
total_seconds = diff.total_seconds()
74+
minutes, seconds = divmod(total_seconds, 60)
6175
hours, minutes = divmod(minutes, 60)
76+
if total_seconds < 60:
77+
return f'{int(seconds):02d}'
78+
elif total_seconds < 3600:
79+
return f'{int(minutes):02d}:{int(seconds):02d}'
6280
return f'{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}'

devdeck/controls/volume_level_control.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import asyncio
2+
from asyncio.events import get_event_loop
13
import logging
24
import os
35

@@ -8,11 +10,12 @@
810

911
class VolumeLevelControl(DeckControl):
1012

11-
def __init__(self, key_no, **kwargs):
13+
def __init__(self, key_no, **kwargs):
14+
self.loop = get_event_loop(),
1215
self.pulse = None
1316
self.volume = None
1417
self.__logger = logging.getLogger('devdeck')
15-
super().__init__(key_no, **kwargs)
18+
super().__init__(key_no, ** kwargs)
1619

1720
def initialize(self):
1821
if self.pulse is None:
@@ -29,10 +32,12 @@ def pressed(self):
2932

3033
def __get_output(self):
3134
sinks = self.pulse.sink_list()
32-
selected_output = [output for output in sinks if output.description == self.settings['output']]
35+
selected_output = [
36+
output for output in sinks if output.description == self.settings['output']]
3337
if len(selected_output) == 0:
3438
possible_ouputs = [output.description for output in sinks]
35-
self.__logger.warning("Output '%s' not found in list of possible outputs:\n%s", self.settings['output'], '\n'.join(possible_ouputs))
39+
self.__logger.warning("Output '%s' not found in list of possible outputs:\n%s",
40+
self.settings['output'], '\n'.join(possible_ouputs))
3641
return None
3742
return selected_output[0]
3843

@@ -72,4 +77,4 @@ def settings_schema(self):
7277
'volume': {
7378
'type': 'integer'
7479
}
75-
}
80+
}

0 commit comments

Comments
 (0)