new sleep screen v1

This commit is contained in:
barry 2025-01-18 20:14:49 +01:00
parent 347c87a154
commit 2a931ce090
5 changed files with 223 additions and 41 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
@ -9,24 +10,39 @@ import yaml
from typing import Dict from typing import Dict
import subprocess import subprocess
import numpy as np
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(): def get_cpu_temp():
try: try:
result = subprocess.run( return float(
subprocess.run(
["vcgencmd", "measure_temp"], capture_output=True, text=True, check=True ["vcgencmd", "measure_temp"], capture_output=True, text=True, check=True
) )
temp_str = result.stdout.strip() .stdout.split("=")[1]
temp_value = temp_str.split("=")[1].replace("'C", "") .replace("'C", "")
return float(temp_value) )
except subprocess.CalledProcessError as e: except (subprocess.CalledProcessError, IndexError, ValueError) as e:
warnings.warn(f"Error executing vcgencmd: {e}") warnings.warn(f"Error getting CPU temp: {e}")
return "None" return None
except (IndexError, ValueError) as e:
warnings.warn(f"Error parsing temperature: {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"):
@ -45,11 +61,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))
@ -57,20 +74,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
@ -78,6 +82,8 @@ 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)
while True: while True:
if time.time() - last_auth_refresh >= auth_interval: if time.time() - last_auth_refresh >= auth_interval:
@ -93,33 +99,94 @@ 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() draw.rectangle(
draw.rectangle([(00, 120), (119, 190)], fill=(0, 0, 0)) [(0, 100), (320, 240)], fill=(0, 0, 0)
offset = 10 if current_time.hour < 9 else 0 ) # refresh stats bg
draw.rectangle([(95, 25), (250, 98)], fill=(0, 0, 0)) # refresh time bg
# 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( draw.text(
(10 + offset, 120), (320 - ((idx + 1) * 16), 230 - bin_i // 65),
f"{current_time.hour}:{current_time.minute:02d}", f"{bin_i / 1024:.2f}Gib",
font=Font00,
fill=tuple(cmap2[col]),
)
# Draw Time
current_time = datetime.now()
offset = 15 if current_time.hour < 10 and current_time.hour != 0 else 0
draw.text(
(100 + offset, 30),
current_time.strftime("%-I:%M"),
font=Font3, font=Font3,
fill=(255, 255, 255), fill=(255, 255, 255),
) )
draw.text( draw.text(
(25, 152), (120, 78),
current_time.strftime("%a %d"), current_time.strftime("%a, %d"),
font=Font2, font=Font2,
fill=(255, 255, 255), fill=(255, 255, 255),
) )
draw.text(
(20, 175), d.ShowImage(not_playing_img)
f"CPU: {get_cpu_temp():.2f}°", time.sleep(0.1)
font=Font0,
fill=(255, 255, 255),
)
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