IS-Matrix-Forge 1.0.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- is_matrix_forge/__init__.py +8 -0
- is_matrix_forge/assets/__init__.py +8 -0
- is_matrix_forge/assets/audio/__init__.py +36 -0
- is_matrix_forge/assets/audio/files/__init__.py +53 -0
- is_matrix_forge/assets/audio/files/plugged.wav +0 -0
- is_matrix_forge/assets/audio/files/unplugged.wav +0 -0
- is_matrix_forge/assets/char_map.json +2150 -0
- is_matrix_forge/assets/font_map.py +119 -0
- is_matrix_forge/common/__init__.py +0 -0
- is_matrix_forge/common/decorators/__init__.py +152 -0
- is_matrix_forge/common/dirs.py +20 -0
- is_matrix_forge/common/helpers/__init__.py +132 -0
- is_matrix_forge/common/helpers/github_api.py +67 -0
- is_matrix_forge/designer_gui/__init__.py +8 -0
- is_matrix_forge/designer_gui/main_window/__init__.py +252 -0
- is_matrix_forge/designer_gui/main_window/layout.py +117 -0
- is_matrix_forge/dev_tools/__init__.py +8 -0
- is_matrix_forge/dev_tools/debug.py +82 -0
- is_matrix_forge/dev_tools/presets.py +47 -0
- is_matrix_forge/inputmodule/__init__.py +164 -0
- is_matrix_forge/inputmodule/b1display.py +160 -0
- is_matrix_forge/inputmodule/c1minimal.py +35 -0
- is_matrix_forge/inputmodule/font.py +2166 -0
- is_matrix_forge/inputmodule/helpers/__init__.py +16 -0
- is_matrix_forge/inputmodule/ledmatrix.py +516 -0
- is_matrix_forge/led_matrix/Scripts/__init__.py +4 -0
- is_matrix_forge/led_matrix/Scripts/battery_monitor.py +152 -0
- is_matrix_forge/led_matrix/Scripts/install_presets/__init__.py +2 -0
- is_matrix_forge/led_matrix/Scripts/install_presets/arguments.py +84 -0
- is_matrix_forge/led_matrix/Scripts/install_presets/main.py +192 -0
- is_matrix_forge/led_matrix/__init__.py +7 -0
- is_matrix_forge/led_matrix/commands/__init__.py +84 -0
- is_matrix_forge/led_matrix/commands/map.py +46 -0
- is_matrix_forge/led_matrix/constants.py +82 -0
- is_matrix_forge/led_matrix/controller/__init__.py +26 -0
- is_matrix_forge/led_matrix/controller/controller.py +635 -0
- is_matrix_forge/led_matrix/controller/helpers/__init__.py +7 -0
- is_matrix_forge/led_matrix/controller/helpers/threading.py +41 -0
- is_matrix_forge/led_matrix/controller/multiton.py +32 -0
- is_matrix_forge/led_matrix/controller/threaded.py +72 -0
- is_matrix_forge/led_matrix/display/__init__.py +13 -0
- is_matrix_forge/led_matrix/display/animations/__init__.py +29 -0
- is_matrix_forge/led_matrix/display/animations/animation.py +593 -0
- is_matrix_forge/led_matrix/display/animations/effects/__init__.py +17 -0
- is_matrix_forge/led_matrix/display/animations/errors.py +23 -0
- is_matrix_forge/led_matrix/display/animations/font/__init__.py +17 -0
- is_matrix_forge/led_matrix/display/animations/font/font.py +4 -0
- is_matrix_forge/led_matrix/display/animations/font/map_builder.py +123 -0
- is_matrix_forge/led_matrix/display/animations/frame/__init__.py +47 -0
- is_matrix_forge/led_matrix/display/animations/frame/base.py +245 -0
- is_matrix_forge/led_matrix/display/animations/frame/helpers.py +67 -0
- is_matrix_forge/led_matrix/display/animations/keep_awake.py +1 -0
- is_matrix_forge/led_matrix/display/animations/plugged.py +0 -0
- is_matrix_forge/led_matrix/display/animations/text_scroller.py +104 -0
- is_matrix_forge/led_matrix/display/effects/__init__.py +17 -0
- is_matrix_forge/led_matrix/display/effects/breather.py +206 -0
- is_matrix_forge/led_matrix/display/effects/fade.py +0 -0
- is_matrix_forge/led_matrix/display/effects/scroller/__init__.py +92 -0
- is_matrix_forge/led_matrix/display/effects/scroller/base.py +76 -0
- is_matrix_forge/led_matrix/display/effects/scroller/helpers.py +62 -0
- is_matrix_forge/led_matrix/display/grid/__init__.py +9 -0
- is_matrix_forge/led_matrix/display/grid/grid.py +208 -0
- is_matrix_forge/led_matrix/display/grid/helpers.py +130 -0
- is_matrix_forge/led_matrix/display/grid/presets/__init__.py +64 -0
- is_matrix_forge/led_matrix/display/grid/presets/manifest.py +172 -0
- is_matrix_forge/led_matrix/display/helpers/__init__.py +110 -0
- is_matrix_forge/led_matrix/display/helpers/columns.py +33 -0
- is_matrix_forge/led_matrix/display/media.py +199 -0
- is_matrix_forge/led_matrix/display/patterns/__init__.py +59 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/__init__.py +91 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/commands.py +3 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/constants.py +29 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/stencils.py +70 -0
- is_matrix_forge/led_matrix/display/patterns/utils.py +2 -0
- is_matrix_forge/led_matrix/display/text/__init__.py +66 -0
- is_matrix_forge/led_matrix/display/text/scroller.py +136 -0
- is_matrix_forge/led_matrix/errors/__init__.py +4 -0
- is_matrix_forge/led_matrix/errors/base.py +14 -0
- is_matrix_forge/led_matrix/errors/grid.py +37 -0
- is_matrix_forge/led_matrix/errors/matrix.py +68 -0
- is_matrix_forge/led_matrix/errors/misc.py +36 -0
- is_matrix_forge/led_matrix/fonts.py +20 -0
- is_matrix_forge/led_matrix/hardware.py +172 -0
- is_matrix_forge/led_matrix/helpers/__init__.py +165 -0
- is_matrix_forge/led_matrix/helpers/core/__init__.py +17 -0
- is_matrix_forge/led_matrix/helpers/core/lazy_mod_load.py +84 -0
- is_matrix_forge/led_matrix/helpers/device.py +118 -0
- is_matrix_forge/led_matrix/helpers/status_handler.py +14 -0
- is_matrix_forge/led_matrix/helpers/stream/__init__.py +70 -0
- is_matrix_forge/led_matrix/helpers/stream/errors.py +17 -0
- is_matrix_forge/led_matrix/led_matrix.py +25 -0
- is_matrix_forge/led_matrix/status.py +8 -0
- is_matrix_forge/led_matrix/tools/__init__.py +8 -0
- is_matrix_forge/led_matrix/tools/identify.py +27 -0
- is_matrix_forge/led_matrix/utils/__init__.py +41 -0
- is_matrix_forge/log_engine.py +25 -0
- is_matrix_forge/monitor/__init__.py +68 -0
- is_matrix_forge/monitor/errors.py +35 -0
- is_matrix_forge/monitor/events.py +78 -0
- is_matrix_forge/monitor/gui/__init__.py +0 -0
- is_matrix_forge/monitor/gui/errors.py +35 -0
- is_matrix_forge/monitor/gui/event/__init__.py +8 -0
- is_matrix_forge/monitor/gui/event/collection.py +153 -0
- is_matrix_forge/monitor/gui/event/errors.py +58 -0
- is_matrix_forge/monitor/gui/event/event.py +226 -0
- is_matrix_forge/monitor/gui/layout/__init__.py +8 -0
- is_matrix_forge/monitor/gui/layout/base.py +147 -0
- is_matrix_forge/monitor/gui/metaclasses/__init__.py +20 -0
- is_matrix_forge/monitor/gui/windows/__init__.py +0 -0
- is_matrix_forge/monitor/gui/windows/base.py +325 -0
- is_matrix_forge/monitor/gui/windows/main/__init__.py +77 -0
- is_matrix_forge/monitor/gui/windows/main/layout.py +21 -0
- is_matrix_forge/monitor/helpers.py +55 -0
- is_matrix_forge/monitor/monitor.py +431 -0
- is_matrix_forge/notify/__init__.py +0 -0
- is_matrix_forge/notify/sounds/__init__.py +6 -0
- is_matrix_forge/notify/sounds/base.py +106 -0
- is_matrix_forge/progress.py +99 -0
- is_matrix_forge-1.0.0.dev1.dist-info/LICENSE.md +19 -0
- is_matrix_forge-1.0.0.dev1.dist-info/METADATA +190 -0
- is_matrix_forge-1.0.0.dev1.dist-info/RECORD +123 -0
- is_matrix_forge-1.0.0.dev1.dist-info/WHEEL +4 -0
- is_matrix_forge-1.0.0.dev1.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from threading import Thread
|
|
5
|
+
from time import sleep
|
|
6
|
+
from typing import Optional, Union
|
|
7
|
+
|
|
8
|
+
from inspy_logger import Loggable
|
|
9
|
+
from inspyre_toolbox.syntactic_sweets.classes import validate_type
|
|
10
|
+
from serial.tools.list_ports_common import ListPortInfo
|
|
11
|
+
|
|
12
|
+
from is_matrix_forge.common.helpers import percentage_to_value
|
|
13
|
+
from is_matrix_forge.led_matrix import pattern, get_animate, animate, percentage, LEDMatrixController
|
|
14
|
+
from is_matrix_forge.led_matrix.display.animations import goodbye_animation
|
|
15
|
+
from is_matrix_forge.led_matrix.helpers.device import check_device
|
|
16
|
+
from is_matrix_forge.monitor import DEFAULT_PLUGGED_SOUND, DEFAULT_UNPLUGGED_SOUND, MOD_LOGGER, get_plugged_status, \
|
|
17
|
+
PowerMonitorNotRunningError, ECH
|
|
18
|
+
from is_matrix_forge.notify.sounds import Sound
|
|
19
|
+
from is_matrix_forge.monitor.helpers import get_battery_percentage
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PowerMonitor(Loggable):
|
|
23
|
+
|
|
24
|
+
# Properties
|
|
25
|
+
#
|
|
26
|
+
_running = False
|
|
27
|
+
DEFAULT_CHECK_INTERVAL = 5
|
|
28
|
+
__cycles = 0
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
device,
|
|
33
|
+
battery_check_interval: int = 5,
|
|
34
|
+
plugged_alert: Optional[Union[str, Path]] = DEFAULT_PLUGGED_SOUND,
|
|
35
|
+
unplugged_alert: Optional[Union[str, Path]] = DEFAULT_UNPLUGGED_SOUND,
|
|
36
|
+
|
|
37
|
+
):
|
|
38
|
+
super().__init__(MOD_LOGGER)
|
|
39
|
+
self.__battery_check_interval = None
|
|
40
|
+
self.__dev = None
|
|
41
|
+
self._last_state = None
|
|
42
|
+
self.__plugged_alert = None
|
|
43
|
+
self.__start_time = None
|
|
44
|
+
self.__stop_time = None
|
|
45
|
+
self.__thread = None
|
|
46
|
+
self.__unplugged_alert = None
|
|
47
|
+
self.__controller = None
|
|
48
|
+
|
|
49
|
+
self.set_device(device)
|
|
50
|
+
|
|
51
|
+
# Set some settings
|
|
52
|
+
# First alerts, if provided
|
|
53
|
+
if plugged_alert:
|
|
54
|
+
self.plugged_alert = plugged_alert
|
|
55
|
+
|
|
56
|
+
if unplugged_alert:
|
|
57
|
+
self.unplugged_alert = unplugged_alert
|
|
58
|
+
|
|
59
|
+
self.dev.brightness = percentage_to_value(5)
|
|
60
|
+
self.__battery_check_interval = battery_check_interval or self.DEFAULT_CHECK_INTERVAL
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def battery_check_interval(self):
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
return self.__battery_check_interval
|
|
70
|
+
|
|
71
|
+
@battery_check_interval.setter
|
|
72
|
+
@validate_type(int, str, float, preferred_type=float, conversion_funcs=[float])
|
|
73
|
+
def battery_check_interval(self, new):
|
|
74
|
+
|
|
75
|
+
self.__battery_check_interval = new
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def controller(self):
|
|
79
|
+
return self.__controller
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def cycles(self):
|
|
83
|
+
return self.__cycles
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def dev(self):
|
|
87
|
+
return self.controller.device
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def device(self):
|
|
91
|
+
return self.dev
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def last_state(self) -> Optional[bool]:
|
|
95
|
+
"""
|
|
96
|
+
The state of the connection to the power supply on last-check.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Optional[bool]:
|
|
100
|
+
True;
|
|
101
|
+
The device was plugged into power.
|
|
102
|
+
False;
|
|
103
|
+
The device was unplugged from power.
|
|
104
|
+
|
|
105
|
+
None;
|
|
106
|
+
The monitor hasn't completed a check yet.
|
|
107
|
+
"""
|
|
108
|
+
return self._last_state
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def plugged_alert(self) -> Sound:
|
|
112
|
+
return self.__plugged_alert or DEFAULT_PLUGGED_SOUND
|
|
113
|
+
|
|
114
|
+
@validate_type(Sound)
|
|
115
|
+
@plugged_alert.setter
|
|
116
|
+
def plugged_alert(self, new):
|
|
117
|
+
if not isinstance(new, Sound):
|
|
118
|
+
raise TypeError(f'plugged_alert must be of type `Sound`, not {type(new)}')
|
|
119
|
+
|
|
120
|
+
self.__plugged_alert = new
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def plugged_in(self):
|
|
124
|
+
"""
|
|
125
|
+
Whether the device is currently plugged into power.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
bool:
|
|
129
|
+
True;
|
|
130
|
+
The device is currently plugged into power. This is not an indicator (necessarily) that;
|
|
131
|
+
- The battery level is increasing
|
|
132
|
+
- The battery level is not decreasing
|
|
133
|
+
- The battery is gaining a net-positive charge level
|
|
134
|
+
|
|
135
|
+
This is an indicator that;
|
|
136
|
+
- A power supply is connected to the device
|
|
137
|
+
|
|
138
|
+
False;
|
|
139
|
+
The device is currently unplugged from power.
|
|
140
|
+
"""
|
|
141
|
+
return get_plugged_status()
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def running(self):
|
|
145
|
+
"""
|
|
146
|
+
Whether the power-monitor is running.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
bool:
|
|
150
|
+
True;
|
|
151
|
+
The monitor is running.
|
|
152
|
+
|
|
153
|
+
False;
|
|
154
|
+
The monitor is not running.
|
|
155
|
+
"""
|
|
156
|
+
return self._running
|
|
157
|
+
|
|
158
|
+
@running.setter
|
|
159
|
+
def running(self, new):
|
|
160
|
+
"""
|
|
161
|
+
The setter for the `running` property. You can only set `running` to `False`, this will stop the monitor.
|
|
162
|
+
|
|
163
|
+
Parameters:
|
|
164
|
+
new:
|
|
165
|
+
The new value for `running`.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
None
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
RuntimeError:
|
|
172
|
+
If you try to set `running` to `True`
|
|
173
|
+
|
|
174
|
+
TypeError:
|
|
175
|
+
If `new` is not of type `bool`
|
|
176
|
+
"""
|
|
177
|
+
if not self._running and new:
|
|
178
|
+
raise RuntimeError('Cannot start monitor by setting running to `True`. Use the `start` method instead.')
|
|
179
|
+
|
|
180
|
+
if not isinstance(new, bool):
|
|
181
|
+
raise TypeError(f'running must be of type `bool`, not {type(new)}')
|
|
182
|
+
|
|
183
|
+
self._running = new
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def run_time(self):
|
|
187
|
+
"""
|
|
188
|
+
The time the monitor has been running. This is the time since the monitor was started, or the time between
|
|
189
|
+
starting and stopping the monitor if it has been stopped.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
float:
|
|
193
|
+
The run time in seconds.
|
|
194
|
+
"""
|
|
195
|
+
if not hasattr(self, 'start_time'):
|
|
196
|
+
raise PowerMonitorNotRunningError('Monitor hasn\'t even been started yet!')
|
|
197
|
+
|
|
198
|
+
recent = self.stop_time if hasattr(self, 'stop_time') else time.time()
|
|
199
|
+
return recent - self.start_time
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def start_time(self) -> Optional[float]:
|
|
203
|
+
return self.__start_time
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def stop_time(self) -> Optional[float]:
|
|
207
|
+
"""
|
|
208
|
+
(**Read-only property**)
|
|
209
|
+
|
|
210
|
+
The time the monitor was last stopped.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Optional[float]:
|
|
214
|
+
The time the monitor was last stopped.
|
|
215
|
+
|
|
216
|
+
`None`;
|
|
217
|
+
The monitor hasn't been stopped yet
|
|
218
|
+
|
|
219
|
+
"""
|
|
220
|
+
return self.__stop_time
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def thread(self) -> Optional[Thread]:
|
|
224
|
+
"""
|
|
225
|
+
The thread object that is running the monitor loop. If the monitor is not running in a separate thread, this
|
|
226
|
+
property will return `None`.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Optional[Thread]:
|
|
230
|
+
The thread that is running the monitor loop; if the monitor is running in a separate thread.
|
|
231
|
+
None otherwise
|
|
232
|
+
"""
|
|
233
|
+
if self.__thread is None:
|
|
234
|
+
self.class_logger.error('Either monitor is not running or it is not running in a separate thread.')
|
|
235
|
+
|
|
236
|
+
return self.__thread
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def unplugged(self):
|
|
240
|
+
"""
|
|
241
|
+
Whether the laptop is currently unplugged from AC power.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
bool:
|
|
245
|
+
True;
|
|
246
|
+
The device is currently unplugged from power, and the battery is discharging.
|
|
247
|
+
|
|
248
|
+
False;
|
|
249
|
+
The device is currently plugged into power. This is not an indicator (necessarily) that;
|
|
250
|
+
- The battery level is increasing
|
|
251
|
+
- The battery level is not decreasing
|
|
252
|
+
- The battery is gaining a net-positive charge level
|
|
253
|
+
"""
|
|
254
|
+
return not self.plugged_in
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def unplugged_alert(self) -> 'Sound':
|
|
258
|
+
"""
|
|
259
|
+
The sound to play when the device is unplugged from power.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Sound:
|
|
263
|
+
The sound to play when the device is unplugged from power.
|
|
264
|
+
"""
|
|
265
|
+
if not self.__unplugged_alert:
|
|
266
|
+
return DEFAULT_UNPLUGGED_SOUND
|
|
267
|
+
|
|
268
|
+
return self.__unplugged_alert
|
|
269
|
+
|
|
270
|
+
@validate_type()
|
|
271
|
+
@unplugged_alert.setter
|
|
272
|
+
def unplugged_alert(self, new):
|
|
273
|
+
"""
|
|
274
|
+
The setter for the `unplugged_alert` property. This must be of type `Sound`.
|
|
275
|
+
|
|
276
|
+
Parameters:
|
|
277
|
+
new:
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
|
|
282
|
+
"""
|
|
283
|
+
if not isinstance(new, Sound):
|
|
284
|
+
raise TypeError(f'unplugged_alert must be of type `Sound`, not {type(new)}')
|
|
285
|
+
|
|
286
|
+
self.__unplugged_alert = new
|
|
287
|
+
|
|
288
|
+
def notify(self, which: str):
|
|
289
|
+
"""
|
|
290
|
+
Notify the user of a power event (plugged, unplugged).
|
|
291
|
+
|
|
292
|
+
Parameters:
|
|
293
|
+
which (str):
|
|
294
|
+
The type of notification to send ('plugged' or 'unplugged').
|
|
295
|
+
"""
|
|
296
|
+
log = self.method_logger
|
|
297
|
+
# Just return if the first check hasn't been completed yet.
|
|
298
|
+
|
|
299
|
+
if self.last_state is None:
|
|
300
|
+
log.debug('Status: Not yet checked')
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
if which.lower() == 'plugged':
|
|
304
|
+
log.debug('Status: Plugged in')
|
|
305
|
+
if not self.last_state:
|
|
306
|
+
log.debug(f'Using {self.plugged_alert} to notify user...')
|
|
307
|
+
self.plugged_alert.notify()
|
|
308
|
+
elif which.lower() == 'unplugged':
|
|
309
|
+
log.debug('Status: Unplugged')
|
|
310
|
+
if self.last_state:
|
|
311
|
+
log.debug(f'Using {self.unplugged_alert} to notify user...')
|
|
312
|
+
self.unplugged_alert.notify()
|
|
313
|
+
|
|
314
|
+
def run(self):
|
|
315
|
+
"""
|
|
316
|
+
Run the power monitor. This is where the main loop of the monitor is located. It (roughly) does the following
|
|
317
|
+
while playing notification sounds when the power supply is plugged in or unplugged:
|
|
318
|
+
1. Check the battery level.
|
|
319
|
+
2. If the power supply is unplugged, display the battery level on the LED matrix.
|
|
320
|
+
3. If the power supply is plugged in, animate the LED matrix.
|
|
321
|
+
4. Repeat.
|
|
322
|
+
|
|
323
|
+
Note:
|
|
324
|
+
This method is called by the `start` method and should not be called directly.
|
|
325
|
+
"""
|
|
326
|
+
from .events import handle_event
|
|
327
|
+
log = self.method_logger
|
|
328
|
+
if not self._running:
|
|
329
|
+
log.error('Monitor is not running')
|
|
330
|
+
raise RuntimeError('Monitor is not running. If you want to start it, use the `start` method.')
|
|
331
|
+
|
|
332
|
+
log.debug('Running monitor...')
|
|
333
|
+
|
|
334
|
+
while self.running:
|
|
335
|
+
|
|
336
|
+
state = 'plugged' if self.plugged_in else 'unplugged'
|
|
337
|
+
handle_event(state, self)
|
|
338
|
+
|
|
339
|
+
if not self.running:
|
|
340
|
+
self.controller.clear()
|
|
341
|
+
break
|
|
342
|
+
|
|
343
|
+
self.__cycles += 1
|
|
344
|
+
|
|
345
|
+
sleep(self.battery_check_interval)
|
|
346
|
+
|
|
347
|
+
def set_device(self, device):
|
|
348
|
+
|
|
349
|
+
if isinstance(device, LEDMatrixController):
|
|
350
|
+
self.__controller = device
|
|
351
|
+
device = self.controller.device
|
|
352
|
+
elif not isinstance(device, ListPortInfo):
|
|
353
|
+
raise TypeError(f'device must be of type `ListPortInfo`, not {type(device)}')
|
|
354
|
+
|
|
355
|
+
if not check_device(device):
|
|
356
|
+
raise ValueError(f'device {device} is not available')
|
|
357
|
+
|
|
358
|
+
self.__dev = device
|
|
359
|
+
self.__controller = LEDMatrixController(self.dev)
|
|
360
|
+
|
|
361
|
+
def start(self, threaded=False):
|
|
362
|
+
"""
|
|
363
|
+
Start the power monitor.
|
|
364
|
+
|
|
365
|
+
Parameters:
|
|
366
|
+
threaded (bool):
|
|
367
|
+
If True, run the monitor in a separate thread.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Optional[Thread]:
|
|
371
|
+
The thread that is running the monitor loop; if `threaded` is True.
|
|
372
|
+
None otherwise.
|
|
373
|
+
|
|
374
|
+
"""
|
|
375
|
+
log = self.method_logger
|
|
376
|
+
log.debug('Starting monitor')
|
|
377
|
+
|
|
378
|
+
if self.running:
|
|
379
|
+
log.warning('Monitor is already running')
|
|
380
|
+
raise RuntimeError('Monitor is already running')
|
|
381
|
+
|
|
382
|
+
self._running = True
|
|
383
|
+
log.debug('Set running to True')
|
|
384
|
+
self.__start_time = time.time()
|
|
385
|
+
|
|
386
|
+
if threaded:
|
|
387
|
+
t = Thread(target=self.run, daemon=True)
|
|
388
|
+
t.start()
|
|
389
|
+
ECH.register_handler(self.stop, kwargs={'reason': 'Program exited.'})
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
self.run()
|
|
393
|
+
except KeyboardInterrupt:
|
|
394
|
+
log.warning('KeyboardInterrupt received, stopping monitor...')
|
|
395
|
+
self.stop(without_salutation=True, reason='Keyboard interrupt.')
|
|
396
|
+
|
|
397
|
+
def stop(self, without_salutation=False, reason=None):
|
|
398
|
+
"""
|
|
399
|
+
Stop the power monitor.
|
|
400
|
+
|
|
401
|
+
Parameters:
|
|
402
|
+
without_salutation (bool):
|
|
403
|
+
If True, skip the goodbye animation that plays by default on the LED matrix.
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
log = self.method_logger
|
|
407
|
+
|
|
408
|
+
if not self.running:
|
|
409
|
+
log.error('Monitor is not running')
|
|
410
|
+
raise PowerMonitorNotRunningError("Can't call stop() on a monitor that is not running")
|
|
411
|
+
else:
|
|
412
|
+
log.debug('Stopping monitor...')
|
|
413
|
+
self.__stop_time = time.time()
|
|
414
|
+
self.running = False
|
|
415
|
+
log.debug('"running" flag set to False...waiting for thread to finish')
|
|
416
|
+
|
|
417
|
+
if reason:
|
|
418
|
+
log.info(f'Stopping monitor due to: {reason}')
|
|
419
|
+
else:
|
|
420
|
+
log.info('Stopping monitor. With no reason.')
|
|
421
|
+
|
|
422
|
+
if not without_salutation:
|
|
423
|
+
goodbye_animation(self.dev)
|
|
424
|
+
else:
|
|
425
|
+
log.debug('Skipping goodbye salutation...')
|
|
426
|
+
|
|
427
|
+
if get_animate(self.dev):
|
|
428
|
+
animate(self.dev, False)
|
|
429
|
+
|
|
430
|
+
log.debug('Clearing LED matrix...')
|
|
431
|
+
self.controller.clear()
|
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Union, Optional
|
|
4
|
+
|
|
5
|
+
from chime import play_wav
|
|
6
|
+
|
|
7
|
+
from inspyre_toolbox.syntactic_sweets.classes.decorators.aliases import add_aliases, method_alias
|
|
8
|
+
|
|
9
|
+
from is_matrix_forge.log_engine import ROOT_LOGGER as PARENT_LOGGER
|
|
10
|
+
from is_matrix_forge.assets.audio import PLUG_ALERT_MAP
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
MOD_LOGGER = PARENT_LOGGER.get_child('notify.sounds.base')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@add_aliases
|
|
17
|
+
class Sound(ABC):
|
|
18
|
+
"""
|
|
19
|
+
Abstract base class for audio notifications.
|
|
20
|
+
"""
|
|
21
|
+
__allowed_types: set = {'plugged', 'unplugged'}
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
wav_file: Union[str, Path],
|
|
26
|
+
notify_type: str,
|
|
27
|
+
) -> None:
|
|
28
|
+
self.__initialized = False
|
|
29
|
+
self.__notify_type = None
|
|
30
|
+
self.__wav_file = None
|
|
31
|
+
|
|
32
|
+
self.wav_file_path = wav_file
|
|
33
|
+
self.notify_type = notify_type
|
|
34
|
+
# Initialize here
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def ALLOWED_TYPES(self):
|
|
38
|
+
"""
|
|
39
|
+
The allowed values for the notify_type property. Read-only property.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
set[str]:
|
|
43
|
+
A set of allowed notify-types.
|
|
44
|
+
"""
|
|
45
|
+
return self.__allowed_types
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def notify_type(self):
|
|
49
|
+
return self.__notify_type
|
|
50
|
+
|
|
51
|
+
@notify_type.setter
|
|
52
|
+
def notify_type(self, new: str) -> None:
|
|
53
|
+
if self.__initialized:
|
|
54
|
+
raise RuntimeError('Cannot change notify type after initialization@')
|
|
55
|
+
|
|
56
|
+
if not isinstance(new, str):
|
|
57
|
+
raise TypeError(f'new must be of type `str`, not {type(new)}')
|
|
58
|
+
|
|
59
|
+
if new not in self.ALLOWED_TYPES:
|
|
60
|
+
raise ValueError(f'new must be one of {self.ALLOWED_TYPES}, not {new}')
|
|
61
|
+
|
|
62
|
+
self.__notify_type = new
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def wav_file_path(self) -> Path:
|
|
66
|
+
"""
|
|
67
|
+
Get the path to the .wav file that will be played.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Path:
|
|
71
|
+
The path to the .wav file.
|
|
72
|
+
"""
|
|
73
|
+
return self.__wav_file
|
|
74
|
+
|
|
75
|
+
@wav_file_path.setter
|
|
76
|
+
def wav_file_path(self, new: Union[str, Path]) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Set the path to the .wav file that will be played. Setting this should be done during initialization. After
|
|
79
|
+
initialization, this should not (and will not) be changed, as it will break the sound.
|
|
80
|
+
"""
|
|
81
|
+
if self.__initialized:
|
|
82
|
+
raise RuntimeError('Cannot change wave file path after initialization.')
|
|
83
|
+
|
|
84
|
+
if isinstance(new, str):
|
|
85
|
+
new = Path(new)
|
|
86
|
+
|
|
87
|
+
if not isinstance(new, Path):
|
|
88
|
+
raise TypeError(f'new must be of type `str` or `Path`, not {type(new)}')
|
|
89
|
+
|
|
90
|
+
self.__wav_file = new
|
|
91
|
+
|
|
92
|
+
@method_alias('play')
|
|
93
|
+
def notify(self) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Play the audio notification.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
None
|
|
99
|
+
"""
|
|
100
|
+
play_wav(self.wav_file_path)
|
|
101
|
+
|
|
102
|
+
def __repr__(self):
|
|
103
|
+
return f"<Sound notify_type={self.notify_type!r} wav_file_path={self.wav_file_path!r}>"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""LED matrix aware progress bars."""
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Any, Iterable, List
|
|
6
|
+
|
|
7
|
+
from tqdm import tqdm as _tqdm
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from is_matrix_forge.led_matrix.helpers.device import DEVICES
|
|
11
|
+
from is_matrix_forge.led_matrix.controller.controller import LEDMatrixController
|
|
12
|
+
except Exception: # pragma: no cover - dependency issues
|
|
13
|
+
DEVICES = []
|
|
14
|
+
LEDMatrixController = None # type: ignore
|
|
15
|
+
|
|
16
|
+
# Global cache of controller instances so multiple bars can share controllers
|
|
17
|
+
if 'LEDMatrixController' in globals() and LEDMatrixController is not None:
|
|
18
|
+
_CONTROLLERS: List[Optional[LEDMatrixController]] = [None for _ in range(len(DEVICES))]
|
|
19
|
+
else: # pragma: no cover - when dependencies missing
|
|
20
|
+
_CONTROLLERS = [] # type: ignore[var-annotated]
|
|
21
|
+
|
|
22
|
+
# Track how many LEDTqdm bars are active to rotate matrices
|
|
23
|
+
_ACTIVE_BARS: List['LEDTqdm'] = []
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LEDTqdm(_tqdm):
|
|
27
|
+
"""A ``tqdm`` subclass that also renders progress on an LED matrix."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, *args: Any, use_led: bool = True, matrix: Optional[Any] = None,
|
|
30
|
+
**kwargs: Any) -> None:
|
|
31
|
+
super().__init__(*args, **kwargs)
|
|
32
|
+
|
|
33
|
+
self._matrix = None
|
|
34
|
+
if use_led:
|
|
35
|
+
index = min(len(_ACTIVE_BARS), max(len(DEVICES) - 1, 0))
|
|
36
|
+
self._matrix = self._init_matrix(matrix, index)
|
|
37
|
+
if self._matrix is not None:
|
|
38
|
+
try:
|
|
39
|
+
self._matrix.clear()
|
|
40
|
+
except Exception: # pragma: no cover - hardware errors
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
self._last_percent = -1
|
|
44
|
+
_ACTIVE_BARS.append(self)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _get_controller(index: int):
|
|
48
|
+
if LEDMatrixController is None or index >= len(DEVICES): # pragma: no cover - hardware missing
|
|
49
|
+
return None
|
|
50
|
+
ctrl = _CONTROLLERS[index]
|
|
51
|
+
if ctrl is None:
|
|
52
|
+
try:
|
|
53
|
+
ctrl = LEDMatrixController(DEVICES[index], 100)
|
|
54
|
+
except Exception:
|
|
55
|
+
ctrl = None
|
|
56
|
+
_CONTROLLERS[index] = ctrl
|
|
57
|
+
return ctrl
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def _init_matrix(cls, matrix: Optional[Any], index: int):
|
|
61
|
+
if matrix is not None:
|
|
62
|
+
if LEDMatrixController and isinstance(matrix, LEDMatrixController):
|
|
63
|
+
return matrix
|
|
64
|
+
try:
|
|
65
|
+
return LEDMatrixController(matrix, 100) # type: ignore
|
|
66
|
+
except Exception:
|
|
67
|
+
return None
|
|
68
|
+
return cls._get_controller(index)
|
|
69
|
+
|
|
70
|
+
def _render_led(self) -> None:
|
|
71
|
+
if not self._matrix or not self.total:
|
|
72
|
+
return
|
|
73
|
+
percent = int(self.n / self.total * 100)
|
|
74
|
+
if percent != self._last_percent:
|
|
75
|
+
try:
|
|
76
|
+
self._matrix.draw_percentage(percent)
|
|
77
|
+
except Exception:
|
|
78
|
+
# Ignore hardware errors in progress display
|
|
79
|
+
pass
|
|
80
|
+
self._last_percent = percent
|
|
81
|
+
|
|
82
|
+
def update(self, n: int = 1) -> None: # type: ignore[override]
|
|
83
|
+
super().update(n)
|
|
84
|
+
self._render_led()
|
|
85
|
+
|
|
86
|
+
def close(self) -> None:
|
|
87
|
+
super().close()
|
|
88
|
+
if self._matrix is not None:
|
|
89
|
+
try:
|
|
90
|
+
self._matrix.clear()
|
|
91
|
+
except Exception: # pragma: no cover - hardware errors
|
|
92
|
+
pass
|
|
93
|
+
if self in _ACTIVE_BARS:
|
|
94
|
+
_ACTIVE_BARS.remove(self)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def tqdm(iterable: Optional[Iterable[Any]] = None, *args: Any, **kwargs: Any):
|
|
98
|
+
"""Return an :class:`LEDTqdm` instance like ``tqdm.tqdm``."""
|
|
99
|
+
return LEDTqdm(iterable, *args, **kwargs)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2025 Inspyre-Softworks
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software (led-matrix-battery) and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|