Skip to content

Commit 002bf1e

Browse files
committed
lokale Änderungen
1 parent 939a371 commit 002bf1e

File tree

10 files changed

+212
-174
lines changed

10 files changed

+212
-174
lines changed

packages/conftest.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,8 @@ def mock_today(monkeypatch, request) -> None:
4141
monkeypatch.setattr(datetime, "datetime", datetime_mock)
4242
now_timestamp = Mock(return_value=1652683252)
4343
monkeypatch.setattr(timecheck, "create_timestamp", now_timestamp)
44-
if "no_mock_full_hour" not in request.keywords:
45-
full_hour_timestamp = Mock(return_value=int(datetime.datetime(2022, 5, 16, 8, 0, 0).timestamp()))
46-
monkeypatch.setattr(timecheck, "create_unix_timestamp_current_full_hour", full_hour_timestamp)
47-
if "no_mock_quarter_hour" not in request.keywords:
48-
quarter_hour_timesatmp = Mock(return_value=int(datetime.datetime(2022, 5, 16, 8, 30, 0).timestamp()))
49-
monkeypatch.setattr(timecheck, "create_unix_timestamp_current_quarter_hour", quarter_hour_timesatmp)
44+
full_hour_timestamp = Mock(return_value=int(datetime.datetime(2022, 5, 16, 8, 0, 0).timestamp()))
45+
monkeypatch.setattr(timecheck, "create_unix_timestamp_current_full_hour", full_hour_timestamp)
5046

5147

5248
@pytest.fixture(autouse=True)

packages/control/optional.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,10 @@
1212
from helpermodules import hardware_configuration
1313
from helpermodules.constants import NO_ERROR
1414
from helpermodules.pub import Pub
15+
from helpermodules.timecheck import create_timestamp
1516
from helpermodules.utils import thread_handler
1617
from modules.common.configurable_tariff import ConfigurableElectricityTariff
1718
from modules.common.configurable_monitoring import ConfigurableMonitoring
18-
from helpermodules.timecheck import (
19-
create_unix_timestamp_current_quarter_hour,
20-
create_unix_timestamp_current_full_hour
21-
)
2219

2320
log = logging.getLogger(__name__)
2421

@@ -68,16 +65,31 @@ def et_charging_allowed(self, max_price: float):
6865
log.exception("Fehler im Optional-Modul")
6966
return False
7067

71-
def et_get_current_price(self) -> float:
68+
def __get_first_entry(self, prices: dict[str, float]) -> tuple[str, float]:
7269
if self.et_provider_available():
7370
prices = self.data.et.get.prices
7471
timestamp, first = next(iter(prices.items()))
72+
price_timeslot_seconds = self.__calculate_price_timeslot_length(prices)
73+
now = int(create_timestamp())
74+
while int(timestamp) > now - price_timeslot_seconds:
75+
prices.remove(timestamp)
76+
self.data.et.get.prices = prices
77+
timestamp, first = next(iter(prices.items()))
78+
7579
log.debug(f"first in prices list: {first} from " +
7680
f"{datetime.datetime.fromtimestamp(int(timestamp)).strftime('%Y-%m-%d %H:%M')}")
77-
return first
81+
return timestamp, first
7882
else:
7983
raise Exception("Kein Anbieter für strompreisbasiertes Laden konfiguriert.")
8084

85+
def __get_current_timeslot_start(self, prices: dict[str, float]) -> float:
86+
timestamp, first = self.__get_first_entry(prices)
87+
return timestamp
88+
89+
def et_get_current_price(self, prices: dict[str, float]) -> float:
90+
timestamp, first = self.__get_first_entry(prices)
91+
return first
92+
8193
def __calculate_price_timeslot_length(self, prices: dict) -> int:
8294
first_timestamps = list(prices.keys())[:2]
8395
return int(first_timestamps[1]) - int(first_timestamps[0])
@@ -99,21 +111,12 @@ def et_get_loading_hours(self, duration: float, remaining_time: float) -> List[i
99111
try:
100112
prices = self.data.et.get.prices
101113
price_timeslot_seconds = self.__calculate_price_timeslot_length(prices)
102-
now = (
103-
create_unix_timestamp_current_full_hour()
104-
if 3600 == price_timeslot_seconds
105-
else create_unix_timestamp_current_quarter_hour()
106-
)
107-
108-
log.debug(f"current full hour: "
109-
f"{int(now)} {datetime.datetime.fromtimestamp(int(now)).strftime('%Y-%m-%d %H:%M')} "
110-
f"Plan target Date: {int(now) + remaining_time} "
111-
f"{datetime.datetime.fromtimestamp(int(now) + remaining_time).strftime('%Y-%m-%d %H:%M')}")
112-
114+
now = self.__get_current_timeslot_start(prices)
113115
prices = {
114116
timestamp: price
115117
for timestamp, price in prices.items()
116-
if ( # is current timeslot or futur
118+
if (
119+
# is current timeslot or futur
117120
int(timestamp) >= int(now) and
118121
# ends before plan target time
119122
int(timestamp) + price_timeslot_seconds <= int(now) + remaining_time

packages/helpermodules/timecheck.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -236,13 +236,6 @@ def create_unix_timestamp_current_full_hour() -> int:
236236
return int(datetime.datetime.strptime(full_hour, "%m/%d/%Y, %H").timestamp())
237237

238238

239-
def create_unix_timestamp_current_quarter_hour() -> int:
240-
def round_to_quarter_hour(current_time: float, quarter_hour: int = 900) -> float:
241-
log.debug(f"current time: {current_time} => modified: {current_time - (current_time % quarter_hour)}")
242-
return current_time - (current_time % quarter_hour)
243-
return int(round_to_quarter_hour(datetime.datetime.today().timestamp()))
244-
245-
246239
def get_relative_date_string(date_string: str, day_offset: int = 0, month_offset: int = 0, year_offset: int = 0) -> str:
247240
print_format = "%Y%m%d" if len(date_string) > 6 else "%Y%m"
248241
my_date = datetime.datetime.strptime(date_string, print_format)
@@ -307,14 +300,14 @@ def duration_sum(first: str, second: str) -> str:
307300
return "00:00"
308301

309302

310-
def __get_timedelta_obj(time: str) -> datetime.timedelta:
303+
def __get_timedelta_obj(time_str: str) -> datetime.timedelta:
311304
""" erstellt aus einem String ein timedelta-Objekt.
312305
Parameter
313306
---------
314-
time: str
307+
time_str: str
315308
Zeitstrings HH:MM ggf DD:HH:MM
316309
"""
317-
time_charged = time.split(":")
310+
time_charged = time_str.split(":")
318311
if len(time_charged) == 2:
319312
delta = datetime.timedelta(hours=int(time_charged[0]),
320313
minutes=int(time_charged[1]))
@@ -323,7 +316,7 @@ def __get_timedelta_obj(time: str) -> datetime.timedelta:
323316
hours=int(time_charged[1]),
324317
minutes=int(time_charged[2]))
325318
else:
326-
raise Exception("Unknown charge duration: "+time)
319+
raise Exception(f"Unknown charge duration: {time_str}")
327320
return delta
328321

329322

packages/helpermodules/timecheck_test.py

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -143,33 +143,3 @@ def test_convert_timestamp_delta_to_time_string(timestamp, expected):
143143

144144
# evaluation
145145
assert time_string == expected
146-
147-
148-
@pytest.mark.no_mock_quarter_hour
149-
@pytest.mark.parametrize("timestamp, expected",
150-
[
151-
pytest.param("2025-10-01 09:00", "2025-10-01 09:00", id="9:00"),
152-
pytest.param("2025-10-01 09:01", "2025-10-01 09:00", id="9:01"),
153-
pytest.param("2025-10-01 09:10", "2025-10-01 09:00", id="9:10"),
154-
pytest.param("2025-10-01 09:14", "2025-10-01 09:00", id="9:14"),
155-
pytest.param("2025-10-01 09:15", "2025-10-01 09:15", id="9:15"),
156-
pytest.param("2025-10-01 09:41", "2025-10-01 09:30", id="9:41"),
157-
pytest.param("2025-10-01 09:46", "2025-10-01 09:45", id="9:46")
158-
]
159-
)
160-
def test_create_unix_timestamp_current_quarter_hour(timestamp, expected, monkeypatch):
161-
# setup
162-
datetime_mock = MagicMock(wraps=datetime.datetime)
163-
current_time = datetime.datetime.strptime(timestamp, "%Y-%m-%d %H:%M")
164-
datetime_mock.today.return_value = current_time
165-
monkeypatch.setattr(datetime, "datetime", datetime_mock)
166-
167-
# execution
168-
qh = timecheck.create_unix_timestamp_current_quarter_hour()
169-
log.debug(f"timestamp: {current_time} , from mock: {datetime.datetime.today().timestamp()}"
170-
f" result: {qh}")
171-
172-
current_quarter_hour = datetime.datetime.fromtimestamp(qh).strftime("%Y-%m-%d %H:%M")
173-
174-
# evaluation
175-
assert current_quarter_hour == expected

packages/modules/common/component_state.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,9 @@ def __init__(self,
232232
@auto_str
233233
class TariffState:
234234
def __init__(self,
235-
prices: Optional[Dict[str, float]] = None,
236-
prices_per_hour: int = 24) -> None:
235+
prices: Optional[Dict[str, float]] = None
236+
) -> None:
237237
self.prices = prices
238-
self.prices_per_hour = prices_per_hour
239238

240239

241240
@auto_str
Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from typing import TypeVar, Generic, Callable
2-
from helpermodules.timecheck import (
3-
create_unix_timestamp_current_quarter_hour,
4-
create_unix_timestamp_current_full_hour
5-
)
2+
from datetime import datetime, timedelta
3+
from helpermodules import timecheck
4+
import random
5+
import logging
66
from modules.common import store
77
from modules.common.component_context import SingleComponentUpdateContext
88
from modules.common.component_state import TariffState
@@ -11,12 +11,16 @@
1111

1212

1313
T_TARIFF_CONFIG = TypeVar("T_TARIFF_CONFIG")
14+
ONE_HOUR_SECONDS: int = 3600
15+
log = logging.getLogger(__name__)
1416

1517

1618
class ConfigurableElectricityTariff(Generic[T_TARIFF_CONFIG]):
1719
def __init__(self,
1820
config: T_TARIFF_CONFIG,
1921
component_initializer: Callable[[], float]) -> None:
22+
self.__next_query_time = datetime.fromtimestamp(1)
23+
self.__tariff_state: TariffState = None
2024
self.config = config
2125
self.store = store.get_electricity_tariff_value_store()
2226
self.fault_state = FaultState(ComponentInfo(None, self.config.name, ComponentType.ELECTRICITY_TARIFF.value))
@@ -26,33 +30,49 @@ def __init__(self,
2630
with SingleComponentUpdateContext(self.fault_state):
2731
self._component_updater = component_initializer(config)
2832

33+
def __calulate_next_query_time(self) -> None:
34+
self.__next_query_time = datetime.now().replace(
35+
hour=14, minute=0, second=0
36+
) + timedelta(
37+
# aktually ET providers issue next day prices up to half an hour earlier then 14:00
38+
# reduce serverload on their site by randomizing query time
39+
minutes=random.randint(-7, 7),
40+
seconds=random.randint(0, 59)
41+
)
42+
if datetime.now() > self.__next_query_time:
43+
self.__next_query_time += timedelta(days=1)
44+
2945
def update(self):
3046
if hasattr(self, "_component_updater"):
31-
# Wenn beim Initialisieren etwas schief gelaufen ist, ursprüngliche Fehlermeldung beibehalten
32-
with SingleComponentUpdateContext(self.fault_state):
33-
tariff_state = self._remove_outdated_prices(self._component_updater())
34-
self.store.set(tariff_state)
35-
self.store.update()
36-
expected_time_slots = 24 * tariff_state.prices_per_hour
37-
if len(tariff_state.prices) < expected_time_slots:
38-
self.fault_state.no_error(
39-
f'Die Preisliste hat nicht {expected_time_slots}, '
40-
f'sondern {len(tariff_state.prices)} Einträge. '
41-
'Die Strompreise werden vom Anbieter erst um 14:00 für den Folgetag aktualisiert.')
42-
43-
def _remove_outdated_prices(self, tariff_state: TariffState, ONE_HOUR_SECONDS: int = 3600) -> TariffState:
44-
first_timestamps = list(tariff_state.prices.keys())[:2]
45-
timeslot_length_seconds = int(first_timestamps[1]) - int(first_timestamps[0])
46-
is_hourely_prices = ONE_HOUR_SECONDS == timeslot_length_seconds
47-
current_hour = (
48-
create_unix_timestamp_current_full_hour()
49-
if is_hourely_prices
50-
else create_unix_timestamp_current_quarter_hour()
51-
)
47+
if datetime.now() > self.__next_query_time:
48+
# Wenn beim Initialisieren etwas schief gelaufen ist, ursprüngliche Fehlermeldung beibehalten
49+
with SingleComponentUpdateContext(self.fault_state):
50+
self.__tariff_state = self._component_updater()
51+
self.__calulate_next_query_time()
52+
log.debug(f'nächster Abruf der Strompreise nach {self.__next_query_time.strftime("%Y%m%d-%H:%M")}')
53+
timeslot_length_seconds = self.__calculate_price_timeslot_length()
54+
self.__tariff_state = self._remove_outdated_prices(self.__tariff_state, timeslot_length_seconds)
55+
self.store.set(self.__tariff_state)
56+
self.store.update()
57+
expected_time_slots = int(24 * ONE_HOUR_SECONDS / timeslot_length_seconds)
58+
if len(self.__tariff_state.prices) < expected_time_slots:
59+
self.fault_state.no_error(
60+
f'Die Preisliste hat nicht {expected_time_slots}, '
61+
f'sondern {len(self.__tariff_state.prices)} Einträge. '
62+
f'nächster Abruf der Strompreise nach {self.__next_query_time.strftime("%Y%m%d-%H:%M")}')
63+
64+
def __calculate_price_timeslot_length(self) -> int:
65+
first_timestamps = list(self.__tariff_state.prices.keys())[:2]
66+
return int(first_timestamps[1]) - int(first_timestamps[0])
67+
68+
def _remove_outdated_prices(self, tariff_state: TariffState, timeslot_length_seconds: int) -> TariffState:
69+
now = timecheck.create_timestamp()
5270
for timestamp in list(tariff_state.prices.keys()):
53-
if int(timestamp) < int(current_hour):
71+
if int(timestamp) < now - (timeslot_length_seconds - 1): # keep current time slot
5472
self.fault_state.warning(
5573
'Die Preisliste startet nicht mit der aktuellen Stunde. '
5674
'Abgelaufene Einträge wurden entfernt.')
5775
tariff_state.prices.pop(timestamp)
76+
self.fault_state.no_error(
77+
f'Die Preisliste hat {len(tariff_state.prices)} Einträge. ')
5878
return tariff_state
Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
from unittest.mock import Mock
3+
from helpermodules import timecheck
34
import pytest
45

56
from modules.common.component_state import TariffState
@@ -8,27 +9,54 @@
89

910

1011
@pytest.mark.parametrize(
11-
"tariff_state, expected",
12+
"now, tariff_state, expected",
1213
[
13-
pytest.param(TariffState(prices={"1652680800": -5.87e-06,
14+
pytest.param(1652680800,
15+
TariffState(prices={"1652680800": -5.87e-06,
1416
"1652684400": 5.467e-05,
1517
"1652688000": 10.72e-05}),
1618
TariffState(prices={"1652680800": -5.87e-06,
1719
"1652684400": 5.467e-05,
1820
"1652688000": 10.72e-05}), id="keine veralteten Einträge"),
19-
pytest.param(TariffState(prices={"1652677200": -5.87e-06,
21+
pytest.param(1652680800,
22+
TariffState(prices={"1652677200": -5.87e-06,
2023
"1652680800": 5.467e-05,
2124
"1652684400": 10.72e-05}),
2225
TariffState(prices={"1652680800": 5.467e-05,
2326
"1652684400": 10.72e-05}), id="Lösche ersten Eintrag"),
27+
pytest.param(1652684000,
28+
TariffState(prices={"1652680800": -5.87e-06,
29+
"1652684400": 5.467e-05,
30+
"1652688000": 10.72e-05}),
31+
TariffState(prices={"1652680800": -5.87e-06,
32+
"1652684400": 5.467e-05,
33+
"1652688000": 10.72e-05}), id="erster time slot noch nicht zu Ende"),
34+
pytest.param(1652684000,
35+
TariffState(prices={"1652680000": -5.87e-06,
36+
"1652681200": 5.467e-05,
37+
"1652682400": 10.72e-05,
38+
"1652683600": 10.72e-05,
39+
"1652684800": 10.72e-05,
40+
"1652686000": 10.72e-05,
41+
"1652687200": 10.72e-05}),
42+
TariffState(prices={"1652683600": 10.72e-05,
43+
"1652684800": 10.72e-05,
44+
"1652686000": 10.72e-05,
45+
"1652687200": 10.72e-05}), id="20 Minuten time slots"),
2446
],
2547
)
26-
def test_remove_outdated_prices(tariff_state: TariffState, expected: TariffState, monkeypatch):
48+
def test_remove_outdated_prices(now: int, tariff_state: TariffState, expected: TariffState, monkeypatch):
2749
# setup
2850
tariff = ConfigurableElectricityTariff(AwattarTariff(), Mock())
51+
time_slot_seconds = [int(timestamp) for timestamp in tariff_state.prices.keys()][:2]
52+
53+
# Montag 16.05.2022, 8:40:52 "05/16/2022, 08:40:52" Unix: 1652683252
54+
monkeypatch.setattr(timecheck,
55+
"create_timestamp",
56+
Mock(return_value=now))
2957

3058
# test
31-
result = tariff._remove_outdated_prices(tariff_state)
59+
result = tariff._remove_outdated_prices(tariff_state, time_slot_seconds[1]-time_slot_seconds[0])
3260

3361
# assert
3462
assert result.prices == expected.prices

packages/modules/common/store/_tariff.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
from modules.common.store import ValueStore
44
from modules.common.store._api import LoggingValueStore
55
from modules.common.store._broker import pub_to_broker
6+
import logging
7+
import threading
8+
from datetime import datetime
9+
10+
log = logging.getLogger(__name__)
611

712

813
class TariffValueStoreBroker(ValueStore[TariffState]):
@@ -12,14 +17,24 @@ def __init__(self):
1217
def set(self, state: TariffState) -> None:
1318
self.state = state
1419

15-
def __update(self):
20+
def update(self):
1621
try:
17-
pub_to_broker("openWB/set/optional/et/get/prices", self.state.prices)
22+
prices = self.state.prices
23+
pub_to_broker("openWB/set/optional/et/get/prices", prices)
24+
log.debug(f"published prices list to MQTT having {len(prices)} entries")
1825
except Exception as e:
1926
raise FaultState.from_exception(e)
2027

21-
def update(self):
22-
self.__update(self)
28+
def __republish(self, full_hour: int, quarter_index: int, fifteen_minutes: int = 900) -> None:
29+
now = datetime.now()
30+
target_time = datetime.datetime.fromtimestamp(
31+
full_hour + (quarter_index * fifteen_minutes))
32+
if now < target_time:
33+
delay = (target_time - now).total_seconds()
34+
log.debug(f"reduce prices list and push to MQTT at {target_time.strftime('%m/%d/%Y, %H:%M')}")
35+
# self.state.prices removes outdated entries itself
36+
timer = threading.Timer(delay, self.__update())
37+
timer.start()
2338

2439

2540
def get_electricity_tariff_value_store() -> ValueStore[TariffState]:

0 commit comments

Comments
 (0)