877 lines
35 KiB
Python
877 lines
35 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,
|
|
get_solar_status, get_solar_history
|
|
)
|
|
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 = 7
|
|
|
|
# View names (Spanish)
|
|
VIEW_NAMES = {
|
|
0: "Principal",
|
|
1: "Discos",
|
|
2: "Fecha",
|
|
3: "Tesla",
|
|
4: "Solar",
|
|
5: "Energia",
|
|
6: "Curva PV",
|
|
}
|
|
|
|
# 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
|
|
self.solar = None
|
|
self.pv_history = [] # list of (hour_float, watts) from HA history
|
|
self.consumption_history = [] # list of (hour_float, watts)
|
|
self.last_pv_history_update = 0
|
|
|
|
# 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()
|
|
|
|
# Menu state
|
|
self.menu_mode = False
|
|
self.menu_selection = 0
|
|
|
|
# Swipe gesture detection
|
|
self.last_button = None
|
|
self.last_button_time = 0
|
|
self.swipe_timeout = 0.4 # seconds
|
|
|
|
# Buttons
|
|
self.button0 = Button(0, pull_up=True, hold_time=1.0)
|
|
self.button1 = Button(1, pull_up=True, hold_time=1.0)
|
|
|
|
self.button0.when_pressed = lambda: self.on_button_press(0)
|
|
self.button1.when_pressed = lambda: self.on_button_press(1)
|
|
self.button0.when_held = self.on_button0_held
|
|
self.button1.when_held = self.on_button1_held
|
|
|
|
def show_feedback(self, message, duration=2):
|
|
self.feedback_message = message
|
|
self.feedback_until = time.time() + duration
|
|
|
|
def on_button_press(self, button_id):
|
|
"""Handle button press for swipe detection.
|
|
Physical layout: Button 1 = left, Button 0 = right
|
|
So swipe left→right = 1→0, swipe right→left = 0→1
|
|
"""
|
|
now = time.time()
|
|
|
|
# Check for swipe gesture
|
|
if self.last_button is not None and (now - self.last_button_time) < self.swipe_timeout:
|
|
if self.last_button == 1 and button_id == 0:
|
|
# Swipe left→right (1→0): Next view / Menu Down
|
|
if self.menu_mode:
|
|
self.menu_selection = (self.menu_selection + 1) % NUM_VIEWS
|
|
else:
|
|
self.current_view = (self.current_view + 1) % NUM_VIEWS
|
|
self.last_view_change = now
|
|
self.show_feedback(VIEW_NAMES[self.current_view], 1.5)
|
|
self.last_button = None
|
|
return
|
|
elif self.last_button == 0 and button_id == 1:
|
|
# Swipe right→left (0→1): Prev view / Menu Up
|
|
if self.menu_mode:
|
|
self.menu_selection = (self.menu_selection - 1) % NUM_VIEWS
|
|
else:
|
|
self.current_view = (self.current_view - 1) % NUM_VIEWS
|
|
self.last_view_change = now
|
|
self.show_feedback(VIEW_NAMES[self.current_view], 1.5)
|
|
self.last_button = None
|
|
return
|
|
|
|
# Record button press for potential swipe or single press
|
|
self.last_button = button_id
|
|
self.last_button_time = now
|
|
|
|
def check_button_timeout(self):
|
|
"""Check if pending button press has timed out (single press)."""
|
|
if self.last_button is not None:
|
|
if (time.time() - self.last_button_time) > self.swipe_timeout:
|
|
# Timeout reached, treat as single press
|
|
if self.menu_mode and self.last_button == 1:
|
|
# Single press on Left button (1) selects in menu
|
|
self.on_menu_select()
|
|
|
|
self.last_button = None
|
|
|
|
def on_menu_select(self):
|
|
"""Called when user wants to select in menu (right button alone after timeout)."""
|
|
if self.menu_mode:
|
|
self.current_view = self.menu_selection
|
|
self.menu_mode = False
|
|
self.show_feedback(VIEW_NAMES[self.current_view], 1.5)
|
|
|
|
def on_button0_held(self):
|
|
"""Long press right button = toggle auto-cycle."""
|
|
if not self.menu_mode:
|
|
self.auto_cycle = not self.auto_cycle
|
|
self.show_feedback("AUTO ON" if self.auto_cycle else "AUTO OFF", 2)
|
|
self.last_button = None
|
|
|
|
def on_button1_held(self):
|
|
"""Long press left button = open menu."""
|
|
self.menu_mode = not self.menu_mode
|
|
if self.menu_mode:
|
|
self.menu_selection = self.current_view
|
|
self.show_feedback("Menú", 1)
|
|
else:
|
|
self.show_feedback("Cerrado", 1)
|
|
self.last_button = None
|
|
|
|
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()
|
|
self.solar = get_solar_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
|
|
|
|
# Dark red background
|
|
bg_color = graphics.Color(40, 0, 0)
|
|
for y in range(20, 48):
|
|
graphics.DrawLine(canvas, 0, y, 63, y, bg_color)
|
|
|
|
# Flashing red border
|
|
flash = int(time.time() * 4) % 2
|
|
border_color = graphics.Color(255, 0, 0) if flash else graphics.Color(100, 0, 0)
|
|
graphics.DrawLine(canvas, 0, 20, 63, 20, border_color)
|
|
graphics.DrawLine(canvas, 0, 47, 63, 47, border_color)
|
|
|
|
# Alert text (centered)
|
|
alert_color = graphics.Color(255, 80, 80)
|
|
graphics.DrawText(canvas, data_font, 12, 32, alert_color, "ALERTA")
|
|
graphics.DrawText(canvas, small_font, 10, 42, alert_color, "Camara ext")
|
|
|
|
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
|
|
|
|
# Dark blue background
|
|
bg_color = graphics.Color(0, 20, 40)
|
|
for y in range(22, 46):
|
|
graphics.DrawLine(canvas, 0, y, 63, y, bg_color)
|
|
|
|
# Border lines
|
|
border_color = graphics.Color(30, 90, 150)
|
|
graphics.DrawLine(canvas, 0, 22, 63, 22, border_color)
|
|
graphics.DrawLine(canvas, 0, 45, 63, 45, border_color)
|
|
|
|
# Message
|
|
msg_color = graphics.Color(100, 255, 150)
|
|
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_menu(self, canvas, fonts, colors):
|
|
"""Draw view selection menu with scrolling."""
|
|
time_font, data_font, small_font = fonts
|
|
time_color, humidity_color, hdd_color, label_color, line_color = colors
|
|
|
|
# Full black background
|
|
for y in range(64):
|
|
graphics.DrawLine(canvas, 0, y, 63, y, graphics.Color(0, 0, 0))
|
|
|
|
# Title
|
|
title = "Vistas"
|
|
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)
|
|
|
|
# Menu items (3 visible at a time, selected centered)
|
|
visible_items = 4
|
|
item_height = 12
|
|
start_y = 22
|
|
|
|
# Calculate scroll offset to keep selection centered
|
|
center_slot = visible_items // 2
|
|
scroll_offset = max(0, min(self.menu_selection - center_slot, NUM_VIEWS - visible_items))
|
|
|
|
for i in range(visible_items):
|
|
view_idx = scroll_offset + i
|
|
if view_idx >= NUM_VIEWS:
|
|
break
|
|
|
|
y = start_y + i * item_height
|
|
name = VIEW_NAMES[view_idx]
|
|
|
|
if view_idx == self.menu_selection:
|
|
# Highlight selected
|
|
highlight_color = graphics.Color(0, 40, 80)
|
|
for hy in range(y - 9, y + 2):
|
|
if 0 <= hy < 64:
|
|
graphics.DrawLine(canvas, 0, hy, 63, hy, highlight_color)
|
|
text_color = graphics.Color(100, 255, 200)
|
|
graphics.DrawText(canvas, data_font, 4, y, text_color, "> " + name)
|
|
else:
|
|
text_color = graphics.Color(100, 100, 100)
|
|
graphics.DrawText(canvas, data_font, 8, y, text_color, name)
|
|
|
|
# Scroll indicators
|
|
if scroll_offset > 0:
|
|
graphics.DrawText(canvas, small_font, 58, 18, label_color, "^")
|
|
if scroll_offset + visible_items < NUM_VIEWS:
|
|
graphics.DrawText(canvas, small_font, 58, 62, label_color, "v")
|
|
|
|
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 and t[0] is not None]
|
|
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 and t[0] is not None]
|
|
|
|
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 draw_view_4(self, canvas, fonts, colors):
|
|
"""Vista Solar"""
|
|
time_font, data_font, small_font = fonts
|
|
time_color, humidity_color, hdd_color, label_color, line_color = colors
|
|
|
|
# Title
|
|
title = "Solar"
|
|
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.solar:
|
|
y = 18
|
|
# Solar production
|
|
production = self.solar.get("production")
|
|
if production is not None:
|
|
prod_color = graphics.Color(255, 200, 0) if production > 0 else graphics.Color(100, 100, 100)
|
|
graphics.DrawText(canvas, small_font, 1, y, label_color, "PV")
|
|
graphics.DrawText(canvas, small_font, 14, y, prod_color, f"{int(production)}W")
|
|
|
|
# Battery
|
|
y += 8
|
|
battery_pct = self.solar.get("battery_pct")
|
|
battery_power = self.solar.get("battery_power")
|
|
if battery_pct is not None:
|
|
if battery_pct > 50:
|
|
bat_color = graphics.Color(0, 255, 80)
|
|
elif battery_pct > 20:
|
|
bat_color = graphics.Color(255, 200, 0)
|
|
else:
|
|
bat_color = graphics.Color(255, 50, 50)
|
|
graphics.DrawText(canvas, small_font, 1, y, label_color, "Bat")
|
|
graphics.DrawText(canvas, small_font, 16, y, bat_color, f"{int(battery_pct)}%")
|
|
if battery_power is not None:
|
|
if battery_power < 0:
|
|
bp_color = graphics.Color(0, 200, 255)
|
|
bp_str = f"{int(abs(battery_power))}W"
|
|
elif battery_power > 0:
|
|
bp_color = graphics.Color(255, 150, 0)
|
|
bp_str = f"{int(battery_power)}W"
|
|
else:
|
|
bp_color = graphics.Color(100, 100, 100)
|
|
bp_str = "0W"
|
|
graphics.DrawText(canvas, small_font, 38, y, bp_color, bp_str)
|
|
|
|
# Grid
|
|
y += 8
|
|
grid_power = self.solar.get("grid_power")
|
|
if grid_power is not None:
|
|
if grid_power < 0:
|
|
grid_color = graphics.Color(0, 255, 100)
|
|
grid_label = "Exp"
|
|
grid_str = f"{int(abs(grid_power))}W"
|
|
else:
|
|
grid_color = graphics.Color(255, 100, 100)
|
|
grid_label = "Imp"
|
|
grid_str = f"{int(grid_power)}W"
|
|
graphics.DrawText(canvas, small_font, 1, y, label_color, grid_label)
|
|
graphics.DrawText(canvas, small_font, 16, y, grid_color, grid_str)
|
|
|
|
# Home consumption (minus Tesla)
|
|
y += 8
|
|
load_power = self.solar.get("load_power")
|
|
tesla_power = self.solar.get("tesla_power", 0)
|
|
if load_power is not None:
|
|
home_power = max(0, int(load_power - tesla_power))
|
|
graphics.DrawText(canvas, small_font, 1, y, label_color, "Casa")
|
|
graphics.DrawText(canvas, small_font, 20, y, humidity_color, f"{home_power}W")
|
|
|
|
# Tesla charging
|
|
if tesla_power > 0:
|
|
y += 8
|
|
tesla_color = graphics.Color(200, 50, 50)
|
|
graphics.DrawText(canvas, small_font, 1, y, label_color, "Tesla")
|
|
graphics.DrawText(canvas, small_font, 24, y, tesla_color, f"{int(tesla_power)}W")
|
|
|
|
# Today's energy (always at bottom)
|
|
today = self.solar.get("today_energy")
|
|
if today is not None:
|
|
graphics.DrawText(canvas, small_font, 1, 63, label_color, "Hoy")
|
|
graphics.DrawText(canvas, small_font, 16, 63, graphics.Color(255, 200, 0), f"{today:.1f}kWh")
|
|
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 draw_view_5(self, canvas, fonts, colors):
|
|
"""Vista Energia - Power flow bars"""
|
|
time_font, data_font, small_font = fonts
|
|
time_color, humidity_color, hdd_color, label_color, line_color = colors
|
|
|
|
title = "Energia"
|
|
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 not self.solar:
|
|
graphics.DrawText(canvas, data_font, 4, 32, label_color, "Sin datos")
|
|
return
|
|
|
|
production = self.solar.get("production", 0) or 0
|
|
load_power = self.solar.get("load_power", 0) or 0
|
|
tesla_power = self.solar.get("tesla_power", 0) or 0
|
|
grid_power = self.solar.get("grid_power", 0) or 0
|
|
battery_power = self.solar.get("battery_power", 0) or 0
|
|
home_power = max(0, load_power - tesla_power)
|
|
|
|
bar_w = 60
|
|
bar_x = 2
|
|
|
|
# --- Production bar (what PV generates) ---
|
|
graphics.DrawText(canvas, small_font, 1, 18, graphics.Color(255, 200, 0), "Produccion")
|
|
max_pv = max(production, 1)
|
|
pv_len = min(bar_w, int(bar_w * production / max(max_pv, 1)))
|
|
for y in range(20, 26):
|
|
graphics.DrawLine(canvas, bar_x, y, bar_x + bar_w - 1, y, graphics.Color(15, 15, 15))
|
|
if pv_len > 0:
|
|
graphics.DrawLine(canvas, bar_x, y, bar_x + pv_len - 1, y, graphics.Color(255, 200, 0))
|
|
graphics.DrawText(canvas, small_font, bar_x, 32, graphics.Color(200, 200, 200), f"{int(production)}W")
|
|
|
|
# --- Consumption bar (stacked: Home + Tesla) ---
|
|
graphics.DrawText(canvas, small_font, 1, 38, humidity_color, "Consumo")
|
|
total_consumption = home_power + tesla_power
|
|
max_val = max(production, total_consumption, 1)
|
|
for y in range(40, 46):
|
|
graphics.DrawLine(canvas, bar_x, y, bar_x + bar_w - 1, y, graphics.Color(15, 15, 15))
|
|
|
|
if total_consumption > 0:
|
|
home_len = int(bar_w * home_power / max_val)
|
|
tesla_len = int(bar_w * tesla_power / max_val)
|
|
# Home portion (blue)
|
|
if home_len > 0:
|
|
for y in range(40, 46):
|
|
graphics.DrawLine(canvas, bar_x, y, bar_x + home_len - 1, y, graphics.Color(80, 160, 255))
|
|
# Tesla portion (red)
|
|
if tesla_len > 0:
|
|
for y in range(40, 46):
|
|
graphics.DrawLine(canvas, bar_x + home_len, y, bar_x + home_len + tesla_len - 1, y, graphics.Color(200, 50, 50))
|
|
|
|
# Legend
|
|
graphics.DrawText(canvas, small_font, bar_x, 52, graphics.Color(80, 160, 255), f"Casa {int(home_power)}W")
|
|
if tesla_power > 0:
|
|
graphics.DrawText(canvas, small_font, 36, 52, graphics.Color(200, 50, 50), f"T {int(tesla_power)}W")
|
|
|
|
# Grid status at bottom
|
|
if grid_power < 0:
|
|
graphics.DrawText(canvas, small_font, 1, 62, graphics.Color(0, 255, 100), f"Export {int(abs(grid_power))}W")
|
|
elif grid_power > 0:
|
|
graphics.DrawText(canvas, small_font, 1, 62, graphics.Color(255, 100, 100), f"Import {int(grid_power)}W")
|
|
else:
|
|
graphics.DrawText(canvas, small_font, 1, 62, graphics.Color(100, 100, 100), "Red: 0W")
|
|
|
|
if self.auto_cycle:
|
|
graphics.DrawText(canvas, small_font, 58, 62, label_color, "A")
|
|
|
|
def draw_view_6(self, canvas, fonts, colors):
|
|
"""Vista Curva PV - Today's production and consumption curves"""
|
|
time_font, data_font, small_font = fonts
|
|
time_color, humidity_color, hdd_color, label_color, line_color = colors
|
|
|
|
title = "Curva PV"
|
|
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)
|
|
|
|
# Graph area: x=2..61 (60px), y=18..57 (40px) - top margin for labels
|
|
graph_x = 2
|
|
graph_w = 60
|
|
graph_y_top = 18
|
|
graph_y_bot = 57
|
|
graph_h = graph_y_bot - graph_y_top
|
|
|
|
# Time axis: 6:00 to 21:00
|
|
t_start = 6.0
|
|
t_end = 21.0
|
|
t_range = t_end - t_start
|
|
|
|
# Axis lines
|
|
graphics.DrawLine(canvas, graph_x, graph_y_bot, graph_x + graph_w - 1, graph_y_bot, line_color)
|
|
graphics.DrawLine(canvas, graph_x, graph_y_top, graph_x, graph_y_bot, line_color)
|
|
|
|
# Time labels
|
|
graphics.DrawText(canvas, small_font, graph_x, 63, label_color, "6")
|
|
graphics.DrawText(canvas, small_font, graph_x + 18, 63, label_color, "10")
|
|
graphics.DrawText(canvas, small_font, graph_x + 38, 63, label_color, "15")
|
|
graphics.DrawText(canvas, small_font, graph_x + 52, 63, label_color, "20")
|
|
|
|
if not self.pv_history and not self.consumption_history:
|
|
graphics.DrawText(canvas, small_font, 10, 38, label_color, "Sin datos")
|
|
return
|
|
|
|
# Find max across both datasets for shared scale
|
|
max_val = 1
|
|
if self.pv_history:
|
|
max_val = max(max_val, max(p for _, p in self.pv_history))
|
|
if self.consumption_history:
|
|
max_val = max(max_val, max(p for _, p in self.consumption_history))
|
|
|
|
# Labels above graph area (with margin)
|
|
if max_val >= 1000:
|
|
max_label = f"{max_val / 1000:.1f}kW"
|
|
else:
|
|
max_label = f"{int(max_val)}W"
|
|
graphics.DrawText(canvas, small_font, graph_x + 1, graph_y_top - 2, label_color, max_label)
|
|
|
|
# Current values in top-right
|
|
if self.solar:
|
|
cur_pv = self.solar.get("production")
|
|
if cur_pv is not None:
|
|
graphics.DrawText(canvas, small_font, 34, graph_y_top - 2, graphics.Color(255, 200, 0), f"{int(cur_pv)}W")
|
|
|
|
def bucket_data(history):
|
|
buckets = [[] for _ in range(graph_w)]
|
|
for t, p in history:
|
|
if t_start <= t <= t_end:
|
|
col = int((t - t_start) / t_range * (graph_w - 1))
|
|
col = max(0, min(graph_w - 1, col))
|
|
buckets[col].append(p)
|
|
return buckets
|
|
|
|
def draw_curve(buckets, r, g, b, fill_r, fill_g, fill_b, fill=True):
|
|
prev_y = None
|
|
for col in range(graph_w):
|
|
if buckets[col]:
|
|
avg_p = sum(buckets[col]) / len(buckets[col])
|
|
pixel_y = graph_y_bot - int((avg_p / max_val) * graph_h)
|
|
pixel_y = max(graph_y_top, min(graph_y_bot, pixel_y))
|
|
|
|
# Fill area under curve
|
|
if fill and pixel_y < graph_y_bot:
|
|
for fy in range(pixel_y + 1, graph_y_bot):
|
|
canvas.SetPixel(graph_x + col, fy, fill_r, fill_g, fill_b)
|
|
|
|
# Draw point
|
|
canvas.SetPixel(graph_x + col, pixel_y, r, g, b)
|
|
|
|
# Connect to previous
|
|
if prev_y is not None:
|
|
dy = pixel_y - prev_y
|
|
steps = max(abs(dy), 1)
|
|
for s in range(1, steps):
|
|
iy = prev_y + int(dy * s / steps)
|
|
canvas.SetPixel(graph_x + col, iy, r, g, b)
|
|
|
|
prev_y = pixel_y
|
|
|
|
# Draw consumption first (behind), then production (on top)
|
|
if self.consumption_history:
|
|
cons_buckets = bucket_data(self.consumption_history)
|
|
draw_curve(cons_buckets, 80, 160, 255, 15, 25, 50)
|
|
if self.pv_history:
|
|
pv_buckets = bucket_data(self.pv_history)
|
|
draw_curve(pv_buckets, 255, 200, 0, 60, 50, 0)
|
|
|
|
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()
|
|
|
|
# Check for button timeout (single press detection)
|
|
self.check_button_timeout()
|
|
|
|
if current_time - self.last_update >= 60:
|
|
self.update_data()
|
|
self.update_hdd_temps()
|
|
self.last_update = current_time
|
|
|
|
# Update PV history every 5 minutes
|
|
if current_time - self.last_pv_history_update >= 300:
|
|
try:
|
|
history = get_solar_history()
|
|
self.pv_history = history.get("production", [])
|
|
self.consumption_history = history.get("consumption", [])
|
|
except Exception as e:
|
|
print(f"Error updating PV history: {e}")
|
|
self.last_pv_history_update = current_time
|
|
|
|
if current_time - self.last_update >= 15:
|
|
self.update_brightness()
|
|
|
|
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()
|
|
|
|
# Menu mode takes priority over views
|
|
if self.menu_mode:
|
|
self.draw_menu(canvas, fonts, colors)
|
|
else:
|
|
# Draw current view
|
|
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)
|
|
elif self.current_view == 4:
|
|
self.draw_view_4(canvas, fonts, colors)
|
|
elif self.current_view == 5:
|
|
self.draw_view_5(canvas, fonts, colors)
|
|
elif self.current_view == 6:
|
|
self.draw_view_6(canvas, fonts, colors)
|
|
|
|
# Camera alert overlay (highest priority)
|
|
if self.mqtt.is_alert_active():
|
|
self.draw_camera_alert(canvas, fonts)
|
|
# Feedback overlay (only when not in menu)
|
|
elif not self.menu_mode and 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() |