Add solar panel view with PV production, battery, grid, home load and Tesla charging
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 ==="
|
||||
|
||||
|
||||
92
matrix.py
92
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():
|
||||
|
||||
Reference in New Issue
Block a user