Merge remote-tracking branch 'refs/remotes/origin/master'

This commit is contained in:
Alessandro 2025-01-21 19:10:58 +01:00
commit e0c1ab407c
5 changed files with 267 additions and 33 deletions

33
poetry.lock generated
View File

@ -387,6 +387,37 @@ nodeenv = ">=0.11.1"
pyyaml = ">=5.1" pyyaml = ">=5.1"
virtualenv = ">=20.10.0" virtualenv = ">=20.10.0"
[[package]]
name = "psutil"
version = "6.1.1"
description = "Cross-platform lib for process and system monitoring in Python."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
groups = ["main"]
files = [
{file = "psutil-6.1.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8"},
{file = "psutil-6.1.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777"},
{file = "psutil-6.1.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8df0178ba8a9e5bc84fed9cfa61d54601b371fbec5c8eebad27575f1e105c0d4"},
{file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:1924e659d6c19c647e763e78670a05dbb7feaf44a0e9c94bf9e14dfc6ba50468"},
{file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:018aeae2af92d943fdf1da6b58665124897cfc94faa2ca92098838f83e1b1bca"},
{file = "psutil-6.1.1-cp27-none-win32.whl", hash = "sha256:6d4281f5bbca041e2292be3380ec56a9413b790579b8e593b1784499d0005dac"},
{file = "psutil-6.1.1-cp27-none-win_amd64.whl", hash = "sha256:c777eb75bb33c47377c9af68f30e9f11bc78e0f07fbf907be4a5d70b2fe5f030"},
{file = "psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8"},
{file = "psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377"},
{file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003"},
{file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160"},
{file = "psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3"},
{file = "psutil-6.1.1-cp36-cp36m-win32.whl", hash = "sha256:384636b1a64b47814437d1173be1427a7c83681b17a450bfc309a1953e329603"},
{file = "psutil-6.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8be07491f6ebe1a693f17d4f11e69d0dc1811fa082736500f649f79df7735303"},
{file = "psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53"},
{file = "psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649"},
{file = "psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5"},
]
[package.extras]
dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"]
test = ["pytest", "pytest-xdist", "setuptools"]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.2" version = "6.0.2"
@ -554,4 +585,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "71a1f02c9a8b16892110f2c82cbeb2b8a831cd016c56b27446d5eedcaa2a5971" content-hash = "135cf9001d97689d659830abaa919636d20e76a494585757f9b59fbe9ca3adb5"

View File

@ -12,6 +12,7 @@ spidev = "^3.6"
numpy = "^2.2.1" numpy = "^2.2.1"
pillow = "^11.1.0" pillow = "^11.1.0"
pyyaml = "^6.0.2" pyyaml = "^6.0.2"
psutil = "^6.1.1"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]

View File

@ -225,6 +225,24 @@ pillow==11.1.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71 \ --hash=sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71 \
--hash=sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9 \ --hash=sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9 \
--hash=sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761 --hash=sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761
psutil==6.1.1 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:018aeae2af92d943fdf1da6b58665124897cfc94faa2ca92098838f83e1b1bca \
--hash=sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377 \
--hash=sha256:1924e659d6c19c647e763e78670a05dbb7feaf44a0e9c94bf9e14dfc6ba50468 \
--hash=sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3 \
--hash=sha256:384636b1a64b47814437d1173be1427a7c83681b17a450bfc309a1953e329603 \
--hash=sha256:6d4281f5bbca041e2292be3380ec56a9413b790579b8e593b1784499d0005dac \
--hash=sha256:8be07491f6ebe1a693f17d4f11e69d0dc1811fa082736500f649f79df7735303 \
--hash=sha256:8df0178ba8a9e5bc84fed9cfa61d54601b371fbec5c8eebad27575f1e105c0d4 \
--hash=sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160 \
--hash=sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8 \
--hash=sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003 \
--hash=sha256:c777eb75bb33c47377c9af68f30e9f11bc78e0f07fbf907be4a5d70b2fe5f030 \
--hash=sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777 \
--hash=sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5 \
--hash=sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53 \
--hash=sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649 \
--hash=sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8
pyyaml==6.0.2 ; python_version >= "3.11" and python_version < "4.0" \ pyyaml==6.0.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \

65
spotiplayer_pi/cmap.py Normal file
View File

@ -0,0 +1,65 @@
cmap = [
[65, 153, 181],
[75, 164, 177],
[87, 178, 171],
[99, 191, 165],
[112, 198, 164],
[129, 204, 164],
[142, 209, 164],
[158, 216, 164],
[172, 221, 163],
[186, 227, 160],
[199, 232, 158],
[211, 237, 155],
[225, 243, 152],
[232, 246, 156],
[238, 248, 165],
[243, 250, 173],
[249, 252, 182],
[254, 254, 190],
[254, 248, 179],
[254, 241, 167],
[254, 234, 157],
[254, 227, 145],
[253, 220, 135],
[253, 208, 125],
[253, 198, 117],
[253, 186, 107],
[253, 174, 97],
[251, 162, 91],
[249, 147, 84],
[247, 134, 78],
[245, 119, 71],
[242, 107, 67],
[235, 96, 70],
[228, 85, 73],
[222, 75, 75],
[214, 64, 78],
[205, 53, 77],
[192, 39, 74],
[181, 27, 71],
[168, 12, 68],
]
cmap2 = [
[183, 29, 72],
[209, 58, 78],
[225, 81, 74],
[240, 103, 68],
[247, 131, 77],
[251, 165, 92],
[253, 190, 110],
[253, 214, 130],
[254, 232, 153],
[254, 247, 177],
[248, 252, 181],
[236, 247, 162],
[218, 240, 154],
[190, 229, 160],
[161, 217, 164],
[126, 203, 164],
[95, 187, 167],
[71, 159, 179],
[52, 132, 187],
[73, 105, 174],
]

View File

@ -1,5 +1,6 @@
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import textwrap import textwrap
from collections import deque
import time import time
import requests import requests
from io import BytesIO from io import BytesIO
@ -7,9 +8,50 @@ import warnings
from datetime import datetime from datetime import datetime
import yaml import yaml
from typing import Dict from typing import Dict
import subprocess
import numpy as np
import psutil
from spotiplayer_pi.lib import LCD_2inch from spotiplayer_pi.lib import LCD_2inch
from spotiplayer_pi.api import Api from spotiplayer_pi.api import Api
from spotiplayer_pi.cmap import cmap, cmap2
def get_cpu_temp():
try:
return float(
subprocess.run(
["vcgencmd", "measure_temp"], capture_output=True, text=True, check=True
)
.stdout.split("=")[1]
.replace("'C", "")
)
except (subprocess.CalledProcessError, IndexError, ValueError) as e:
warnings.warn(f"Error getting CPU temp: {e}")
return None
def get_cpu_util():
try:
return psutil.cpu_percent(interval=0.1)
except Exception as e:
warnings.warn(f"Error getting CPU util: {e}")
return None
def get_mem_util():
try:
return int(
subprocess.run(
["bash", "-c", "free -m | awk '/^Mem:/ {print $3}'"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
)
except Exception as e:
warnings.warn(f"Error getting Memory utilization: {e}")
def parse_config(path="spotiplayer_pi/config.yaml"): def parse_config(path="spotiplayer_pi/config.yaml"):
@ -28,11 +70,12 @@ def display_loop(api: Api, cfg: Dict):
bg = Image.new("RGB", (d.height, d.width), (0, 0, 0)) bg = Image.new("RGB", (d.height, d.width), (0, 0, 0))
d.ShowImage(bg) d.ShowImage(bg)
Font00 = ImageFont.truetype("Font/GothamBold.ttf", 10)
Font0 = ImageFont.truetype("Font/GothamBold.ttf", 14) Font0 = ImageFont.truetype("Font/GothamBold.ttf", 14)
Font1 = ImageFont.truetype("Font/GothamMedium.ttf", 18) Font1 = ImageFont.truetype("Font/GothamMedium.ttf", 18)
Font1b = ImageFont.truetype("Font/GothamBold.ttf", 19) Font1b = ImageFont.truetype("Font/GothamBold.ttf", 19)
Font2 = ImageFont.truetype("Font/GothamMedium.ttf", 20) Font2 = ImageFont.truetype("Font/GothamBold.ttf", 23)
Font3 = ImageFont.truetype("Font/GothamMedium.ttf", 36) Font3 = ImageFont.truetype("Font/GothamMedium.ttf", 52)
unavailable_img = Image.open("imgs/unavailable.jpg") unavailable_img = Image.open("imgs/unavailable.jpg")
error_img = Image.new("RGB", (240, 320), color=(255, 0, 0)) error_img = Image.new("RGB", (240, 320), color=(255, 0, 0))
@ -40,20 +83,7 @@ def display_loop(api: Api, cfg: Dict):
draw.text((150, 150), "Error :(", font=Font1, fill=cfg["color_theme"]["text"]) draw.text((150, 150), "Error :(", font=Font1, fill=cfg["color_theme"]["text"])
del draw del draw
spoti_logo = Image.open("imgs/logo64.jpg")
qr = Image.open("imgs/qr.jpg")
spoti_logo = spoti_logo.resize((64, 64))
not_playing_img = Image.new("RGB", (320, 240), (0, 0, 0)) not_playing_img = Image.new("RGB", (320, 240), (0, 0, 0))
not_playing_img.paste(spoti_logo, (25, 40))
not_playing_img.paste(qr, (120, 40))
draw = ImageDraw.Draw(not_playing_img)
draw.text(
(112, 15),
"Connect to speakers",
font=Font2,
fill=(255, 255, 255),
)
del spoti_logo, qr, draw
last_api_call = 0 last_api_call = 0
last_auth_refresh = 0 last_auth_refresh = 0
@ -61,6 +91,10 @@ def display_loop(api: Api, cfg: Dict):
last_track = None last_track = None
data = None data = None
auth_interval = 0 auth_interval = 0
buffer_temp = deque([None] * 320, maxlen=320)
buffer_mem = deque([None] * 320, maxlen=320)
buffer_cpu = deque([None] * 320, maxlen=320)
alpha = 0.3
while True: while True:
if time.time() - last_auth_refresh >= auth_interval: if time.time() - last_auth_refresh >= auth_interval:
@ -76,27 +110,112 @@ def display_loop(api: Api, cfg: Dict):
d.ShowImage(error_img) d.ShowImage(error_img)
elif data == "not-playing": elif data == "not-playing":
pixels = not_playing_img.load()
draw = ImageDraw.Draw(not_playing_img) draw = ImageDraw.Draw(not_playing_img)
current_time = datetime.now().time() draw.rectangle([(0, 0), (320, 240)], fill=(0, 0, 0)) # refresh bg
draw.rectangle([(00, 120), (119, 170)], fill=(0, 0, 0))
offset = 10 if current_time.hour < 9 else 0 # Draw CPU usage
draw.text( buffer_cpu.append(get_cpu_util())
(10 + offset, 120), cpu_values = np.array([x for x in buffer_cpu if x is not None])
f"{current_time.hour}:{current_time.minute:02d}", cpu_values_idxs = [i for i, x in enumerate(buffer_cpu) if x is not None]
font=Font3, if len(cpu_values) >= 2:
fill=(255, 255, 255), smoothed = np.empty_like(cpu_values, dtype=float)
) smoothed[0] = cpu_values[0]
draw.text( for t in range(1, len(cpu_values)):
(20, 152), smoothed[t] = (
current_time.strftime("%a %d"), alpha * cpu_values[t] + (1 - alpha) * cpu_values[t - 1]
font=Font2, )
fill=(255, 255, 255), for i in range(1, len(cpu_values_idxs)):
) idx = cpu_values_idxs[i]
val, prev_val = cpu_values[i], cpu_values[i - 1]
draw.rectangle(
[(idx, 200 - val), (idx - 1, 200 - prev_val)],
fill=(255, 253, 195),
)
draw.text(
(290, 180 - val),
f"{int(val)}%",
fill=(255, 255, 255),
font=Font0,
)
# Draw Temp
buffer_temp.append(get_cpu_temp())
temp_values = [x for x in buffer_temp if x is not None]
min_temp, max_temp = min(temp_values), max(temp_values)
if len(temp_values) >= 2 and max_temp != min_temp:
temp_data_scaled = [
(idx, ((x - min_temp) / (max_temp - min_temp)) * 40)
for idx, x in enumerate(buffer_temp)
if x is not None
]
for idx, val in temp_data_scaled:
color_idx = max(
0, min(int(buffer_temp[idx]) - 40, len(cmap) - 1)
)
pixels[idx, 150 - val] = tuple(cmap[color_idx])
draw.rectangle(
[(0, 149), (3, 151)],
fill=tuple(
cmap[max(0, min(len(cmap) - 1, int(min_temp) - 40))]
),
) # min
draw.rectangle(
[(0, 109), (3, 111)],
fill=tuple(
cmap[max(0, min(len(cmap) - 1, int(max_temp) - 40))]
),
) # max
draw.text((5, 150), f"{int(min_temp)}°", font=Font0)
draw.text((5, 100), f"{int(max_temp)}°", font=Font0)
# Draw Mem usage
buffer_mem.append(get_mem_util())
mem_data = [x for x in buffer_mem if x is not None]
group_size = 16
n_bins = len(mem_data) // group_size
bins = np.arange(len(mem_data)) // group_size
if n_bins > 0:
binned_data = [
np.mean(np.array(mem_data)[bins == i]) for i in range(n_bins)
]
min_bin, max_bin = np.argmin(binned_data), np.argmax(binned_data)
for idx_, bin_i in enumerate(binned_data):
idx = len(binned_data) - idx_ - 1
draw.rectangle(
[
(321 - ((idx + 1) * 16), 240),
(319 - (idx * 16), 240 - bin_i // 65),
],
fill=tuple(cmap2[idx]),
)
if idx_ in [min_bin, max_bin]:
col = 0 if idx_ == max_bin else n_bins - 1
draw.text(
(320 - ((idx + 1) * 16), 230 - bin_i // 65),
f"{bin_i / 1024:.1f}Gi",
font=Font00,
fill=tuple(cmap2[col]),
)
# Draw Time
current_time = datetime.now()
time_text = current_time.strftime("%-I:%M")
date_text = current_time.strftime("%a, %d")
time_bbox = draw.textbbox((0, 0), time_text, font=Font3)
date_bbox = draw.textbbox((0, 0), date_text, font=Font2)
time_width = time_bbox[2] - time_bbox[0]
date_width = date_bbox[2] - date_bbox[0]
time_x = (320 - time_width) // 2
date_x = (320 - date_width) // 2
draw.text((time_x, 30), time_text, font=Font3, fill=(255, 255, 255))
draw.text((date_x, 78), date_text, font=Font2, fill=(255, 255, 255))
d.ShowImage(not_playing_img)
if current_mode != 0: if current_mode != 0:
current_mode = 0 current_mode = 0
print("Standby mode") print("Standby mode")
d.ShowImage(not_playing_img)
time.sleep(cfg["api_interval"])
elif type(data) == dict: elif type(data) == dict:
current_mode = 1 current_mode = 1