amd-debug-tools 0.2.0__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.
Potentially problematic release.
This version of amd-debug-tools might be problematic. Click here for more details.
- amd_debug/__init__.py +45 -0
- amd_debug/acpi.py +107 -0
- amd_debug/bash/amd-s2idle +89 -0
- amd_debug/battery.py +87 -0
- amd_debug/bios.py +138 -0
- amd_debug/common.py +324 -0
- amd_debug/database.py +331 -0
- amd_debug/failures.py +588 -0
- amd_debug/installer.py +404 -0
- amd_debug/kernel.py +389 -0
- amd_debug/prerequisites.py +1215 -0
- amd_debug/pstate.py +314 -0
- amd_debug/s2idle-hook +72 -0
- amd_debug/s2idle.py +406 -0
- amd_debug/sleep_report.py +453 -0
- amd_debug/templates/html +427 -0
- amd_debug/templates/md +39 -0
- amd_debug/templates/stdout +13 -0
- amd_debug/templates/txt +23 -0
- amd_debug/validator.py +863 -0
- amd_debug/wake.py +111 -0
- amd_debug_tools-0.2.0.dist-info/METADATA +180 -0
- amd_debug_tools-0.2.0.dist-info/RECORD +27 -0
- amd_debug_tools-0.2.0.dist-info/WHEEL +5 -0
- amd_debug_tools-0.2.0.dist-info/entry_points.txt +4 -0
- amd_debug_tools-0.2.0.dist-info/licenses/LICENSE +19 -0
- amd_debug_tools-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import math
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from tabulate import tabulate
|
|
9
|
+
from jinja2 import Environment, FileSystemLoader
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from amd_debug.database import SleepDatabase
|
|
13
|
+
from amd_debug.common import (
|
|
14
|
+
AmdTool,
|
|
15
|
+
Colors,
|
|
16
|
+
version,
|
|
17
|
+
clear_temporary_message,
|
|
18
|
+
get_group_color,
|
|
19
|
+
get_log_priority,
|
|
20
|
+
print_color,
|
|
21
|
+
print_temporary_message,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from amd_debug.failures import (
|
|
25
|
+
SpuriousWakeup,
|
|
26
|
+
LowHardwareSleepResidency,
|
|
27
|
+
)
|
|
28
|
+
from amd_debug.wake import WakeIRQ, WakeGPIO
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def remove_duplicates(x):
|
|
32
|
+
"""Remove duplicates from a string"""
|
|
33
|
+
temp = re.findall(r"\d+", x)
|
|
34
|
+
res = list(map(int, temp))
|
|
35
|
+
return list(set(res))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def format_gpio_as_str(x):
|
|
39
|
+
"""Format GPIO as a nicer format"""
|
|
40
|
+
ret = []
|
|
41
|
+
for y in remove_duplicates(x):
|
|
42
|
+
ret.append(str(WakeGPIO(y)))
|
|
43
|
+
return ", ".join(ret)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def format_irq_as_str(x):
|
|
47
|
+
"""Format IRQ as a nicer format"""
|
|
48
|
+
ret = []
|
|
49
|
+
for y in remove_duplicates(x):
|
|
50
|
+
ret.append(str(WakeIRQ(y)))
|
|
51
|
+
return ", ".join(ret)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def format_as_human(x):
|
|
55
|
+
"""Format as a human readable date"""
|
|
56
|
+
return datetime.strptime(str(x), "%Y%m%d%H%M%S")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def format_as_seconds(x):
|
|
60
|
+
"""Format as seconds"""
|
|
61
|
+
return format_as_human(x).timestamp()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def format_watts(val):
|
|
65
|
+
"""Format watts as a nicer format"""
|
|
66
|
+
return f"{val:.02f}W"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def format_percent(val):
|
|
70
|
+
"""Format percent as a nicer format"""
|
|
71
|
+
return f"{val:.02f}%"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def format_timedelta(val):
|
|
75
|
+
"""Format seconds as a nicer format"""
|
|
76
|
+
return str(timedelta(seconds=val))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def parse_hw_sleep(hw):
|
|
80
|
+
"""Parse the hardware sleep value, throwing out garbage values"""
|
|
81
|
+
if hw > 1:
|
|
82
|
+
return 0
|
|
83
|
+
return hw * 100
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class SleepReport(AmdTool):
|
|
87
|
+
"""Sleep report class"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, since, until, fname, fmt, tool_debug, report_debug):
|
|
90
|
+
log_prefix = "s2idle" if tool_debug else None
|
|
91
|
+
super().__init__(log_prefix)
|
|
92
|
+
|
|
93
|
+
self.db = SleepDatabase()
|
|
94
|
+
self.fname = fname
|
|
95
|
+
self.since = since
|
|
96
|
+
self.until = until
|
|
97
|
+
self.debug = report_debug
|
|
98
|
+
self.format = fmt
|
|
99
|
+
self.failures = []
|
|
100
|
+
self.df = self.db.report_summary_dataframe(self.since, self.until)
|
|
101
|
+
self.pre_process_dataframe()
|
|
102
|
+
self.battery_svg = None
|
|
103
|
+
self.hwsleep_svg = None
|
|
104
|
+
|
|
105
|
+
def analyze_duration(self, index, t0, t1, requested, hw):
|
|
106
|
+
"""Analyze the duration of the cycle"""
|
|
107
|
+
duration = t1 - t0
|
|
108
|
+
if duration.total_seconds() >= 60 and hw < 90:
|
|
109
|
+
failure = LowHardwareSleepResidency(duration.seconds, hw)
|
|
110
|
+
problem = failure.get_description()
|
|
111
|
+
data = str(failure)
|
|
112
|
+
if self.format == "html":
|
|
113
|
+
self.failures.append(
|
|
114
|
+
{"cycle_num": index, "problem": problem, "data": data}
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
self.failures.append([index, problem, data])
|
|
118
|
+
|
|
119
|
+
if not math.isnan(requested):
|
|
120
|
+
min_suspend_duration = timedelta(seconds=requested * 0.9)
|
|
121
|
+
expected_wake_time = t0 + min_suspend_duration
|
|
122
|
+
|
|
123
|
+
if t1 < expected_wake_time:
|
|
124
|
+
failure = SpuriousWakeup(requested, duration)
|
|
125
|
+
problem = failure.get_description()
|
|
126
|
+
data = str(failure)
|
|
127
|
+
if self.format == "html":
|
|
128
|
+
self.failures.append(
|
|
129
|
+
{"cycle_num": index, "problem": problem, "data": data}
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
self.failures.append([index, problem, data])
|
|
133
|
+
|
|
134
|
+
def pre_process_dataframe(self):
|
|
135
|
+
"""Pre-process the pandas dataframe"""
|
|
136
|
+
self.df["Duration"] = self.df["t1"].apply(format_as_seconds) - self.df[
|
|
137
|
+
"t0"
|
|
138
|
+
].apply(format_as_seconds)
|
|
139
|
+
self.df["Hardware Sleep"] = (self.df["hw"] / self.df["Duration"]).apply(
|
|
140
|
+
parse_hw_sleep
|
|
141
|
+
)
|
|
142
|
+
if not self.df["b0"].isnull().all():
|
|
143
|
+
self.df["Battery Start"] = self.df["b0"] / self.df["full"] * 100
|
|
144
|
+
self.df["Battery Delta"] = (
|
|
145
|
+
(self.df["b1"] - self.df["b0"]) / self.df["full"] * 100
|
|
146
|
+
)
|
|
147
|
+
self.df["Battery Ave Rate"] = (
|
|
148
|
+
(self.df["b1"] - self.df["b0"]) / self.df["Duration"] / 360
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Wake sources
|
|
152
|
+
self.df["Wake Pin"] = self.df["gpio"].apply(format_gpio_as_str)
|
|
153
|
+
self.df["Wake Interrupt"] = self.df["wake_irq"].apply(format_irq_as_str)
|
|
154
|
+
del self.df["gpio"]
|
|
155
|
+
del self.df["wake_irq"]
|
|
156
|
+
|
|
157
|
+
# Look for spurious wakeups and low hardware residency
|
|
158
|
+
[
|
|
159
|
+
self.analyze_duration(index, t0, t1, requested, hw)
|
|
160
|
+
for index, t0, t1, requested, hw in zip(
|
|
161
|
+
self.df.index,
|
|
162
|
+
self.df["t0"].apply(format_as_human),
|
|
163
|
+
self.df["t1"].apply(format_as_human),
|
|
164
|
+
self.df["requested"],
|
|
165
|
+
self.df["Hardware Sleep"],
|
|
166
|
+
)
|
|
167
|
+
]
|
|
168
|
+
del self.df["requested"]
|
|
169
|
+
|
|
170
|
+
# Only keep data needed
|
|
171
|
+
self.df.rename(columns={"t0": "Start Time"}, inplace=True)
|
|
172
|
+
self.df["Start Time"] = self.df["Start Time"].apply(format_as_human)
|
|
173
|
+
del self.df["b1"]
|
|
174
|
+
del self.df["b0"]
|
|
175
|
+
del self.df["full"]
|
|
176
|
+
del self.df["t1"]
|
|
177
|
+
del self.df["hw"]
|
|
178
|
+
|
|
179
|
+
def post_process_dataframe(self):
|
|
180
|
+
"""Display pandas dataframe in a more user friendly format"""
|
|
181
|
+
self.df["Duration"] = self.df["Duration"].apply(format_timedelta)
|
|
182
|
+
self.df["Hardware Sleep"] = self.df["Hardware Sleep"].apply(format_percent)
|
|
183
|
+
if "Battery Start" in self.df.columns:
|
|
184
|
+
self.df["Battery Start"] = self.df["Battery Start"].apply(format_percent)
|
|
185
|
+
self.df["Battery Delta"] = self.df["Battery Delta"].apply(format_percent)
|
|
186
|
+
self.df["Battery Ave Rate"] = self.df["Battery Ave Rate"].apply(
|
|
187
|
+
format_watts
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def convert_gpio_dataframe(self, content):
|
|
191
|
+
header = False
|
|
192
|
+
rows = []
|
|
193
|
+
for line in content.split("\n"):
|
|
194
|
+
# only include header once
|
|
195
|
+
if "int|active" in line:
|
|
196
|
+
if header:
|
|
197
|
+
continue
|
|
198
|
+
header = True
|
|
199
|
+
if "|" in line:
|
|
200
|
+
# first column missing '|'
|
|
201
|
+
rows.append(line.replace("\t", "|"))
|
|
202
|
+
columns = [row.split("|") for row in rows]
|
|
203
|
+
df = pd.DataFrame(columns[1:], columns=columns[0])
|
|
204
|
+
return df.to_html(index=False, table_id="gpio")
|
|
205
|
+
|
|
206
|
+
def get_prereq_data(self):
|
|
207
|
+
"""Get the prereq data"""
|
|
208
|
+
prereq = []
|
|
209
|
+
prereq_debug = []
|
|
210
|
+
ts = self.db.get_last_prereq_ts()
|
|
211
|
+
if not ts:
|
|
212
|
+
return [], "", []
|
|
213
|
+
t0 = datetime.strptime(str(ts), "%Y%m%d%H%M%S")
|
|
214
|
+
for row in self.db.report_prereq(t0):
|
|
215
|
+
prereq.append({"symbol": row[3], "text": row[2]})
|
|
216
|
+
if self.debug:
|
|
217
|
+
for row in self.db.report_debug(t0):
|
|
218
|
+
content = row[0]
|
|
219
|
+
if self.format == "html" and "int|active" in content:
|
|
220
|
+
content = self.convert_gpio_dataframe(content)
|
|
221
|
+
prereq_debug.append(
|
|
222
|
+
{"data": "{message}".format(message=content.strip())}
|
|
223
|
+
)
|
|
224
|
+
return prereq, t0, prereq_debug
|
|
225
|
+
|
|
226
|
+
def get_cycle_data(self):
|
|
227
|
+
"""Get the cycle data"""
|
|
228
|
+
cycles = []
|
|
229
|
+
debug = []
|
|
230
|
+
num = 0
|
|
231
|
+
for cycle in self.df["Start Time"]:
|
|
232
|
+
if self.format == "html":
|
|
233
|
+
data = ""
|
|
234
|
+
for line in self.db.report_cycle_data(cycle).split("\n"):
|
|
235
|
+
data += "<p>{line}</p>".format(line=line)
|
|
236
|
+
cycles.append({"cycle_num": num, "data": data})
|
|
237
|
+
else:
|
|
238
|
+
cycles.append([num, self.db.report_cycle_data(cycle)])
|
|
239
|
+
if self.debug:
|
|
240
|
+
messages = []
|
|
241
|
+
priorities = []
|
|
242
|
+
for row in self.db.report_debug(cycle):
|
|
243
|
+
messages.append(row[0])
|
|
244
|
+
priorities.append(get_log_priority(row[1]))
|
|
245
|
+
debug.append(
|
|
246
|
+
{"cycle_num": num, "messages": messages, "priorities": priorities}
|
|
247
|
+
)
|
|
248
|
+
num += 1
|
|
249
|
+
return cycles, debug
|
|
250
|
+
|
|
251
|
+
def build_template(self, inc_prereq) -> str:
|
|
252
|
+
"""Build the template for the report using jinja2"""
|
|
253
|
+
import amd_debug # pylint: disable=import-outside-toplevel
|
|
254
|
+
|
|
255
|
+
# Load the template
|
|
256
|
+
p = os.path.dirname(amd_debug.__file__)
|
|
257
|
+
environment = Environment(loader=FileSystemLoader(os.path.join(p, "templates")))
|
|
258
|
+
template = environment.get_template(self.format)
|
|
259
|
+
|
|
260
|
+
# Load the prereq data
|
|
261
|
+
prereq = None
|
|
262
|
+
prereq_debug = None
|
|
263
|
+
prereq_date = None
|
|
264
|
+
if inc_prereq:
|
|
265
|
+
prereq, prereq_date, prereq_debug = self.get_prereq_data()
|
|
266
|
+
|
|
267
|
+
# Load the cycle and/or debug data
|
|
268
|
+
cycles, debug = self.get_cycle_data()
|
|
269
|
+
|
|
270
|
+
self.post_process_dataframe()
|
|
271
|
+
failures = None
|
|
272
|
+
if self.format == "md":
|
|
273
|
+
summary = self.df.to_markdown(floatfmt=".02f")
|
|
274
|
+
cycle_data = tabulate(cycles, headers=["Cycle", "data"], tablefmt="pipe")
|
|
275
|
+
if self.failures:
|
|
276
|
+
failures = tabulate(
|
|
277
|
+
self.failures,
|
|
278
|
+
headers=["Cycle", "Problem", "Explanation"],
|
|
279
|
+
tablefmt="pipe",
|
|
280
|
+
)
|
|
281
|
+
elif self.format == "txt":
|
|
282
|
+
summary = tabulate(self.df, headers=self.df.columns, tablefmt="fancy_grid")
|
|
283
|
+
cycle_data = tabulate(
|
|
284
|
+
cycles, headers=["Cycle", "data"], tablefmt="fancy_grid"
|
|
285
|
+
)
|
|
286
|
+
if self.failures:
|
|
287
|
+
failures = tabulate(
|
|
288
|
+
self.failures,
|
|
289
|
+
headers=["Cycle", "Problem", "Explanation"],
|
|
290
|
+
tablefmt="fancy_grid",
|
|
291
|
+
)
|
|
292
|
+
elif self.format == "html":
|
|
293
|
+
summary = ""
|
|
294
|
+
row = 0
|
|
295
|
+
# we will use javascript to highlight the high values
|
|
296
|
+
for line in self.df.to_html(table_id="summary", render_links=True).split(
|
|
297
|
+
"\n"
|
|
298
|
+
):
|
|
299
|
+
if "<tr>" in line:
|
|
300
|
+
line = line.replace(
|
|
301
|
+
"<tr>",
|
|
302
|
+
'<tr class="row-low" onclick="pick_summary_cycle(%d)">' % row,
|
|
303
|
+
)
|
|
304
|
+
row = row + 1
|
|
305
|
+
summary += line
|
|
306
|
+
cycle_data = cycles
|
|
307
|
+
failures = self.failures
|
|
308
|
+
# only show one cycle in stdout output even if we found more
|
|
309
|
+
else:
|
|
310
|
+
df = self.df.tail(1)
|
|
311
|
+
summary = tabulate(
|
|
312
|
+
df, headers=self.df.columns, tablefmt="fancy_grid", showindex=False
|
|
313
|
+
)
|
|
314
|
+
if cycles[-1][0] == df.index.start:
|
|
315
|
+
cycle_data = cycles[-1][-1]
|
|
316
|
+
else:
|
|
317
|
+
cycle_data = None
|
|
318
|
+
if self.failures and self.failures[-1][0] == df.index.start:
|
|
319
|
+
failures = self.failures[-1][-1]
|
|
320
|
+
|
|
321
|
+
# let it burn
|
|
322
|
+
context = {
|
|
323
|
+
"prereq": prereq,
|
|
324
|
+
"prereq_date": prereq_date,
|
|
325
|
+
"cycle_data": cycle_data,
|
|
326
|
+
"summary": summary,
|
|
327
|
+
"prereq_debug_data": prereq_debug,
|
|
328
|
+
"debug_data": debug,
|
|
329
|
+
"date": datetime.now(),
|
|
330
|
+
"version": version(),
|
|
331
|
+
"battery_svg": self.battery_svg,
|
|
332
|
+
"hwsleep_svg": self.hwsleep_svg,
|
|
333
|
+
"failures": failures,
|
|
334
|
+
}
|
|
335
|
+
if self.fname:
|
|
336
|
+
with open(self.fname, "w") as f:
|
|
337
|
+
f.write(template.render(context))
|
|
338
|
+
if "SUDO_UID" in os.environ:
|
|
339
|
+
os.chown(
|
|
340
|
+
self.fname, int(os.environ["SUDO_UID"]), int(os.environ["SUDO_GID"])
|
|
341
|
+
)
|
|
342
|
+
return "Report written to {f}".format(f=self.fname)
|
|
343
|
+
else:
|
|
344
|
+
return template.render(context)
|
|
345
|
+
|
|
346
|
+
def build_battery_chart(self):
|
|
347
|
+
"""Build a battery chart using matplotlib and seaborn"""
|
|
348
|
+
import matplotlib.pyplot as plt
|
|
349
|
+
import seaborn as sns
|
|
350
|
+
import io
|
|
351
|
+
|
|
352
|
+
if "Battery Ave Rate" not in self.df.columns:
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
plt.set_loglevel("warning")
|
|
356
|
+
fig, ax1 = plt.subplots()
|
|
357
|
+
lns3 = ax1.plot(
|
|
358
|
+
self.df["Battery Ave Rate"], color="green", label="Charge/Discharge Rate"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
ax2 = ax1.twinx()
|
|
362
|
+
lns1 = sns.barplot(
|
|
363
|
+
x=self.df.index,
|
|
364
|
+
y=self.df["Battery Delta"],
|
|
365
|
+
color="grey",
|
|
366
|
+
label="Battery Change",
|
|
367
|
+
alpha=0.3,
|
|
368
|
+
)
|
|
369
|
+
max = int(len(self.df.index) / 10)
|
|
370
|
+
if max:
|
|
371
|
+
ax1.set_xticks(range(0, len(self.df.index), max))
|
|
372
|
+
ax1.set_xlabel("Cycle")
|
|
373
|
+
ax1.set_ylabel("Rate (Watts)")
|
|
374
|
+
ax2.set_ylabel("Battery Change (%)")
|
|
375
|
+
|
|
376
|
+
lines, labels = ax1.get_legend_handles_labels()
|
|
377
|
+
lines2, labels2 = ax2.get_legend_handles_labels()
|
|
378
|
+
ax2.legend(
|
|
379
|
+
lines + lines2, labels + labels2, loc="lower left", bbox_to_anchor=(0, 1)
|
|
380
|
+
)
|
|
381
|
+
battery_svg = io.BytesIO()
|
|
382
|
+
plt.savefig(battery_svg, format="svg")
|
|
383
|
+
battery_svg.seek(0)
|
|
384
|
+
self.battery_svg = battery_svg.read().decode("utf-8")
|
|
385
|
+
|
|
386
|
+
def build_hw_sleep_chart(self):
|
|
387
|
+
"""Build the hardware sleep chart using matplotlib and seaborn"""
|
|
388
|
+
import matplotlib.pyplot as plt
|
|
389
|
+
import seaborn as sns
|
|
390
|
+
import io
|
|
391
|
+
|
|
392
|
+
plt.set_loglevel("warning")
|
|
393
|
+
fig, ax1 = plt.subplots()
|
|
394
|
+
lns3 = ax1.plot(
|
|
395
|
+
self.df["Hardware Sleep"],
|
|
396
|
+
color="red",
|
|
397
|
+
label="Hardware Sleep",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
ax2 = ax1.twinx()
|
|
401
|
+
lns1 = sns.barplot(
|
|
402
|
+
x=self.df.index,
|
|
403
|
+
y=self.df["Duration"] / 60,
|
|
404
|
+
color="grey",
|
|
405
|
+
label="Cycle Duration",
|
|
406
|
+
alpha=0.3,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
max = int(len(self.df.index) / 10)
|
|
410
|
+
if max:
|
|
411
|
+
ax1.set_xticks(range(0, len(self.df.index), max))
|
|
412
|
+
ax1.set_xlabel("Cycle")
|
|
413
|
+
ax1.set_ylabel("Percent")
|
|
414
|
+
ax2.set_yscale("log")
|
|
415
|
+
ax2.set_ylabel("Duration (minutes)")
|
|
416
|
+
|
|
417
|
+
lines, labels = ax1.get_legend_handles_labels()
|
|
418
|
+
lines2, labels2 = ax2.get_legend_handles_labels()
|
|
419
|
+
ax2.legend(
|
|
420
|
+
lines + lines2, labels + labels2, loc="lower left", bbox_to_anchor=(0, 1)
|
|
421
|
+
)
|
|
422
|
+
hwsleep_svg = io.BytesIO()
|
|
423
|
+
plt.savefig(hwsleep_svg, format="svg")
|
|
424
|
+
hwsleep_svg.seek(0)
|
|
425
|
+
self.hwsleep_svg = hwsleep_svg.read().decode("utf-8")
|
|
426
|
+
|
|
427
|
+
def run(self, inc_prereq=True):
|
|
428
|
+
"""Run the report"""
|
|
429
|
+
|
|
430
|
+
if self.df.empty:
|
|
431
|
+
raise ValueError(f"No data found between {self.since} and {self.until}")
|
|
432
|
+
|
|
433
|
+
characters = print_temporary_message("Building report, please wait...")
|
|
434
|
+
|
|
435
|
+
# Build charts in the page for html and markdown formats
|
|
436
|
+
if len(self.df.index) > 1 and (self.format == "html" or self.format == "md"):
|
|
437
|
+
self.build_battery_chart()
|
|
438
|
+
self.build_hw_sleep_chart()
|
|
439
|
+
|
|
440
|
+
# Render the template using jinja
|
|
441
|
+
msg = self.build_template(inc_prereq)
|
|
442
|
+
clear_temporary_message(characters)
|
|
443
|
+
for line in msg.split("\n"):
|
|
444
|
+
color = Colors.OK
|
|
445
|
+
text = line.strip()
|
|
446
|
+
if not text:
|
|
447
|
+
continue
|
|
448
|
+
for group in ["🗣️", "❌", "🚦", "🦟", "💯", "○"]:
|
|
449
|
+
if line.startswith(group):
|
|
450
|
+
text = line.split(group)[-1]
|
|
451
|
+
color = get_group_color(group)
|
|
452
|
+
break
|
|
453
|
+
print_color(text, color)
|