Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions packages/control/chargelog/chargelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from helpermodules.measurement_logging.process_log import (
FILE_ERRORS, CalculationType, _analyse_energy_source, _process_entries, get_totals)

from helpermodules.measurement_logging.write_log import LogType, LegacySmartHomeLogData, create_entry

# alte Daten: Startzeitpunkt der Ladung, Endzeitpunkt, Geladene Reichweite, Energie, Leistung, Ladedauer, LP-Nummer,
# Lademodus, ID-Tag
# json-Objekt: new_entry = {
Expand Down Expand Up @@ -328,7 +330,7 @@ def write_new_entry(new_entry):
def calc_energy_costs(cp, create_log_entry: bool = False):
try:
if cp.data.set.log.imported_since_plugged != 0 and cp.data.set.log.imported_since_mode_switch != 0:
processed_entries, reference_entries = _get_reference_entries(cp)
processed_entries, reference_entries = _get_reference_entries(cp, create_log_entry)
charged_energy_by_source = calculate_charged_energy_by_source(
cp, processed_entries, reference_entries, create_log_entry)
_add_charged_energy_by_source(cp, charged_energy_by_source)
Expand Down Expand Up @@ -361,15 +363,17 @@ def calculate_charged_energy_by_source(cp, processed_entries, reference_entries,
reference_entries[0]["cp"][f"cp{cp.num}"]["imported"])
elif reference == ReferenceTime.END:
if sum(cp.data.set.log.charged_energy_by_source.values()) == 0:
# Kurze Ladung, für die noch kein 5min-Intervall vergangen ist
charged_energy = cp.data.set.log.imported_since_mode_switch
else:
log.debug(f"cp.data.get.imported {cp.data.get.imported}")
charged_energy = cp.data.get.imported - \
reference_entries[-1]["cp"][f"cp{cp.num}"]["imported"]
# Nur das noch nicht verbuchte Intervall
charged_energy = (reference_entries[-1]["cp"][f"cp{cp.num}"]["imported"] -
reference_entries[0]["cp"][f"cp{cp.num}"]["imported"])
else:
raise TypeError(f"Unbekannter Referenz-Zeitpunkt {reference}")
log.debug(f'power source {relative_energy_source}')
log.debug(f"charged_energy {charged_energy}")
# Wenn keine Energie geladen wurde, keine Anteile berechnen
if charged_energy <= 0:
# wenn nur entladen wurde, keine Anteile berechnen
return {source: 0 for source in ENERGY_SOURCES}
Expand Down Expand Up @@ -402,14 +406,26 @@ def _get_reference_position(cp, create_log_entry: bool) -> ReferenceTime:
return ReferenceTime.MIDDLE


def _get_reference_entries(cp) -> Tuple[List[Dict], List]:
def _get_reference_entries(cp, create_log_entry: bool = False) -> Tuple[List[Dict], List]:
processed_entries = {}
reference_entries = []
try:
log_data = get_todays_daily_log()
names = log_data["names"]
entries = log_data["entries"]
if len(entries) >= 2:
if create_log_entry:
# beim beenden einer Ladung den aktuellen Messwert benutzen
if entries:
previous_entry = entries[-1]
else:
# Bei Tageswechsel
date_day_before = (datetime.datetime.now() + datetime.timedelta(days=-1)).strftime("%Y%m%d")
entries_day_before = get_daily_log(date_day_before)["entries"]
previous_entry = entries_day_before[-1]

current_entry = create_entry(LogType.DAILY, LegacySmartHomeLogData(), previous_entry)
reference_entries = [previous_entry, current_entry]
elif len(entries) >= 2:
reference_entries = [entries[-2], entries[-1]]
else:
date_day_before = (datetime.datetime.now() + datetime.timedelta(days=-1)).strftime("%Y%m%d")
Expand Down
87 changes: 87 additions & 0 deletions packages/control/chargelog/chargelog_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ def test_calc_charge_cost_reference_end(mock_data, monkeypatch):
cp.data.get.imported = 4100
cp.data.set.log.charged_energy_by_source = {'grid': 1243, 'pv': 386, 'bat': 671, 'cp': 0.0}
daily_log = mock_daily_log(monkeypatch)
current_entry = json.loads(json.dumps(daily_log["entries"][-1]))
current_entry["bat"]["all"]["exported"] = 4000
current_entry["bat"]["bat2"]["exported"] = 4000
current_entry["counter"]["counter0"]["imported"] = 4500
current_entry["cp"]["cp4"]["imported"] = 4100
current_entry["pv"]["all"]["exported"] = 3000
current_entry["pv"]["pv1"]["exported"] = 3000
monkeypatch.setattr(chargelog, "create_entry", Mock(return_value=current_entry))

with patch("builtins.open", mock_open(read_data=json.dumps(daily_log))):
calc_energy_costs(cp, True)
Expand All @@ -100,13 +108,92 @@ def test_calc_charge_cost_reference_end(mock_data, monkeypatch):
assert round(cp.data.set.log.costs, 5) == 0.025


def test_calc_charge_cost_short_charge_uses_current_entry(mock_data, monkeypatch):
cp = Chargepoint(4, None)

# Kurzer Ladevorgang: insgesamt nur 80 Wh geladen.
# Es wurde vorher noch kein Energieanteil verbucht.
cp.data.set.log.imported_since_plugged = 80
cp.data.set.log.imported_since_mode_switch = 80
cp.data.set.log.timestamp_mode_switch = 1652683260 # 8:41
cp.data.get.imported = 4080
cp.data.set.log.charged_energy_by_source = {
'bat': 0,
'cp': 0,
'grid': 0,
'pv': 0
}

daily_log = mock_daily_log(monkeypatch)

# Letzter gespeicherter Daily-Log ist 8:40:
#
# Grid imported = 2500 Wh
# PV exported = 2500 Wh
# CP imported = 4000 Wh
#
# Jetzt erzeugen wir den Live-Snapshot beim Ladeende um 8:42.
current_entry = json.loads(json.dumps(daily_log["entries"][-1]))

current_entry["date"] = "8:42"
current_entry["timestamp"] = 1652683320

# Zwischen 8:40 und 8:42:
#
# +20 Wh Netz
# +60 Wh PV
# =80 Wh Verbrauch/Ladung
#
# Erwarteter Strommix:
# Netz = 20 / 80 = 25 %
# PV = 60 / 80 = 75 %

current_entry["counter"]["counter0"]["imported"] = 2520

current_entry["pv"]["all"]["exported"] = 2560
current_entry["pv"]["pv1"]["exported"] = 2560

current_entry["cp"]["all"]["imported"] = 4080
current_entry["cp"]["cp4"]["imported"] = 4080

# _get_reference_entries() soll diesen aktuellen Snapshot verwenden.
monkeypatch.setattr(
chargelog,
"create_entry",
Mock(return_value=current_entry)
)

calc_energy_costs(cp, True)

assert cp.data.set.log.charged_energy_by_source == pytest.approx({
'bat': 0,
'cp': 0,
'grid': 20,
'pv': 60
})

# Kosten:
# Netz: 20 Wh * 0.0003 €/Wh = 0.006 €
# PV: 60 Wh * 0.00015 €/Wh = 0.009 €
# Gesamt 0.015 €
assert cp.data.set.log.costs == pytest.approx(0.015)


def test_calc_charge_cost_reference_end_unique_price(mock_data, monkeypatch):
cp = Chargepoint(4, None)
cp.data.set.log.imported_since_plugged = cp.data.set.log.imported_since_mode_switch = 3950
cp.data.set.log.timestamp_mode_switch = 1652682600 # 8:30
cp.data.get.imported = 4100
cp.data.set.log.charged_energy_by_source = {'grid': 1243, 'pv': 386, 'bat': 671, 'cp': 0.0}
daily_log = mock_daily_log(monkeypatch)
current_entry = json.loads(json.dumps(daily_log["entries"][-1]))
current_entry["bat"]["all"]["exported"] = 4000
current_entry["bat"]["bat2"]["exported"] = 4000
current_entry["counter"]["counter0"]["imported"] = 4500
current_entry["cp"]["cp4"]["imported"] = 4100
current_entry["pv"]["all"]["exported"] = 3000
current_entry["pv"]["pv1"]["exported"] = 3000
monkeypatch.setattr(chargelog, "create_entry", Mock(return_value=current_entry))
data.data.general_data.data.prices = Prices(bat=0.0002, cp=0, grid=0.0002, pv=0.0002)

with patch("builtins.open", mock_open(read_data=json.dumps(daily_log))):
Expand Down
43 changes: 27 additions & 16 deletions packages/helpermodules/measurement_logging/process_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,18 @@ def analyse_percentage(entry) -> Tuple[Dict, str]:
def format(value):
return round(value, 4)

def get_valid_energy(section: str, value: str) -> Union[int, float]:
"""Liefert den Energiewert ohne fehlerhafte Einzelmodule."""
modules = entry.get(section, {})
if (len(modules) <= 1 or
safe_get_nested(entry, section, "all", "fault_state") != 2):
return safe_get_nested(entry, section, "all", value)
return sum(
module_data.get(value, 0)
for module_name, module_data in modules.items()
if module_name != "all" and module_data.get("fault_state", 0) != 2
)

def get_grid_counter(entry) -> Dict:
# es gibt nur einen Zähler am EVU-Punkt
for counter in entry["counter"].values():
Expand All @@ -419,27 +431,26 @@ def get_grid_counter(entry) -> Dict:
try:
message = ""
grid_counter = get_grid_counter(entry)
# Wenn neben dem "all" Eintrag kein weiterer Eintrag existiert, dann gibt es keine Komponenten.
if ((safe_get_nested(entry, "bat", "all", "fault_state") == 2 and len(entry.get("bat", {})) > 1) or
(safe_get_nested(entry, "cp", "all", "fault_state") == 2 and len(entry.get("cp", {})) > 1) or
(safe_get_nested(entry, "pv", "all", "fault_state") == 2 and len(entry.get("pv", {})) > 1) or
grid_counter.get("fault_state", None) == 2):

# nur wenn der Grid-Zähler im Feherlzustand ist
# ins Fallback gehen
if (grid_counter.get("fault_state", None) == 2):
entry["energy_source"] = {"grid": 1, "pv": 0, "bat": 0, "cp": 0}
message += EOOR_STATE_MSG.format("der Zähler für das Netz")
else:
if safe_get_nested(entry, "bat", "all", "fault_state") == 2 and len(entry.get("bat", {})) > 1:
message += EOOR_STATE_MSG.format("mind. einer der Speicher")
message += f"Der Strom-Mix um {entry['date'] } wird trotz Fehlerzustand " \
"mindestens eines Speichers aus den vorhandenen Messwerten berechnet. \n"
if safe_get_nested(entry, "cp", "all", "fault_state") == 2 and len(entry.get("cp", {})) > 1:
message += EOOR_STATE_MSG.format("mind. einer der Ladepunkte")
message += f"Der Strom-Mix um {entry['date'] } wird trotz Fehlerzustand " \
"mindestens eines Ladepunkts aus den vorhandenen Messwerten berechnet. \n"
if safe_get_nested(entry, "pv", "all", "fault_state") == 2 and len(entry.get("pv", {})) > 1:
message += EOOR_STATE_MSG.format("mind. einer der Wechselrichter")
if grid_counter.get("fault_state", None) == 2:
message += EOOR_STATE_MSG.format("der Zähler für das Netz")
message += f"Der Strom-Mix um {entry['date'] } wird trotz Fehlerzustand " \
"mindestens eines Wechselrichters aus den vorhandenen Messwerten berechnet. \n"

else:
bat_imported = safe_get_nested(entry, "bat", "all", "energy_imported")
bat_exported = safe_get_nested(entry, "bat", "all", "energy_exported")
cp_exported = safe_get_nested(entry, "cp", "all", "energy_exported")
pv_exported = safe_get_nested(entry, "pv", "all", "energy_exported")
bat_imported = get_valid_energy("bat", "energy_imported")
bat_exported = get_valid_energy("bat", "energy_exported")
cp_exported = get_valid_energy("cp", "energy_exported")
pv_exported = get_valid_energy("pv", "energy_exported")
grid_imported = grid_counter.get("energy_imported", 0)
grid_exported = grid_counter.get("energy_exported", 0)
consumption = grid_imported - grid_exported + pv_exported + bat_exported - bat_imported + cp_exported
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,53 @@ def test_analyse_percentage(name: str,
assert message == ""


def test_analyse_percentage_ignores_faulty_modules():
entry = {
"timestamp": 1234567890,
"date": "00:31",
"bat": {
"all": {"energy_imported": 0, "energy_exported": 120, "fault_state": 2},
"bat1": {"energy_imported": 0, "energy_exported": 20, "fault_state": 0},
"bat2": {"energy_imported": 0, "energy_exported": 100, "fault_state": 2},
},
"cp": {
"all": {"energy_exported": 80, "fault_state": 2},
"cp1": {"energy_exported": 30, "fault_state": 0},
"cp2": {"energy_exported": 50, "fault_state": 2},
},
"pv": {
"all": {"energy_exported": 100, "fault_state": 2},
"pv1": {"energy_exported": 40, "fault_state": 0},
"pv2": {"energy_exported": 60, "fault_state": 2},
},
"counter": {
"counter0": {
"grid": True,
"energy_imported": 100,
"energy_exported": 0,
"fault_state": 0,
}
},
}

result, message = analyse_percentage(entry)

assert result["energy_source"] == {
"grid": 0.5263,
"pv": 0.2105,
"bat": 0.1053,
"cp": 0.1579,
}
assert message == (
"Der Strom-Mix um 00:31 wird trotz Fehlerzustand mindestens eines Speichers aus den vorhandenen Messwerten "
"berechnet. \n"
"Der Strom-Mix um 00:31 wird trotz Fehlerzustand mindestens eines Ladepunkts aus den vorhandenen Messwerten "
"berechnet. \n"
"Der Strom-Mix um 00:31 wird trotz Fehlerzustand mindestens eines Wechselrichters aus den vorhandenen "
"Messwerten berechnet. \n"
)


@pytest.mark.parametrize("test_case, entry_data, expected_energy_source, should_be_unchanged", [
(
"zero_consumption",
Expand Down
Loading