Files
matrix64/matrix.py

465 lines
17 KiB
Python

#!/usr/bin/env python3
"""
Matrix64 LED Display with Multi-View Pagination
Views:
0: Principal (Clima + Reloj + Temps)
1: Discos (HDD Details)
2: Fecha (Date + Weather)
3: Tesla (Model 3 Status)
Buttons (GPIO 0 & 1):
- Button 1: Siguiente vista
- Button 2: Vista anterior
- Mantener: Auto-ciclo
"""
from samplebase import SampleBase
from rgbmatrix import graphics
from gpiozero import Button
import time
import datetime
import locale
import math
from config import LED_ROWS, LED_COLS
from weather_icons import draw_weather_icon
from home_assistant import (
get_weather, get_weather_description,
get_interior_weather, get_brightness, get_tesla_status
)
from netdata import get_hdd_temps
from mqtt_listener import MQTTListener
# Set Spanish locale for dates
try:
locale.setlocale(locale.LC_TIME, 'es_ES.UTF-8')
except:
try:
locale.setlocale(locale.LC_TIME, 'es_ES')
except:
pass
# Number of views
NUM_VIEWS = 4
# View names (Spanish)
VIEW_NAMES = {
0: "Principal",
1: "Discos",
2: "Fecha",
3: "Tesla",
}
# Spanish translations
SPANISH_DAYS = {
'Monday': 'Lunes', 'Tuesday': 'Martes', 'Wednesday': 'Miércoles',
'Thursday': 'Jueves', 'Friday': 'Viernes', 'Saturday': 'Sábado', 'Sunday': 'Domingo'
}
SPANISH_MONTHS = {
'Jan': 'Ene', 'Feb': 'Feb', 'Mar': 'Mar', 'Apr': 'Abr', 'May': 'May', 'Jun': 'Jun',
'Jul': 'Jul', 'Aug': 'Ago', 'Sep': 'Sep', 'Oct': 'Oct', 'Nov': 'Nov', 'Dec': 'Dic'
}
def format_temp(value):
try:
return f"{float(value):.1f}°"
except (ValueError, TypeError):
return ""
def format_humidity(value):
try:
return f"{int(float(value))}%"
except (ValueError, TypeError):
return "?%"
def get_temperature_color(temp):
try:
temp_value = float(temp) if isinstance(temp, (int, float)) else float(str(temp).replace('°', '').strip())
if temp_value < 0:
return graphics.Color(0, 100, 255)
elif temp_value < 10:
return graphics.Color(100, 180, 255)
elif temp_value < 20:
return graphics.Color(0, 255, 100)
elif temp_value < 30:
return graphics.Color(255, 200, 0)
else:
return graphics.Color(255, 50, 50)
except ValueError:
return graphics.Color(255, 255, 255)
def get_hdd_color(temp):
try:
temp_value = int(temp)
if temp_value < 32:
return graphics.Color(0, 255, 80)
elif temp_value <= 40:
return graphics.Color(255, 200, 0)
else:
return graphics.Color(255, 50, 50)
except:
return graphics.Color(255, 255, 255)
def get_spanish_day(now):
return SPANISH_DAYS.get(now.strftime("%A"), now.strftime("%A"))
def get_spanish_date(now):
month = SPANISH_MONTHS.get(now.strftime("%b"), now.strftime("%b"))
return f"{now.day} {month}"
class Matrix64Display(SampleBase):
def __init__(self, *args, **kwargs):
super(Matrix64Display, self).__init__(*args, **kwargs)
self.parser.add_argument("-t", "--text", help="Text", default="")
# Data
self.temperature = None
self.humidity = None
self.interior_temperature = None
self.interior_humidity = None
self.last_update = time.time()
self.weather_desc = "unknown"
self.hdd_temps = None
self.tesla = None
# View state
self.current_view = 0
self.auto_cycle = False
self.last_view_change = time.time()
self.view_cycle_interval = 5
# Feedback
self.feedback_message = None
self.feedback_until = 0
# Animation
self.animation_frame = 0
# MQTT listener
self.mqtt = MQTTListener()
self.mqtt.start()
# Buttons
self.button1 = Button(0, pull_up=True, hold_time=1.5)
self.button2 = Button(1, pull_up=True, hold_time=1.5)
self.button1.when_pressed = self.on_button1_press
self.button1.when_held = self.on_button_held
self.button2.when_pressed = self.on_button2_press
self.button2.when_held = self.on_button_held
def show_feedback(self, message, duration=2):
self.feedback_message = message
self.feedback_until = time.time() + duration
def on_button1_press(self):
if not self.button1.is_held:
self.current_view = (self.current_view + 1) % NUM_VIEWS
self.last_view_change = time.time()
self.show_feedback(VIEW_NAMES[self.current_view], 1.5)
def on_button2_press(self):
if not self.button2.is_held:
self.current_view = (self.current_view - 1) % NUM_VIEWS
self.last_view_change = time.time()
self.show_feedback(VIEW_NAMES[self.current_view], 1.5)
def on_button_held(self):
self.auto_cycle = not self.auto_cycle
self.show_feedback("AUTO ON" if self.auto_cycle else "AUTO OFF", 2)
def update_data(self):
try:
weather = get_weather()
if weather.get("temperature") is not None:
self.temperature = float(weather.get("temperature"))
self.humidity = weather.get("humidity")
self.weather_desc = get_weather_description()
interior = get_interior_weather()
if interior.get("temperature") is not None:
self.interior_temperature = float(interior.get("temperature"))
self.interior_humidity = interior.get("humidity")
self.tesla = get_tesla_status()
except Exception as e:
print(f"Error updating data: {e}")
def update_brightness(self):
try:
brightness = get_brightness()
if brightness:
self.matrix.brightness = int(float(brightness)) / 10 + 1
except:
self.matrix.brightness = 5
def update_hdd_temps(self):
self.hdd_temps = get_hdd_temps()
def draw_camera_alert(self, canvas, fonts):
"""Draw flashing camera alert overlay."""
time_font, data_font, small_font = fonts
# Flashing red border
flash = int(time.time() * 4) % 2
if flash:
border_color = graphics.Color(255, 0, 0)
for i in range(2):
graphics.DrawLine(canvas, i, 0, i, 63, border_color)
graphics.DrawLine(canvas, 63-i, 0, 63-i, 63, border_color)
graphics.DrawLine(canvas, 0, i, 63, i, border_color)
graphics.DrawLine(canvas, 0, 63-i, 63, 63-i, border_color)
# Alert text
alert_color = graphics.Color(255, 50, 50)
graphics.DrawText(canvas, data_font, 8, 32, alert_color, "ALERTA")
graphics.DrawText(canvas, small_font, 6, 42, alert_color, "Camara")
def draw_feedback(self, canvas, fonts, colors):
time_font, data_font, small_font = fonts
time_color, humidity_color, hdd_color, label_color, line_color = colors
# Black background
for y in range(22, 46):
graphics.DrawLine(canvas, 0, y, 63, y, graphics.Color(0, 0, 0))
# Border lines
border_color = graphics.Color(60, 60, 60)
graphics.DrawLine(canvas, 0, 22, 63, 22, border_color)
graphics.DrawLine(canvas, 0, 45, 63, 45, border_color)
# Message
msg_color = graphics.Color(0, 255, 100)
msg_len = len(self.feedback_message) * 7
x = (64 - msg_len) // 2
graphics.DrawText(canvas, time_font, x, 38, msg_color, self.feedback_message)
def draw_view_0(self, canvas, fonts, colors):
"""Vista Principal"""
time_font, data_font, small_font = fonts
time_color, humidity_color, hdd_color, label_color, line_color = colors
now = datetime.datetime.now()
time_str = f"{now.hour:02d}:{now.minute:02d}:{now.second:02d}"
# Weather Icon (cloud moves, sun/icon stays fixed)
cloud_offset = int(math.sin(self.animation_frame * 0.3) * 2)
draw_weather_icon(canvas, 0, 0, self.weather_desc, cloud_offset)
graphics.DrawLine(canvas, 28, 0, 28, 25, line_color)
if self.hdd_temps and len(self.hdd_temps) > 1:
temps = [str(int(t[0])) for t in self.hdd_temps[1:] if t]
if len(temps) >= 2:
graphics.DrawText(canvas, data_font, 34, 8, hdd_color, " ".join(temps[:2]))
if len(temps) >= 4:
graphics.DrawText(canvas, data_font, 34, 16, hdd_color, " ".join(temps[2:4]))
if len(temps) > 4:
graphics.DrawText(canvas, data_font, 34, 24, hdd_color, " ".join(temps[4:6]))
graphics.DrawLine(canvas, 0, 26, 63, 26, line_color)
time_x = (64 - len(time_str) * 7) // 2
graphics.DrawText(canvas, time_font, time_x, 39, time_color, time_str)
graphics.DrawLine(canvas, 0, 42, 63, 42, line_color)
graphics.DrawText(canvas, data_font, 0, 52, label_color, "Ext")
if self.temperature is not None:
temp_color = get_temperature_color(self.temperature)
graphics.DrawText(canvas, data_font, 18, 52, temp_color, format_temp(self.temperature))
if self.humidity is not None:
graphics.DrawText(canvas, data_font, 46, 52, humidity_color, format_humidity(self.humidity))
graphics.DrawText(canvas, data_font, 0, 62, label_color, "Int")
if self.interior_temperature is not None:
temp_color = get_temperature_color(self.interior_temperature)
graphics.DrawText(canvas, data_font, 18, 62, temp_color, format_temp(self.interior_temperature))
if self.interior_humidity is not None:
graphics.DrawText(canvas, data_font, 46, 62, humidity_color, format_humidity(self.interior_humidity))
def draw_view_1(self, canvas, fonts, colors):
"""Vista Discos"""
time_font, data_font, small_font = fonts
time_color, humidity_color, hdd_color, label_color, line_color = colors
# Centered title
title = "Discos"
title_len = len(title) * 5
title_x = (64 - title_len) // 2
graphics.DrawText(canvas, data_font, title_x, 8, time_color, title)
graphics.DrawLine(canvas, 0, 10, 63, 10, line_color)
if self.hdd_temps and len(self.hdd_temps) > 1:
temps = [(i+1, int(t[0])) for i, t in enumerate(self.hdd_temps[1:7]) if t]
y = 22
for idx, (num, temp_val) in enumerate(temps[:3]):
temp_color = get_hdd_color(temp_val)
graphics.DrawText(canvas, data_font, 2, y, label_color, f"{num}:")
graphics.DrawText(canvas, data_font, 16, y, temp_color, f"{temp_val}C")
y += 11
y = 22
for idx, (num, temp_val) in enumerate(temps[3:6]):
temp_color = get_hdd_color(temp_val)
graphics.DrawText(canvas, data_font, 34, y, label_color, f"{num}:")
graphics.DrawText(canvas, data_font, 48, y, temp_color, f"{temp_val}C")
y += 11
if self.auto_cycle:
graphics.DrawText(canvas, small_font, 58, 62, label_color, "A")
def draw_view_2(self, canvas, fonts, colors):
"""Vista Fecha"""
time_font, data_font, small_font = fonts
time_color, humidity_color, hdd_color, label_color, line_color = colors
now = datetime.datetime.now()
# Day name centered
day_str = get_spanish_day(now)
day_len = len(day_str) * 5
day_x = (64 - day_len) // 2
graphics.DrawText(canvas, data_font, day_x, 12, label_color, day_str)
# Date centered
date_str = get_spanish_date(now)
date_len = len(date_str) * 7
date_x = (64 - date_len) // 2
graphics.DrawText(canvas, time_font, date_x, 26, time_color, date_str)
graphics.DrawLine(canvas, 0, 30, 63, 30, line_color)
# Weather icon (cloud animates, position fixed)
cloud_offset = int(math.sin(self.animation_frame * 0.3) * 2)
draw_weather_icon(canvas, 4, 34, self.weather_desc, cloud_offset)
if self.temperature is not None:
temp_color = get_temperature_color(self.temperature)
graphics.DrawText(canvas, time_font, 32, 52, temp_color, f"{self.temperature:.0f}C")
if self.auto_cycle:
graphics.DrawText(canvas, small_font, 58, 62, label_color, "A")
def draw_view_3(self, canvas, fonts, colors):
"""Vista Tesla"""
time_font, data_font, small_font = fonts
time_color, humidity_color, hdd_color, label_color, line_color = colors
# Centered title
title = "Tesla"
title_len = len(title) * 5
title_x = (64 - title_len) // 2
graphics.DrawText(canvas, data_font, title_x, 8, time_color, title)
graphics.DrawLine(canvas, 0, 10, 63, 10, line_color)
if self.tesla:
# Battery (with margin)
battery = self.tesla.get("battery")
if battery is not None:
battery_color = graphics.Color(0, 255, 80) if battery > 20 else graphics.Color(255, 50, 50)
graphics.DrawText(canvas, data_font, 4, 24, label_color, "Bat")
graphics.DrawText(canvas, time_font, 26, 26, battery_color, f"{battery}%")
# Range (with margin)
range_km = self.tesla.get("range")
if range_km is not None:
graphics.DrawText(canvas, data_font, 4, 38, label_color, "Km")
graphics.DrawText(canvas, time_font, 22, 40, time_color, f"{range_km}")
# Status (centered)
charging = self.tesla.get("charging", False)
plugged = self.tesla.get("plugged", False)
if charging:
status = "Cargando"
status_color = graphics.Color(0, 255, 100)
elif plugged:
status = "Conectado"
status_color = graphics.Color(255, 200, 0)
else:
status = "Listo"
status_color = graphics.Color(100, 100, 100)
status_len = len(status) * 5
status_x = (64 - status_len) // 2
graphics.DrawText(canvas, data_font, status_x, 56, status_color, status)
else:
graphics.DrawText(canvas, data_font, 4, 32, label_color, "Sin datos")
if self.auto_cycle:
graphics.DrawText(canvas, small_font, 58, 62, label_color, "A")
def run(self):
canvas = self.matrix.CreateFrameCanvas()
font_dir = "/home/pi/rpi-rgb-led-matrix/fonts"
time_font = graphics.Font()
time_font.LoadFont(f"{font_dir}/7x13.bdf")
data_font = graphics.Font()
data_font.LoadFont(f"{font_dir}/5x8.bdf")
small_font = graphics.Font()
small_font.LoadFont(f"{font_dir}/4x6.bdf")
fonts = (time_font, data_font, small_font)
time_color = graphics.Color(30, 90, 220)
humidity_color = graphics.Color(80, 160, 255)
hdd_color = graphics.Color(140, 120, 100)
label_color = graphics.Color(120, 120, 120)
line_color = graphics.Color(40, 40, 40)
colors = (time_color, humidity_color, hdd_color, label_color, line_color)
self.update_brightness()
self.update_data()
self.update_hdd_temps()
while True:
current_time = time.time()
if current_time - self.last_update >= 60:
self.update_data()
self.update_brightness()
self.update_hdd_temps()
self.last_update = current_time
if self.auto_cycle and (current_time - self.last_view_change >= self.view_cycle_interval):
self.current_view = (self.current_view + 1) % NUM_VIEWS
self.last_view_change = current_time
self.animation_frame += 1
canvas.Clear()
if self.current_view == 0:
self.draw_view_0(canvas, fonts, colors)
elif self.current_view == 1:
self.draw_view_1(canvas, fonts, colors)
elif self.current_view == 2:
self.draw_view_2(canvas, fonts, colors)
elif self.current_view == 3:
self.draw_view_3(canvas, fonts, colors)
# Camera alert overlay (highest priority)
if self.mqtt.is_alert_active():
self.draw_camera_alert(canvas, fonts)
# Feedback overlay
elif self.feedback_message and current_time < self.feedback_until:
self.draw_feedback(canvas, fonts, colors)
else:
self.feedback_message = None
time.sleep(0.5)
canvas = self.matrix.SwapOnVSync(canvas)
if __name__ == "__main__":
display = Matrix64Display()
if not display.process():
display.print_help()