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_CHARGING_ENTITY=binary_sensor.lovelace_charging
|
||||||
TESLA_PLUGGED_ENTITY=binary_sensor.lovelace_plugged_in
|
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 Configuration
|
||||||
MQTT_BROKER=10.2.10.165
|
MQTT_BROKER=10.2.10.165
|
||||||
MQTT_PORT=1883
|
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_CHARGING_ENTITY = os.getenv('TESLA_CHARGING_ENTITY', 'binary_sensor.lovelace_charging')
|
||||||
TESLA_PLUGGED_ENTITY = os.getenv('TESLA_PLUGGED_ENTITY', 'binary_sensor.lovelace_plugged_in')
|
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 Configuration
|
||||||
MQTT_BROKER = os.getenv('MQTT_BROKER', '10.2.10.165')
|
MQTT_BROKER = os.getenv('MQTT_BROKER', '10.2.10.165')
|
||||||
MQTT_PORT = int(os.getenv('MQTT_PORT', '1883'))
|
MQTT_PORT = int(os.getenv('MQTT_PORT', '1883'))
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ from config import (
|
|||||||
BRIGHTNESS_ENTITY_ID, WEATHER_ENTITY_ID,
|
BRIGHTNESS_ENTITY_ID, WEATHER_ENTITY_ID,
|
||||||
INTERIOR_TEMP_ENTITY_ID, INTERIOR_HUMIDITY_ENTITY_ID,
|
INTERIOR_TEMP_ENTITY_ID, INTERIOR_HUMIDITY_ENTITY_ID,
|
||||||
TESLA_BATTERY_ENTITY, TESLA_RANGE_ENTITY,
|
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,
|
"charging": charging.get("state") == "on" if charging else False,
|
||||||
"plugged": plugged.get("state") == "on" if plugged 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
|
set -e
|
||||||
|
|
||||||
INSTALL_DIR="/opt/matrix64"
|
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 ==="
|
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 weather_icons import draw_weather_icon
|
||||||
from home_assistant import (
|
from home_assistant import (
|
||||||
get_weather, get_weather_description,
|
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 netdata import get_hdd_temps
|
||||||
from mqtt_listener import MQTTListener
|
from mqtt_listener import MQTTListener
|
||||||
@@ -40,7 +41,7 @@ except:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Number of views
|
# Number of views
|
||||||
NUM_VIEWS = 4
|
NUM_VIEWS = 5
|
||||||
|
|
||||||
# View names (Spanish)
|
# View names (Spanish)
|
||||||
VIEW_NAMES = {
|
VIEW_NAMES = {
|
||||||
@@ -48,6 +49,7 @@ VIEW_NAMES = {
|
|||||||
1: "Discos",
|
1: "Discos",
|
||||||
2: "Fecha",
|
2: "Fecha",
|
||||||
3: "Tesla",
|
3: "Tesla",
|
||||||
|
4: "Solar",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Spanish translations
|
# Spanish translations
|
||||||
@@ -128,6 +130,7 @@ class Matrix64Display(SampleBase):
|
|||||||
self.weather_desc = "unknown"
|
self.weather_desc = "unknown"
|
||||||
self.hdd_temps = None
|
self.hdd_temps = None
|
||||||
self.tesla = None
|
self.tesla = None
|
||||||
|
self.solar = None
|
||||||
|
|
||||||
# View state
|
# View state
|
||||||
self.current_view = 0
|
self.current_view = 0
|
||||||
@@ -251,6 +254,7 @@ class Matrix64Display(SampleBase):
|
|||||||
self.interior_humidity = interior.get("humidity")
|
self.interior_humidity = interior.get("humidity")
|
||||||
|
|
||||||
self.tesla = get_tesla_status()
|
self.tesla = get_tesla_status()
|
||||||
|
self.solar = get_solar_status()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error updating data: {e}")
|
print(f"Error updating data: {e}")
|
||||||
|
|
||||||
@@ -511,6 +515,88 @@ class Matrix64Display(SampleBase):
|
|||||||
if self.auto_cycle:
|
if self.auto_cycle:
|
||||||
graphics.DrawText(canvas, small_font, 58, 62, label_color, "A")
|
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):
|
def run(self):
|
||||||
canvas = self.matrix.CreateFrameCanvas()
|
canvas = self.matrix.CreateFrameCanvas()
|
||||||
|
|
||||||
@@ -571,6 +657,8 @@ class Matrix64Display(SampleBase):
|
|||||||
self.draw_view_2(canvas, fonts, colors)
|
self.draw_view_2(canvas, fonts, colors)
|
||||||
elif self.current_view == 3:
|
elif self.current_view == 3:
|
||||||
self.draw_view_3(canvas, fonts, colors)
|
self.draw_view_3(canvas, fonts, colors)
|
||||||
|
elif self.current_view == 4:
|
||||||
|
self.draw_view_4(canvas, fonts, colors)
|
||||||
|
|
||||||
# Camera alert overlay (highest priority)
|
# Camera alert overlay (highest priority)
|
||||||
if self.mqtt.is_alert_active():
|
if self.mqtt.is_alert_active():
|
||||||
|
|||||||
Reference in New Issue
Block a user