Fix solar spacing, add power bar and PV curve views

This commit is contained in:
Joan
2026-03-20 13:24:06 +01:00
parent d53ac19887
commit 0df6d57483

237
matrix.py
View File

@@ -41,7 +41,7 @@ except:
pass
# Number of views
NUM_VIEWS = 5
NUM_VIEWS = 7
# View names (Spanish)
VIEW_NAMES = {
@@ -50,6 +50,8 @@ VIEW_NAMES = {
2: "Fecha",
3: "Tesla",
4: "Solar",
5: "Energia",
6: "Curva PV",
}
# Spanish translations
@@ -131,6 +133,8 @@ class Matrix64Display(SampleBase):
self.hdd_temps = None
self.tesla = None
self.solar = None
self.pv_history = [] # list of (timestamp, production_w) for today's curve
self.pv_history_date = None # track current day to reset history
# View state
self.current_view = 0
@@ -255,6 +259,14 @@ class Matrix64Display(SampleBase):
self.tesla = get_tesla_status()
self.solar = get_solar_status()
# Record PV history for today's curve
if self.solar and self.solar.get("production") is not None:
now = datetime.datetime.now()
today = now.date()
if self.pv_history_date != today:
self.pv_history = []
self.pv_history_date = today
self.pv_history.append((now.hour + now.minute / 60.0, self.solar["production"]))
except Exception as e:
print(f"Error updating data: {e}")
@@ -528,15 +540,16 @@ class Matrix64Display(SampleBase):
graphics.DrawLine(canvas, 0, 10, 63, 10, line_color)
if self.solar:
# Solar production (W)
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, data_font, 2, 20, label_color, "PV")
prod_str = f"{int(production)}W"
graphics.DrawText(canvas, data_font, 16, 20, prod_color, prod_str)
graphics.DrawText(canvas, small_font, 1, y, label_color, "PV")
graphics.DrawText(canvas, small_font, 14, y, prod_color, f"{int(production)}W")
# Battery % and power
# Battery
y += 8
battery_pct = self.solar.get("battery_pct")
battery_power = self.solar.get("battery_power")
if battery_pct is not None:
@@ -546,57 +559,225 @@ class Matrix64Display(SampleBase):
bat_color = graphics.Color(255, 200, 0)
else:
bat_color = graphics.Color(255, 50, 50)
graphics.DrawText(canvas, data_font, 2, 30, label_color, "Bat")
graphics.DrawText(canvas, data_font, 20, 30, bat_color, f"{int(battery_pct)}%")
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) # Charging (blue)
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) # Discharging (orange)
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, data_font, 44, 30, bp_color, bp_str)
graphics.DrawText(canvas, small_font, 38, y, bp_color, bp_str)
# Grid power (negative = exporting)
# 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) # Exporting (green)
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) # Importing (red)
grid_color = graphics.Color(255, 100, 100)
grid_label = "Imp"
grid_str = f"{int(grid_power)}W"
graphics.DrawText(canvas, data_font, 2, 40, label_color, grid_label)
graphics.DrawText(canvas, data_font, 20, 40, grid_color, grid_str)
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 charger)
# 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, data_font, 2, 50, label_color, "Casa")
graphics.DrawText(canvas, data_font, 26, 50, humidity_color, f"{home_power}W")
if tesla_power > 0:
tesla_color = graphics.Color(200, 50, 50)
graphics.DrawText(canvas, small_font, 2, 57, label_color, "Tesla")
graphics.DrawText(canvas, small_font, 26, 57, tesla_color, f"{int(tesla_power)}W")
graphics.DrawText(canvas, small_font, 1, y, label_color, "Casa")
graphics.DrawText(canvas, small_font, 20, y, humidity_color, f"{home_power}W")
# Today's energy
# 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, data_font, 2, 60, label_color, "Hoy")
graphics.DrawText(canvas, data_font, 20, 60, graphics.Color(255, 200, 0), f"{today:.1f}kWh")
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 photovoltaic production curve"""
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=14..57 (44px)
graph_x = 2
graph_w = 60
graph_y_top = 14
graph_y_bot = 57
graph_h = graph_y_bot - graph_y_top
# Time axis: 6:00 to 21:00 (15 hours of daylight)
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:
graphics.DrawText(canvas, small_font, 10, 36, label_color, "Sin datos")
return
# Find max production for scaling
max_prod = max(p for _, p in self.pv_history)
if max_prod <= 0:
max_prod = 1
# Draw max value label
if max_prod >= 1000:
max_label = f"{max_prod / 1000:.1f}kW"
else:
max_label = f"{int(max_prod)}W"
graphics.DrawText(canvas, small_font, graph_x + 1, graph_y_top + 5, graphics.Color(255, 200, 0), max_label)
# Plot curve - bucket data points into pixel columns
buckets = [[] for _ in range(graph_w)]
for t, p in self.pv_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)
prev_y = None
pv_color = graphics.Color(255, 200, 0)
fill_color = graphics.Color(60, 50, 0)
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_prod) * graph_h)
pixel_y = max(graph_y_top, min(graph_y_bot, pixel_y))
# Fill area under curve
if pixel_y < graph_y_bot:
for fy in range(pixel_y + 1, graph_y_bot):
canvas.SetPixel(graph_x + col, fy, 60, 50, 0)
# Draw point
canvas.SetPixel(graph_x + col, pixel_y, 255, 200, 0)
# Connect to previous point
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, 255, 200, 0)
prev_y = pixel_y
# Current production value
if self.solar and self.solar.get("production") is not None:
cur = int(self.solar["production"])
graphics.DrawText(canvas, small_font, 30, graph_y_top + 5, graphics.Color(200, 200, 200), f"{cur}W")
if self.auto_cycle:
graphics.DrawText(canvas, small_font, 58, 62, label_color, "A")
def run(self):
canvas = self.matrix.CreateFrameCanvas()
@@ -659,6 +840,10 @@ class Matrix64Display(SampleBase):
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():