Skip to content

Commit c99899c

Browse files
committed
Add power data retrieval and database storage functionality
Implemented methods to fetch power data from the SEMS Portal API and write energy-related metrics to the database. Updated the inverter charge controller to call this functionality. Improved InfluxDB usage instructions in the README for better clarity and usage.
1 parent 0292c00 commit c99899c

File tree

3 files changed

+122
-10
lines changed

3 files changed

+122
-10
lines changed

README.md

+16-4
Original file line numberDiff line numberDiff line change
@@ -213,14 +213,26 @@ It saves the following data:
213213

214214
## InfluxDB commands
215215

216-
- Create bucket: `influx bucket create -org default -token <token> --name default`
217-
- Delete bucket: `influx bucket delete -org default -token <token> --name default`
218-
- Retrieve all solar forecast values: `influx query -org default -token <token> 'import "experimental"
216+
- Create bucket: `influx bucket create -org default -token ${TOKEN} --name default`
217+
- Delete bucket: `influx bucket delete -org default -token ${TOKEN} --name default`
218+
- Retrieve all solar forecast values: `influx query -org default -token ${TOKEN} 'import "experimental"
219219
from(bucket: "default")
220220
|> range(start: 0, stop: experimental.addDuration(d: 2d, to: now()))
221221
|> filter(fn: (r) => r._measurement == "sun_forecast")
222222
|> pivot(rowKey:["_time"], columnKey:["_field"], valueColumn:"_value")'`
223-
- Retrieve all energy prices: `influx query -org default -token <token> 'import "experimental"
223+
- Retrieve all energy prices: `influx query -org default -token ${TOKEN} 'import "experimental"
224224
from(bucket: "default")
225225
|> range(start: 0, stop: experimental.addDuration(d: 2d, to: now()))
226226
|> filter(fn: (r) => r._measurement == "energy_prices")'`
227+
- Retrieve all power data (semsportal): `influx query -org default -token ${TOKEN} 'import "experimental"
228+
from(bucket: "default")
229+
|> range(start: 0, stop: experimental.addDuration(d: 2d, to: now()))
230+
|> filter(fn: (r) => r._measurement == "power")
231+
|> pivot(rowKey:["_time"], columnKey:["_field"], valueColumn:"_value")'`
232+
- Copy data from one measurement to another: `influx query -org default -token ${TOKEN} 'import "experimental"
233+
from(bucket: "default")
234+
|> range(start: 0, stop: experimental.addDuration(d: 2d, to: now()))
235+
|> filter(fn: (r) => r._measurement == "<old_measurement>")
236+
|> set(key: "_measurement", value: "<new_measurement>")
237+
|> to(bucket: "default")'`
238+
- Delete data from one measurement: `influx delete --bucket default -org default -token ${TOKEN} --start='1970-01-01T00:00:00Z' --stop=$(date +"%Y-%m-%dT%H:%M:%SZ" -d "+2 days") --predicate '_measurement=<old_measurement>'`

source/inverter_charge_controller.py

+2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ def _start(self) -> None:
7575
duration_to_wait_in_cause_of_error = timedelta(minutes=10)
7676
while True:
7777
try:
78+
self.sems_portal_api_handler.write_values_to_database()
79+
7880
if first_iteration:
7981
next_price_minimum = self.tibber_api_handler.get_next_price_minimum(first_iteration)
8082
first_iteration = False

source/sems_portal_api_handler.py

+104-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from datetime import datetime, time
1+
from datetime import date, datetime, time, timedelta
22

33
import requests
4+
from database_handler import DatabaseHandler, InfluxDBField
45
from energy_amount import EnergyAmount, Power
56
from environment_variable_getter import EnvironmentVariableGetter
67
from logger import LoggerMixin
@@ -16,6 +17,8 @@ def __init__(self):
1617
self.timestamp = None
1718
self.user_id = None
1819

20+
self.database_handler = DatabaseHandler("power")
21+
1922
def login(self) -> None:
2023
"""
2124
Authenticates a user by sending a POST request to the SEMS Portal API and retrieves
@@ -63,15 +66,51 @@ def get_average_energy_consumption_per_day(self) -> EnergyAmount:
6366
"""
6467
self.log.debug("Determining average energy consumption per day")
6568

66-
self.login()
67-
6869
api_response = self._retrieve_energy_consumption_data()
6970
consumption_data = self._extract_energy_usage_data_of_response(api_response)
7071
average_consumption_per_day = sum([consumption.watt_hours for consumption in consumption_data]) / len(
7172
consumption_data
7273
)
7374
return EnergyAmount(watt_hours=average_consumption_per_day)
7475

76+
def _retrieve_power_data(self, date_to_crawl: date) -> dict:
77+
"""
78+
Retrieves the power data from the SEMSPORTAL API. This includes:
79+
- solar generation
80+
- battery charge/discharge
81+
- grid consumption/feed
82+
- power usage
83+
- state of charge
84+
85+
This method sends a POST request to the SEMSPORTAL API to fetch the energy consumption data of a specified plant station.
86+
It constructs the necessary headers and payload required by the API and handles the response appropriately.
87+
88+
Returns:
89+
dict: A dictionary containing the power data retrieved from the SEMSPORTAL API.
90+
"""
91+
self.login()
92+
93+
self.log.debug("Crawling the SEMSPORTAL API for power data...")
94+
95+
url = "https://eu.semsportal.com/api/v2/Charts/GetPlantPowerChart"
96+
headers = {
97+
"Content-Type": "application/json",
98+
"Token": f'{{"version":"v2.1.0","client":"ios","language":"en", "timestamp": "{self.timestamp}", "uid": "{self.user_id}", "token": "{self.token}"}}',
99+
}
100+
payload = {
101+
"id": EnvironmentVariableGetter.get("SEMSPORTAL_POWERSTATION_ID"),
102+
"date": date_to_crawl.strftime("%Y-%m-%d"),
103+
"full_script": False,
104+
}
105+
106+
response = requests.post(url, headers=headers, json=payload, timeout=20)
107+
response.raise_for_status()
108+
response = response.json()
109+
110+
self.log.trace(f"Retrieved data: {response}")
111+
112+
return response
113+
75114
def _retrieve_energy_consumption_data(self) -> dict:
76115
"""
77116
Retrieves energy consumption data from the SEMSPORTAL API.
@@ -82,6 +121,8 @@ def _retrieve_energy_consumption_data(self) -> dict:
82121
Returns:
83122
dict: A dictionary containing the energy consumption data retrieved from the SEMSPORTAL API.
84123
"""
124+
self.login()
125+
85126
self.log.debug("Crawling the SEMSPORTAL API for energy consumption data...")
86127

87128
url = "https://eu.semsportal.com/api/v2/Charts/GetChartByPlant"
@@ -218,7 +259,64 @@ def estimate_energy_usage_in_timeframe(self, timestamp_start: datetime, timestam
218259

219260
return energy_usage_during_the_day + energy_usage_during_the_night
220261

262+
def write_values_to_database(self) -> None:
263+
"""
264+
Writes energy-related metrics to the database for the last three days.
265+
266+
This method retrieves and processes energy data for the past three days, including solar generation, battery
267+
discharge, grid feed, power usage and state of charge. It writes the resulting records to the database.
268+
"""
269+
self.log.debug("Writing values to database...")
270+
today = date.today()
271+
for days_in_past in range(3):
272+
date_to_crawl = today - timedelta(days=days_in_past)
273+
data = self._retrieve_power_data(date_to_crawl)
274+
self.log.debug(f"Retrieved power data: {data}")
275+
lines = data["data"]["lines"]
276+
277+
time_keys = [line["x"] for line in lines[0]["xy"]]
278+
for time_key in time_keys:
279+
timestamp = datetime.combine(date_to_crawl, datetime.strptime(time_key, "%H:%M").time())
280+
timestamp = timestamp.replace(tzinfo=TimeHandler.get_timezone())
281+
self.database_handler.write_to_database(
282+
[
283+
InfluxDBField(
284+
"solar_generation", self._get_value_of_line_by_line_index_and_time_key(lines, 0, time_key)
285+
),
286+
InfluxDBField(
287+
"battery_discharge", self._get_value_of_line_by_line_index_and_time_key(lines, 1, time_key)
288+
),
289+
InfluxDBField(
290+
"grid_feed", self._get_value_of_line_by_line_index_and_time_key(lines, 2, time_key)
291+
),
292+
InfluxDBField(
293+
"power_usage", self._get_value_of_line_by_line_index_and_time_key(lines, 3, time_key)
294+
),
295+
InfluxDBField(
296+
"state_of_charge", self._get_value_of_line_by_line_index_and_time_key(lines, 4, time_key)
297+
),
298+
],
299+
timestamp,
300+
)
301+
302+
@staticmethod
303+
def _get_value_of_line_by_line_index_and_time_key(lines: dict, line_index: int, time_key: str) -> int:
304+
"""
305+
Retrieves the value associated with a specific time key from a line in a nested dictionary structure.
306+
307+
This method searches for a specific time key ('x') within the 'xy' list of dictionaries
308+
contained in a given line at a specified index. It extracts the associated value ('y')
309+
for the matching time key and converts it to an integer.
310+
311+
Args:
312+
lines (dict): A dictionary where each key corresponds to a line index. Each line index
313+
maps to a dictionary containing a key 'xy', which is a list of dictionaries.
314+
Each dictionary within 'xy' contains keys 'x' and 'y'.
315+
line_index (int): The index of the line to search within the 'lines' dictionary.
316+
time_key (str): The time key to search for, used to identify the specific dictionary in
317+
the 'xy' list where the 'x' value matches.
221318
222-
if __name__ == "__main__":
223-
s = SemsPortalApiHandler()
224-
print(s.get_energy_buy(1))
319+
Returns:
320+
int: The integer representation of the value ('y') corresponding to the provided time key.
321+
"""
322+
return int([line for line in lines[line_index]["xy"] if line["x"] == time_key][0]["y"])

0 commit comments

Comments
 (0)