diff --git a/.env.example b/.env.example index c4ced4d..f7b5ae5 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,15 @@ TESLA_RANGE_ENTITY=sensor.lovelace_range TESLA_CHARGING_ENTITY=binary_sensor.lovelace_charging TESLA_PLUGGED_ENTITY=binary_sensor.lovelace_plugged_in +# Solar Configuration +SOLAR_PRODUCTION_ENTITY=sensor.solar_production +SOLAR_BATTERY_ENTITY=sensor.inverter_battery +SOLAR_BATTERY_POWER_ENTITY=sensor.inverter_battery_power +SOLAR_GRID_POWER_ENTITY=sensor.inverter_grid_power +SOLAR_LOAD_POWER_ENTITY=sensor.inverter_load_power +SOLAR_TODAY_ENERGY_ENTITY=sensor.inverter_today_energy +TESLA_CHARGER_POWER_ENTITY=sensor.tesla_carga_en_casa + # MQTT Configuration MQTT_BROKER=10.2.10.165 MQTT_PORT=1883 diff --git a/config.py b/config.py index 405d578..40ff177 100644 --- a/config.py +++ b/config.py @@ -31,6 +31,15 @@ TESLA_RANGE_ENTITY = os.getenv('TESLA_RANGE_ENTITY', 'sensor.lovelace_range') TESLA_CHARGING_ENTITY = os.getenv('TESLA_CHARGING_ENTITY', 'binary_sensor.lovelace_charging') TESLA_PLUGGED_ENTITY = os.getenv('TESLA_PLUGGED_ENTITY', 'binary_sensor.lovelace_plugged_in') +# Solar Configuration +SOLAR_PRODUCTION_ENTITY = os.getenv('SOLAR_PRODUCTION_ENTITY', 'sensor.solar_production') +SOLAR_BATTERY_ENTITY = os.getenv('SOLAR_BATTERY_ENTITY', 'sensor.inverter_battery') +SOLAR_BATTERY_POWER_ENTITY = os.getenv('SOLAR_BATTERY_POWER_ENTITY', 'sensor.inverter_battery_power') +SOLAR_GRID_POWER_ENTITY = os.getenv('SOLAR_GRID_POWER_ENTITY', 'sensor.inverter_grid_power') +SOLAR_LOAD_POWER_ENTITY = os.getenv('SOLAR_LOAD_POWER_ENTITY', 'sensor.inverter_load_power') +SOLAR_TODAY_ENERGY_ENTITY = os.getenv('SOLAR_TODAY_ENERGY_ENTITY', 'sensor.inverter_today_energy') +TESLA_CHARGER_POWER_ENTITY = os.getenv('TESLA_CHARGER_POWER_ENTITY', 'sensor.tesla_carga_en_casa') + # MQTT Configuration MQTT_BROKER = os.getenv('MQTT_BROKER', '10.2.10.165') MQTT_PORT = int(os.getenv('MQTT_PORT', '1883')) diff --git a/home_assistant.py b/home_assistant.py index 4b70e13..4eec435 100644 --- a/home_assistant.py +++ b/home_assistant.py @@ -7,7 +7,11 @@ from config import ( BRIGHTNESS_ENTITY_ID, WEATHER_ENTITY_ID, INTERIOR_TEMP_ENTITY_ID, INTERIOR_HUMIDITY_ENTITY_ID, TESLA_BATTERY_ENTITY, TESLA_RANGE_ENTITY, - TESLA_CHARGING_ENTITY, TESLA_PLUGGED_ENTITY + TESLA_CHARGING_ENTITY, TESLA_PLUGGED_ENTITY, + SOLAR_PRODUCTION_ENTITY, SOLAR_BATTERY_ENTITY, + SOLAR_BATTERY_POWER_ENTITY, SOLAR_GRID_POWER_ENTITY, + SOLAR_LOAD_POWER_ENTITY, SOLAR_TODAY_ENERGY_ENTITY, + TESLA_CHARGER_POWER_ENTITY ) @@ -78,3 +82,35 @@ def get_tesla_status(): "charging": charging.get("state") == "on" if charging else False, "plugged": plugged.get("state") == "on" if plugged else False, } + + +def get_solar_status(): + """Get solar/inverter status from Home Assistant.""" + production = get_entity_value(SOLAR_PRODUCTION_ENTITY) + battery = get_entity_value(SOLAR_BATTERY_ENTITY) + battery_power = get_entity_value(SOLAR_BATTERY_POWER_ENTITY) + grid_power = get_entity_value(SOLAR_GRID_POWER_ENTITY) + load_power = get_entity_value(SOLAR_LOAD_POWER_ENTITY) + today_energy = get_entity_value(SOLAR_TODAY_ENERGY_ENTITY) + tesla_charger = get_entity_value(TESLA_CHARGER_POWER_ENTITY) + + def safe_float(entity, default=None): + if entity: + try: + return float(entity.get("state", 0)) + except (ValueError, TypeError): + pass + return default + + # Tesla charger power (already in W) + tesla_power_w = safe_float(tesla_charger, 0) + + return { + "production": safe_float(production), + "battery_pct": safe_float(battery), + "battery_power": safe_float(battery_power), + "grid_power": safe_float(grid_power), + "load_power": safe_float(load_power), + "today_energy": safe_float(today_energy), + "tesla_power": tesla_power_w, + } diff --git a/install.sh b/install.sh index 845cac9..0149876 100644 --- a/install.sh +++ b/install.sh @@ -5,7 +5,7 @@ set -e INSTALL_DIR="/opt/matrix64" -REPO_URL="https://gitlab.kingstudio.es/jocaru/matrix64" +REPO_URL="https://git.kingstudio.es/jocaru/matrix64" echo "=== Matrix64 LED Display Installation ===" diff --git a/matrix.py b/matrix.py index 42252ba..31f27b0 100644 --- a/matrix.py +++ b/matrix.py @@ -25,7 +25,8 @@ 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_interior_weather, get_brightness, get_tesla_status, + get_solar_status ) from netdata import get_hdd_temps from mqtt_listener import MQTTListener @@ -40,7 +41,7 @@ except: pass # Number of views -NUM_VIEWS = 4 +NUM_VIEWS = 5 # View names (Spanish) VIEW_NAMES = { @@ -48,6 +49,7 @@ VIEW_NAMES = { 1: "Discos", 2: "Fecha", 3: "Tesla", + 4: "Solar", } # Spanish translations @@ -128,6 +130,7 @@ class Matrix64Display(SampleBase): self.weather_desc = "unknown" self.hdd_temps = None self.tesla = None + self.solar = None # View state self.current_view = 0 @@ -251,6 +254,7 @@ class Matrix64Display(SampleBase): 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}") @@ -511,6 +515,88 @@ class Matrix64Display(SampleBase): 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: + # Solar production (W) + 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) + + # Battery % and power + 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, data_font, 2, 30, label_color, "Bat") + graphics.DrawText(canvas, data_font, 20, 30, 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_str = f"{int(abs(battery_power))}W" + elif battery_power > 0: + bp_color = graphics.Color(255, 150, 0) # Discharging (orange) + 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) + + # Grid power (negative = exporting) + 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_label = "Exp" + grid_str = f"{int(abs(grid_power))}W" + else: + grid_color = graphics.Color(255, 100, 100) # Importing (red) + 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) + + # Home consumption (minus Tesla charger) + 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") + + # Today's energy + 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") + 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() @@ -571,6 +657,8 @@ class Matrix64Display(SampleBase): 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) # Camera alert overlay (highest priority) if self.mqtt.is_alert_active():