314 lines
12 KiB
Python
314 lines
12 KiB
Python
from PIL import Image, ImageDraw, ImageFont
|
|
import textwrap
|
|
from collections import deque
|
|
import time
|
|
import requests
|
|
from io import BytesIO
|
|
import warnings
|
|
from datetime import datetime
|
|
import yaml
|
|
from typing import Dict
|
|
import subprocess
|
|
|
|
import numpy as np
|
|
import psutil
|
|
|
|
from spotiplayer_pi.lib import LCD_2inch
|
|
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)
|
|
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"):
|
|
with open(path, "r") as file:
|
|
data = yaml.safe_load(file)
|
|
for key, value in data["color_theme"].items():
|
|
data["color_theme"][key] = tuple(value)
|
|
return data
|
|
|
|
|
|
def display_loop(api: Api, cfg: Dict):
|
|
try:
|
|
d = LCD_2inch()
|
|
d.Init()
|
|
d.clear()
|
|
bg = Image.new("RGB", (d.height, d.width), (0, 0, 0))
|
|
d.ShowImage(bg)
|
|
|
|
Font00 = ImageFont.truetype("Font/GothamBold.ttf", 10)
|
|
Font0 = ImageFont.truetype("Font/GothamBold.ttf", 14)
|
|
Font1 = ImageFont.truetype("Font/GothamMedium.ttf", 18)
|
|
Font1b = ImageFont.truetype("Font/GothamBold.ttf", 19)
|
|
Font2 = ImageFont.truetype("Font/GothamBold.ttf", 23)
|
|
Font3 = ImageFont.truetype("Font/GothamMedium.ttf", 52)
|
|
|
|
unavailable_img = Image.open("imgs/unavailable.jpg")
|
|
error_img = Image.new("RGB", (240, 320), color=(255, 0, 0))
|
|
draw = ImageDraw.Draw(error_img)
|
|
draw.text((150, 150), "Error :(", font=Font1, fill=cfg["color_theme"]["text"])
|
|
del draw
|
|
|
|
not_playing_img = Image.new("RGB", (320, 240), (0, 0, 0))
|
|
|
|
last_api_call = 0
|
|
last_auth_refresh = 0
|
|
current_mode = None # 0: not-playing, 1: playing
|
|
last_track = None
|
|
data = None
|
|
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:
|
|
if time.time() - last_auth_refresh >= auth_interval:
|
|
auth_interval = api.refreshAuth() - 120
|
|
last_auth_refresh = time.time()
|
|
print(f"Refreshed auth at {datetime.now().strftime('%d-%m %H:%M:%S')}")
|
|
|
|
if time.time() - last_api_call >= cfg["api_interval"]:
|
|
data = api.getPlaying()
|
|
last_api_call = time.time()
|
|
if data == None:
|
|
warnings.warn("No data found")
|
|
d.ShowImage(error_img)
|
|
|
|
elif data == "not-playing":
|
|
pixels = not_playing_img.load()
|
|
draw = ImageDraw.Draw(not_playing_img)
|
|
draw.rectangle([(0, 100), (320, 240)], fill=(0, 0, 0)) # refresh bg
|
|
draw.rectangle([(95, 25), (250, 98)], fill=(0, 0, 0)) # refresh bg
|
|
draw.rectangle([(240, 0), (320, 150)], fill=(0, 0, 0)) # refresh bg
|
|
|
|
# Draw CPU usage
|
|
buffer_cpu.append(get_cpu_util())
|
|
cpu_values = np.array([x for x in buffer_cpu if x is not None])
|
|
cpu_values_idxs = [i for i, x in enumerate(buffer_cpu) if x is not None]
|
|
if len(cpu_values) >= 2:
|
|
smoothed = np.empty_like(cpu_values, dtype=float)
|
|
smoothed[0] = cpu_values[0]
|
|
for t in range(1, len(cpu_values)):
|
|
smoothed[t] = (
|
|
alpha * cpu_values[t] + (1 - alpha) * cpu_values[t - 1]
|
|
)
|
|
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()
|
|
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,
|
|
fill=(255, 255, 255),
|
|
)
|
|
draw.text(
|
|
(120, 78),
|
|
current_time.strftime("%a, %d"),
|
|
font=Font2,
|
|
fill=(255, 255, 255),
|
|
)
|
|
|
|
d.ShowImage(not_playing_img)
|
|
time.sleep(0.1)
|
|
if current_mode != 0:
|
|
current_mode = 0
|
|
print("Standby mode")
|
|
|
|
elif type(data) == dict:
|
|
current_mode = 1
|
|
current_track = data["track"] + data["artists"][0] + data["album"]
|
|
if current_track != last_track:
|
|
print(f"Updating track to : {data['track']} - {data['artists'][0]}")
|
|
last_track = current_track
|
|
img = None
|
|
try:
|
|
img = requests.get(
|
|
data["img_url"], timeout=cfg["refresh_interval"] / 2
|
|
)
|
|
img = Image.open(BytesIO(img.content))
|
|
except Exception as e:
|
|
warnings.warn(
|
|
f"Failed to fetch album cover at: {data['img_url']}\n{e}"
|
|
)
|
|
if img == None:
|
|
img = unavailable_img
|
|
img = img.resize((128, 128))
|
|
bg = Image.new("RGB", (320, 240), cfg["color_theme"]["background"])
|
|
bg.paste(img, (10, 30))
|
|
draw = ImageDraw.Draw(bg)
|
|
draw.text(
|
|
(145, 34),
|
|
"\n".join(textwrap.wrap(", ".join(data["artists"]), width=16)),
|
|
font=Font1,
|
|
fill=cfg["color_theme"]["text"],
|
|
)
|
|
draw.text(
|
|
(11, 164),
|
|
"\n".join(textwrap.wrap(data["track"], width=28)),
|
|
font=Font1b,
|
|
fill=cfg["color_theme"]["text"],
|
|
)
|
|
|
|
w, h = bg.size
|
|
w -= 90
|
|
h += 4
|
|
progress_time = min(
|
|
data["duration_ms"],
|
|
data["progress_ms"] + int((time.time() - last_api_call) * 1000),
|
|
)
|
|
progress = min(1, progress_time / data["duration_ms"])
|
|
bar_width = int((w - 10) * progress)
|
|
draw.rectangle(
|
|
[(9, h - 23), (w + 3, h - 7)],
|
|
outline=cfg["color_theme"]["text"],
|
|
)
|
|
|
|
draw.rectangle(
|
|
[(10, h - 22), (w + 2, h - 8)],
|
|
outline=cfg["color_theme"]["text"],
|
|
)
|
|
draw.rectangle(
|
|
[(12, h - 20), (w, h - 10)], fill=cfg["color_theme"]["background"]
|
|
)
|
|
draw.rectangle(
|
|
[(12, h - 20), (12 + bar_width, h - 10)],
|
|
fill=cfg["color_theme"]["bar_inside"],
|
|
)
|
|
f_current_time = "{}:{:02.0f}".format(
|
|
*divmod(progress_time // 1000, 60)
|
|
)
|
|
f_total_time = "{}:{:02d}".format(
|
|
*divmod(data["duration_ms"] // 1000, 60)
|
|
)
|
|
draw.rectangle(
|
|
[(237, 218), (320, 238)], fill=cfg["color_theme"]["background"]
|
|
)
|
|
draw.text(
|
|
(238, 223),
|
|
f"{f_current_time}/{f_total_time}",
|
|
font=Font0,
|
|
fill=cfg["color_theme"]["text"],
|
|
)
|
|
d.ShowImage(bg)
|
|
time.sleep(cfg["refresh_interval"])
|
|
|
|
except IOError as e:
|
|
raise e
|
|
except KeyboardInterrupt:
|
|
d.module_exit()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cfg = parse_config()
|
|
api = Api(cfg["refresh_token"], cfg["base_64"])
|
|
display_loop(api, cfg)
|