Skip to content

Commit cf6925e

Browse files
authored
add electricity tariff westfalen wind (#3017)
* add westfalen wind * imrpove
1 parent f038d36 commit cf6925e

3 files changed

Lines changed: 195 additions & 0 deletions

File tree

packages/modules/electricity_pricing/flexible_tariffs/westfalen_wind/__init__.py

Whitespace-only changes.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from typing import Optional
2+
3+
4+
class WestfalenWindToken():
5+
def __init__(self,
6+
access_token: Optional[str] = None,
7+
refresh_token: Optional[str] = None,
8+
token_type: Optional[str] = None,
9+
expires: Optional[int] = None,
10+
created_at: Optional[float] = None) -> None:
11+
self.access_token = access_token # don't show in UI
12+
self.refresh_token = refresh_token # don't show in UI
13+
self.token_type = token_type # don't show in UI
14+
self.expires = expires # don't show in UI
15+
self.created_at = created_at # don't show in UI
16+
17+
18+
class WestfalenWindTariffConfiguration:
19+
def __init__(self,
20+
username: Optional[str] = None,
21+
password: Optional[str] = None,
22+
contract_id: Optional[str] = None,
23+
token: WestfalenWindToken = None):
24+
self.username = username
25+
self.password = password
26+
self.contract_id = contract_id
27+
self.token = token or WestfalenWindToken()
28+
29+
30+
class WestfalenWindTariff:
31+
def __init__(self,
32+
name: str = "WestfalenWind",
33+
type: str = "westfalen_wind",
34+
official: bool = True,
35+
configuration: WestfalenWindTariffConfiguration = None) -> None:
36+
self.name = name
37+
self.type = type
38+
self.official = official
39+
self.configuration = configuration or WestfalenWindTariffConfiguration()
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env python3
2+
import datetime
3+
import logging
4+
from typing import Dict
5+
import pytz
6+
from requests.exceptions import HTTPError
7+
8+
9+
from dataclass_utils import asdict
10+
from helpermodules import timecheck
11+
from helpermodules.pub import Pub
12+
from modules.common import req
13+
from modules.common.abstract_device import DeviceDescriptor
14+
from modules.common.component_state import TariffState
15+
from modules.electricity_pricing.flexible_tariffs.westfalen_wind.config import WestfalenWindTariff, WestfalenWindToken
16+
17+
log = logging.getLogger(__name__)
18+
19+
20+
def validate_token(config: WestfalenWindTariff) -> None:
21+
"""Prüft ob ein gültiger Token vorhanden ist, ansonsten wird ein neuer abgerufen."""
22+
if (config.configuration.token.access_token and
23+
config.configuration.token.expires and
24+
config.configuration.token.created_at):
25+
26+
# Prüfe ob Token noch gültig ist (mit 5 Min Puffer)
27+
expires_timestamp = config.configuration.token.created_at + config.configuration.token.expires - 300
28+
current_timestamp = timecheck.create_timestamp()
29+
30+
if current_timestamp < expires_timestamp:
31+
log.debug("Access Token ist noch gültig.")
32+
return
33+
else:
34+
log.debug("Access Token ist abgelaufen. Versuche Refresh Token.")
35+
if config.configuration.token.refresh_token:
36+
_refresh_token(config)
37+
else:
38+
_authenticate(config)
39+
else:
40+
log.debug("Kein gültiger Token vorhanden. Authentifizierung erforderlich.")
41+
_authenticate(config)
42+
43+
44+
def _authenticate(config: WestfalenWindTariff) -> None:
45+
"""Authentifizierung mit Benutzername und Passwort."""
46+
if not config.configuration.username or not config.configuration.password:
47+
raise ValueError("Benutzername und Passwort sind für die Authentifizierung erforderlich.")
48+
49+
data = {
50+
'grant_type': 'password',
51+
'username': config.configuration.username,
52+
'password': config.configuration.password
53+
}
54+
token_data = req.get_http_session().post(
55+
'https://api.wws.tarifdynamik.de/public/tokens',
56+
data=data,
57+
headers={'Content-Type': 'application/x-www-form-urlencoded'}
58+
).json()
59+
60+
config.configuration.token = WestfalenWindToken(
61+
access_token=token_data.get("access_token"),
62+
refresh_token=token_data.get("refresh_token"),
63+
token_type=token_data.get("token_type", "Bearer"),
64+
expires=token_data.get("expires"),
65+
created_at=timecheck.create_timestamp()
66+
)
67+
Pub().pub("openWB/set/optional/ep/flexible_tariff/provider", asdict(config))
68+
log.debug("Erfolgreich authentifiziert.")
69+
70+
71+
def _refresh_token(config: WestfalenWindTariff) -> None:
72+
"""Erneuert den Access Token mit dem Refresh Token."""
73+
if not config.configuration.token.refresh_token:
74+
log.debug("Kein Refresh Token vorhanden. Führe neue Authentifizierung durch.")
75+
_authenticate(config)
76+
return
77+
78+
data = {
79+
'grant_type': 'refresh_token',
80+
'refresh_token': config.configuration.token.refresh_token
81+
}
82+
try:
83+
token_data = req.get_http_session().post(
84+
'https://api.wws.tarifdynamik.de/public/tokens',
85+
data=data,
86+
headers={'Content-Type': 'application/x-www-form-urlencoded'}
87+
).json()
88+
config.configuration.token = WestfalenWindToken(
89+
access_token=token_data.get("access_token"),
90+
refresh_token=token_data.get("refresh_token"),
91+
token_type=token_data.get("token_type", "Bearer"),
92+
expires=token_data.get("expires"),
93+
created_at=timecheck.create_timestamp()
94+
)
95+
Pub().pub("openWB/set/optional/ep/flexible_tariff/provider", asdict(config))
96+
log.debug("Token erfolgreich erneuert.")
97+
except HTTPError as e:
98+
log.error(f"Token-Erneuerung fehlgeschlagen: {e}. Führe neue Authentifizierung durch.")
99+
_authenticate(config)
100+
101+
102+
def _get_raw_prices(config: WestfalenWindTariff):
103+
headers = {
104+
"Content-Type": "application/json",
105+
"Authorization": f"{config.configuration.token.token_type} {config.configuration.token.access_token}"
106+
}
107+
now = datetime.datetime.now(pytz.timezone("Europe/Berlin"))
108+
start_of_today = now.replace(hour=0, minute=0, second=0, microsecond=0)
109+
end_of_tomorrow = start_of_today + datetime.timedelta(days=2) - datetime.timedelta(minutes=15)
110+
filters = [
111+
f"valid_from:gte:{now.isoformat()}",
112+
f"valid_from:lte:{end_of_tomorrow.isoformat()}"
113+
]
114+
params = {
115+
'page_size': 200, # Maximal 200 Einträge (2 Tage * 96 Viertelstunden)
116+
'sort': 'valid_from:asc',
117+
'filters': filters
118+
}
119+
if config.configuration.contract_id:
120+
params['contract_id'] = config.configuration.contract_id
121+
122+
return req.get_http_session().get(
123+
"https://api.wws.tarifdynamik.de/public/energyprices",
124+
headers=headers,
125+
params=params
126+
).json()
127+
128+
129+
def fetch(config: WestfalenWindTariff) -> Dict[str, float]:
130+
validate_token(config)
131+
try:
132+
raw_data = _get_raw_prices(config)
133+
except HTTPError as error:
134+
if error.response.status_code == 401:
135+
log.debug("401 Unauthorized - Token ungültig. Versuche Erneuerung.")
136+
_authenticate(config)
137+
raw_data = _get_raw_prices(config)
138+
else:
139+
raise error
140+
prices: Dict[str, float] = {}
141+
for price_entry in raw_data['data']:
142+
timestamp = int(datetime.datetime.fromisoformat(price_entry['start'].replace('Z', '+00:00')).timestamp())
143+
price_euro_per_wh = price_entry['price_ct_kwh'] / 100000
144+
prices[str(timestamp)] = price_euro_per_wh
145+
return prices
146+
147+
148+
def create_electricity_tariff(config: WestfalenWindTariff):
149+
validate_token(config)
150+
151+
def updater():
152+
return TariffState(prices=fetch(config))
153+
return updater
154+
155+
156+
device_descriptor = DeviceDescriptor(configuration_factory=WestfalenWindTariff)

0 commit comments

Comments
 (0)