Skip to content

Commit f97fc52

Browse files
authored
Merge pull request #338 from mathoudebine/feature/306-custom-data-sources-for-themes
2 parents 512333e + 263707b commit f97fc52

File tree

10 files changed

+236
-7
lines changed

10 files changed

+236
-7
lines changed

.github/workflows/themes-screenshot-on-pr.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@ jobs:
5555
cp screencap.png "screenshot-$theme.png"
5656

5757
# Compare with original theme preview to detect changes, generate a diff
58-
python3 tools/compare-images.py "screenshot-$theme.png" "$dir/preview.png" "diff-$theme.png"
59-
if [ -f "diff-$theme.png" ]; then
60-
echo "::warning::$theme theme rendering has changed, check if it is intentional or a side-effect. A diff is included in the job artifacts."
58+
if [ -f "$dir/preview.png" ]; then
59+
python3 tools/compare-images.py "screenshot-$theme.png" "$dir/preview.png" "diff-$theme.png"
60+
if [ -f "diff-$theme.png" ]; then
61+
echo "::warning::$theme theme rendering has changed, check if it is intentional or a side-effect. A diff is included in the job artifacts."
62+
fi
6163
fi
6264
fi
6365
done

library/scheduler.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,13 @@ def DateStats():
146146
stats.Date.stats()
147147

148148

149+
@async_job("Custom_Stats")
150+
@schedule(timedelta(seconds=config.THEME_DATA['STATS']['CUSTOM'].get("INTERVAL", None)).total_seconds())
151+
def CustomStats():
152+
# print("Refresh custom stats")
153+
stats.Custom.stats()
154+
155+
149156
@async_job("Queue_Handler")
150157
@schedule(timedelta(milliseconds=1).total_seconds())
151158
def QueueHandler():

library/sensors/sensors_custom.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# turing-smart-screen-python - a Python system monitor and library for USB-C displays like Turing Smart Screen or XuanFang
2+
# https://github.com/mathoudebine/turing-smart-screen-python/
3+
4+
# Copyright (C) 2021-2023 Matthieu Houdebine (mathoudebine)
5+
#
6+
# This program is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
# This file allows to add custom data source as sensors and display them in System Monitor themes
20+
# There is no limitation on how much custom data source classes can be added to this file
21+
# See CustomDataExample theme for the theme implementation part
22+
23+
import platform
24+
from abc import ABC, abstractmethod
25+
26+
27+
# Custom data classes must be implemented in this file, inherit the CustomDataSource and implement its 2 methods
28+
class CustomDataSource(ABC):
29+
@abstractmethod
30+
def as_numeric(self) -> float:
31+
# Numeric value will be used for graph and radial progress bars
32+
# If there is no numeric value, keep this function empty
33+
pass
34+
35+
@abstractmethod
36+
def as_string(self) -> str:
37+
# Text value will be used for text display and radial progress bar inner text
38+
# Numeric value can be formatted here to be displayed as expected
39+
# It is also possible to return a text unrelated to the numeric value
40+
# If this function is empty, the numeric value will be used as string without formatting
41+
pass
42+
43+
44+
# Example for a custom data class that has numeric and text values
45+
class ExampleCustomNumericData(CustomDataSource):
46+
def as_numeric(self) -> float:
47+
# Numeric value will be used for graph and radial progress bars
48+
# Here a Python function from another module can be called to get data
49+
# Example: return my_module.get_rgb_led_brightness() / return audio.system_volume() ...
50+
return 75.845
51+
52+
def as_string(self) -> str:
53+
# Text value will be used for text display and radial progress bar inner text.
54+
# Numeric value can be formatted here to be displayed as expected
55+
# It is also possible to return a text unrelated to the numeric value
56+
# If this function is empty, the numeric value will be used as string without formatting
57+
# Example here: format numeric value: add unit as a suffix, and keep 1 digit decimal precision
58+
return f'{self.as_numeric(): .1f}%'
59+
60+
61+
# Example for a custom data class that only has text values
62+
class ExampleCustomTextOnlyData(CustomDataSource):
63+
def as_numeric(self) -> float:
64+
# If there is no numeric value, keep this function empty
65+
pass
66+
67+
def as_string(self) -> str:
68+
# If a custom data class only has text values, it won't be possible to display graph or radial bars
69+
return "Python version: " + platform.python_version()

library/stats.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@
7070
except:
7171
os._exit(0)
7272

73+
import library.sensors.sensors_custom as sensors_custom
74+
7375

7476
def get_theme_file_path(name):
7577
if name:
@@ -118,14 +120,17 @@ def display_themed_progress_bar(theme_data, value):
118120
)
119121

120122

121-
def display_themed_radial_bar(theme_data, value, min_size=0, unit=''):
123+
def display_themed_radial_bar(theme_data, value, min_size=0, unit='', custom_text=None):
122124
if not theme_data.get("SHOW", False):
123125
return
124126

125127
if theme_data.get("SHOW_TEXT", False):
126-
text = f"{{:>{min_size}}}".format(value)
127-
if theme_data.get("SHOW_UNIT", True) and unit:
128-
text += str(unit)
128+
if custom_text:
129+
text = custom_text
130+
else:
131+
text = f"{{:>{min_size}}}".format(value)
132+
if theme_data.get("SHOW_UNIT", True) and unit:
133+
text += str(unit)
129134
else:
130135
text = ""
131136

@@ -490,3 +495,41 @@ def stats():
490495
theme_data=hour_theme_data,
491496
value=f"{babel.dates.format_time(date_now, format=time_format, locale=lc_time)}"
492497
)
498+
499+
500+
class Custom:
501+
@staticmethod
502+
def stats():
503+
for custom_stat in config.THEME_DATA['STATS']['CUSTOM']:
504+
if custom_stat != "INTERVAL":
505+
506+
# Load the custom sensor class from sensors_custom.py based on the class name
507+
try:
508+
custom_stat_class = getattr(sensors_custom, str(custom_stat))()
509+
string_value = custom_stat_class.as_string()
510+
numeric_value = custom_stat_class.as_numeric()
511+
except:
512+
logger.error("Custom sensor class " + str(custom_stat) + " not found in sensors_custom.py")
513+
return
514+
515+
if not string_value:
516+
string_value = str(numeric_value)
517+
518+
# Display text
519+
theme_data = config.THEME_DATA['STATS']['CUSTOM'][custom_stat].get("TEXT", None)
520+
if theme_data and string_value:
521+
display_themed_value(theme_data=theme_data, value=string_value)
522+
523+
# Display graph from numeric value
524+
theme_data = config.THEME_DATA['STATS']['CUSTOM'][custom_stat].get("GRAPH", None)
525+
if theme_data and numeric_value:
526+
display_themed_progress_bar(theme_data=theme_data, value=numeric_value)
527+
528+
# Display radial from numeric and text value
529+
theme_data = config.THEME_DATA['STATS']['CUSTOM'][custom_stat].get("RADIAL", None)
530+
if theme_data and numeric_value and string_value:
531+
display_themed_radial_bar(
532+
theme_data=theme_data,
533+
value=numeric_value,
534+
custom_text=string_value
535+
)

main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ def on_win32_wm_event(hWnd, msg, wParam, lParam):
207207
scheduler.DiskStats()
208208
scheduler.NetStats()
209209
scheduler.DateStats()
210+
scheduler.CustomStats()
210211
scheduler.QueueHandler()
211212

212213
if tray_icon and platform.system() == "Darwin": # macOS-specific
123 KB
Loading
91.3 KB
Loading
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# This theme is an example of how to implement and display custom data in System Monitor
2+
# It is possible to add external sensor sources that come from custom Python code or external modules, and integrate them in an existing theme
3+
# This file is the theme part to display the custom data, custom data gathering must first be implemented in Python into the sensors_custom.py file
4+
# Names of the custom sensor classes listed here must be an exact match to class names in sensors_custom.py
5+
---
6+
display:
7+
DISPLAY_ORIENTATION: landscape
8+
DISPLAY_RGB_LED: 61, 184, 225
9+
10+
static_images:
11+
BACKGROUND:
12+
PATH: background.png
13+
X: 0
14+
Y: 0
15+
WIDTH: 480
16+
HEIGHT: 320
17+
18+
STATS:
19+
DATE:
20+
INTERVAL: 1
21+
HOUR:
22+
TEXT:
23+
SHOW: True
24+
X: 20
25+
Y: 30
26+
FONT: roboto-mono/RobotoMono-Bold.ttf
27+
FONT_SIZE: 20
28+
FONT_COLOR: 200, 200, 200
29+
# BACKGROUND_COLOR: 50, 0, 0
30+
BACKGROUND_IMAGE: background.png
31+
32+
# All custom sensor classes are listed under CUSTOM
33+
CUSTOM:
34+
35+
# For now the refresh interval (in seconds) is the same for all custom data classes
36+
INTERVAL: 3
37+
38+
39+
40+
# The name of the class must be an exact match to the class name in sensors_custom.py
41+
ExampleCustomNumericData:
42+
43+
# There are 3 ways of displaying a custom sensor containing numeric values: as text, as graph progress bar, as radial progress bar
44+
TEXT:
45+
SHOW: True
46+
X: 50
47+
Y: 80
48+
FONT: roboto-mono/RobotoMono-Bold.ttf
49+
FONT_SIZE: 40
50+
FONT_COLOR: 200, 200, 200
51+
# BACKGROUND_COLOR: 50, 0, 0
52+
BACKGROUND_IMAGE: background.png
53+
54+
GRAPH:
55+
SHOW: True
56+
X: 50
57+
Y: 150
58+
WIDTH: 180
59+
HEIGHT: 30
60+
MIN_VALUE: 0
61+
MAX_VALUE: 100
62+
BAR_COLOR: 255, 135, 0
63+
BAR_OUTLINE: True
64+
# BACKGROUND_COLOR: 0, 0, 0
65+
BACKGROUND_IMAGE: background.png
66+
67+
RADIAL:
68+
SHOW: True
69+
X: 350
70+
Y: 130
71+
RADIUS: 68
72+
WIDTH: 18
73+
MIN_VALUE: 0
74+
MAX_VALUE: 100
75+
ANGLE_START: 110
76+
ANGLE_END: 70
77+
ANGLE_STEPS: 1
78+
ANGLE_SEP: 25
79+
CLOCKWISE: True
80+
BAR_COLOR: 61, 184, 225
81+
SHOW_TEXT: True
82+
SHOW_UNIT: True
83+
FONT: roboto-mono/RobotoMono-Bold.ttf
84+
FONT_SIZE: 25
85+
FONT_COLOR: 255, 135, 0
86+
# BACKGROUND_COLOR: 0, 0, 0
87+
BACKGROUND_IMAGE: background.png
88+
89+
90+
91+
92+
# The name of the class must be an exact match to the class name in sensors_custom.py
93+
ExampleCustomTextOnlyData:
94+
95+
# There are only 1 way of displaying a custom sensor containing text-only values: as text
96+
TEXT:
97+
SHOW: True
98+
X: 34
99+
Y: 250
100+
FONT: roboto-mono/RobotoMono-Bold.ttf
101+
FONT_SIZE: 28
102+
FONT_COLOR: 61, 184, 225
103+
# BACKGROUND_COLOR: 50, 0, 0
104+
BACKGROUND_IMAGE: background.png

res/themes/default.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,5 @@ STATS:
135135
HOUR:
136136
TEXT:
137137
SHOW: False
138+
CUSTOM:
139+
INTERVAL: 3

theme-editor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def refresh_theme():
110110
stats.Disk.stats()
111111
stats.Net.stats()
112112
stats.Date.stats()
113+
stats.Custom.stats()
113114

114115

115116
if __name__ == "__main__":

0 commit comments

Comments
 (0)