Add consumption curve to PV graph, fix text-curve overlap
This commit is contained in:
@@ -118,8 +118,8 @@ def get_solar_status():
|
|||||||
|
|
||||||
|
|
||||||
def get_solar_history():
|
def get_solar_history():
|
||||||
"""Get today's solar production history from HA History API.
|
"""Get today's solar production and consumption history from HA History API.
|
||||||
Returns list of (hour_float, watts) tuples."""
|
Returns dict with 'production' and 'consumption' lists of (hour_float, watts)."""
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {HA_TOKEN}",
|
"Authorization": f"Bearer {HA_TOKEN}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -127,35 +127,45 @@ def get_solar_history():
|
|||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
start_str = start.strftime("%Y-%m-%dT%H:%M:%S")
|
start_str = start.strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
|
entities = f"{SOLAR_PRODUCTION_ENTITY},{SOLAR_LOAD_POWER_ENTITY}"
|
||||||
url = (f"{HASS_URL}/api/history/period/{start_str}"
|
url = (f"{HASS_URL}/api/history/period/{start_str}"
|
||||||
f"?filter_entity_id={SOLAR_PRODUCTION_ENTITY}"
|
f"?filter_entity_id={entities}"
|
||||||
f"&minimal_response&no_attributes")
|
f"&minimal_response&no_attributes")
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, headers=headers, timeout=15)
|
response = requests.get(url, headers=headers, timeout=15)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return []
|
return {"production": [], "consumption": []}
|
||||||
data = response.json()
|
data = response.json()
|
||||||
if not data or not data[0]:
|
utc_offset = now - datetime.datetime.utcnow()
|
||||||
return []
|
|
||||||
points = []
|
def parse_entries(entries):
|
||||||
for entry in data[0]:
|
points = []
|
||||||
try:
|
for entry in entries:
|
||||||
state = float(entry.get("state", 0))
|
try:
|
||||||
ts = entry.get("last_changed", "")
|
state = float(entry.get("state", 0))
|
||||||
# Parse ISO timestamp
|
ts = entry.get("last_changed", "")
|
||||||
if "+" in ts:
|
if "+" in ts:
|
||||||
ts = ts.split("+")[0]
|
ts = ts.split("+")[0]
|
||||||
elif ts.endswith("Z"):
|
elif ts.endswith("Z"):
|
||||||
ts = ts[:-1]
|
ts = ts[:-1]
|
||||||
dt = datetime.datetime.fromisoformat(ts)
|
dt = datetime.datetime.fromisoformat(ts)
|
||||||
# Convert UTC to local time
|
dt_local = dt + utc_offset
|
||||||
utc_offset = now - datetime.datetime.utcnow()
|
hour_f = dt_local.hour + dt_local.minute / 60.0
|
||||||
dt_local = dt + utc_offset
|
points.append((hour_f, state))
|
||||||
hour_f = dt_local.hour + dt_local.minute / 60.0
|
except (ValueError, TypeError):
|
||||||
points.append((hour_f, state))
|
continue
|
||||||
except (ValueError, TypeError):
|
return points
|
||||||
|
|
||||||
|
result = {"production": [], "consumption": []}
|
||||||
|
for entity_data in data:
|
||||||
|
if not entity_data:
|
||||||
continue
|
continue
|
||||||
return points
|
entity_id = entity_data[0].get("entity_id", "")
|
||||||
|
if entity_id == SOLAR_PRODUCTION_ENTITY:
|
||||||
|
result["production"] = parse_entries(entity_data)
|
||||||
|
elif entity_id == SOLAR_LOAD_POWER_ENTITY:
|
||||||
|
result["consumption"] = parse_entries(entity_data)
|
||||||
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error fetching solar history: {e}")
|
print(f"Error fetching solar history: {e}")
|
||||||
return []
|
return {"production": [], "consumption": []}
|
||||||
|
|||||||
111
matrix.py
111
matrix.py
@@ -134,6 +134,7 @@ class Matrix64Display(SampleBase):
|
|||||||
self.tesla = None
|
self.tesla = None
|
||||||
self.solar = None
|
self.solar = None
|
||||||
self.pv_history = [] # list of (hour_float, watts) from HA history
|
self.pv_history = [] # list of (hour_float, watts) from HA history
|
||||||
|
self.consumption_history = [] # list of (hour_float, watts)
|
||||||
self.last_pv_history_update = 0
|
self.last_pv_history_update = 0
|
||||||
|
|
||||||
# View state
|
# View state
|
||||||
@@ -678,7 +679,7 @@ class Matrix64Display(SampleBase):
|
|||||||
graphics.DrawText(canvas, small_font, 58, 62, label_color, "A")
|
graphics.DrawText(canvas, small_font, 58, 62, label_color, "A")
|
||||||
|
|
||||||
def draw_view_6(self, canvas, fonts, colors):
|
def draw_view_6(self, canvas, fonts, colors):
|
||||||
"""Vista Curva PV - Today's photovoltaic production curve"""
|
"""Vista Curva PV - Today's production and consumption curves"""
|
||||||
time_font, data_font, small_font = fonts
|
time_font, data_font, small_font = fonts
|
||||||
time_color, humidity_color, hdd_color, label_color, line_color = colors
|
time_color, humidity_color, hdd_color, label_color, line_color = colors
|
||||||
|
|
||||||
@@ -688,14 +689,14 @@ class Matrix64Display(SampleBase):
|
|||||||
graphics.DrawText(canvas, data_font, title_x, 8, time_color, title)
|
graphics.DrawText(canvas, data_font, title_x, 8, time_color, title)
|
||||||
graphics.DrawLine(canvas, 0, 10, 63, 10, line_color)
|
graphics.DrawLine(canvas, 0, 10, 63, 10, line_color)
|
||||||
|
|
||||||
# Graph area: x=2..61 (60px), y=14..57 (44px)
|
# Graph area: x=2..61 (60px), y=18..57 (40px) - top margin for labels
|
||||||
graph_x = 2
|
graph_x = 2
|
||||||
graph_w = 60
|
graph_w = 60
|
||||||
graph_y_top = 14
|
graph_y_top = 18
|
||||||
graph_y_bot = 57
|
graph_y_bot = 57
|
||||||
graph_h = graph_y_bot - graph_y_top
|
graph_h = graph_y_bot - graph_y_top
|
||||||
|
|
||||||
# Time axis: 6:00 to 21:00 (15 hours of daylight)
|
# Time axis: 6:00 to 21:00
|
||||||
t_start = 6.0
|
t_start = 6.0
|
||||||
t_end = 21.0
|
t_end = 21.0
|
||||||
t_range = t_end - t_start
|
t_range = t_end - t_start
|
||||||
@@ -710,62 +711,72 @@ class Matrix64Display(SampleBase):
|
|||||||
graphics.DrawText(canvas, small_font, graph_x + 38, 63, label_color, "15")
|
graphics.DrawText(canvas, small_font, graph_x + 38, 63, label_color, "15")
|
||||||
graphics.DrawText(canvas, small_font, graph_x + 52, 63, label_color, "20")
|
graphics.DrawText(canvas, small_font, graph_x + 52, 63, label_color, "20")
|
||||||
|
|
||||||
if not self.pv_history:
|
if not self.pv_history and not self.consumption_history:
|
||||||
graphics.DrawText(canvas, small_font, 10, 36, label_color, "Sin datos")
|
graphics.DrawText(canvas, small_font, 10, 38, label_color, "Sin datos")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Find max production for scaling
|
# Find max across both datasets for shared scale
|
||||||
max_prod = max(p for _, p in self.pv_history)
|
max_val = 1
|
||||||
if max_prod <= 0:
|
if self.pv_history:
|
||||||
max_prod = 1
|
max_val = max(max_val, max(p for _, p in self.pv_history))
|
||||||
|
if self.consumption_history:
|
||||||
|
max_val = max(max_val, max(p for _, p in self.consumption_history))
|
||||||
|
|
||||||
# Draw max value label
|
# Labels above graph area (with margin)
|
||||||
if max_prod >= 1000:
|
if max_val >= 1000:
|
||||||
max_label = f"{max_prod / 1000:.1f}kW"
|
max_label = f"{max_val / 1000:.1f}kW"
|
||||||
else:
|
else:
|
||||||
max_label = f"{int(max_prod)}W"
|
max_label = f"{int(max_val)}W"
|
||||||
graphics.DrawText(canvas, small_font, graph_x + 1, graph_y_top + 5, graphics.Color(255, 200, 0), max_label)
|
graphics.DrawText(canvas, small_font, graph_x + 1, graph_y_top - 2, label_color, max_label)
|
||||||
|
|
||||||
# Plot curve - bucket data points into pixel columns
|
# Current values in top-right
|
||||||
buckets = [[] for _ in range(graph_w)]
|
if self.solar:
|
||||||
for t, p in self.pv_history:
|
cur_pv = self.solar.get("production")
|
||||||
if t_start <= t <= t_end:
|
if cur_pv is not None:
|
||||||
col = int((t - t_start) / t_range * (graph_w - 1))
|
graphics.DrawText(canvas, small_font, 34, graph_y_top - 2, graphics.Color(255, 200, 0), f"{int(cur_pv)}W")
|
||||||
col = max(0, min(graph_w - 1, col))
|
|
||||||
buckets[col].append(p)
|
|
||||||
|
|
||||||
prev_y = None
|
def bucket_data(history):
|
||||||
pv_color = graphics.Color(255, 200, 0)
|
buckets = [[] for _ in range(graph_w)]
|
||||||
fill_color = graphics.Color(60, 50, 0)
|
for t, p in 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)
|
||||||
|
return buckets
|
||||||
|
|
||||||
for col in range(graph_w):
|
def draw_curve(buckets, r, g, b, fill_r, fill_g, fill_b, fill=True):
|
||||||
if buckets[col]:
|
prev_y = None
|
||||||
avg_p = sum(buckets[col]) / len(buckets[col])
|
for col in range(graph_w):
|
||||||
pixel_y = graph_y_bot - int((avg_p / max_prod) * graph_h)
|
if buckets[col]:
|
||||||
pixel_y = max(graph_y_top, min(graph_y_bot, pixel_y))
|
avg_p = sum(buckets[col]) / len(buckets[col])
|
||||||
|
pixel_y = graph_y_bot - int((avg_p / max_val) * graph_h)
|
||||||
|
pixel_y = max(graph_y_top, min(graph_y_bot, pixel_y))
|
||||||
|
|
||||||
# Fill area under curve
|
# Fill area under curve
|
||||||
if pixel_y < graph_y_bot:
|
if fill and pixel_y < graph_y_bot:
|
||||||
for fy in range(pixel_y + 1, graph_y_bot):
|
for fy in range(pixel_y + 1, graph_y_bot):
|
||||||
canvas.SetPixel(graph_x + col, fy, 60, 50, 0)
|
canvas.SetPixel(graph_x + col, fy, fill_r, fill_g, fill_b)
|
||||||
|
|
||||||
# Draw point
|
# Draw point
|
||||||
canvas.SetPixel(graph_x + col, pixel_y, 255, 200, 0)
|
canvas.SetPixel(graph_x + col, pixel_y, r, g, b)
|
||||||
|
|
||||||
# Connect to previous point
|
# Connect to previous
|
||||||
if prev_y is not None:
|
if prev_y is not None:
|
||||||
dy = pixel_y - prev_y
|
dy = pixel_y - prev_y
|
||||||
steps = max(abs(dy), 1)
|
steps = max(abs(dy), 1)
|
||||||
for s in range(1, steps):
|
for s in range(1, steps):
|
||||||
iy = prev_y + int(dy * s / steps)
|
iy = prev_y + int(dy * s / steps)
|
||||||
canvas.SetPixel(graph_x + col, iy, 255, 200, 0)
|
canvas.SetPixel(graph_x + col, iy, r, g, b)
|
||||||
|
|
||||||
prev_y = pixel_y
|
prev_y = pixel_y
|
||||||
|
|
||||||
# Current production value
|
# Draw consumption first (behind), then production (on top)
|
||||||
if self.solar and self.solar.get("production") is not None:
|
if self.consumption_history:
|
||||||
cur = int(self.solar["production"])
|
cons_buckets = bucket_data(self.consumption_history)
|
||||||
graphics.DrawText(canvas, small_font, 30, graph_y_top + 5, graphics.Color(200, 200, 200), f"{cur}W")
|
draw_curve(cons_buckets, 80, 160, 255, 15, 25, 50)
|
||||||
|
if self.pv_history:
|
||||||
|
pv_buckets = bucket_data(self.pv_history)
|
||||||
|
draw_curve(pv_buckets, 255, 200, 0, 60, 50, 0)
|
||||||
|
|
||||||
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")
|
||||||
@@ -809,7 +820,9 @@ class Matrix64Display(SampleBase):
|
|||||||
# Update PV history every 5 minutes
|
# Update PV history every 5 minutes
|
||||||
if current_time - self.last_pv_history_update >= 300:
|
if current_time - self.last_pv_history_update >= 300:
|
||||||
try:
|
try:
|
||||||
self.pv_history = get_solar_history()
|
history = get_solar_history()
|
||||||
|
self.pv_history = history.get("production", [])
|
||||||
|
self.consumption_history = history.get("consumption", [])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error updating PV history: {e}")
|
print(f"Error updating PV history: {e}")
|
||||||
self.last_pv_history_update = current_time
|
self.last_pv_history_update = current_time
|
||||||
|
|||||||
Reference in New Issue
Block a user