66import json
77import logging
88import math
9+ import re
910import time
1011from collections import Counter
1112from 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]]
0 commit comments