Skip to content

Commit ea7aac7

Browse files
authored
Fix charge log (#1757)
* fix chargelog * fix rebase * test * test * fix * f * fix * fix * test * running test only local * undo
1 parent 46c9079 commit ea7aac7

File tree

10 files changed

+526
-63
lines changed

10 files changed

+526
-63
lines changed

packages/conftest.py

+11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime
22
from unittest.mock import MagicMock, Mock
33
import pytest
4+
import pytz
45

56
from control import data
67
from control.bat import Bat, BatData
@@ -27,6 +28,16 @@ def mock_today(monkeypatch) -> None:
2728
monkeypatch.setattr(timecheck, "create_timestamp", mock_today_timestamp)
2829

2930

31+
@pytest.fixture(autouse=False)
32+
def mock_strptime_timestamp(monkeypatch):
33+
# Timezone-Objekt muss vor dem Mock von open initialisiert werden, da dies auch open verwendet und mit dem Mock
34+
# nicht mehr funktioniert
35+
timezone = pytz.timezone('Europe/Berlin')
36+
original_strptime = datetime.datetime.strptime
37+
monkeypatch.setattr(datetime.datetime, "strptime",
38+
lambda s, fmt: timezone.localize(original_strptime(s, fmt)))
39+
40+
3041
@pytest.fixture(autouse=True)
3142
def mock_pub(monkeypatch) -> Mock:
3243
pub_mock = Mock()

packages/control/chargelog/chargelog.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -450,8 +450,7 @@ def get_reference_time(cp, reference_position):
450450
return timecheck.create_timestamp() - 3540
451451
elif reference_position == ReferenceTime.END:
452452
# Wenn der Ladevorgang erst innerhalb der letzten Stunde gestartet wurde.
453-
one_hour_back = timecheck.create_timestamp() - 3600
454-
if (one_hour_back - cp.data.set.log.timestamp_start_charging) < 0:
453+
if timecheck.create_unix_timestamp_current_full_hour() <= cp.data.set.log.timestamp_start_charging:
455454
return cp.data.set.log.timestamp_start_charging
456455
else:
457456
return timecheck.create_unix_timestamp_current_full_hour() + 60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
2+
import datetime
3+
from unittest.mock import Mock
4+
5+
import pytest
6+
7+
from control import data
8+
from control.chargelog import chargelog
9+
from control.chargelog.chargelog import calculate_charge_cost
10+
from control.chargepoint.chargepoint import Chargepoint
11+
from helpermodules import timecheck
12+
from test_utils.test_environment import running_on_github
13+
14+
15+
def mock_daily_log_with_charging(date: str, num_of_intervalls, monkeypatch):
16+
"""erzeugt ein daily_log, im ersten Eintrag gibt es keine Änderung, danach wird bis inklusive dem letzten Beitrag
17+
geladen"""
18+
bat_exported = pv_exported = cp_imported = counter_imported = 2000
19+
date = datetime.datetime.strptime(date, "%m/%d/%Y, %H:%M")
20+
daily_log = {"entries": []}
21+
for i in range(0, num_of_intervalls):
22+
if i != 0:
23+
bat_exported += 1000
24+
pv_exported += 500
25+
cp_imported += 2000
26+
counter_imported += 500
27+
daily_log["entries"].append({'bat': {'all': {'exported': bat_exported, 'imported': 2000, 'soc': 100},
28+
'bat2': {'exported': bat_exported, 'imported': 2000, 'soc': 100}},
29+
'counter': {'counter0': {'exported': 2000,
30+
'grid': True,
31+
'imported': counter_imported}},
32+
'cp': {'all': {'exported': 0, 'imported': cp_imported},
33+
'cp4': {'exported': 0, 'imported': cp_imported}},
34+
'date': date.strftime("%H:%M"),
35+
'ev': {'ev0': {'soc': None}},
36+
'hc': {'all': {'imported': 0}},
37+
'pv': {'all': {'exported': pv_exported}, 'pv1': {'exported': pv_exported}},
38+
'sh': {},
39+
'timestamp': date.timestamp()})
40+
date += datetime.timedelta(minutes=5)
41+
mock_todays_daily_log = Mock(return_value=daily_log)
42+
monkeypatch.setattr(chargelog, "get_todays_daily_log", mock_todays_daily_log)
43+
return daily_log
44+
45+
46+
@pytest.fixture()
47+
def mock_data() -> None:
48+
data.data_init(Mock())
49+
data.data.optional_data.et_module = None
50+
51+
52+
def mock_create_entry_reference_end(clock, daily_log, monkeypatch):
53+
current_log = daily_log["entries"][-1]
54+
current_log["cp"]["all"]["imported"] += 500
55+
current_log["cp"]["cp4"]["imported"] += 500
56+
current_log["counter"]["counter0"]["imported"] += 500
57+
current_log["date"] = clock
58+
current_log["timestamp"] = datetime.datetime.strptime(f"05/16/2022, {clock}", "%m/%d/%Y, %H:%M").timestamp()
59+
mock_create_entry = Mock(return_value=current_log)
60+
monkeypatch.setattr(chargelog, "create_entry", mock_create_entry)
61+
62+
63+
def init_cp(charged_energy, costs, start_hour, start_minute=47):
64+
cp = Chargepoint(4, None)
65+
cp.data.set.log.imported_since_plugged = cp.data.set.log.imported_since_mode_switch = charged_energy
66+
cp.data.set.log.timestamp_start_charging = datetime.datetime(2022, 5, 16, start_hour, start_minute).timestamp()
67+
cp.data.get.imported = charged_energy + 2000
68+
cp.data.set.log.costs = costs
69+
return cp
70+
71+
72+
def test_calc_charge_cost_no_hour_change_reference_end(mock_data, monkeypatch):
73+
cp = init_cp(6500, 0, 10, start_minute=27)
74+
daily_log = mock_daily_log_with_charging("05/16/2022, 10:25", 4, monkeypatch)
75+
mock_create_entry_reference_end("10:42", daily_log, monkeypatch)
76+
77+
calculate_charge_cost(cp, True)
78+
79+
assert cp.data.set.log.costs == 1.425
80+
81+
82+
def test_calc_charge_cost_first_hour_change_reference_begin(mock_data, monkeypatch):
83+
cp = init_cp(6000, 0, 7)
84+
daily_log = mock_daily_log_with_charging("05/16/2022, 07:45", 4, monkeypatch)
85+
current_log = daily_log["entries"][-1]
86+
current_log["date"] = "08:00"
87+
current_log["timestamp"] = datetime.datetime.strptime("05/16/2022, 08:00", "%m/%d/%Y, %H:%M").timestamp()
88+
mock_create_entry = Mock(return_value=current_log)
89+
monkeypatch.setattr(chargelog, "create_entry", mock_create_entry)
90+
91+
calculate_charge_cost(cp, False)
92+
93+
assert cp.data.set.log.costs == 1.275
94+
95+
96+
def test_calc_charge_cost_first_hour_change_reference_begin_day_change(mock_data, monkeypatch):
97+
cp = init_cp(6000, 0, 23)
98+
daily_log = mock_daily_log_with_charging("05/16/2022, 23:45", 4, monkeypatch)
99+
current_log = daily_log["entries"][-1]
100+
current_log["date"] = "00:00"
101+
current_log["timestamp"] = datetime.datetime.strptime("05/17/2022, 00:00", "%m/%d/%Y, %H:%M").timestamp()
102+
mock_create_entry = Mock(return_value=current_log)
103+
monkeypatch.setattr(chargelog, "create_entry", mock_create_entry)
104+
mock_today_timestamp = Mock(return_value=1652738421)
105+
monkeypatch.setattr(timecheck, "create_timestamp", mock_today_timestamp)
106+
107+
calculate_charge_cost(cp, False)
108+
109+
assert cp.data.set.log.costs == 1.275
110+
111+
112+
def test_calc_charge_cost_one_hour_change_reference_end(mock_data, monkeypatch):
113+
if running_on_github():
114+
# ToDo Zeitzonen berücksichtigen, damit Tests auf Github laufen
115+
return
116+
cp = init_cp(22500, 1.275, 7)
117+
daily_log = mock_daily_log_with_charging("05/16/2022, 07:45", 12, monkeypatch)
118+
mock_create_entry_reference_end("08:40", daily_log, monkeypatch)
119+
120+
calculate_charge_cost(cp, True)
121+
122+
assert cp.data.set.log.costs == 4.8248999999999995
123+
124+
125+
def test_calc_charge_cost_two_hour_change_reference_middle(mock_data, monkeypatch):
126+
if running_on_github():
127+
# ToDo Zeitzonen berücksichtigen, damit Tests auf Github laufen
128+
return
129+
cp = init_cp(22500, 1.275, 6)
130+
daily_log = mock_daily_log_with_charging("05/16/2022, 06:45", 16, monkeypatch)
131+
current_log = daily_log["entries"][-1]
132+
current_log["date"] = "08:00"
133+
current_log["timestamp"] = datetime.datetime(2022, 5, 16, 8).timestamp()
134+
mock_create_entry = Mock(return_value=current_log)
135+
monkeypatch.setattr(chargelog, "create_entry", mock_create_entry)
136+
mock_today_timestamp = Mock(return_value=1652680801)
137+
monkeypatch.setattr(timecheck, "create_timestamp", mock_today_timestamp)
138+
139+
calculate_charge_cost(cp, False)
140+
141+
assert cp.data.set.log.costs == 6.375
142+
143+
144+
def test_calc_charge_cost_two_hour_change_reference_end(mock_data, monkeypatch):
145+
if running_on_github():
146+
# ToDo Zeitzonen berücksichtigen, damit Tests auf Github laufen
147+
return
148+
cp = init_cp(46500, 6.375, 6)
149+
daily_log = mock_daily_log_with_charging("05/16/2022, 06:45", 24, monkeypatch)
150+
mock_create_entry_reference_end("08:40", daily_log, monkeypatch)
151+
152+
calculate_charge_cost(cp, True)
153+
154+
assert cp.data.set.log.costs == 9.924900000000001

packages/helpermodules/update_config.py

+111-12
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
from dataclasses import asdict
22
import datetime
33
import glob
4+
import importlib
45
import json
56
import logging
67
from pathlib import Path
78
import re
9+
from shutil import copyfile
810
import time
911
from typing import List, Optional
1012
from paho.mqtt.client import Client as MqttClient, MQTTMessage
1113
from control.bat_all import BatConsiderationMode
14+
from control.chargelog.chargelog import ReferenceTime
1215
from control.general import ChargemodeConfig
1316
import dataclass_utils
1417

1518
from control.chargepoint.chargepoint_template import get_chargepoint_template_default
19+
from dataclass_utils._dataclass_from_dict import dataclass_from_dict
1620
from helpermodules import timecheck
1721
from helpermodules import hardware_configuration
1822
from helpermodules.broker import InternalBrokerClient
1923
from helpermodules.constants import NO_ERROR
2024
from helpermodules.hardware_configuration import get_hardware_configuration_setting, update_hardware_configuration
21-
from helpermodules.measurement_logging.process_log import get_totals
25+
from helpermodules.measurement_logging.process_log import CalculationType, analyse_percentage, get_totals, process_entry
2226
from helpermodules.measurement_logging.write_log import get_names
2327
from helpermodules.messaging import MessageType, pub_system_message
2428
from helpermodules.pub import Pub
@@ -42,7 +46,7 @@
4246

4347

4448
class UpdateConfig:
45-
DATASTORE_VERSION = 52
49+
DATASTORE_VERSION = 53
4650
valid_topic = [
4751
"^openWB/bat/config/configured$",
4852
"^openWB/bat/set/charging_power_left$",
@@ -1615,14 +1619,109 @@ def upgrade(topic: str, payload) -> Optional[dict]:
16151619
self._loop_all_received_topics(upgrade)
16161620
self.__update_topic("openWB/system/datastore_version", 51)
16171621

1622+
def upgrade_datastore_51(self) -> None:
1623+
def upgrade(topic: str, payload) -> None:
1624+
if re.search("openWB/system/device/[0-9]+", topic) is not None:
1625+
payload = decode_payload(payload)
1626+
# update version and firmware of GoodWe
1627+
if payload.get("type") == "deye" and "device_type" in payload["configuration"]:
1628+
payload["configuration"].pop("device_type")
1629+
Pub().pub(topic, payload)
1630+
self._loop_all_received_topics(upgrade)
1631+
self.__update_topic("openWB/system/datastore_version", 52)
16181632

1619-
def upgrade_datastore_51(self) -> None:
1620-
def upgrade(topic: str, payload) -> None:
1621-
if re.search("openWB/system/device/[0-9]+", topic) is not None:
1622-
payload = decode_payload(payload)
1623-
# update version and firmware of GoodWe
1624-
if payload.get("type") == "deye" and "device_type" in payload["configuration"]:
1625-
payload["configuration"].pop("device_type")
1626-
Pub().pub(topic, payload)
1627-
self._loop_all_received_topics(upgrade)
1628-
self.__update_topic("openWB/system/datastore_version", 52)
1633+
def upgrade_datastore_52(self) -> None:
1634+
# alle Einträge des ladelogs seit januar 2024 durchgehen
1635+
path_list = [f"{self.base_path}/data/charge_log/2024{i:02d}.json" for i in range(1, 8)]
1636+
for topic, payload in self.all_received_topics.items():
1637+
if "openWB/general/prices/bat" in topic:
1638+
bat_price = decode_payload(payload)
1639+
elif "openWB/general/prices/grid" in topic:
1640+
grid_price = decode_payload(payload)
1641+
elif "openWB/general/prices/pv" in topic:
1642+
pv_price = decode_payload(payload)
1643+
elif "openWB/optional/et/provider" in topic:
1644+
et_active = True if decode_payload(payload)["type"] is not None else False
1645+
for chargelog in path_list:
1646+
fixed = False
1647+
try:
1648+
with open(chargelog, "r") as jsonFile:
1649+
content = json.load(jsonFile)
1650+
for chargelog_entry in content:
1651+
# prüfen, ob der Ladevorgang nur einen Stundenwechsel enthält
1652+
begin = datetime.datetime.strptime(chargelog_entry["time"]["begin"], "%m/%d/%Y, %H:%M:%S")
1653+
end = datetime.datetime.strptime(chargelog_entry["time"]["end"], "%m/%d/%Y, %H:%M:%S")
1654+
hour_change = datetime.datetime.strptime(end.strftime("%m/%d/%Y, %H"), "%m/%d/%Y, %H")
1655+
if ((end.hour - begin.hour) == 1 or
1656+
((end.day - begin.day) == 1 and begin.hour == 23 and end.hour == 0)):
1657+
def calc(first: datetime.datetime, second: datetime.datetime, reference: ReferenceTime):
1658+
with open(f"{self.base_path}/data/daily_log/2024{first.month:02d}{first.day:02d}.json",
1659+
"r") as jsonFile:
1660+
daily_log = json.load(jsonFile)
1661+
first_entry, second_entry = None, None
1662+
if ReferenceTime.MIDDLE == reference:
1663+
for daily_entry in reversed(daily_log["entries"]):
1664+
# -1, damit auch Einträge um XX:00:00 berücksichtigt werden
1665+
if daily_entry["timestamp"] <= first.timestamp() - 1:
1666+
first_entry = daily_entry
1667+
break
1668+
else:
1669+
for daily_entry in daily_log["entries"]:
1670+
if daily_entry["timestamp"] >= first.timestamp() - 1:
1671+
first_entry = daily_entry
1672+
break
1673+
if begin.day != end.day:
1674+
with open(f"{self.base_path}/data/daily_log/2024{end.month:02d}{end.day:02d}.json",
1675+
"r") as jsonFile:
1676+
daily_log = json.load(jsonFile)
1677+
for daily_entry in daily_log["entries"]:
1678+
if daily_entry["timestamp"] >= second.timestamp() - 1:
1679+
second_entry = daily_entry
1680+
break
1681+
energy_entry = process_entry(first_entry, second_entry, CalculationType.ENERGY)
1682+
energy_source = analyse_percentage(energy_entry)["energy_source"]
1683+
cp_num = chargelog_entry['chargepoint']['id']
1684+
charged_energy = (second_entry["cp"][f"cp{cp_num}"]["imported"]
1685+
- first_entry["cp"][f"cp{cp_num}"]["imported"])
1686+
1687+
bat_costs = bat_price * charged_energy * energy_source["bat"]
1688+
if et_active:
1689+
if reference == ReferenceTime.MIDDLE:
1690+
price = et_prices[0]
1691+
else:
1692+
price = et_prices[1]
1693+
grid_costs = price * charged_energy * \
1694+
energy_source["grid"]
1695+
else:
1696+
grid_costs = grid_price * charged_energy * energy_source["grid"]
1697+
pv_costs = pv_price * charged_energy * energy_source["pv"]
1698+
1699+
log.debug(
1700+
f'Ladepreis für die letzte Stunde: {bat_costs}€ Speicher ({energy_source["bat"]}%), '
1701+
f'{grid_costs}€ Netz ({energy_source["grid"]}%), {pv_costs}€ Pv ({energy_source["pv"]}%'
1702+
')')
1703+
return round(bat_costs + grid_costs + pv_costs, 4)
1704+
1705+
if et_active:
1706+
config_dict = decode_payload(payload)
1707+
mod = importlib.import_module(
1708+
f".electricity_tariffs.{config_dict['type']}.tariff", "modules")
1709+
config = dataclass_from_dict(
1710+
mod.device_descriptor.configuration_factory, config_dict)
1711+
tariff_state = mod.create_electricity_tariff(config)(begin.timestamp())
1712+
et_prices = [tariff_state.prices[hour_change.timestamp()-3600],
1713+
tariff_state.prices[hour_change.timestamp()]]
1714+
log.debug(f'ET-Preise {chargelog_entry["time"]["begin"]} {begin.timestamp()}: '
1715+
f'{et_prices}')
1716+
new_costs = calc(begin, hour_change, ReferenceTime.MIDDLE)
1717+
new_costs += calc(hour_change, end, ReferenceTime.END)
1718+
if chargelog_entry["data"]["costs"] != new_costs:
1719+
fixed = True
1720+
chargelog_entry["data"]["costs"] = new_costs
1721+
if fixed:
1722+
copyfile(chargelog, chargelog+".1")
1723+
with open(chargelog, "w") as jsonFile:
1724+
json.dump(content, jsonFile)
1725+
except FileNotFoundError:
1726+
log.debug(f"Keine Logdatei {chargelog} gefunden")
1727+
self.__update_topic("openWB/system/datastore_version", 53)

0 commit comments

Comments
 (0)