Skip to content

Commit 7cf465a

Browse files
committed
Improve planner reasons and mobile load
1 parent 821de03 commit 7cf465a

File tree

6 files changed

+280
-78
lines changed

6 files changed

+280
-78
lines changed

custom_components/oig_cloud/oig_cloud_battery_forecast.py

Lines changed: 125 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import json
77
import logging
88
import math
9+
import re
910
import time
1011
from collections import Counter
1112
from datetime import date, datetime, timedelta, timezone
@@ -7443,13 +7444,25 @@ def _summarize_block_reason(
74437444
for iv in group_intervals
74447445
if isinstance(iv.get("planned"), dict)
74457446
]
7446-
if not planned_entries:
7447+
actual_entries = [
7448+
iv.get("actual")
7449+
for iv in group_intervals
7450+
if isinstance(iv.get("actual"), dict)
7451+
]
7452+
entries_source = planned_entries if planned_entries else actual_entries
7453+
if not entries_source:
74477454
return None
74487455

7449-
metrics_list = [p.get("decision_metrics") or {} for p in planned_entries]
7456+
metrics_list = (
7457+
[p.get("decision_metrics") or {} for p in planned_entries]
7458+
if planned_entries
7459+
else []
7460+
)
74507461

7451-
guard_metrics = next(
7452-
(m for m in metrics_list if m.get("guard_active")), None
7462+
guard_metrics = (
7463+
next((m for m in metrics_list if m.get("guard_active")), None)
7464+
if metrics_list
7465+
else None
74537466
)
74547467
if guard_metrics:
74557468
guard_type = guard_metrics.get("guard_type")
@@ -7490,23 +7503,37 @@ def _mean(values: List[Optional[float]]) -> Optional[float]:
74907503
return None
74917504
return sum(vals) / len(vals)
74927505

7506+
def _avg_from_metrics(key: str) -> Optional[float]:
7507+
if not metrics_list:
7508+
return None
7509+
return _mean([m.get(key) for m in metrics_list if m.get(key) is not None])
7510+
7511+
def _avg_from_entries(key: str) -> Optional[float]:
7512+
return _mean(
7513+
[
7514+
entry.get(key)
7515+
for entry in entries_source
7516+
if isinstance(entry.get(key), (int, float))
7517+
]
7518+
)
7519+
74937520
prices: List[Optional[float]] = []
7494-
for planned in planned_entries:
7495-
price = planned.get("spot_price")
7521+
for entry in entries_source:
7522+
price = entry.get("spot_price")
74967523
if price is None:
7497-
price = planned.get("spot_price_czk")
7524+
price = entry.get("spot_price_czk")
74987525
if price is None:
7499-
price = (planned.get("decision_metrics") or {}).get("spot_price_czk")
7526+
price = (entry.get("decision_metrics") or {}).get("spot_price_czk")
75007527
prices.append(price)
75017528
avg_price = _mean(prices)
75027529

7503-
avg_future_ups = _mean(
7504-
[
7505-
m.get("future_ups_avg_price_czk")
7506-
for m in metrics_list
7507-
if m.get("future_ups_avg_price_czk") is not None
7508-
]
7509-
)
7530+
avg_future_ups = _avg_from_metrics("future_ups_avg_price_czk")
7531+
avg_grid_charge = _avg_from_metrics("grid_charge_kwh")
7532+
avg_home1_saving = _avg_from_metrics("home1_saving_czk")
7533+
avg_recharge_cost = _avg_from_metrics("recharge_cost_czk")
7534+
avg_deficit = _avg_from_metrics("deficit_kwh")
7535+
avg_solar = _avg_from_entries("solar_kwh")
7536+
avg_load = _avg_from_entries("consumption_kwh")
75107537

75117538
start_kwh = block.get("battery_kwh_start")
75127539
end_kwh = block.get("battery_kwh_end")
@@ -7531,6 +7558,13 @@ def _mean(values: List[Optional[float]]) -> Optional[float]:
75317558
if dominant_code:
75327559
if dominant_code == "price_band_hold":
75337560
if avg_price is not None:
7561+
if avg_future_ups is not None and avg_price <= avg_future_ups - 0.01:
7562+
return (
7563+
"UPS držíme v cenovém pásmu ±"
7564+
f"{band_pct * 100:.0f}% "
7565+
f"(průměr {avg_price:.2f} Kč/kWh, "
7566+
f"levnější než další okna {avg_future_ups:.2f} Kč/kWh)."
7567+
)
75347568
return (
75357569
"UPS držíme v cenovém pásmu ±"
75367570
f"{band_pct * 100:.0f}% "
@@ -7542,45 +7576,57 @@ def _mean(values: List[Optional[float]]) -> Optional[float]:
75427576
dominant_code, spot_price=avg_price
75437577
)
75447578
if reason_text:
7579+
if avg_price is not None and "Kč/kWh" not in reason_text:
7580+
reason_text = (
7581+
f"{reason_text} (průměr {avg_price:.2f} Kč/kWh)."
7582+
)
75457583
return reason_text
75467584

75477585
if "UPS" in mode_upper:
7586+
charge_kwh = None
7587+
if avg_grid_charge is not None and avg_grid_charge > 0.01:
7588+
charge_kwh = avg_grid_charge
7589+
elif delta_kwh is not None and delta_kwh > 0.05:
7590+
charge_kwh = delta_kwh
7591+
75487592
if avg_price is not None:
75497593
if avg_price <= max_ups_price + 0.0001:
7550-
return (
7551-
"Nabíjíme ze sítě: průměrná cena "
7552-
f"{avg_price:.2f} Kč/kWh je pod limitem "
7553-
f"{max_ups_price:.2f} Kč/kWh."
7594+
detail = (
7595+
f"Nabíjíme ze sítě"
7596+
+ (f" (+{charge_kwh:.2f} kWh)" if charge_kwh else "")
7597+
+ f": {avg_price:.2f} Kč/kWh ≤ limit {max_ups_price:.2f}."
75547598
)
7555-
return (
7556-
"UPS režim při vyšší ceně "
7557-
f"{avg_price:.2f} Kč/kWh (limit {max_ups_price:.2f})."
7599+
if avg_future_ups is not None and avg_price <= avg_future_ups - 0.01:
7600+
detail += (
7601+
f" Je levnější než další UPS okna ({avg_future_ups:.2f} Kč/kWh)."
7602+
)
7603+
return detail
7604+
detail = (
7605+
f"UPS režim i přes vyšší cenu {avg_price:.2f} Kč/kWh "
7606+
f"(limit {max_ups_price:.2f})"
75587607
)
7608+
if charge_kwh:
7609+
detail += f", nabíjení +{charge_kwh:.2f} kWh."
7610+
else:
7611+
detail += "."
7612+
return detail
75597613
return "UPS režim (plánované nabíjení)."
75607614

75617615
if "HOME II" in mode_upper or "HOME 2" in mode_upper:
7562-
avg_save = _mean(
7563-
[
7564-
m.get("home1_saving_czk")
7565-
for m in metrics_list
7566-
if m.get("home1_saving_czk") is not None
7567-
]
7568-
)
7569-
avg_recharge = _mean(
7570-
[
7571-
m.get("recharge_cost_czk")
7572-
for m in metrics_list
7573-
if m.get("recharge_cost_czk") is not None
7574-
]
7575-
)
7576-
if avg_save is not None and avg_recharge is not None:
7616+
if avg_home1_saving is not None and avg_recharge_cost is not None:
75777617
return (
7578-
"Držíme baterii: HOME I by ušetřil ~"
7579-
f"{avg_save:.2f} Kč, dobíjení v UPS ~{avg_recharge:.2f} Kč."
7618+
"Držíme baterii (HOME II): HOME I by ušetřil ~"
7619+
f"{avg_home1_saving:.2f} Kč, dobíjení v UPS ~{avg_recharge_cost:.2f} Kč."
75807620
)
75817621
return "Držíme baterii (HOME II), bez vybíjení do zátěže."
75827622

75837623
if "HOME III" in mode_upper or "HOME 3" in mode_upper:
7624+
if avg_solar is not None and avg_load is not None and avg_solar > avg_load:
7625+
return (
7626+
"HOME III: FVE pokrývá spotřebu "
7627+
f"({avg_solar:.2f} kWh > {avg_load:.2f} kWh), "
7628+
"maximalizujeme nabíjení."
7629+
)
75847630
return "Maximalizujeme nabíjení z FVE, spotřeba jde ze sítě."
75857631

75867632
if "HOME I" in mode_upper or "HOME 1" in mode_upper:
@@ -7609,11 +7655,17 @@ def _mean(values: List[Optional[float]]) -> Optional[float]:
76097655
"Solár pokrývá spotřebu, přebytky ukládáme do baterie "
76107656
f"(+{delta_kwh:.2f} kWh)."
76117657
)
7658+
if avg_solar is not None and avg_load is not None and avg_solar >= avg_load:
7659+
return (
7660+
"Solár pokrývá spotřebu "
7661+
f"({avg_solar:.2f} kWh ≥ {avg_load:.2f} kWh), "
7662+
"baterie se výrazně nemění."
7663+
)
76127664
return "Solár pokrývá spotřebu, baterie se výrazně nemění."
76137665

76147666
reasons = [
76157667
p.get("decision_reason")
7616-
for p in planned_entries
7668+
for p in entries_source
76177669
if p.get("decision_reason")
76187670
]
76197671
if reasons:
@@ -12600,14 +12652,24 @@ def _format_profile_description(self, profile: Optional[Dict[str, Any]]) -> str:
1260012652

1260112653
# Získat název z profile["ui"]["name"]
1260212654
ui = profile.get("ui", {})
12603-
description = ui.get("name", "Neznámý profil")
12655+
raw_name = ui.get("name", "Neznámý profil") or "Neznámý profil"
12656+
cleaned_name = re.sub(
12657+
r"\s*\([^)]*(podobn|shoda)[^)]*\)",
12658+
"",
12659+
str(raw_name),
12660+
flags=re.IGNORECASE,
12661+
).strip()
12662+
if not cleaned_name:
12663+
cleaned_name = str(raw_name).strip()
12664+
cleaned_name = re.sub(r"\s{2,}", " ", cleaned_name)
1260412665

1260512666
# Získat season z profile["characteristics"]["season"]
1260612667
characteristics = profile.get("characteristics", {})
1260712668
season = characteristics.get("season", "")
1260812669

1260912670
# Počet dnů z profile["sample_count"]
12610-
day_count = profile.get("sample_count", 0)
12671+
day_count = ui.get("sample_count", profile.get("sample_count", 0))
12672+
similarity_score = ui.get("similarity_score")
1261112673

1261212674
# České názvy ročních období
1261312675
season_names = {
@@ -12618,13 +12680,27 @@ def _format_profile_description(self, profile: Optional[Dict[str, Any]]) -> str:
1261812680
}
1261912681
season_cz = season_names.get(season, season)
1262012682

12621-
# Formát: "Popis (season, X podobných dnů)"
12622-
if season and day_count > 0:
12623-
return f"{description} ({season_cz}, {day_count} podobných dnů)"
12624-
elif season:
12625-
return f"{description} ({season_cz})"
12626-
else:
12627-
return description
12683+
suffix_parts: List[str] = []
12684+
if season_cz:
12685+
suffix_parts.append(str(season_cz))
12686+
try:
12687+
day_count_val = int(day_count) if day_count is not None else 0
12688+
except (TypeError, ValueError):
12689+
day_count_val = 0
12690+
if day_count_val > 0:
12691+
suffix_parts.append(f"{day_count_val} dnů")
12692+
try:
12693+
similarity_val = (
12694+
float(similarity_score) if similarity_score is not None else None
12695+
)
12696+
except (TypeError, ValueError):
12697+
similarity_val = None
12698+
if similarity_val is not None:
12699+
suffix_parts.append(f"shoda {similarity_val:.2f}")
12700+
12701+
if suffix_parts:
12702+
return f"{cleaned_name} ({', '.join(suffix_parts)})"
12703+
return cleaned_name
1262812704

1262912705
def _process_adaptive_consumption_for_dashboard(
1263012706
self, adaptive_profiles: Optional[Dict[str, Any]]

custom_components/oig_cloud/www/dashboard.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -842,7 +842,7 @@ <h4 style="margin-bottom: 8px; font-size: 14px;">📊 Vlastní dlaždice</h4>
842842
style="background: linear-gradient(135deg, rgba(33, 150, 243, 0.15) 0%, rgba(33, 150, 243, 0.05) 100%); border: 1px solid rgba(33, 150, 243, 0.3); min-height: 160px;">
843843
<div class="stat-label" style="color: #2196F3; font-weight: 600;">📊 Plánovaná spotřeba</div>
844844
<!-- Profil nahoru -->
845-
<div id="consumption-profile-display" style="font-size: 0.7em; color: var(--text-secondary); margin-top: 2px; line-height: 1.3; white-space: normal; overflow-wrap: anywhere;" title="Zbývá dnes + celý zítřek">
845+
<div id="consumption-profile-display" style="font-size: 0.7em; color: var(--text-secondary); margin-top: 2px; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; max-height: 2.6em;" title="Zbývá dnes + celý zítřek">
846846
--
847847
</div>
848848
<div class="stat-value" id="planned-consumption-main" style="font-size: 1.8em; margin: 10px 0;">--</div>

custom_components/oig_cloud/www/js/core/core.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,31 @@ function toggleControlPanel() {
2525
icon.textContent = panel.classList.contains('minimized') ? '+' : '−';
2626
}
2727

28+
function runWhenIdle(task, timeoutMs = 2000, fallbackDelayMs = 600) {
29+
if (typeof window.requestIdleCallback === 'function') {
30+
window.requestIdleCallback(() => task(), { timeout: timeoutMs });
31+
return;
32+
}
33+
setTimeout(task, fallbackDelayMs);
34+
}
35+
36+
function detectHaApp() {
37+
try {
38+
const ua = window.navigator?.userAgent || '';
39+
return /Home Assistant/i.test(ua);
40+
} catch (e) {
41+
return false;
42+
}
43+
}
44+
45+
window.OIG_RUNTIME = window.OIG_RUNTIME || {};
46+
if (window.OIG_RUNTIME.isHaApp === undefined) {
47+
window.OIG_RUNTIME.isHaApp = detectHaApp();
48+
}
49+
if (window.OIG_RUNTIME.initialLoadComplete === undefined) {
50+
window.OIG_RUNTIME.initialLoadComplete = false;
51+
}
52+
2853
// === SHIELD (moved to dashboard-shield.js) ===
2954
// Import shield functions
3055
var subscribeToShield = window.DashboardShield?.subscribeToShield;
@@ -340,12 +365,20 @@ function init() {
340365
// Pokud byl načten custom layout, particles byly zastaveny
341366
// a needsFlowReinitialize je TRUE, takže loadData() je restartuje
342367
setTimeout(() => {
343-
// Initial full load
344-
forceFullRefresh();
368+
// Initial full load (defer heavy work in HA app to avoid UI freeze)
369+
const startHeavyLoad = () => {
370+
forceFullRefresh();
371+
};
372+
if (window.OIG_RUNTIME?.isHaApp) {
373+
setTimeout(() => runWhenIdle(startHeavyLoad, 2500, 900), 200);
374+
} else {
375+
startHeavyLoad();
376+
}
377+
345378
updateTime();
346379

347380
// NOVÉ: Load extended timeline for Today Plan Tile
348-
buildExtendedTimeline();
381+
runWhenIdle(buildExtendedTimeline, 2500, 900);
349382

350383
// OPRAVA: Načíst pricing data pokud je pricing tab aktivní při načtení stránky
351384
const pricingTab = document.getElementById('pricing-tab');

custom_components/oig_cloud/www/js/features/detail-tabs.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -633,9 +633,6 @@ class DetailTabsDialog {
633633
if (!intervalReasons || intervalReasons.length === 0) {
634634
return '';
635635
}
636-
if (status === 'completed') {
637-
return '';
638-
}
639636

640637
const items = intervalReasons.map(item => {
641638
const timeLabel = this.formatTimeLabel(item.time);
@@ -644,7 +641,7 @@ class DetailTabsDialog {
644641

645642
return `
646643
<div class="block-item block-reasons">
647-
<span class="item-label">🧠 Důvod:</span>
644+
<span class="item-label">🧠 Důvod${status === 'completed' ? ' (plán)' : ''}:</span>
648645
<div class="item-value reason-list">
649646
${items}
650647
</div>

0 commit comments

Comments
 (0)