From 0df6d5748344e9e41f5e11c01d8429cbda0c41f7 Mon Sep 17 00:00:00 2001 From: Joan Date: Fri, 20 Mar 2026 13:24:06 +0100 Subject: [PATCH] Fix solar spacing, add power bar and PV curve views --- matrix.py | 237 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 211 insertions(+), 26 deletions(-) diff --git a/matrix.py b/matrix.py index 31f27b0..b2c2a7c 100644 --- a/matrix.py +++ b/matrix.py @@ -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():