Skip to content

Commit bcb5373

Browse files
LKuemmelbenderl
andauthored
Feature electricity tariff (#1216)
* electricity tariff aWATTar Tibber improve code style improve timecheck * move setting * fixes * pricing * handler * fix electronic tariff calc * use enum * fix calc costs * fix parent file * fix test * improve status str * improve status messages * Update packages/modules/common/configurable_tariff.py Co-authored-by: benderl <[email protected]> * fix timezone * fix f-string * fix chargelog costs * fix chargelog costs * fix test * fix test * fix test * fix test * fix test * fix * draft * draft * add veltego * fix scheduled charging with electricity tariff * fix test * fix * fix * fix chargelog * fix test * fix * fix test * fix sorting of configurable et * rename token -> access_token * fix timestamp * sync datastore_version for update from master --------- Co-authored-by: benderl <[email protected]> Co-authored-by: Lutz Bender <[email protected]>
1 parent 439f4e3 commit bcb5373

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1261
-258
lines changed

.github/workflows/github-actions-python.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
run: |
1616
python -m pip install --upgrade pip
1717
pip install flake8 pytest paho-mqtt requests-mock jq pyjwt==2.6.0 bs4 pkce typing_extensions python-dateutil==2.8.2 cryptography==40.0.1 msal
18-
pip install umodbus
18+
pip install umodbus pytz
1919
- name: Flake8 with annotations in packages folder
2020
uses: TrueBrain/[email protected]
2121
with:

docs/samples/sample_electricity_tariff/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import Optional
2+
3+
4+
class SampleTariffConfiguration:
5+
def __init__(self, ip_address: Optional[str] = None, password: Optional[str] = None):
6+
self.ip_address = ip_address
7+
self.password = password
8+
9+
10+
class SampleTariff:
11+
def __init__(self,
12+
name: str = "Sample",
13+
type: str = "sample",
14+
configuration: SampleTariffConfiguration = None) -> None:
15+
self.name = name
16+
self.type = type
17+
self.configuration = configuration or SampleTariffConfiguration()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env python3
2+
import logging
3+
4+
from docs.samples.sample_electricity_tariff.config import SampleTariff, SampleTariffConfiguration
5+
from modules.common import req
6+
from modules.common.abstract_device import DeviceDescriptor
7+
from modules.common.component_state import TariffState
8+
from modules.common.configurable_tariff import ConfigurableTariff
9+
10+
log = logging.getLogger(__name__)
11+
12+
13+
def fetch(config: SampleTariffConfiguration) -> None:
14+
# request prices
15+
response = req.get_http_session().get().json()
16+
return TariffState(prices=response["price_list"])
17+
18+
19+
def create_electricity_tariff(config: SampleTariff):
20+
def updater():
21+
return fetch(config.configuration)
22+
return ConfigurableTariff(config=config, component_updater=updater)
23+
24+
25+
device_descriptor = DeviceDescriptor(configuration_factory=SampleTariff)

packages/control/chargelog/__init__.py

Whitespace-only changes.

packages/control/chargelog.py renamed to packages/control/chargelog/chargelog.py

+228-130
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import json
2+
from unittest.mock import Mock
3+
4+
import pytest
5+
from control import data
6+
from control.chargelog import chargelog
7+
from control.chargelog.chargelog import (ReferenceTime, _calc, _get_reference_entry, _get_reference_position,
8+
calculate_charge_cost, get_reference_time)
9+
from control.chargepoint.chargepoint import Chargepoint
10+
from control.general import General
11+
from control.optional import Optional
12+
from helpermodules import timecheck
13+
14+
15+
@pytest.fixture(autouse=True)
16+
def data_module() -> None:
17+
data.data_init(Mock())
18+
data.data.general_data = General()
19+
data.data.optional_data = Optional()
20+
data.data.cp_data["cp3"] = Chargepoint(3, Mock)
21+
22+
23+
EXPECTED_ENTRY_TODAYS_DAILY = {"timestamp": 1698880500, "date": "00:15", "cp":
24+
{"cp5":
25+
{"imported": 0, "exported": 0}, "cp3":
26+
{"imported": 17726.504, "exported": 0}, "cp4":
27+
{"imported": 0, "exported": 0}, "all":
28+
{"imported": 17726.504, "exported": 0}}, "ev":
29+
{"ev0":
30+
{"soc": 0}}, "counter":
31+
{"counter0":
32+
{"imported": 7144.779, "exported": 20.152, "grid": True}}, "pv":
33+
{"all":
34+
{"exported": 11574}, "pv1":
35+
{"exported": 11574}}, "bat":
36+
{"all":
37+
{"imported": 6.33, "exported": 2506.763, "soc": 0}, "bat2":
38+
{"imported": 6.33, "exported": 2506.763, "soc": 0}}, "sh":
39+
{}, "hc":
40+
{"all":
41+
{"imported": 111133.74099238854}}}
42+
EXPECTED_ENTRY_YESTERDAYS_DAILY = {"timestamp": 1698879000, "date": "23:50", "cp":
43+
{"cp5":
44+
{"imported": 0, "exported": 0}, "cp3":
45+
{"imported": 16767.105, "exported": 0}, "cp4":
46+
{"imported": 0, "exported": 0}, "all":
47+
{"imported": 16767.105, "exported": 0}}, "ev":
48+
{"ev0":
49+
{"soc": 0}}, "counter":
50+
{"counter0":
51+
{"imported": 6616.028, "exported": 20.152, "grid": True}}, "pv":
52+
{"all":
53+
{"exported": 10944}, "pv1":
54+
{"exported": 10944}}, "bat":
55+
{"all":
56+
{"imported": 6.33, "exported": 2506.763, "soc": 0}, "bat2":
57+
{"imported": 6.33, "exported": 2506.763, "soc": 0}}, "sh":
58+
{}, "hc":
59+
{"all":
60+
{"imported": 110943.62398798217}}}
61+
62+
63+
@pytest.mark.parametrize(
64+
"start_charging, create_log_entry, expected_timestamp",
65+
(pytest.param(1652679772, False, ReferenceTime.START,
66+
id="innerhalb der letzten Stunde angesteckt"), # "05/16/2022, 07:42:52"
67+
pytest.param(1652676052, False, ReferenceTime.MIDDLE,
68+
id="vor mehr als einer Stunde angesteckt"), # "05/16/2022, 06:40:52"
69+
pytest.param(1652676052, True, ReferenceTime.END,
70+
id="vor mehr als einer Stunde angesteckt, Ladevorgang beenden"), # "05/16/2022, 06:40:52"
71+
)
72+
)
73+
def test_get_reference_position(start_charging: str, create_log_entry: bool, expected_timestamp: float):
74+
# setup
75+
# jetzt ist "05/16/2022, 08:40:52"
76+
cp = Chargepoint(0, Mock())
77+
cp.data.set.log.timestamp_start_charging = start_charging
78+
79+
# execution
80+
timestamp = _get_reference_position(cp, create_log_entry)
81+
82+
# evaluation
83+
assert timestamp == expected_timestamp
84+
85+
86+
@pytest.mark.parametrize(
87+
"reference_position, start_charging, expected_timestamp",
88+
(pytest.param(ReferenceTime.START, 1652679772, 1652679772,
89+
id="innerhalb der letzten Stunde angesteckt"), # "05/16/2022, 07:42:52"
90+
pytest.param(ReferenceTime.MIDDLE, 1652676052, 1652679652,
91+
id="vor mehr als einer Stunde angesteckt"), # "05/16/2022, 06:40:52"
92+
pytest.param(ReferenceTime.END, 1652676052, 1652680800,
93+
id="vor mehr als einer Stunde angesteckt, Ladevorgang beenden"), # "05/16/2022, 06:40:52"
94+
)
95+
)
96+
def test_get_reference_time(reference_position: ReferenceTime, start_charging: str, expected_timestamp: float):
97+
# setup
98+
# jetzt ist "05/16/2022, 08:40:52"
99+
cp = Chargepoint(0, Mock())
100+
cp.data.set.log.timestamp_start_charging = start_charging
101+
102+
# execution
103+
timestamp = get_reference_time(cp, reference_position)
104+
105+
# evaluation
106+
assert timestamp == expected_timestamp
107+
108+
109+
@pytest.mark.parametrize(
110+
"reference_time, expected_entry",
111+
(
112+
pytest.param(1698880731, EXPECTED_ENTRY_TODAYS_DAILY, id="Referenz-Eintrag im heutigen Log"), # 00:18
113+
pytest.param(1698879171, EXPECTED_ENTRY_YESTERDAYS_DAILY, id="Referenz-Eintrag im gestrigen Log"), # 23:52
114+
)
115+
)
116+
def test_get_reference_entry(reference_time, expected_entry, monkeypatch):
117+
# setup
118+
with open("packages/control/chargelog/sample_daily_today.json", "r") as json_file:
119+
entries_todays_daily_log = json.load(json_file)["entries"]
120+
with open("packages/control/chargelog/sample_daily_yesterday.json", "r") as json_file:
121+
content_yesterday = json.load(json_file)
122+
monkeypatch.setattr(chargelog, "_get_yesterdays_daily_log", Mock(return_value=content_yesterday))
123+
124+
# execution
125+
entry = _get_reference_entry(entries_todays_daily_log, reference_time)
126+
127+
# evaluation
128+
assert entry == expected_entry
129+
130+
131+
@pytest.mark.parametrize("et_active, expected_costs",
132+
(
133+
pytest.param(True, 1.1649, id="Et aktiv"),
134+
pytest.param(False, 2.5953, id="Et inaktiv"),
135+
))
136+
def test_calc(et_active, expected_costs, monkeypatch):
137+
# setup
138+
mock_et_get_current_price = Mock(return_value=0.00008)
139+
monkeypatch.setattr(data.data.optional_data, "et_get_current_price", mock_et_get_current_price)
140+
141+
# execution
142+
costs = _calc({'bat': 0.24, 'cp': 0.0, 'grid': 0.6502, 'pv': 0.1098}, 10000, et_active)
143+
144+
# evaluation
145+
assert costs == expected_costs
146+
147+
148+
def test_calculate_charge_cost(monkeypatch):
149+
# integration test
150+
# setup
151+
data.data.cp_data["cp3"].data.set.log.timestamp_start_charging = 1698822760
152+
data.data.cp_data["cp3"].data.set.log.imported_since_plugged = 1000
153+
data.data.cp_data["cp3"].data.set.log.imported_since_mode_switch = 1000
154+
# Mock today() to values in log-file
155+
# Thu Nov 02 2023 07:00:51
156+
mock_today_timestamp = Mock(return_value=1698904851)
157+
monkeypatch.setattr(timecheck, "create_timestamp", mock_today_timestamp)
158+
with open("packages/control/chargelog/sample_daily_yesterday.json", "r") as json_file:
159+
content_yesterday = json.load(json_file)
160+
monkeypatch.setattr(chargelog, "_get_yesterdays_daily_log", Mock(return_value=content_yesterday))
161+
with open("packages/control/chargelog/sample_daily_today.json", "r") as json_file:
162+
content_today = json.load(json_file)
163+
monkeypatch.setattr(chargelog, "get_todays_daily_log", Mock(return_value=content_today))
164+
165+
# execution
166+
calculate_charge_cost(data.data.cp_data["cp3"])
167+
168+
# evaluation
169+
# charged energy 2.3kWh, 45,45% Grid, 54,55% PV, Grid 0,3ct/kWh, Pv 0,15ct/kWh
170+
assert data.data.cp_data["cp3"].data.set.log.costs == 0.5023

packages/control/chargelog/sample_daily_today.json

+1
Large diffs are not rendered by default.

packages/control/chargelog/sample_daily_yesterday.json

+1
Large diffs are not rendered by default.

packages/control/chargepoint/chargepoint.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
import traceback
2222
from typing import Dict, Optional, Tuple
2323

24-
from control import chargelog
24+
from control.chargelog import chargelog
2525
from control import cp_interruption
2626
from control import data
2727
from control.chargemode import Chargemode
28-
from control.chargepoint.chargepoint_data import ChargepointData, ConnectedConfig, ConnectedInfo, ConnectedSoc, Get
28+
from control.chargepoint.chargepoint_data import ChargepointData, ConnectedConfig, ConnectedInfo, ConnectedSoc, Get, Log
2929
from control.chargepoint.chargepoint_template import CpTemplate
3030
from control.chargepoint.control_parameter import ControlParameter
3131
from control.chargepoint.rfid import ChargepointRfidMixin
@@ -224,7 +224,7 @@ def _process_charge_stop(self) -> None:
224224
self.data.set.manual_lock = True
225225
Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/set/manual_lock", True)
226226
# Ev wurde noch nicht aktualisiert.
227-
chargelog.reset_data(self, data.data.ev_data["ev"+str(self.data.set.charging_ev_prev)])
227+
chargelog.save_and_reset_data(self, data.data.ev_data["ev"+str(self.data.set.charging_ev_prev)])
228228
self.data.set.charging_ev_prev = -1
229229
Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/set/charging_ev_prev",
230230
self.data.set.charging_ev_prev)
@@ -288,6 +288,19 @@ def remember_previous_values(self):
288288
self.data.set.plug_state_prev = self.data.get.plug_state
289289
Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/set/plug_state_prev", self.data.set.plug_state_prev)
290290

291+
def reset_log_data_chargemode_switch(self) -> None:
292+
reset_log = Log()
293+
# Wenn ein Zwischeneintrag, zB bei Wechsel des Lademodus, erstellt wird, Zählerstände nicht verwerfen.
294+
reset_log.imported_at_mode_switch = self.data.get.imported
295+
reset_log.imported_at_plugtime = self.data.set.log.imported_at_plugtime
296+
reset_log.imported_since_plugged = self.data.set.log.imported_since_plugged
297+
self.data.set.log = reset_log
298+
Pub().pub(f"openWB/set/chargepoint/{self.num}/set/log", asdict(self.data.set.log))
299+
300+
def reset_log_data(self) -> None:
301+
self.data.set.log = Log()
302+
Pub().pub(f"openWB/set/chargepoint/{self.num}/set/log", asdict(self.data.set.log))
303+
291304
def prepare_cp(self) -> Tuple[int, Optional[str]]:
292305
try:
293306
# Für Control-Pilot-Unterbrechung set current merken.
@@ -626,7 +639,7 @@ def update(self, ev_list: Dict[str, Ev]) -> None:
626639
# Ein Eintrag muss nur erstellt werden, wenn vorher schon geladen wurde und auch danach noch
627640
# geladen werden soll.
628641
if charging_ev.chargemode_changed and self.data.get.charge_state and state:
629-
chargelog.save_data(self, charging_ev)
642+
chargelog.save_interim_data(self, charging_ev)
630643

631644
# Wenn die Nachrichten gesendet wurden, EV wieder löschen, wenn das EV im Algorithmus nicht
632645
# berücksichtigt werden soll.

packages/control/chargepoint/chargepoint_data.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,17 @@ class ConnectedVehicle:
6565
@dataclass
6666
class Log:
6767
chargemode_log_entry: str = "_"
68+
costs: float = 0
6869
imported_at_mode_switch: float = 0
6970
imported_at_plugtime: float = 0
7071
imported_since_mode_switch: float = 0
7172
imported_since_plugged: float = 0
7273
range_charged: float = 0
73-
time_charged: float = 0
74-
timestamp_start_charging: Optional[float] = None
74+
time_charged: str = "00:00"
75+
timestamp_start_charging: Optional[str] = None
76+
ev: int = -1
77+
prio: bool = False
78+
rfid: Optional[str] = None
7579

7680

7781
def connected_vehicle_factory() -> ConnectedVehicle:

0 commit comments

Comments
 (0)