"].join("");s.DEFAULTS={"default":"",fromnow:0,placement:"bottom",align:"left",donetext:"完成",autoclose:!1,twelvehour:!1,vibrate:!0},s.prototype.toggle=function(){this[this.isShown?"hide":"show"]()},s.prototype.locate=function(){var t=this.element,i=this.popover,e=t.offset(),s=t.outerWidth(),o=t.outerHeight(),c=this.options.placement,n=this.options.align,r={};switch(i.show(),c){case"bottom":r.top=e.top+o;break;case"right":r.left=e.left+s;break;case"top":r.top=e.top-i.outerHeight();break;case"left":r.left=e.left-i.outerWidth()}switch(n){case"left":r.left=e.left;break;case"right":r.left=e.left+s-i.outerWidth();break;case"top":r.top=e.top;break;case"bottom":r.top=e.top+o-i.outerHeight()}i.css(r)},s.prototype.show=function(){if(!this.isShown){o(this.options.beforeShow);var t=this;this.isAppended||(c=n(document.body).append(this.popover),r.on("resize.clockpicker"+this.id,function(){t.isShown&&t.locate()}),this.isAppended=!0);var e=((this.input.prop("value")||this.options["default"]||"")+"").split(":");if("now"===e[0]){var s=new Date(+new Date+this.options.fromnow);e=[s.getHours(),s.getMinutes()]}this.hours=+e[0]||0,this.minutes=+e[1]||0,this.spanHours.html(i(this.hours)),this.spanMinutes.html(i(this.minutes)),this.toggleView("hours"),this.locate(),this.isShown=!0,a.on("click.clockpicker."+this.id+" focusin.clockpicker."+this.id,function(i){var e=n(i.target);0===e.closest(t.popover).length&&0===e.closest(t.addon).length&&0===e.closest(t.input).length&&t.hide()}),a.on("keyup.clockpicker."+this.id,function(i){27===i.keyCode&&t.hide()}),o(this.options.afterShow)}},s.prototype.hide=function(){o(this.options.beforeHide),this.isShown=!1,a.off("click.clockpicker."+this.id+" focusin.clockpicker."+this.id),a.off("keyup.clockpicker."+this.id),this.popover.hide(),o(this.options.afterHide)},s.prototype.toggleView=function(t,i){var e=!1;"minutes"===t&&"visible"===n(this.hoursView).css("visibility")&&(o(this.options.beforeHourSelect),e=!0);var s="hours"===t,c=s?this.hoursView:this.minutesView,r=s?this.minutesView:this.hoursView;this.currentView=t,this.spanHours.toggleClass("text-primary",s),this.spanMinutes.toggleClass("text-primary",!s),r.addClass("clockpicker-dial-out"),c.css("visibility","visible").removeClass("clockpicker-dial-out"),this.resetClock(i),clearTimeout(this.toggleViewTimer),this.toggleViewTimer=setTimeout(function(){r.css("visibility","hidden")},A),e&&o(this.options.afterHourSelect)},s.prototype.resetClock=function(t){var i=this.currentView,e=this[i],s="hours"===i,o=Math.PI/(s?6:30),c=e*o,n=s&&e>0&&13>e?w:g,r=Math.sin(c)*n,a=-Math.cos(c)*n,p=this;l&&t?(p.canvas.addClass("clockpicker-canvas-out"),setTimeout(function(){p.canvas.removeClass("clockpicker-canvas-out"),p.setHand(r,a)},t)):this.setHand(r,a)},s.prototype.setHand=function(t,e,s,o){var c,r=Math.atan2(t,-e),a="hours"===this.currentView,p=Math.PI/(a||s?6:30),h=Math.sqrt(t*t+e*e),u=this.options,k=a&&(g+w)/2>h,d=k?w:g;if(u.twelvehour&&(d=g),0>r&&(r=2*Math.PI+r),c=Math.round(r/p),r=c*p,u.twelvehour?a?0===c&&(c=12):(s&&(c*=5),60===c&&(c=0)):a?(12===c&&(c=0),c=k?0===c?12:c:0===c?0:c+12):(s&&(c*=5),60===c&&(c=0)),this[this.currentView]!==c&&v&&this.options.vibrate&&(this.vibrateTimer||(navigator[v](10),this.vibrateTimer=setTimeout(n.proxy(function(){this.vibrateTimer=null},this),100))),this[this.currentView]=c,this[a?"spanHours":"spanMinutes"].html(i(c)),!l)return void this[a?"hoursView":"minutesView"].find(".clockpicker-tick").each(function(){var t=n(this);t.toggleClass("active",c===+t.html())});o||!a&&c%5?(this.g.insertBefore(this.hand,this.bearing),this.g.insertBefore(this.bg,this.fg),this.bg.setAttribute("class","clockpicker-canvas-bg clockpicker-canvas-bg-trans")):(this.g.insertBefore(this.hand,this.bg),this.g.insertBefore(this.fg,this.bg),this.bg.setAttribute("class","clockpicker-canvas-bg"));var f=Math.sin(r)*d,m=-Math.cos(r)*d;this.hand.setAttribute("x2",f),this.hand.setAttribute("y2",m),this.bg.setAttribute("cx",f),this.bg.setAttribute("cy",m),this.fg.setAttribute("cx",f),this.fg.setAttribute("cy",m)},s.prototype.done=function(){o(this.options.beforeDone),this.hide();var t=this.input.prop("value"),e=i(this.hours)+":"+i(this.minutes);this.options.twelvehour&&(e+=this.amOrPm),this.input.prop("value",e),e!==t&&(this.input.triggerHandler("change"),this.isInput||this.element.trigger("change")),this.options.autoclose&&this.input.trigger("blur"),o(this.options.afterDone)},s.prototype.remove=function(){this.element.removeData("clockpicker"),this.input.off("focus.clockpicker click.clockpicker"),this.addon.off("click.clockpicker"),this.isShown&&this.hide(),this.isAppended&&(r.off("resize.clockpicker"+this.id),this.popover.remove())},n.fn.clockpicker=function(t){var i=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=n(this),o=e.data("clockpicker");if(o)"function"==typeof o[t]&&o[t].apply(o,i);else{var c=n.extend({},s.DEFAULTS,e.data(),"object"==typeof t&&t);e.data("clockpicker",new s(e,c))}})}}();
--------------------------------------------------------------------------------
/backend/vehicle.py:
--------------------------------------------------------------------------------
1 | # vehicle.py
2 |
3 | # This project is licensed under the MIT License.
4 |
5 | # Disclaimer: This code has been created under the help of AI (ChatGPT) and may not be suitable for
6 | # AI-Training. This code ist Alpha-Stage
7 |
8 | import logging
9 | import numpy as np
10 | import datetime
11 | import requests
12 | import initialize_smartcharge
13 |
14 |
15 | # Logging configuration with color scheme for debug information
16 | logger = logging.getLogger('smartCharge')
17 | RESET = "\033[0m"
18 | RED = "\033[91m"
19 | GREEN = "\033[92m"
20 | YELLOW = "\033[93m"
21 | BLUE = "\033[94m"
22 | CYAN = "\033[96m"
23 | GREY = "\033[37m"
24 | LILAC = "\033[95m"
25 |
26 | EVCC_API_BASE_URL = initialize_smartcharge.settings['EVCC']['EVCC_API_BASE_URL']
27 |
28 |
29 | def sort_trips_by_earliest_departure_time(usage_plan):
30 | """
31 | Sort all trips in the usage plan by the earliest departure time, regardless of car.
32 | Args:
33 | usage_plan (dict): A dictionary containing the usage plan with car names as keys and lists of trips as values.
34 | Returns:
35 | list: A list of trips sorted by earliest departure time. Each trip includes 'departure_time', 'return_time', and 'car_name'.
36 | """
37 |
38 | all_trips = []
39 | now = datetime.datetime.now()
40 |
41 | for car_name, car_trips in usage_plan.items():
42 | # Process recurring trips
43 | for trip in car_trips.get('recurring', []):
44 | departure_day_name = trip['departure_date']
45 | departure_time_str = trip['departure_time']
46 | return_day_name = trip.get('return_date', departure_day_name)
47 | return_time_str = trip['return_time']
48 |
49 | # Convert day names to indexes
50 | weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
51 | departure_weekday = weekdays.index(departure_day_name)
52 | return_weekday = weekdays.index(return_day_name)
53 |
54 | departure_time = datetime.datetime.strptime(departure_time_str, '%H:%M').time()
55 | return_time = datetime.datetime.strptime(return_time_str, '%H:%M').time()
56 |
57 | # Calculate next departure date
58 | days_ahead_departure = (departure_weekday - now.weekday()) % 7
59 | if days_ahead_departure == 0 and departure_time <= now.time():
60 | days_ahead_departure = 7
61 | departure_date = now.date() + datetime.timedelta(days=days_ahead_departure)
62 | departure_datetime = datetime.datetime.combine(departure_date, departure_time)
63 |
64 | # Calculate next return date
65 | days_ahead_return = (return_weekday - departure_weekday) % 7
66 | return_date = departure_date + datetime.timedelta(days=days_ahead_return)
67 | return_datetime = datetime.datetime.combine(return_date, return_time)
68 |
69 | trip['departure_datetime'] = departure_datetime
70 | trip['return_datetime'] = return_datetime
71 | trip['car_name'] = car_name
72 |
73 | all_trips.append(trip)
74 |
75 | # Process non-recurring trips
76 | for trip in car_trips.get('non_recurring', []):
77 | departure_date_str = trip['departure_date'] # Updated key
78 | departure_time_str = trip['departure_time']
79 | return_date_str = trip['return_date'] # old code: trip.get('return_date', departure_date_str)
80 | return_time_str = trip['return_time']
81 |
82 | departure_date = datetime.datetime.strptime(departure_date_str, '%Y-%m-%d').date()
83 | departure_time = datetime.datetime.strptime(departure_time_str, '%H:%M').time()
84 | departure_datetime = datetime.datetime.combine(departure_date, departure_time)
85 |
86 | return_date = datetime.datetime.strptime(return_date_str, '%Y-%m-%d').date()
87 | return_time = datetime.datetime.strptime(return_time_str, '%H:%M').time()
88 | return_datetime = datetime.datetime.combine(return_date, return_time)
89 |
90 | if departure_datetime >= now:
91 | trip['departure_datetime'] = departure_datetime
92 | trip['return_datetime'] = return_datetime
93 | trip['car_name'] = car_name
94 | all_trips.append(trip)
95 |
96 | # Sort all trips by departure_datetime
97 | sorted_trips = sorted(all_trips, key=lambda x: x['departure_datetime'])
98 |
99 | return sorted_trips
100 |
101 | # Definierte Gaussian-ähnliche Funktion zur Berechnung des Energieverbrauchs
102 | def calculate_ev_energy_consumption(departure_temperature, return_temperature, distance, CONSUMPTION, BUFFER_DISTANCE, car_name, evcc_state, loadpoint_id):
103 | """
104 | Calculate the energy consumption of an electric vehicle (EV) for a round trip based on temperatures and distance.
105 | Parameters:
106 | departure_temperature (float): The temperature at the start of the trip in degrees Celsius.
107 | return_temperature (float): The temperature at the end of the trip in degrees Celsius.
108 | distance (float): The total distance of the round trip in kilometers.
109 | CONSUMPTION (float): The base energy consumption of the EV in kWh per 100 km.
110 | BUFFER_DISTANCE (float): Additional buffer distance in kilometers to account for deviations.
111 | a (float, optional): Parameter for the correction factor calculation. Default is 80.145.
112 | b (float, optional): Parameter for the correction factor calculation. Default is 22.170.
113 | c (float, optional): Parameter for the correction factor calculation. Default is 17.776.
114 | d (float, optional): Parameter for the correction factor calculation. Default is 44.805.
115 | Source for graph:
116 | https://www.geotab.com/de/blog/elektrofahrzeuge-batterie-temperatur/
117 | Gauss-Formula for graph created with ChatGPT
118 | Returns:
119 | float: The total energy consumption for the round trip in kWh.
120 | """
121 | """"""
122 | logging.info(f"{GREEN}Calculating energy needed for {distance} km with departure temperature {departure_temperature}°C and return temperature {return_temperature}°C{RESET}")
123 | half_distance = distance / 2 # Berechne die Hälfte der Strecke für Hin- und Rückfahrt
124 | # unkorrigierter Energieverbrauch für die Fahrten
125 | uncorrected_energy_departure = (CONSUMPTION / 100) * (half_distance + BUFFER_DISTANCE / 2)
126 | uncorrected_energy_return = uncorrected_energy_departure
127 |
128 | a=80.145
129 | b=22.170
130 | c=17.776
131 | d=44.805
132 |
133 | # Berechnung des Korrekturfaktors für die Abfahrtstemperatur und Rückfahrtstemperatur
134 | correction_factor_departure = (a * np.exp(-((departure_temperature - b) ** 2) / (2 * c ** 2)) + d) / 100
135 | correction_factor_return = (a * np.exp(-((return_temperature - b) ** 2) / (2 * c ** 2)) + d) / 100
136 |
137 |
138 | # Energieverbrauch (Hinfahrt + Rückfahrt) unter Berücksichtigung der Korrekturfaktoren
139 | energy_consumption_departure = uncorrected_energy_departure * correction_factor_departure
140 | energy_consumption_return = uncorrected_energy_return * correction_factor_return
141 |
142 | # Gesamtenergieverbrauch (Hinfahrt + Rückfahrt)
143 | total_energy_consumption = energy_consumption_departure + energy_consumption_return
144 |
145 | logging.debug(f"{GREY}Hinfahrt: {uncorrected_energy_departure / 1000:.1f} kWh x {correction_factor_departure:.2f} = "
146 | f"{energy_consumption_departure / 1000:.2f} kWh bei {departure_temperature:.1f}°C | "
147 | f"Rückfahrt: {uncorrected_energy_return / 1000:.1f} kWh x {correction_factor_return:.2f} = "
148 | f"{energy_consumption_return / 1000:.2f} kWh bei {return_temperature:.1f}°C{RESET}")
149 |
150 | logging.info(f"{GREEN}Gesamtenergieverbrauch zur Erreichung von {distance} km: {total_energy_consumption / 1000:.2f} kWh{RESET}")
151 |
152 |
153 | degradated_battery_capacity = calculate_car_battery_degradation(evcc_state, car_name, loadpoint_id)
154 | if total_energy_consumption > degradated_battery_capacity:
155 | logging.warning(f"{CYAN}However: Energy consumption exceeds battery capacity of the car!{RESET}")
156 | total_energy_consumption = degradated_battery_capacity
157 |
158 | return total_energy_consumption
159 |
160 | def calculate_car_battery_degradation(evcc_state, car_name, loadpoint_id):
161 | """
162 | Calculate the battery degradation of an electric vehicle (EV) given its car name.
163 | Parameters:
164 | car_name (str): Name of the car as defined in cars_settings.
165 | Returns:
166 | float: The degraded battery capacity in kWh.
167 | """
168 | # get variables we need for the calculation
169 | cars_settings = initialize_smartcharge.settings['Cars']
170 | car_id = None
171 | for i, car in initialize_smartcharge.settings['Cars'].items():
172 | if car['CAR_NAME'] == car_name:
173 | car_id = i
174 | break
175 | if car_id is None:
176 | logging.error(f"{RED}Car with name {car_name} not found in settings{RESET}")
177 | return 0
178 | cars_settings = initialize_smartcharge.settings['Cars'][car_id]
179 | battery_capacity = cars_settings.get('BATTERY_CAPACITY')
180 | degradation = cars_settings.get('DEGRADATION')
181 | battery_year = cars_settings.get('BATTERY_YEAR')
182 |
183 | # get odometer from evcc_state
184 | odometer = evcc_state['result']['loadpoints'][loadpoint_id]['vehicleOdometer']
185 |
186 |
187 | # If odometer is 0, calculate degradation by age
188 | if odometer == 0:
189 | logging.warning(f"{YELLOW}Odometer is 0, calculating degradation by age{RESET}")
190 | # Use values from car_settings since we have a zero odometer
191 | degradated_battery_capacity = battery_capacity * (1 - degradation) ** (datetime.datetime.now().year - battery_year)
192 | return degradated_battery_capacity
193 | else:
194 | logging.info(f"{GREEN}Calculating degradation by odometer{RESET}")
195 | # Polynomial-based degradation by odometer
196 | a = 2.3027725883259073e-12
197 | b = -1.2056443455051694e-6
198 | c = 1.00
199 | degradation_car_battery_percentage = a * odometer**2 + b * odometer + c
200 | if degradation_car_battery_percentage <= 0:
201 | degradation_car_battery_percentage = 0
202 | logging.critical(f"{RED}Degradation by odometer is negative or zero{RESET}")
203 | degradated_battery_capacity = battery_capacity * degradation_car_battery_percentage
204 | return degradated_battery_capacity
205 |
206 | def calculate_required_soc_topup(energy_consumption, car, evcc_state, loadpoint_id, trip_name):
207 | logging.info(f"{GREEN}Calculating required state of charge (SoC) for energy consumption of {energy_consumption/1000:.2f} kWh for car {car} for trip {trip_name}{RESET}")
208 | # Calculate the required state of charge (SoC) in percentage
209 | # we need kWh not Wh --> /1000
210 | degradated_battery_capacity = calculate_car_battery_degradation(evcc_state, car, loadpoint_id)
211 | required_soc_topup = (energy_consumption / degradated_battery_capacity) * 100
212 | if required_soc_topup > 100:
213 | required_soc_topup = 100
214 | if required_soc_topup < 0:
215 | required_soc_topup = 0
216 | logging.debug(f"{BLUE}Required energy: {energy_consumption/1000:.2f} kWh, Required SoC: {required_soc_topup:.2f}%{RESET}")
217 | return required_soc_topup
218 |
219 | # Function to get the current SoC of EVCC
220 | def get_evcc_soc(loadpoint_id, evcc_state):
221 | if loadpoint_id is None:
222 | logging.error(f"{RED}loadpoint_id is None{RESET}")
223 | return 0
224 | logging.debug(f"{GREEN}Retrieving current SoC from EVCC for loadpoint_id {loadpoint_id}{RESET}")
225 |
226 | loadpoints = evcc_state.get('result', {}).get('loadpoints', [])
227 | if len(loadpoints) > loadpoint_id:
228 | current_soc = loadpoints[loadpoint_id].get('vehicleSoc')
229 | if current_soc is not None:
230 | logging.debug(f"{GREEN}Current SoC: {current_soc}%{RESET}")
231 | return float(current_soc)
232 | else:
233 | logging.error(f"{RED}Current SoC not found{RESET}")
234 | return 0 # Default to 0 if not found
235 | else:
236 | logging.error(f"{RED}No loadpoints found{RESET}")
237 | return 0
238 |
239 | def get_next_trip(car_name, usage_plan):
240 | car_schedule = usage_plan.get(car_name)
241 | if not car_schedule:
242 | logging.debug(f"{YELLOW}Kein Fahrplan für Auto {car_name} gefunden. Überspringe.{RESET}")
243 | return None
244 |
245 | # Annahme: Der Fahrplan ist bereits sortiert
246 | next_trip = car_schedule[0]
247 | return next_trip
248 |
249 |
250 | def calculate_energy_gap(required_soc_final, current_soc, car, evcc_state, loadpoint_id):
251 | logging.debug(f"{GREEN}Calculating energy gap for required SoC {required_soc_final}% and current SoC {current_soc}%{RESET}")
252 | degradated_battery_capacity = calculate_car_battery_degradation(evcc_state, car, loadpoint_id)
253 | soc_gap = required_soc_final - current_soc
254 | if soc_gap < 0:
255 | soc_gap = 0 # No gap, car is already charged
256 | energy_gap_Wh = (soc_gap / 100) * degradated_battery_capacity
257 | logging.info(f"{GREEN}the energy gap is {energy_gap_Wh/1000:.2f} kWh before PV!{RESET}")
258 | return energy_gap_Wh
259 |
260 |
--------------------------------------------------------------------------------
/backend/initialize_smartcharge.py:
--------------------------------------------------------------------------------
1 | # initialize.py
2 |
3 | # This project is licensed under the MIT License.
4 |
5 | # Disclaimer: This code has been created with the help of AI (ChatGPT) and may not be suitable for
6 | # AI-Training. This code ist Alpha-Stage
7 |
8 | import os
9 | import logging
10 | import json
11 | import requests
12 | import datetime
13 | from influxdb_client import InfluxDBClient, Point, WritePrecision
14 | from influxdb_client.client.write_api import SYNCHRONOUS
15 | import pandas as pd
16 |
17 | # Logging configuration with color scheme for debug information
18 | logger = logging.getLogger('smartCharge')
19 | RESET = "\033[0m"
20 | RED = "\033[91m"
21 | GREEN = "\033[92m"
22 | YELLOW = "\033[93m"
23 | BLUE = "\033[94m"
24 | CYAN = "\033[96m"
25 | GREY = "\033[37m"
26 |
27 |
28 | # Lade die Einstellungen aus der settings.json-Datei
29 | def load_settings():
30 | script_dir = os.path.dirname(os.path.abspath(__file__))
31 | settings_file = os.path.join(script_dir, 'data', 'settings.json')
32 | with open(settings_file, 'r', encoding='utf-8') as f:
33 | settings = json.load(f)
34 | return settings
35 |
36 | settings = load_settings()
37 |
38 | def save_settings(settings):
39 | with open('settings.json', 'w', encoding='utf-8') as f:
40 | json.dump(settings, f, ensure_ascii=False, indent=4)
41 |
42 | # def load_influx():
43 | # return settings['Influx']
44 |
45 |
46 | def load_cars():
47 | """
48 | Load the list of cars from the settings.
49 |
50 | Returns:
51 | list: A list of cars as defined in the settings.
52 | """
53 | return settings['Cars']
54 |
55 | # Funktion zum Einlesen des Fahrplans
56 | def read_usage_plan():
57 | script_dir = os.path.dirname(os.path.abspath(__file__))
58 | file_path = os.path.join(script_dir, 'data', 'usage_plan.json')
59 |
60 | logging.debug(f"Lese den Fahrplan aus der JSON-Datei: {file_path}")
61 | usage_plan = {}
62 |
63 | # Überprüfen, ob die Datei existiert
64 | if not os.path.exists(file_path):
65 | logging.error(f"Fahrplan-Datei nicht gefunden: {file_path}")
66 | exit(1)
67 |
68 | # Öffnen und Lesen der JSON-Datei
69 | with open(file_path, 'r') as f:
70 | # create a list of dictionaries from the JSON file
71 | usage_plan = json.load(f)
72 | return usage_plan
73 |
74 | def get_home_battery_data_from_json():
75 | """
76 | Reads home battery data from settings and returns relevant information.
77 |
78 | Returns:
79 | list: A list of dictionaries containing battery info and calculated marginal costs.
80 | """
81 | battery_data = []
82 | home_batteries = settings['House']['HomeBatteries'].keys()
83 | for battery_id in home_batteries:
84 | battery_info = settings['House']['HomeBatteries'][battery_id].copy()
85 | battery_info['battery_id'] = battery_id
86 | battery_data.append(battery_info)
87 | # logging.debug(f"{GREY}Home battery data: {battery_data}{RESET}")
88 | return battery_data
89 |
90 |
91 | def get_home_battery_data_from_api(evcc_state):
92 | """
93 | Retrieves the state of charge (SoC) and capacity of home batteries from the API.
94 |
95 | Returns:
96 | list: A list of dictionaries containing battery SoC, capacity.
97 | """
98 | home_batteries = settings['House']['HomeBatteries'].keys()
99 | if not home_batteries:
100 | logging.warning(f"{RED}No home batteries defined in settings.json{RESET}")
101 | return [{'battery_id': 0, 'battery_soc': 0, 'battery_capacity': 0}]
102 |
103 | battery_data = []
104 | batteries_info = evcc_state['result']['battery']
105 | # if batteries_info is empty, return 'battery_id': 0, 'battery_soc': 0 'battery_capacity': 0
106 | if not batteries_info:
107 | logging.warning(f"{RED}No home batteries defined in settings.json{RESET}")
108 | return [{'battery_id': 0, 'battery_soc': 0, 'battery_capacity': 0}]
109 |
110 | for battery_index, battery_id in enumerate(home_batteries):
111 | battery_info = batteries_info[battery_index]
112 | battery_soc = battery_info['soc']
113 | battery_capacity = battery_info['capacity']
114 | battery_data.append({
115 | 'battery_id': battery_id,
116 | 'battery_soc': battery_soc,
117 | 'battery_capacity': battery_capacity
118 | })
119 | logging.debug(f"{GREY}Home battery API data: {battery_data}{RESET}")
120 | return battery_data
121 |
122 |
123 | def get_loadpoint_id_for_car(car_name, evcc_state):
124 | loadpoints = evcc_state['result']['loadpoints']
125 | for loadpoint in loadpoints:
126 | if loadpoint.get('vehicleName') == car_name:
127 | for loadpoint in loadpoints:
128 | if loadpoint.get('vehicleName') == car_name:
129 | return loadpoints.index(loadpoint) + 1 # Loadpoint IDs are 1-indexed in POST but 0-indexed in /api/state
130 |
131 | def get_baseload_from_influxdb():
132 | INFLUX_BASE_URL = settings['InfluxDB']['INFLUX_BASE_URL']
133 | INFLUX_ORGANIZATION = settings['InfluxDB']['INFLUX_ORGANIZATION']
134 | INFLUX_BUCKET = settings['InfluxDB']['INFLUX_BUCKET']
135 | INFLUX_ACCESS_TOKEN = settings['InfluxDB']['INFLUX_ACCESS_TOKEN']
136 | TIMESPAN_WEEKS_BASELOAD = settings['InfluxDB']['TIMESPAN_WEEKS_BASELOAD']
137 |
138 | # Initialize InfluxDB client
139 | client = InfluxDBClient(
140 | url=INFLUX_BASE_URL,
141 | token=INFLUX_ACCESS_TOKEN,
142 | org=INFLUX_ORGANIZATION
143 | )
144 |
145 |
146 | # Define start and stop times explicitly
147 | start_time = f"-{TIMESPAN_WEEKS_BASELOAD * 7}d"
148 |
149 | # Log the time range
150 | logging.debug(f"Querying data from {start_time} till now to get baseload")
151 |
152 | # divison by 3600 to convert from Ws to kWh:
153 | # 1 hour has 3600 seconds
154 | flux_query_baseload = f"""
155 | from(bucket: "{INFLUX_BUCKET}")
156 | |> range(start: {start_time}, stop: today())
157 | |> filter(fn: (r) => r["_measurement"] == "homePower")
158 | |> aggregateWindow(every: 1h, fn: integral, createEmpty: false)
159 | |> map(fn: (r) => ({{_value: r._value / 3600.0, _time: r._time}}))
160 | |> yield(name: "integral")
161 | """
162 | # Query InfluxDB
163 | query_api = client.query_api()
164 | result_baseload = query_api.query(org=INFLUX_ORGANIZATION, query=flux_query_baseload)
165 | # logging.debug(f"Flux Query (Baseload): {flux_query_baseload}")
166 | logging.debug(f"Query Result (Baseload): {result_baseload}")
167 | # Check if results are empty
168 | if not result_baseload:
169 | logging.warning("No data returned from baseload query")
170 |
171 |
172 | records = []
173 | for table in result_baseload:
174 | for record in table.records:
175 | records.append(record.values)
176 |
177 | # In DataFrame umwandeln
178 | df = pd.DataFrame(records)
179 | df['_time'] = pd.to_datetime(df['_time'])
180 |
181 | # Wochentag, Stunde und Minute extrahieren
182 | df['dayOfWeek'] = df['_time'].dt.day_name()
183 | df['hour'] = df['_time'].dt.hour
184 | df['minute'] = df['_time'].dt.minute
185 |
186 | # Durchschnitt pro Zeitpunkt berechnen
187 | floating_average_baseload = df.groupby(['dayOfWeek', 'hour', 'minute'])['_value'].mean().reset_index()
188 | floating_average_baseload.rename(columns={'_value': 'floating_average_baseload'}, inplace=True)
189 |
190 | logging.debug(f"{GREY}Floating average baseload: {floating_average_baseload}{RESET}")
191 | return floating_average_baseload
192 |
193 |
194 | def get_baseload():
195 | """
196 | Fetches the baseload energy consumption data, either from a cache file if it is less than a week old,
197 | or by fetching new data if the cache is older than a week or does not exist.
198 | The function checks for a cache file named 'baseload_cache.json' in the 'data' directory
199 | relative to the script's location. If the cache file exists and is less than a week old,
200 | it returns the cached baseload data. Otherwise, it fetches new baseload data, updates the
201 | cache file, and returns the new data.
202 | Returns:
203 | baseload (type): The baseload data, either from the cache or newly fetched.
204 | """
205 | baseload_serializable = []
206 |
207 | script_dir = os.path.dirname(os.path.abspath(__file__))
208 | cache_file = os.path.join(script_dir, 'cache', 'baseload_cache.json')
209 | # Check if cache file exists
210 | if not os.path.exists(cache_file):
211 | # Create cache directory if it doesn't exist
212 | cache_dir = os.path.dirname(cache_file)
213 | os.makedirs(cache_dir, exist_ok=True)
214 |
215 | # Create an empty cache file with the current timestamp
216 | baseload = get_baseload_from_influxdb()
217 | baseload_serializable = baseload.to_dict(orient='records')
218 | cache_data = {
219 | 'timestamp': datetime.datetime.now().isoformat(),
220 | 'baseload': []
221 | }
222 | with open(cache_file, 'w', encoding='utf-8') as f:
223 | json.dump(cache_data, f, ensure_ascii=False, indent=4)
224 |
225 | if os.path.exists(cache_file):
226 | with open(cache_file, 'r') as f:
227 | cache_data = json.load(f)
228 | cache_timestamp = datetime.datetime.fromisoformat(cache_data['timestamp']).astimezone()
229 | current_time = datetime.datetime.now().astimezone()
230 |
231 | # Check if the cache is older than a week
232 | if (current_time - cache_timestamp).days < 7:
233 | logging.debug(f"{GREY}Using cached baseload data{RESET}")
234 | # logging.debug(f"{GREY}Cached baseload data timestamp: {cache_data}{RESET}")
235 | return cache_data['baseload']
236 | else:
237 | # Fetch new baseload data (this is a placeholder, replace with actual fetching logic)
238 | baseload = get_baseload_from_influxdb()
239 |
240 | # Convert DataFrame to a serializable format
241 | baseload_serializable = baseload.to_dict(orient='records')
242 |
243 | # Write new data to cache
244 | cache_data = {
245 | 'timestamp': datetime.datetime.now().isoformat(),
246 | 'baseload': baseload_serializable
247 | }
248 | with open(cache_file, 'w', encoding='utf-8') as f:
249 | json.dump(cache_data, f, ensure_ascii=False, indent=4)
250 |
251 |
252 | logging.debug(f"{GREY}Fetched new baseload data and updated cache{RESET}")
253 | return baseload_serializable
254 |
255 | def get_usage_plan_from_json():
256 | usage_plan_path = os.path.join(os.path.dirname(__file__), 'data', 'usage_plan.json')
257 | with open(usage_plan_path, 'r') as f:
258 | usage_plan = json.load(f)
259 | # logging.debug(f"{GREY}Usage plan loaded from JSON: {usage_plan}{RESET}")
260 | return usage_plan
261 |
262 | def delete_deprecated_trips():
263 | """
264 | Deletes trips from the usage plan that are older than the current date.
265 | """
266 | usage_plan = read_usage_plan()
267 | current_date = datetime.datetime.now().date()
268 | usage_plan = [trip for trip in usage_plan if isinstance(trip, dict)] # Ensure each trip is a dictionary
269 | for trip in usage_plan:
270 | trip_date = datetime.datetime.strptime(trip.get('departure_date', ''), '%Y-%m-%d').date()
271 | if trip_date < current_date:
272 | usage_plan.remove(trip)
273 | return usage_plan
274 |
275 | def github_check_new_version(current_version):
276 | """
277 | Checks for a new version of the script on GitHub.
278 | """
279 | # Get the latest release from the GitHub API
280 | github_api_url = "https://api.github.com/repos/Coernel82/smartCharge4evcc/releases/latest"
281 | try:
282 | response = requests.get(github_api_url)
283 | response.raise_for_status()
284 | latest_release = response.json()
285 | latest_version = latest_release['tag_name']
286 | if latest_version != current_version:
287 | logging.info(f"{YELLOW}A new version of the script is available: {latest_version}{RESET}")
288 | else:
289 | logging.info(f"{GREEN}The script is up to date{RESET}")
290 | except Exception as e:
291 | logging.error(f"Failed to check for a new version: {e}")
292 |
293 | def get_evcc_state():
294 | """
295 | Retrieves the state of the EVCC from the API.
296 |
297 | Returns:
298 | dict: The state of the EVCC as a dictionary.
299 | """
300 | evcc_api_base_url = settings['EVCC']['EVCC_API_BASE_URL']
301 | try:
302 | response = requests.get(f"{evcc_api_base_url}/api/state")
303 | response.raise_for_status()
304 | evcc_state = response.json()
305 | # logging.debug(f"{GREY}EVCC state: {evcc_state}{RESET}")
306 | return evcc_state
307 | except Exception as e:
308 | logging.error(f"Failed to retrieve EVCC state: {e}")
309 | return {}
310 |
311 | def create_influxdb_bucket():
312 | INFLUX_BASE_URL = settings['InfluxDB']['INFLUX_BASE_URL']
313 | INFLUX_ORGANIZATION = settings['InfluxDB']['INFLUX_ORGANIZATION']
314 | INFLUX_ACCESS_TOKEN = settings['InfluxDB']['INFLUX_ACCESS_TOKEN']
315 |
316 | # Initialize InfluxDB client
317 | client = InfluxDBClient(
318 | url=INFLUX_BASE_URL,
319 | token=INFLUX_ACCESS_TOKEN,
320 | org=INFLUX_ORGANIZATION
321 | )
322 | # check if the bucket smartCharge4evcc exists - if not create it
323 | bucket_api = client.buckets_api()
324 | existing_buckets = [b.name for b in bucket_api.find_buckets().buckets]
325 | if "smartCharge4evcc" not in existing_buckets:
326 | bucket_api.create_bucket(
327 | bucket_name="smartCharge4evcc",
328 | description="Bucket for corrected energy consumption data",
329 | org=settings['InfluxDB']['INFLUX_ORGANIZATION']
330 | )
331 |
--------------------------------------------------------------------------------
/www/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Trip Planner
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
68 |
69 |
70 |
71 | Editing is not supported yet. You are welcome to open a pull request on GitHub.
72 | For now create a new trip and delete the old one or edit usage_plan.json in /backend/data From my part editing is not planned as it will be rarely used by myself. Also after closing this alert the edit icons seem not react any more unless you reload as the alert will be removed completely (normal bootstrap behavior!).
73 |
74 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | 
2 | # 🚨 Disclaimer
3 |
4 | **Warning:** This project is in **alpha status**. Expect inconsistencies in the code and naming conventions, bugs and missing docstrings. I am a hobby programmer Use at your own risk! There are inconsistensies in the naming conventions of the variables a mix of German and English. Also this code was created with the help of AI and therefore should not be used for AI traning. ⚠️
5 | ## 📋 Table of Contents
6 |
7 | - [🚨 Disclaimer](#-disclaimer)
8 | - [SmartCharge 🚗⚡](#smartcharge-)
9 | - [🌟 Features](#-features)
10 | - [Prerequisites](#prerequisites)
11 | - [🛠️ Installation](#️-installation)
12 | - [🚀 Usage](#-usage)
13 | - [🧐 How It Works](#-how-it-works)
14 | - [🤝 Contributing](#-contributing)
15 | - [📄 License](#-license)
16 | - [📷 Screenshots](#-screenshots)
17 |
18 | ---
19 |
20 | # SmartCharge 🚗⚡
21 |
22 | Welcome to **SmartCharge**, a smart charging solution for electric vehicles (EVs) that integrates multiple load points and home battery systems. This program optimizes your EV charging schedule based on solar production forecasts, weather conditions, electricity prices, and home energy consumption. 🌞🌧️💡
23 |
24 | ### What this program does in some short sentences:
25 | Using evcc's api it sets up your car trips (using a schedule) and loads the car to the minimum amount possible using the cheapest energy price possible taking into consideration future charges from PV, energy consumption of the car considering the trip lenght and the temperatures. PV charge is estimated with solcast and added to the EV. Remaining energy is used to "cache" this in the home battery. Also the energy consumption of the house is estimated by multiple factors. This energy is precharged into the battery if this is economically resonable - of course at cheapest cost possible.
26 |
27 | ### Prospect
28 | - create a web interface using websockets to have the data available in real time and also to make setup of trips without danger of error
29 |
30 | # Participation
31 | I highly depend on your participation now. Before creating pull requests please open an issue and let me assign the issue to you to make sure that not multiple people are working on the same function.
32 | There is a lot to do:
33 | - testing
34 | - looking at the TODO: / FIXME: / BUG: comments here and in the code
35 |
36 |
37 |
38 | ---
39 |
40 |
41 | ## 🌟 Features
42 |
43 | - **Multiple Load Point Support**: Manage charging for multiple EVs simultaneously.
44 | - **Home Battery Integration**: Optimize charging based on home battery status and capacity.
45 | - **Solar Forecasting**: Utilize solar production forecasts to prioritize charging when solar energy is abundant. 🌞
46 | - **Weather Integration**: Adjust charging plans based on weather conditions. ☔
47 | - **Electricity Price Optimization**: Schedule charging during off-peak hours to save on electricity costs. 💰
48 | - **evcc Integration**: Seamlessly integrate with [evcc (Electric Vehicle Charge Controller) GitHub Link ](https://github.com/evcc-io/evcc) / [Non GitHub link](https://www.evcc.io)
49 | - **Webserver**: Edit trips using the web interface
50 |
51 | ---
52 |
53 | ## Prerequisites
54 | - PV installation
55 | - home battery
56 | - Python
57 | - evcc
58 | - InfluxDB (also set up in evcc)
59 | - A Solcast account with your photovoltaic (PV) system set up. You can create an account and set up your PV system [here](https://www.solcast.com/free-rooftop-solar-forecasting).
60 | - An OpenWeather account to retrieve weather data. You can create an account and get your API key [here](https://home.openweathermap.org/users/sign_up).
61 | - a contract with tibber and your [acces token] (https://developer.tibber.com/settings/accesstoken), alternatively: integrate another source for energy prices such as Fraunhofer or Awattar - see - [Contributing](#contributing)
62 | - a fake loadpoint and charger to be able to lock the home battery with a "quick and dirty" trick: https://github.com/evcc-io/evcc/wiki/aaa-Lifehacks#entladung-eines-steuerbaren-hausspeicher-preisgesteuert-oder-manuell-sperren
63 | - heatpump set up with SG Ready (https://docs.evcc.io/docs/faq#heizstab--w%C3%A4rmepumpe)
64 | - if not set up: you will just get an error message - the program keeps operable
65 | - furthermore the relay or relays need to react to different conditions. For Viessmann Vitocal the Smart Grid conditions are: 1/0 is "boost light", 1/1 is "boost" and 0/1 is "block" (german EVU-Sperre).
66 | - in my case one shelly is controlled by evcc directly, the other shelly reacts to MQTT payloads
67 |
68 |
69 | ### Example Script for the Shelly relay which is *not* set up in evcc
70 | ```javascript
71 | var currentMode = null; // mode from "evcc/loadpoints/4/mode"
72 | var enabled = null; // value from "evcc/loadpoints/4/enabled"
73 | var loadpointID = 4; // ID of the loadpoint to control
74 |
75 | function updateRelay() {
76 | var relayOn = false;
77 |
78 |
79 | // • When mode is "off": switch relay on 0/1: the other relay controlled by evcc is off. This is the block condition.
80 | // • When mode is "pv" or "minpv":
81 | // - if enabled is "true": switch relay on: this enables this relay and the other one is enabled from evcc → 1/1 → "boost
82 | // - if enabled is "false": switch relay off → both relays are off → 0/0 → normal operation
83 | // - conditions 1/0 boost light is not set up. of course you can change the script to your liking
84 | if (currentMode === "off") {
85 | relayOn = true;
86 | } else if (currentMode === "pv" || currentMode === "minpv") {
87 | if (enabled === "true") {
88 | relayOn = true;
89 | } else if (enabled === "false") {
90 | relayOn = false;
91 | }
92 | } else {
93 | print("[DEBUG] Unknown mode:", currentMode);
94 | }
95 |
96 | Shelly.call("Switch.Set", { id: 0, on: relayOn });
97 | print("[DEBUG] Relay set to", relayOn);
98 | }
99 | // Subscription for mode updates.
100 | MQTT.subscribe("evcc/loadpoints/" + loadpointID + "/mode", function (topic, payload) {
101 | currentMode = payload;
102 | print("[DEBUG] Mode updated:", currentMode);
103 | updateRelay();
104 | });
105 |
106 | // Subscription for enabled updates.
107 | MQTT.subscribe("evcc/loadpoints/" + loadpointID + "/enabled", function (topic, payload) {
108 | enabled = payload;
109 | print("[DEBUG] Enabled updated:", enabled);
110 | updateRelay();
111 | });
112 | print("[DEBUG] Enabled updated:", enabled);
113 | updateRelay();
114 |
115 | ```
116 |
117 | ## 🛠️ Installation
118 | If these instructions say ``sudo`` do so. If not, do not!
119 | Follow these steps to set up SmartCharge on your system:
120 |
121 | ### 0. Installing pip and git
122 | You may not be able to use ``git`` and ``pip``. If you encounter this problem: `sudo apt-get install -y git pip`
123 |
124 | ### 1. Clone the Repository
125 |
126 | ```bash
127 | git clone https://github.com/Coernel82/smartCharge4evcc.git
128 | cd smartCharge4evcc
129 | ```
130 | *To update to new versions:* ``git pull origin main``
131 |
132 | ### 2. Set Up a Virtual Environment on Debian based Systems (Raspberry Pi!)
133 |
134 | It's recommended to use a Python virtual environment to manage dependencies:
135 |
136 | ```bash
137 | sudo apt update
138 | sudo apt install python3-venv
139 | python3 -m venv myenv
140 | source myenv/bin/activate
141 | ```
142 | You may replace `myenv` to your liking.
143 |
144 | To deactivate / leave the virtual environment simply use ``deactivate``
145 | Don't do that now!
146 |
147 | ### 3. Install Dependencies
148 |
149 | ```bash
150 | pip install -r requirements.txt
151 | ```
152 |
153 | ### 4. Configure Settings
154 |
155 | - `mv settings_example.json settings.json`
156 | - edit the settings via Webserver `:5000` **after** your webserver is running
157 |
158 | ### 5. Running the Webserver
159 | The webserver is a Flask-Server which should only be run in your private network as it is not safe to open it to the internet. The server is included in /www/server.py
160 |
161 | Create a bash
162 |
163 | ---
164 |
165 | ## 🚀 Usage
166 |
167 | ### Running SmartCharge Manually for testing
168 |
169 | Activate your virtual environment and run the `smartCharge.py` script:
170 |
171 | ```bash
172 | source myenv/bin/activate
173 | python smartCharge.py
174 | ```
175 |
176 | ### Running SmartCharge as a Systemd Service
177 |
178 | To keep SmartCharge running continuously, restart it automatically if it crashes, and start it on boot, you can set it up as a systemd service. The script loops itself. Every ten minutes the SoC of the home battery is checked, every hour the calculations are done.
179 |
180 | #### 1. Create a Systemd Service File for the main program and the server
181 |
182 | Create a new service file to run SmartCharge and its server in the virtual environment:
183 | `nano run_smartcharge.sh`
184 |
185 | and paste this:
186 | ```bash
187 | #!/bin/bash
188 |
189 | # switching to the working directory
190 | cd /home/evcc-admin/smartCharge4evcc
191 |
192 | # Activate virtual environment
193 | source /home/evcc-admin/myenv/bin/activate
194 |
195 | # run both scripts simultaniously by using the &-sign
196 | python /home/evcc-admin/smartcharge4evcc/backend/smartCharge.py &
197 | python /home/evcc-admin/smartCharge4evcc/www/server.py &
198 |
199 | # wait till the scripts finish (they never should)
200 | wait
201 |
202 | # Deaktivieren der virtuellen Umgebung
203 | deactivate
204 | ```
205 |
206 | Make it executable: `chmod +x /home/evcc/run_smartcharge.sh`
207 |
208 | Then make this a system service:
209 |
210 | ```bash
211 | sudo nano /etc/systemd/system/smartcharge.service
212 | ```
213 |
214 | Paste the following content into the file:
215 |
216 | ```ini
217 | [Unit]
218 | Description=SmartCharge Service
219 | After=network.target
220 |
221 | [Service]
222 | User=evcc-admin
223 | WorkingDirectory=/home/evcc-admin/smartCharge4evcc
224 | ExecStart=/home/evcc-admin/run_smartcharge.sh
225 | Restart=always
226 | RestartSec=5
227 |
228 | [Install]
229 | WantedBy=multi-user.target
230 | ```
231 |
232 |
233 | *Note:* Replace `/home/evcc-admin/smartCharge4evcc` and `evcc-admin` with your actual installation path and username if they are different.
234 |
235 | #### 2. Reload Systemd and Enable the Service
236 |
237 | Reload systemd to recognize the new service:
238 |
239 | ```bash
240 | sudo systemctl daemon-reload
241 | ```
242 |
243 | Enable the service to start on boot:
244 |
245 | ```bash
246 | sudo systemctl enable smartcharge.service
247 | ```
248 |
249 | #### 3. Start the Service
250 |
251 | Start the SmartCharge service:
252 |
253 | ```bash
254 | sudo systemctl start smartcharge.service
255 |
256 | ```
257 |
258 | #### 4. Verify the Service Status
259 |
260 | Check the status of the service to ensure it's running:
261 |
262 | ```bash
263 | sudo systemctl status smartcharge.service
264 | ```
265 | ```
266 |
267 | You should see that the service is active and running.
268 |
269 | #### 5. View Service Logs
270 |
271 | To view the logs for the SmartCharge service, use:
272 |
273 | ```bash
274 | sudo journalctl -u smartcharge.service
275 | ```
276 |
277 | #### Notes
278 |
279 | - The `Restart=always` option ensures that the service restarts automatically if it stops or crashes.
280 | - The `RestartSec=5` option sets a 5-second delay before the service restarts.
281 | - Ensure that your Python virtual environment and paths are correctly specified in the `ExecStart` directive.
282 |
283 |
284 | ---
285 |
286 | ## 🧐 How It Works
287 |
288 | SmartCharge intelligently schedules your EV charging by considering several factors:
289 |
290 | 1. Get many pieces of information from APIs 💻
291 | 1. energy forcast from Solcast
292 | 2. weather forcast from Openweather
293 | 3. settings from evcc
294 | 2. Calculate the energy consumption of your house in hourly increments
295 | 1. using the value of the energy certificate of the house the energy consumption is calculated: ``x kWh / ΔK / m² / year``. Break it down to an hour ``/365/24``
296 | 2. apply a correction factor: heating energy comes for free through your windows when the sun is shining. I estimate the energy by a correction factor: Normalize the prognosed yield of the pv by dividing through the kWP value of your pv. So the incoming radiation through the windows is somehow proportional to your PV yield. In another function the real energy used for heating and the calculated are compared and the correction factor gets adapted to make this prognosis more precise. For this I write the real and the calculated values to InfluxDB
297 | 3. Substract baseload and heating energy:
298 | 1. the baseload also comes from InfluxDB after it has run for some weeks. It is calculated over 4 weeks per day of the weeks and in hourly increments. So for every hour ``(Monday1 + Monday2 + Monday3 + Monday4) / 4 = baseload``
299 | 2. we have a value containing the remaining energy per hour
300 | 4. Calculate energy needed for ev
301 | 1. we have trip data in a json for recurring and non recurring trips.
302 | 2. (delete old non recurring trips takes place somewhere in the program as well)
303 | 3. we have a total degradated battery capacity which we calculate by mileage
304 | 4. get weather data for departure and return
305 | 5. calculate energy consumption for return and departure trip and take into consideration departure and return temperature (complicated gauss formula derived from a graph - link to graph in source code)
306 | 5. "load" energy to the ev with the remaining pv energy (i.e. reserve this for the vehicle)
307 | 6. Calculate loading plan for ev
308 | 1. the energy which can not be loaded till departure by solar energy has to be charged at cheapest cost:
309 | 1. calculate the charging time at the loadpoint for this amount of energy ``amount / speed = time``
310 | 2. filter energy prices from ``now till departure``
311 | 3. sort energy prices from ``low to high``
312 | 4. iterate through them till ``time (in hours) = number of iteration``. Return the price at that hour and post it to evcc
313 | 7. Store remaining energy in home battery (= reserve it)
314 | 8. Now we have a thorough energy profile which also has energy deficits for the home battery but also might have grid feedin (what we cannot do anything about as we have used the energy to the maximum possible)
315 | 9. Calculate charging costs of home battery
316 | 1. consider efficiency: ``charging cost = charing costs * (1/efficiency)``
317 | 2. consider wear and tear: break down purchase price to Wh for battery and inverter:
318 | ``charging cost = charging cost + wear and tear``
319 | 10. Charge battery when charging and using charged energy is still cheaper then grid energy
320 | 1. for every hour compare: how much energy is needed?
321 | 2. is charging beforehand (with losses, see above) cheaper:
322 | 3. sum up the energy need for all the times where charging beforehand is cheaper
323 | 4. calculate charging time ``amount / speed = time``
324 | 5. iterate as above with the loading plan for the ev
325 | 6. set cheapest price via evcc api
326 | 7. this can charge a bit more than needed as evcc does not support a "stop soc"
327 |
328 |
329 | ### Components breakdown
330 |
331 | - **utils.py**: Helper functions for calculations and data handling.
332 | - **initialize_smartcharge.py**: Loads settings and initializes the application.
333 | - **smartCharge.py**: The main script that orchestrates the charging schedule.
334 | - **vehicle.py**: Handles vehicle-specific calculations like energy consumption and SOC (State of Charge).
335 | - **home.py**: Manages home energy consumption, battery status, and interactions with home devices.
336 | - **solarweather.py**: Fetches and processes weather and solar data.
337 | - **evcc.py**: Interfaces with the EVCC API to set charging parameters.
338 | - **settings.json**: Configuration file containing API keys and user settings.
339 | - **usage_plan.json**: User-defined schedule for vehicle usage.
340 | - **www**: the webserver directory
341 | ---
342 |
343 | ## 🤝 Contributing
344 |
345 | Contributions are welcome! Please fork the repository and create a pull request. For major changes, please open an issue first to discuss what you would like to change. 🛠️
346 |
347 | ---
348 |
349 | ## 📄 License
350 |
351 | MIT
352 |
353 | # 📷 Screenshots
354 | 
355 | 
356 | 
357 | 
358 |
359 |
360 | ---
361 |
362 | Enjoy smart charging! If you encounter any issues or have suggestions, feel free to open an issue on GitHub. 😊
363 |
--------------------------------------------------------------------------------
/backend/solarweather.py:
--------------------------------------------------------------------------------
1 | # solarweather.py
2 |
3 | # This project is licensed under the MIT License.
4 |
5 | # Disclaimer: This code has been created with the help of AI (ChatGPT) and may not be suitable for
6 | # AI-Training. This code ist Alpha-Stage
7 |
8 | import logging
9 | import datetime
10 | import os
11 | import json
12 | import requests
13 | import initialize_smartcharge
14 | from math import floor
15 |
16 |
17 |
18 | # Logging configuration with color scheme for debug information
19 | logger = logging.getLogger('smartCharge')
20 | RED = "\033[91m"
21 | GREEN = "\033[92m"
22 | YELLOW = "\033[93m"
23 | BLUE = "\033[94m"
24 | CYAN = "\033[96m"
25 | GREY = "\033[37m"
26 | RESET = "\033[0m"
27 |
28 |
29 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
30 | SETTINGS_FILE = os.path.join(SCRIPT_DIR, 'data', 'settings.json')
31 | CACHE_DIR = os.path.join(SCRIPT_DIR, "cache")
32 |
33 |
34 |
35 | settings = initialize_smartcharge.load_settings()
36 | # Solar functions
37 |
38 |
39 | # Function to retrieve solar production forecast from Solcast API (cached every 6 hours)
40 | def get_solar_forecast(SOLCAST_API_URL1, SOLCAST_API_URL2):
41 | cache_file = os.path.join(CACHE_DIR, "solar_forecast_cache.json")
42 | current_time = datetime.datetime.now().astimezone() # Verwende lokale Zeit mit Zeitzoneninfo
43 |
44 | # Check if cache exists and is still valid (within 6 hours)
45 | if os.path.exists(cache_file):
46 | with open(cache_file, "r") as f:
47 | cached_data = json.load(f)
48 | cache_time = datetime.datetime.fromisoformat(cached_data["timestamp"])
49 | if cache_time.tzinfo is None:
50 | cache_time = cache_time.astimezone()
51 | if (current_time - cache_time).total_seconds() < 6 * 3600:
52 | logging.debug(f"{CYAN}Using cached solar forecast data from {cache_time}{RESET}")
53 | solar_forecast = cached_data["solar_forecast"]
54 | # Convert time strings back to aware datetime objects
55 | for entry in solar_forecast:
56 | if isinstance(entry['time'], str):
57 | entry['time'] = datetime.datetime.fromisoformat(entry['time'])
58 | if entry['time'].tzinfo is None:
59 | entry['time'] = entry['time'].astimezone()
60 | return solar_forecast
61 | else:
62 | # Create cache directory asit does not exist
63 | if not os.path.exists(CACHE_DIR):
64 | os.makedirs(CACHE_DIR)
65 | if not os.path.exists(cache_file):
66 | with open(cache_file, "w") as f:
67 | json.dump({"timestamp": current_time.isoformat()}, f)
68 |
69 | logging.debug(f"{CYAN}Abrufen der Solarprognose von Solcast{RESET}")
70 | try:
71 | SOLCAST_API_URLS = [
72 | SOLCAST_API_URL1,
73 | SOLCAST_API_URL2
74 | ]
75 |
76 | solar_forecast = []
77 | forecasts_by_hour = {}
78 |
79 | # Iterate through the array of URLs
80 | for url in SOLCAST_API_URLS:
81 | response = requests.get(url)
82 | response.raise_for_status()
83 | data = response.json()
84 |
85 | # Process forecasts from the current API URL
86 | for forecast in data['forecasts']:
87 | pv_estimate = forecast['pv_estimate'] * 1000 / 2 # Convert kW to W and divide by 2 for 30-minute intervals
88 | period_end = datetime.datetime.fromisoformat(forecast['period_end'].replace('Z', '+00:00'))
89 | period_end = period_end.astimezone() - datetime.timedelta(hours=1)
90 | hour_key = (period_end + datetime.timedelta(minutes=30)).replace(minute=0, second=0, microsecond=0)
91 | forecasts_by_hour[hour_key] = forecasts_by_hour.get(hour_key, 0) + pv_estimate
92 | # Sum forecasts for all URLs into a single dictionary
93 | forecasts_by_hour[hour_key] = forecasts_by_hour.get(hour_key, 0) + pv_estimate
94 |
95 | # Combine the forecasts by summing pv_estimates for each hour
96 | solar_forecast = []
97 | for hour_end in sorted(forecasts_by_hour.keys()):
98 | solar_forecast.append({
99 | 'time': hour_end.isoformat(),
100 | 'pv_estimate': forecasts_by_hour[hour_end]
101 | })
102 |
103 | # Speichere die neuen Daten im Cache
104 | with open(cache_file, "w") as f:
105 | json.dump({"timestamp": current_time.isoformat(), "solar_forecast": solar_forecast}, f)
106 |
107 | logging.debug(f"{CYAN}Solar forecast data cached successfully.{RESET}")
108 | return solar_forecast
109 |
110 | except Exception as e:
111 | logging.error(f"{RED}Fehler beim Abrufen der Solarprognose: {e}{RESET}")
112 | exit(1)
113 |
114 |
115 | # Weather functions
116 | # Function to retrieve weather forecast (temperature) and sunrise and sunet
117 | # for the next 24 hours from a weather API (cached every 6 hours)
118 |
119 |
120 |
121 | def get_weather_forecast():
122 | # get api key, lat, lon from settings
123 | api_key = settings["OneCallAPI"]["API_KEY"]
124 | lat = settings["OneCallAPI"]["LATITUDE"]
125 | lon = settings["OneCallAPI"]["LONGITUDE"]
126 |
127 | cache_file = os.path.join(CACHE_DIR, "weather_forecast_cache.json")
128 | current_time = datetime.datetime.now().astimezone()
129 |
130 | if not os.path.exists(cache_file):
131 | # Create cache directory if it does not exist
132 | if not os.path.exists(CACHE_DIR):
133 | os.makedirs(CACHE_DIR)
134 | # Create an empty cache file if it does not exist
135 | with open(cache_file, "w") as f:
136 | json.dump({"timestamp": current_time.isoformat(), "forecast": [], "sunrise": None, "sunset": None}, f)
137 |
138 | # Überprüfen, ob der Cache existiert und noch gültig ist (innerhalb von 12 Stunden)
139 | if os.path.exists(cache_file):
140 | with open(cache_file, "r") as f:
141 | cached_data = json.load(f)
142 | cache_time = datetime.datetime.fromisoformat(cached_data["timestamp"]).astimezone()
143 | if (current_time - cache_time).total_seconds() < 12 * 3600:
144 | logging.debug(f"{CYAN}Verwende zwischengespeicherte Wetterdaten vom {cache_time}{RESET}")
145 | forecast = cached_data["forecast"]
146 | sunrise = cached_data.get("sunrise")
147 | sunset = cached_data.get("sunset")
148 | # Konvertiere Zeitstempel zurück zu datetime-Objekten
149 | for entry in forecast:
150 | if isinstance(entry['dt'], str):
151 | entry['dt'] = datetime.datetime.fromisoformat(entry['dt'])
152 | if isinstance(sunrise, str):
153 | sunrise = datetime.datetime.fromisoformat(sunrise)
154 | if isinstance(sunset, str):
155 | sunset = datetime.datetime.fromisoformat(sunset)
156 | return forecast, sunrise, sunset
157 | else:
158 | # Create cache directory if it does not exist
159 | if not os.path.exists(CACHE_DIR):
160 | os.makedirs(CACHE_DIR)
161 | # Create an empty cache file if it does not exist
162 | with open(cache_file, "w") as f:
163 | json.dump({"timestamp": current_time.isoformat(), "forecast": [], "sunrise": None, "sunset": None}, f)
164 |
165 | logging.debug(f"{CYAN}Abrufen der Wettervorhersage von OpenWeatherMap{RESET}")
166 | # Abrufen der Wettervorhersage
167 | exclude = 'minutely,daily,alerts'
168 | url = f"https://api.openweathermap.org/data/3.0/onecall?lat={lat}&lon={lon}&exclude={exclude}&appid={api_key}&units=metric"
169 | try:
170 | response = requests.get(url)
171 | response.raise_for_status()
172 | weather_data = response.json()
173 | # Extrahiere Zeitzonen-Offset
174 | timezone_offset = weather_data.get('timezone_offset', 0)
175 | # Extrahiere Sonnenaufgang und Sonnenuntergang aus 'current' Daten
176 | current_weather = weather_data.get('current', {})
177 | sunrise_unix = current_weather.get('sunrise')
178 | sunset_unix = current_weather.get('sunset')
179 | if sunrise_unix is not None:
180 | sunrise_utc = datetime.datetime.fromtimestamp(sunrise_unix, tz=datetime.timezone.utc)
181 | sunrise = sunrise_utc + datetime.timedelta(seconds=timezone_offset)
182 | sunrise = sunrise.astimezone() # Sicherstellen, dass Zeitzoneninformationen vorhanden sind
183 | else:
184 | sunrise = None
185 | if sunset_unix is not None:
186 | sunset_utc = datetime.datetime.fromtimestamp(sunset_unix, tz=datetime.timezone.utc)
187 | sunset = sunset_utc + datetime.timedelta(seconds=timezone_offset)
188 | sunset = sunset.astimezone()
189 | else:
190 | sunset = None
191 |
192 | # Extrahieren der stündlichen Temperaturen
193 | hourly_forecast = weather_data.get('hourly', [])
194 | forecast = []
195 | for hour_data in hourly_forecast:
196 | # Konvertieren des Unix-Zeitstempels in datetime mit Zeitzoneninformation
197 | utc_dt = datetime.datetime.fromtimestamp(hour_data['dt'], tz=datetime.timezone.utc)
198 | # Anwenden des Zeitzonen-Offsets
199 | local_dt = utc_dt + datetime.timedelta(seconds=timezone_offset)
200 | local_dt = local_dt.astimezone() # Stelle sicher, dass local_dt Zeitzoneninformation hat
201 | temp = hour_data['temp']
202 | forecast.append({'dt': local_dt, 'temp': temp})
203 | # Speichern der neuen Daten im Cache
204 | with open(cache_file, "w") as f:
205 | # Zeitstempel in Strings konvertieren
206 | for entry in forecast:
207 | entry['dt'] = entry['dt'].isoformat()
208 | cache_data = {
209 | "timestamp": current_time.isoformat(),
210 | "forecast": forecast,
211 | "sunrise": sunrise.isoformat() if sunrise else None,
212 | "sunset": sunset.isoformat() if sunset else None
213 | }
214 | json.dump(cache_data, f)
215 | return forecast, sunrise, sunset
216 | except requests.RequestException as e:
217 | logging.error(f"{RED}Fehler beim Abrufen der Wetterdaten: {e}{RESET}")
218 | # Wenn zwischengespeicherte Daten vorhanden sind, verwenden wir diese
219 | if os.path.exists(cache_file):
220 | with open(cache_file, "r") as f:
221 | cached_data = json.load(f)
222 | logging.debug(f"{CYAN}Verwende zwischengespeicherte Wetterdaten{RESET}")
223 | forecast = cached_data["forecast"]
224 | sunrise = cached_data.get("sunrise")
225 | sunset = cached_data.get("sunset")
226 | # Konvertiere Zeitstempel zurück zu datetime-Objekten
227 | for entry in forecast:
228 | if isinstance(entry['dt'], str):
229 | entry['dt'] = datetime.datetime.fromisoformat(entry['dt'])
230 | if isinstance(sunrise, str):
231 | sunrise = datetime.datetime.fromisoformat(sunrise)
232 | if isinstance(sunset, str):
233 | sunset = datetime.datetime.fromisoformat(sunset)
234 | return forecast, sunrise, sunset
235 | else:
236 | logging.error(f"{RED}No weather data available{RESET}")
237 | exit(1)
238 |
239 | def get_temperature_for_times(weather_forecast, departure_time, return_time):
240 | logging.debug(f"{GREEN}Retrieving temperatures for departure time {departure_time} and return time {return_time}{RESET}")
241 | current_time = datetime.datetime.now().astimezone()
242 |
243 | # Ensure departure_time and return_time are offset-aware
244 | if departure_time.tzinfo is None:
245 | departure_time = departure_time.astimezone()
246 | if return_time.tzinfo is None:
247 | return_time = return_time.astimezone()
248 |
249 | # Initialize variables
250 | departure_temperature = None
251 | return_temperature = None
252 | outside_temperatures = []
253 |
254 | # Convert 'dt' from string to datetime if needed and ensure it's offset-aware
255 | for forecast in weather_forecast:
256 | if isinstance(forecast['dt'], str):
257 | forecast['dt'] = datetime.datetime.fromisoformat(forecast['dt'])
258 | if forecast['dt'].tzinfo is None:
259 | forecast['dt'] = forecast['dt'].astimezone()
260 |
261 | # Collect temperatures from now until departure time
262 | for forecast in weather_forecast:
263 | forecast_time = forecast['dt']
264 |
265 | # Ensure forecast_time is timezone-aware
266 | if forecast_time.tzinfo is None:
267 | forecast_time = forecast_time.astimezone()
268 |
269 | # Collect temperatures within the range from current time to departure time
270 | if current_time <= forecast_time <= departure_time:
271 | outside_temperatures.append(forecast['temp'])
272 |
273 | # Capture the departure temperature at or after the departure time
274 | if departure_temperature is None and forecast_time >= departure_time:
275 | departure_temperature = forecast['temp']
276 |
277 | # Capture the return temperature at or after the return time
278 | if return_temperature is None and forecast_time >= return_time:
279 | return_temperature = forecast['temp']
280 |
281 | # Break the loop if both temperatures have been found and further data is unnecessary
282 | if forecast_time > return_time and return_temperature is not None and departure_temperature is not None:
283 | break # We have all we need, so we can break the loop
284 |
285 | # Handle cases where temperature data is missing after loop completion
286 |
287 | # Check if the departure temperature was not found
288 | if departure_temperature is None:
289 | logging.error(f"{RED}No weather data available for departure time{RESET}")
290 | return None # Signal a missing data error
291 |
292 | # Check if the return temperature was not found
293 | if return_temperature is None:
294 | logging.error(f"{RED}and no weather data available for return time:{RESET}")
295 | return None # Signal a missing data error
296 |
297 | # Check if there are no temperatures between now and departure time
298 | if not outside_temperatures:
299 | logging.error(f"{RED}No weather data available between now and departure time{RESET}")
300 | return None # Signal a missing data error
301 |
302 | # If all required data is available, return the collected values
303 | logging.debug(f"{GREEN}Departure temperature: {departure_temperature}°C, Return temperature: {return_temperature}°C{RESET}")
304 | logging.debug(f"{GREEN}Collected {len(outside_temperatures)} outside temperatures from now until departure{RESET}")
305 | return departure_temperature, return_temperature, outside_temperatures
306 |
307 | def calculate_hours_till_sunrise(sunrise):
308 | """
309 | Berechnet die verbleibenden Stunden bis zum Sonnenaufgang.
310 | sunrise ist ein datetime-Objekt.
311 | Gibt die Stunden bis zum Sonnenaufgang zurück.
312 | """
313 | current_time = datetime.datetime.now().astimezone()
314 | time_until_sunrise = (sunrise - current_time).total_seconds() / 3600
315 | if time_until_sunrise < 0:
316 | time_until_sunrise += 24 # Falls Sonnenaufgang erst am nächsten Tag ist
317 | return time_until_sunrise
318 |
319 |
320 |
321 | def weather_data_available_for_next_trip(weather_forecast, return_time):
322 | if not weather_forecast:
323 | return False
324 |
325 | last_weather_time = max(
326 | [entry['dt'] if isinstance(entry['dt'], datetime.datetime) else datetime.datetime.fromisoformat(entry['dt']) for entry in weather_forecast]
327 | )
328 |
329 | # Sicherstellen, dass last_weather_time und return_time beide Zeitzoneninformationen haben
330 | if last_weather_time.tzinfo is None:
331 | last_weather_time = last_weather_time.astimezone()
332 |
333 | if return_time.tzinfo is None:
334 | return_time = return_time.astimezone()
335 |
336 | if last_weather_time >= return_time:
337 | logging.warning(f"{YELLOW}Keine Wetterdaten bis zum Rückkehrzeitpunkt verfügbar.{RESET}")
338 | return False
339 |
340 | return True
341 |
342 | # TODO: delete if not needed
343 | def get_current_temperature_delete(weather_forecast):
344 | if not weather_forecast:
345 | logging.error("No weather forecast data available")
346 | return None
347 | # Flatten the forecast list in case of nested lists
348 | def flatten(lst):
349 | flattened_list = []
350 | for item in lst:
351 | if isinstance(item, list):
352 | flattened_list.extend(item)
353 | else:
354 | flattened_list.append(item)
355 | return flattened_list
356 | weather_forecast = flatten(weather_forecast)
357 | current_time = datetime.datetime.now().astimezone()
358 | for forecast in weather_forecast:
359 | forecast_time = forecast['dt']
360 | if forecast_time >= current_time:
361 | return forecast['temp']
362 | return weather_forecast[-1]['temp']
363 |
364 |
365 |
--------------------------------------------------------------------------------
/backend/data/correction_factor_nominal_log.txt:
--------------------------------------------------------------------------------
1 | {"timestamp": "2025-01-03T14:25:49.158672", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 11.257522370963628, "quality_of_calculation_nominal": 0.8441666200645903}
2 | {"timestamp": "2025-01-03T14:33:37.946632", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 14.473110298992435, "quality_of_calculation_nominal": 0.844282362443638}
3 | {"timestamp": "2025-01-07T09:34:58.388954", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 14.83865767176398, "quality_of_calculation_nominal": 3.751215948484621}
4 | {"timestamp": "2025-01-07T09:45:26.096002", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 15.192842563694741, "quality_of_calculation_nominal": 3.753074602766193}
5 | {"timestamp": "2025-01-07T09:46:32.416394", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 15.536372590812281, "quality_of_calculation_nominal": 3.7532122616452113}
6 | {"timestamp": "2025-01-07T09:47:59.595007", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 15.86959302137949, "quality_of_calculation_nominal": 3.7532296151836175}
7 | {"timestamp": "2025-01-07T09:51:41.945303", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 16.192811477017532, "quality_of_calculation_nominal": 3.753254793096217}
8 | {"timestamp": "2025-01-07T09:54:29.978153", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 16.506329609509194, "quality_of_calculation_nominal": 3.7532724932897277}
9 | {"timestamp": "2025-01-07T10:01:06.951168", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 16.81043410992596, "quality_of_calculation_nominal": 3.7533104728429634}
10 | {"timestamp": "2025-01-07T10:04:05.831965", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 17.105411584116666, "quality_of_calculation_nominal": 3.7533287452140796}
11 | {"timestamp": "2025-01-07T10:07:06.275850", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 17.391535707943156, "quality_of_calculation_nominal": 3.753347651353367}
12 | {"timestamp": "2025-01-07T10:15:00.013336", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 17.66906622788843, "quality_of_calculation_nominal": 3.753394047932279}
13 | {"timestamp": "2025-01-07T10:17:43.551057", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 17.938216187901542, "quality_of_calculation_nominal": 3.7536506746653764}
14 | {"timestamp": "2025-01-07T10:19:48.853199", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.199240702344067, "quality_of_calculation_nominal": 3.7538899681060722}
15 | {"timestamp": "2025-01-07T10:20:25.690681", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.452431482100465, "quality_of_calculation_nominal": 3.753904056339308}
16 | {"timestamp": "2025-01-07T10:21:57.296391", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.698022039816586, "quality_of_calculation_nominal": 3.753925187799101}
17 | {"timestamp": "2025-01-07T10:22:29.952939", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.93624342052679, "quality_of_calculation_nominal": 3.753932047185171}
18 | {"timestamp": "2025-01-07T10:23:02.057770", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 19.167316720678695, "quality_of_calculation_nominal": 3.7539388073063007}
19 | {"timestamp": "2025-01-09T14:25:27.599935", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 19.117996009865813, "quality_of_calculation_nominal": 5.706689938152564}
20 | {"timestamp": "2025-01-09T14:27:09.673159", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 19.07011773363897, "quality_of_calculation_nominal": 5.707093644993433}
21 | {"timestamp": "2025-01-09T14:29:18.445622", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 19.02362616585383, "quality_of_calculation_nominal": 5.707632634331228}
22 | {"timestamp": "2025-01-09T14:36:26.234611", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.97834286257693, "quality_of_calculation_nominal": 5.7096583711565}
23 | {"timestamp": "2025-01-09T14:37:34.574892", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.93439315651785, "quality_of_calculation_nominal": 5.709928986066027}
24 | {"timestamp": "2025-01-09T14:44:50.969113", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.891587108180808, "quality_of_calculation_nominal": 5.711829667147239}
25 | {"timestamp": "2025-01-09T14:45:40.742947", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.850040220627548, "quality_of_calculation_nominal": 5.712101779841017}
26 | {"timestamp": "2025-01-09T14:47:26.627750", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.809689757179395, "quality_of_calculation_nominal": 5.712645443262754}
27 | {"timestamp": "2025-01-09T14:49:30.644373", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.770499766290563, "quality_of_calculation_nominal": 5.713189850184131}
28 | {"timestamp": "2025-01-09T14:51:30.293737", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.732477927736603, "quality_of_calculation_nominal": 5.713271968341305}
29 | {"timestamp": "2025-01-09T14:55:35.395001", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.695594652332826, "quality_of_calculation_nominal": 5.713294730491658}
30 | {"timestamp": "2025-01-09T14:58:19.026563", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.65981670397706, "quality_of_calculation_nominal": 5.713307474007512}
31 | {"timestamp": "2025-01-09T14:59:23.196414", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.625111624602802, "quality_of_calculation_nominal": 5.713312582131202}
32 | {"timestamp": "2025-01-09T15:01:31.437210", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.591446552049973, "quality_of_calculation_nominal": 5.713325046590034}
33 | {"timestamp": "2025-01-09T15:03:15.204603", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.558790745931216, "quality_of_calculation_nominal": 5.713332507954494}
34 | {"timestamp": "2025-01-09T15:06:30.522822", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.527112921064173, "quality_of_calculation_nominal": 5.713350928336003}
35 | {"timestamp": "2025-01-09T15:09:25.792354", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.496383812343435, "quality_of_calculation_nominal": 5.713368540038566}
36 | {"timestamp": "2025-01-09T20:27:52.734094", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.459170608730307, "quality_of_calculation_nominal": 5.795104677289064}
37 | {"timestamp": "2025-01-09T20:29:38.236546", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.422977783575206, "quality_of_calculation_nominal": 5.7961797378971225}
38 | {"timestamp": "2025-01-09T20:31:00.806487", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.387772918139728, "quality_of_calculation_nominal": 5.797275445151873}
39 | {"timestamp": "2025-01-09T20:32:56.818164", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.353491919994315, "quality_of_calculation_nominal": 5.798757715679792}
40 | {"timestamp": "2025-01-09T20:34:42.075876", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.320139010699233, "quality_of_calculation_nominal": 5.799882610052087}
41 | {"timestamp": "2025-01-09T20:40:20.759826", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.28737731646733, "quality_of_calculation_nominal": 5.804476487112398}
42 | {"timestamp": "2025-01-23T09:50:06.776588", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.21270774750928, "quality_of_calculation_nominal": 6.329758243549999}
43 | {"timestamp": "2025-01-23T09:54:25.191968", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.14027325387651, "quality_of_calculation_nominal": 6.329825177494019}
44 | {"timestamp": "2025-01-23T09:55:45.503211", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.070009962241652, "quality_of_calculation_nominal": 6.32984965581106}
45 | {"timestamp": "2025-01-23T10:02:04.587103", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 18.000332821663722, "quality_of_calculation_nominal": 6.350239075627084}
46 | {"timestamp": "2025-01-23T10:03:08.580812", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 17.93273914426401, "quality_of_calculation_nominal": 6.350331167571543}
47 | {"timestamp": "2025-01-23T10:04:36.226406", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 17.86717137154317, "quality_of_calculation_nominal": 6.350356783778088}
48 | {"timestamp": "2025-01-23T10:06:56.612920", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 17.80356810342249, "quality_of_calculation_nominal": 6.350390774021331}
49 | {"timestamp": "2025-01-23T10:07:59.298157", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 17.74187167594593, "quality_of_calculation_nominal": 6.350407676643577}
50 | {"timestamp": "2025-01-23T10:09:40.371791", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 17.682023642222543, "quality_of_calculation_nominal": 6.350441270732876}
51 | {"timestamp": "2025-01-23T10:14:28.201660", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 17.62396232850491, "quality_of_calculation_nominal": 6.350558506776405}
52 | {"timestamp": "2025-01-23T10:15:14.484547", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 17.567607533838697, "quality_of_calculation_nominal": 6.351033360866964}
53 | {"timestamp": "2025-01-23T10:16:27.576665", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 17.512898819656037, "quality_of_calculation_nominal": 6.351632581005662}
54 | {"timestamp": "2025-01-27T12:21:14.426612", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 17.35044107310414, "quality_of_calculation_nominal": 8.266074625294205}
55 | {"timestamp": "2025-01-27T12:26:46.024954", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 17.192808328302068, "quality_of_calculation_nominal": 8.267184663382332}
56 | {"timestamp": "2025-01-27T12:31:22.005547", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 17.03990008217388, "quality_of_calculation_nominal": 8.267286812129948}
57 | {"timestamp": "2025-01-27T12:34:18.658286", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 16.89157620544509, "quality_of_calculation_nominal": 8.26735238084022}
58 | {"timestamp": "2025-01-27T12:35:49.258925", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 16.74770059815806, "quality_of_calculation_nominal": 8.267385344841172}
59 | {"timestamp": "2025-01-27T12:39:09.570218", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 16.608137924573814, "quality_of_calculation_nominal": 8.267461316547397}
60 | {"timestamp": "2025-01-27T12:39:47.649315", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 16.472761658458015, "quality_of_calculation_nominal": 8.267472087277906}
61 | {"timestamp": "2025-01-27T12:41:29.824139", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 16.34144526727071, "quality_of_calculation_nominal": 8.26750428202152}
62 | {"timestamp": "2025-01-27T12:42:25.402389", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 16.21406739974042, "quality_of_calculation_nominal": 8.267526338663023}
63 | {"timestamp": "2025-01-27T12:43:29.023076", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 16.0905098783387, "quality_of_calculation_nominal": 8.26754889254271}
64 | {"timestamp": "2025-01-27T12:48:00.918504", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 15.970654660517228, "quality_of_calculation_nominal": 8.26764964656429}
65 | {"timestamp": "2025-01-27T12:48:35.559636", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 15.85439461562828, "quality_of_calculation_nominal": 8.267660665296994}
66 | {"timestamp": "2025-01-27T12:50:27.131076", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 15.741620475822659, "quality_of_calculation_nominal": 8.267703871385512}
67 | {"timestamp": "2025-01-27T12:55:09.725861", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 15.632224805193777, "quality_of_calculation_nominal": 8.267812215762183}
68 | {"timestamp": "2025-01-27T12:56:48.390353", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 15.526109586903708, "quality_of_calculation_nominal": 8.267844520819246}
69 | {"timestamp": "2025-01-27T12:57:45.310376", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 15.423176814563853, "quality_of_calculation_nominal": 8.267867548128915}
70 | {"timestamp": "2025-01-27T12:58:08.608194", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 15.323331477742993, "quality_of_calculation_nominal": 8.26788002686114}
71 | {"timestamp": "2025-01-27T12:59:39.342190", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 15.226477393993088, "quality_of_calculation_nominal": 8.267973610570767}
72 | {"timestamp": "2025-01-27T13:01:55.575870", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 15.131866607512164, "quality_of_calculation_nominal": 8.283093258762108}
73 | {"timestamp": "2025-01-27T13:04:52.940879", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 15.040012438032681, "quality_of_calculation_nominal": 8.284962300219034}
74 | {"timestamp": "2025-01-27T13:06:31.984561", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 14.950880880501993, "quality_of_calculation_nominal": 8.285717716239349}
75 | {"timestamp": "2025-01-27T13:08:59.205808", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 14.864377648924517, "quality_of_calculation_nominal": 8.286761850565705}
76 | {"timestamp": "2025-01-27T13:12:23.957375", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 14.780423699914849, "quality_of_calculation_nominal": 8.287810680887375}
77 | {"timestamp": "2025-01-27T13:13:45.186378", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 14.698970181396794, "quality_of_calculation_nominal": 8.288227132595393}
78 | {"timestamp": "2025-01-27T13:14:59.387590", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 14.619948093197738, "quality_of_calculation_nominal": 8.288505933417166}
79 | {"timestamp": "2025-01-27T13:17:26.706585", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 14.543266123655567, "quality_of_calculation_nominal": 8.289205442982228}
80 | {"timestamp": "2025-01-27T13:20:36.285914", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 14.468850570926184, "quality_of_calculation_nominal": 8.289985208382355}
81 | {"timestamp": "2025-01-27T13:25:52.835359", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 14.396662233945882, "quality_of_calculation_nominal": 8.290105495951272}
82 | {"timestamp": "2025-01-28T07:53:57.430323", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 14.330496290331066, "quality_of_calculation_nominal": 8.202684542034477}
83 | {"timestamp": "2025-01-28T14:19:25.076251", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 14.260129258472933, "quality_of_calculation_nominal": 8.343812771595982}
84 | {"timestamp": "2025-01-28T14:22:56.795936", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 14.19187228943927, "quality_of_calculation_nominal": 8.343834774371206}
85 | {"timestamp": "2025-01-28T16:52:39.517126", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 14.124121820975423, "quality_of_calculation_nominal": 8.379754842344855}
86 | {"timestamp": "2025-01-28T16:55:19.816612", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 14.058403158094622, "quality_of_calculation_nominal": 8.379771425387927}
87 | {"timestamp": "2025-01-28T16:59:39.653037", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 13.994654927249675, "quality_of_calculation_nominal": 8.37979782490742}
88 | {"timestamp": "2025-02-01T09:39:35.711655", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 13.886651700408727, "quality_of_calculation_nominal": 9.62042852661421}
89 | {"timestamp": "2025-02-01T09:42:12.095083", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 13.781862697474386, "quality_of_calculation_nominal": 9.621226794580117}
90 | {"timestamp": "2025-02-01T09:47:21.834661", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 13.680165604479425, "quality_of_calculation_nominal": 9.622824170976084}
91 | {"timestamp": "2025-02-01T09:50:38.314219", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 13.581482995004702, "quality_of_calculation_nominal": 9.623948737265309}
92 | {"timestamp": "2025-02-01T09:52:28.637620", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 13.485745228357079, "quality_of_calculation_nominal": 9.624431482188067}
93 | {"timestamp": "2025-02-01T09:55:29.226953", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 13.392848404085102, "quality_of_calculation_nominal": 9.625394637746009}
94 | {"timestamp": "2025-02-01T09:56:20.253847", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 13.302728094081408, "quality_of_calculation_nominal": 9.625715534321444}
95 | {"timestamp": "2025-02-01T09:58:18.810670", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 13.215290657127188, "quality_of_calculation_nominal": 9.626356011885385}
96 | {"timestamp": "2025-02-01T10:05:34.681311", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 13.130299760254973, "quality_of_calculation_nominal": 9.631813561446656}
97 | {"timestamp": "2025-02-01T10:35:38.736234", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 13.047495779589783, "quality_of_calculation_nominal": 9.64304618347407}
98 | {"timestamp": "2025-02-01T10:39:23.518245", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 12.967139463259944, "quality_of_calculation_nominal": 9.644176281828598}
99 | {"timestamp": "2025-02-01T10:56:40.267228", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 12.88904182609578, "quality_of_calculation_nominal": 9.648891419632665}
100 | {"timestamp": "2025-02-02T11:23:32.042223", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 12.799649597380364, "quality_of_calculation_nominal": 10.091529293826662}
101 | {"timestamp": "2025-02-02T11:25:09.707964", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 12.71293756075257, "quality_of_calculation_nominal": 10.091582751888396}
102 | {"timestamp": "2025-02-02T11:30:12.385569", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 12.628823062327074, "quality_of_calculation_nominal": 10.09171252820572}
103 | {"timestamp": "2025-02-02T11:32:02.117588", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 12.54723084611458, "quality_of_calculation_nominal": 10.091751661052706}
104 | {"timestamp": "2025-02-02T11:33:27.997938", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 12.468085233947651, "quality_of_calculation_nominal": 10.091791123535154}
105 | {"timestamp": "2025-02-02T11:36:16.205854", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 12.391311584395597, "quality_of_calculation_nominal": 10.091872794800539}
106 | {"timestamp": "2025-02-02T11:45:03.179038", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 12.31683452502316, "quality_of_calculation_nominal": 10.092097516221132}
107 | {"timestamp": "2025-02-02T12:32:15.393086", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 12.243836169846562, "quality_of_calculation_nominal": 10.117815875822412}
108 | {"timestamp": "2025-02-02T12:37:39.595716", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 12.172879281052737, "quality_of_calculation_nominal": 10.12288520256488}
109 | {"timestamp": "2025-02-02T12:44:19.926902", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 12.103881416439904, "quality_of_calculation_nominal": 10.128684469634164}
110 | {"timestamp": "2025-02-02T12:47:01.413196", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 12.036887241388083, "quality_of_calculation_nominal": 10.130950387221127}
111 | {"timestamp": "2025-02-02T12:51:15.074226", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 11.97178067053894, "quality_of_calculation_nominal": 10.135133546641239}
112 | {"timestamp": "2025-02-02T13:00:51.775262", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 11.908363476282501, "quality_of_calculation_nominal": 10.144174902072844}
113 | {"timestamp": "2025-02-02T13:02:33.660292", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 11.846807809972537, "quality_of_calculation_nominal": 10.145581039820527}
114 | {"timestamp": "2025-02-02T13:04:52.748935", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 11.787028151972299, "quality_of_calculation_nominal": 10.148006087851035}
115 | {"timestamp": "2025-02-02T13:05:35.920196", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 11.729012200113017, "quality_of_calculation_nominal": 10.149025149246615}
116 | {"timestamp": "2025-02-02T13:06:20.843032", "correction_factor_summer_nominal": 0, "correction_factor_winter_nominal": 11.672723354547763, "quality_of_calculation_nominal": 10.149484296423994}
117 |
--------------------------------------------------------------------------------
/backend/data/correction_factor_log.txt:
--------------------------------------------------------------------------------
1 | {"timestamp": "2024-12-31T17:56:55.838677", "correction_factor_summer": 0.0046, "correction_factor_winter": 100, "quality_of_calculation": 0.32404832349354207}
2 | {"timestamp": "2025-01-03T12:36:49.223443", "correction_factor_summer": 0.0046, "correction_factor_winter": -242.03062755508498, "quality_of_calculation": 0.3430960812121464}
3 | {"timestamp": "2025-01-03T12:42:33.224986", "correction_factor_summer": 0.0046, "correction_factor_winter": -391.9496041850314, "quality_of_calculation": 0.343103162688696}
4 | {"timestamp": "2025-01-03T13:04:37.120087", "correction_factor_summer": 0.0046, "correction_factor_winter": -537.3710115160794, "quality_of_calculation": 0.34313191361917683}
5 | {"timestamp": "2025-01-03T13:07:23.195614", "correction_factor_summer": 0.0046, "correction_factor_winter": -678.429776627196, "quality_of_calculation": 0.34313589585425186}
6 | {"timestamp": "2025-01-03T13:11:40.238251", "correction_factor_summer": 0.0046, "correction_factor_winter": -815.2567787849791, "quality_of_calculation": 0.3431410170290361}
7 | {"timestamp": "2025-01-03T13:14:31.951462", "correction_factor_summer": 0.0046, "correction_factor_winter": -947.9789708780287, "quality_of_calculation": 0.34314487626019297}
8 | {"timestamp": "2025-01-03T13:15:19.227335", "correction_factor_summer": 0.0046, "correction_factor_winter": -1076.7194972082868, "quality_of_calculation": 0.3431461689186177}
9 | {"timestamp": "2025-01-03T13:19:40.598324", "correction_factor_summer": 0.0046, "correction_factor_winter": -1201.5978077486373, "quality_of_calculation": 0.3431514419071746}
10 | {"timestamp": "2025-01-03T13:22:56.670648", "correction_factor_summer": 0.0046, "correction_factor_winter": -1322.729768972777, "quality_of_calculation": 0.3431566693974153}
11 | {"timestamp": "2025-01-03T13:26:16.713752", "correction_factor_summer": 0.0046, "correction_factor_winter": -1440.2277713601927, "quality_of_calculation": 0.3432331077296902}
12 | {"timestamp": "2025-01-03T13:27:34.030767", "correction_factor_summer": 0.0046, "correction_factor_winter": -1554.200833675986, "quality_of_calculation": 0.3432343974405727}
13 | {"timestamp": "2025-01-03T13:28:43.190239", "correction_factor_summer": 0.0046, "correction_factor_winter": -1664.7547041223054, "quality_of_calculation": 0.3432363149159179}
14 | {"timestamp": "2025-01-03T13:29:55.481547", "correction_factor_summer": 0.0046, "correction_factor_winter": -1771.9919584552351, "quality_of_calculation": 0.3432375925735198}
15 | {"timestamp": "2025-01-03T13:35:24.517559", "correction_factor_summer": 0.0046, "correction_factor_winter": -1876.012095158177, "quality_of_calculation": 0.3432448410921829}
16 | {"timestamp": "2025-01-03T14:12:39.388979", "correction_factor_summer": 0.0046, "correction_factor_winter": -1976.9116277600308, "quality_of_calculation": 0.34337094196070783}
17 | {"timestamp": "2025-01-03T14:14:23.091341", "correction_factor_summer": 0.0046, "correction_factor_winter": -2074.784174383829, "quality_of_calculation": 0.343373521784307}
18 | {"timestamp": "2025-01-03T14:18:48.119611", "correction_factor_summer": 0.0046, "correction_factor_winter": -2169.720544608913, "quality_of_calculation": 0.34337875671340834}
19 | {"timestamp": "2025-01-03T14:20:55.431732", "correction_factor_summer": 0.0046, "correction_factor_winter": -2261.8088237272445, "quality_of_calculation": 0.34338211612221503}
20 | {"timestamp": "2025-01-03T14:23:51.646727", "correction_factor_summer": 0.0046, "correction_factor_winter": -2351.134454472026, "quality_of_calculation": 0.34338476283771735}
21 | {"timestamp": "2025-01-03T14:25:40.896765", "correction_factor_summer": 0.0046, "correction_factor_winter": -2437.7803162944642, "quality_of_calculation": 0.3433879998401501}
22 | {"timestamp": "2025-01-03T14:33:29.843759", "correction_factor_summer": 0.0046, "correction_factor_winter": -2521.826802262229, "quality_of_calculation": 0.34343517395277745}
23 | {"timestamp": "2025-01-07T09:34:50.814604", "correction_factor_summer": 0.0046, "correction_factor_winter": -2505.590107002784, "quality_of_calculation": 3.3170369322915616}
24 | {"timestamp": "2025-01-07T09:45:18.537722", "correction_factor_summer": 0.0046, "correction_factor_winter": -2489.840512601122, "quality_of_calculation": 3.3185184232552345}
25 | {"timestamp": "2025-01-07T09:46:24.904908", "correction_factor_summer": 0.0046, "correction_factor_winter": -2474.56340603151, "quality_of_calculation": 3.318708017477401}
26 | {"timestamp": "2025-01-07T09:47:52.063866", "correction_factor_summer": 0.0046, "correction_factor_winter": -2459.7446126589866, "quality_of_calculation": 3.318722543460373}
27 | {"timestamp": "2025-01-07T09:51:34.384433", "correction_factor_summer": 0.0046, "correction_factor_winter": -2445.370383087639, "quality_of_calculation": 3.3187436189148434}
28 | {"timestamp": "2025-01-07T09:54:22.446239", "correction_factor_summer": 0.0046, "correction_factor_winter": -2431.4273804034315, "quality_of_calculation": 3.3187584350515498}
29 | {"timestamp": "2025-01-07T10:00:59.413477", "correction_factor_summer": 0.0046, "correction_factor_winter": -2417.9026677997504, "quality_of_calculation": 3.3187902262185376}
30 | {"timestamp": "2025-01-07T10:03:58.272408", "correction_factor_summer": 0.0046, "correction_factor_winter": -2404.7836965741794, "quality_of_calculation": 3.318805521278312}
31 | {"timestamp": "2025-01-07T10:06:58.735063", "correction_factor_summer": 0.0046, "correction_factor_winter": -2392.058294485376, "quality_of_calculation": 3.3188213468313132}
32 | {"timestamp": "2025-01-07T10:14:52.471909", "correction_factor_summer": 0.0046, "correction_factor_winter": -2379.714654459236, "quality_of_calculation": 3.3188601834666653}
33 | {"timestamp": "2025-01-07T10:17:36.134083", "correction_factor_summer": 0.0046, "correction_factor_winter": -2367.7413236338807, "quality_of_calculation": 3.319074994044624}
34 | {"timestamp": "2025-01-07T10:19:41.298779", "correction_factor_summer": 0.0046, "correction_factor_winter": -2356.127192733286, "quality_of_calculation": 3.3192752942992287}
35 | {"timestamp": "2025-01-07T10:20:18.200201", "correction_factor_summer": 0.0046, "correction_factor_winter": -2344.861485759709, "quality_of_calculation": 3.3192811749092943}
36 | {"timestamp": "2025-01-07T10:21:49.769972", "correction_factor_summer": 0.0046, "correction_factor_winter": -2333.9337499953394, "quality_of_calculation": 3.3192989659847405}
37 | {"timestamp": "2025-01-07T10:22:22.410160", "correction_factor_summer": 0.0046, "correction_factor_winter": -2323.333846303901, "quality_of_calculation": 3.3193105163952685}
38 | {"timestamp": "2025-01-07T10:22:54.522093", "correction_factor_summer": 0.0046, "correction_factor_winter": -2313.0519397232056, "quality_of_calculation": 3.3193161749190625}
39 | {"timestamp": "2025-01-09T14:25:20.432107", "correction_factor_summer": 0.0046, "correction_factor_winter": -2278.9112186286916, "quality_of_calculation": 5.501553809429815}
40 | {"timestamp": "2025-01-09T14:27:02.467097", "correction_factor_summer": 0.0046, "correction_factor_winter": -2245.7947191670128, "quality_of_calculation": 5.502060605308301}
41 | {"timestamp": "2025-01-09T14:29:11.227223", "correction_factor_summer": 0.0046, "correction_factor_winter": -2213.6717146891847, "quality_of_calculation": 5.502568486042437}
42 | {"timestamp": "2025-01-09T14:36:19.020501", "correction_factor_summer": 0.0046, "correction_factor_winter": -2182.5124003456913, "quality_of_calculation": 5.504349808669962}
43 | {"timestamp": "2025-01-09T14:37:27.516216", "correction_factor_summer": 0.0046, "correction_factor_winter": -2152.2878654325027, "quality_of_calculation": 5.504732275988178}
44 | {"timestamp": "2025-01-09T14:44:43.835300", "correction_factor_summer": 0.0046, "correction_factor_winter": -2122.9700665667096, "quality_of_calculation": 5.50652320644438}
45 | {"timestamp": "2025-01-09T14:45:33.673432", "correction_factor_summer": 0.0046, "correction_factor_winter": -2094.5318016668903, "quality_of_calculation": 5.506779604386402}
46 | {"timestamp": "2025-01-09T14:47:19.520974", "correction_factor_summer": 0.0046, "correction_factor_winter": -2066.9466847140657, "quality_of_calculation": 5.507163887877975}
47 | {"timestamp": "2025-01-09T14:49:23.563899", "correction_factor_summer": 0.0046, "correction_factor_winter": -2040.1891212698258, "quality_of_calculation": 5.5076765156994885}
48 | {"timestamp": "2025-01-09T14:51:23.212845", "correction_factor_summer": 0.0046, "correction_factor_winter": -2014.234284728913, "quality_of_calculation": 5.507879477942579}
49 | {"timestamp": "2025-01-09T14:55:28.274622", "correction_factor_summer": 0.0046, "correction_factor_winter": -1989.0580932842279, "quality_of_calculation": 5.507903654547286}
50 | {"timestamp": "2025-01-09T14:58:11.996118", "correction_factor_summer": 0.0046, "correction_factor_winter": -1964.6371875828831, "quality_of_calculation": 5.507915661986685}
51 | {"timestamp": "2025-01-09T14:59:16.083936", "correction_factor_summer": 0.0046, "correction_factor_winter": -1940.9489090525788, "quality_of_calculation": 5.507920475060413}
52 | {"timestamp": "2025-01-09T15:01:24.310959", "correction_factor_summer": 0.0046, "correction_factor_winter": -1917.9712788781835, "quality_of_calculation": 5.507929863957129}
53 | {"timestamp": "2025-01-09T15:03:08.129875", "correction_factor_summer": 0.0046, "correction_factor_winter": -1895.68297760902, "quality_of_calculation": 5.5079392499478015}
54 | {"timestamp": "2025-01-09T15:06:23.450906", "correction_factor_summer": 0.0046, "correction_factor_winter": -1874.0633253779315, "quality_of_calculation": 5.507953824645129}
55 | {"timestamp": "2025-01-09T15:09:18.606752", "correction_factor_summer": 0.0046, "correction_factor_winter": -1853.0922627137757, "quality_of_calculation": 5.507970391379391}
56 | {"timestamp": "2025-01-09T20:27:45.465423", "correction_factor_summer": 0.0046, "correction_factor_winter": -1832.7503319295445, "quality_of_calculation": 5.584632817775981}
57 | {"timestamp": "2025-01-09T20:29:31.229821", "correction_factor_summer": 0.0046, "correction_factor_winter": -1813.0186590688402, "quality_of_calculation": 5.58597553158533}
58 | {"timestamp": "2025-01-09T20:30:53.536939", "correction_factor_summer": 0.0046, "correction_factor_winter": -1793.878936393957, "quality_of_calculation": 5.587007268030875}
59 | {"timestamp": "2025-01-09T20:32:49.858740", "correction_factor_summer": 0.0046, "correction_factor_winter": -1775.3134053993203, "quality_of_calculation": 5.588402984735441}
60 | {"timestamp": "2025-01-09T20:34:35.037394", "correction_factor_summer": 0.0046, "correction_factor_winter": -1757.3048403345229, "quality_of_calculation": 5.589462182522054}
61 | {"timestamp": "2025-01-09T20:40:13.694935", "correction_factor_summer": 0.0046, "correction_factor_winter": -1739.8365322216694, "quality_of_calculation": 5.593421956996991}
62 | {"timestamp": "2025-01-23T09:49:58.556678", "correction_factor_summer": 0.0046, "correction_factor_winter": -1725.485462790695, "quality_of_calculation": 6.067038854436668}
63 | {"timestamp": "2025-01-23T09:54:17.274805", "correction_factor_summer": 0.0046, "correction_factor_winter": -1711.5649254426498, "quality_of_calculation": 6.067108285445766}
64 | {"timestamp": "2025-01-23T09:55:37.517386", "correction_factor_summer": 0.0046, "correction_factor_winter": -1698.062004215046, "quality_of_calculation": 6.067130815339472}
65 | {"timestamp": "2025-01-23T10:01:56.429309", "correction_factor_summer": 0.0046, "correction_factor_winter": -1684.9399128305154, "quality_of_calculation": 6.087457767684623}
66 | {"timestamp": "2025-01-23T10:03:00.628532", "correction_factor_summer": 0.0046, "correction_factor_winter": -1672.2114841875207, "quality_of_calculation": 6.08753519701275}
67 | {"timestamp": "2025-01-23T10:04:28.255053", "correction_factor_summer": 0.0046, "correction_factor_winter": -1659.864908403816, "quality_of_calculation": 6.08755824233257}
68 | {"timestamp": "2025-01-23T10:06:48.719403", "correction_factor_summer": 0.0046, "correction_factor_winter": -1647.8887298936222, "quality_of_calculation": 6.087597407938638}
69 | {"timestamp": "2025-01-23T10:07:51.358594", "correction_factor_summer": 0.0046, "correction_factor_winter": -1636.2718367387342, "quality_of_calculation": 6.087612966995715}
70 | {"timestamp": "2025-01-23T10:09:32.420481", "correction_factor_summer": 0.0046, "correction_factor_winter": -1625.003450378493, "quality_of_calculation": 6.087636185459444}
71 | {"timestamp": "2025-01-23T10:14:20.238372", "correction_factor_summer": 0.0046, "correction_factor_winter": -1614.073115609059, "quality_of_calculation": 6.087751807712016}
72 | {"timestamp": "2025-01-23T10:15:06.517799", "correction_factor_summer": 0.0046, "correction_factor_winter": -1603.470690882708, "quality_of_calculation": 6.0881889141616625}
73 | {"timestamp": "2025-01-23T10:16:19.595853", "correction_factor_summer": 0.0046, "correction_factor_winter": -1593.1863388981474, "quality_of_calculation": 6.0887404967037595}
74 | {"timestamp": "2025-01-27T12:21:05.830874", "correction_factor_summer": 0.0046, "correction_factor_winter": -1564.4797105885411, "quality_of_calculation": 8.150450701161072}
75 | {"timestamp": "2025-01-27T12:26:37.296860", "correction_factor_summer": 0.0046, "correction_factor_winter": -1536.6342811282232, "quality_of_calculation": 8.151648927810907}
76 | {"timestamp": "2025-01-27T12:31:13.458137", "correction_factor_summer": 0.0046, "correction_factor_winter": -1509.6242145517147, "quality_of_calculation": 8.151746496906465}
77 | {"timestamp": "2025-01-27T12:34:10.153236", "correction_factor_summer": 0.0046, "correction_factor_winter": -1483.4244499725014, "quality_of_calculation": 8.151809125923904}
78 | {"timestamp": "2025-01-27T12:35:40.672418", "correction_factor_summer": 0.0046, "correction_factor_winter": -1458.0106783306646, "quality_of_calculation": 8.151840612012485}
79 | {"timestamp": "2025-01-27T12:39:00.748453", "correction_factor_summer": 0.0046, "correction_factor_winter": -1433.359319838083, "quality_of_calculation": 8.151902766773024}
80 | {"timestamp": "2025-01-27T12:39:39.075225", "correction_factor_summer": 0.0046, "correction_factor_winter": -1409.4475021002786, "quality_of_calculation": 8.151923465389842}
81 | {"timestamp": "2025-01-27T12:41:21.136218", "correction_factor_summer": 0.0046, "correction_factor_winter": -1386.2530388946084, "quality_of_calculation": 8.151954216682311}
82 | {"timestamp": "2025-01-27T12:42:16.838337", "correction_factor_summer": 0.0046, "correction_factor_winter": -1363.7544095851083, "quality_of_calculation": 8.151975284410085}
83 | {"timestamp": "2025-01-27T12:43:20.515448", "correction_factor_summer": 0.0046, "correction_factor_winter": -1341.9307391548932, "quality_of_calculation": 8.151996827078634}
84 | {"timestamp": "2025-01-27T12:47:52.193261", "correction_factor_summer": 0.0046, "correction_factor_winter": -1320.7617788375846, "quality_of_calculation": 8.152093063713274}
85 | {"timestamp": "2025-01-27T12:48:26.995759", "correction_factor_summer": 0.0046, "correction_factor_winter": -1300.2278873297953, "quality_of_calculation": 8.152103588407854}
86 | {"timestamp": "2025-01-27T12:50:18.562041", "correction_factor_summer": 0.0046, "correction_factor_winter": -1280.3100125672397, "quality_of_calculation": 8.152144857290978}
87 | {"timestamp": "2025-01-27T12:55:01.192220", "correction_factor_summer": 0.0046, "correction_factor_winter": -1260.9896740475608, "quality_of_calculation": 8.152237897218528}
88 | {"timestamp": "2025-01-27T12:56:39.807598", "correction_factor_summer": 0.0046, "correction_factor_winter": -1242.2489456834721, "quality_of_calculation": 8.152279200417222}
89 | {"timestamp": "2025-01-27T12:57:36.757850", "correction_factor_summer": 0.0046, "correction_factor_winter": -1224.0704391703061, "quality_of_calculation": 8.152301195236333}
90 | {"timestamp": "2025-01-27T12:58:00.085038", "correction_factor_summer": 0.0046, "correction_factor_winter": -1206.4372878525353, "quality_of_calculation": 8.152301195236333}
91 | {"timestamp": "2025-01-27T12:59:30.783005", "correction_factor_summer": 0.0046, "correction_factor_winter": -1189.3331310742974, "quality_of_calculation": 8.152337001754129}
92 | {"timestamp": "2025-01-27T13:01:46.816973", "correction_factor_summer": 0.0046, "correction_factor_winter": -1172.730376302919, "quality_of_calculation": 8.167936065878688}
93 | {"timestamp": "2025-01-27T13:04:44.351222", "correction_factor_summer": 0.0046, "correction_factor_winter": -1156.625704174682, "quality_of_calculation": 8.169721420194321}
94 | {"timestamp": "2025-01-27T13:06:23.451202", "correction_factor_summer": 0.0046, "correction_factor_winter": -1141.004172210292, "quality_of_calculation": 8.170443004978523}
95 | {"timestamp": "2025-01-27T13:08:50.605629", "correction_factor_summer": 0.0046, "correction_factor_winter": -1125.8512862048337, "quality_of_calculation": 8.171440371063909}
96 | {"timestamp": "2025-01-27T13:12:15.288806", "correction_factor_summer": 0.0046, "correction_factor_winter": -1111.152986779539, "quality_of_calculation": 8.172442214880737}
97 | {"timestamp": "2025-01-27T13:13:36.588853", "correction_factor_summer": 0.0046, "correction_factor_winter": -1096.8956363370035, "quality_of_calculation": 8.172840007783822}
98 | {"timestamp": "2025-01-27T13:14:50.855575", "correction_factor_summer": 0.0046, "correction_factor_winter": -1083.0660064077438, "quality_of_calculation": 8.173106316445233}
99 | {"timestamp": "2025-01-27T13:17:18.158885", "correction_factor_summer": 0.0046, "correction_factor_winter": -1069.6512653763618, "quality_of_calculation": 8.173774480785823}
100 | {"timestamp": "2025-01-27T13:20:27.571546", "correction_factor_summer": 0.0046, "correction_factor_winter": -1056.6389665759214, "quality_of_calculation": 8.174519300510951}
101 | {"timestamp": "2025-01-27T13:25:43.759530", "correction_factor_summer": 0.0046, "correction_factor_winter": -1044.0170367394942, "quality_of_calculation": 8.174634196926323}
102 | {"timestamp": "2025-01-28T07:53:48.452777", "correction_factor_summer": 0.0046, "correction_factor_winter": -1026.0013484545927, "quality_of_calculation": 8.088304117199119}
103 | {"timestamp": "2025-01-28T14:19:16.270327", "correction_factor_summer": 0.0046, "correction_factor_winter": -1008.348073831414, "quality_of_calculation": 8.232907330247329}
104 | {"timestamp": "2025-01-28T14:22:48.032562", "correction_factor_summer": 0.0046, "correction_factor_winter": -991.2243974469307, "quality_of_calculation": 8.232928198773614}
105 | {"timestamp": "2025-01-28T16:52:30.651807", "correction_factor_summer": 0.0046, "correction_factor_winter": -974.557128811239, "quality_of_calculation": 8.270424618391425}
106 | {"timestamp": "2025-01-28T16:55:11.097384", "correction_factor_summer": 0.0046, "correction_factor_winter": -958.3898782346181, "quality_of_calculation": 8.270443517291348}
107 | {"timestamp": "2025-01-28T16:59:30.700563", "correction_factor_summer": 0.0046, "correction_factor_winter": -942.7076451752959, "quality_of_calculation": 8.270465886249633}
108 | {"timestamp": "2025-02-01T09:39:25.865253", "correction_factor_summer": 0.0046, "correction_factor_winter": -925.3958409956622, "quality_of_calculation": 9.598442708885646}
109 | {"timestamp": "2025-02-01T09:42:02.540079", "correction_factor_summer": 0.0046, "correction_factor_winter": -908.6033909414175, "quality_of_calculation": 9.599215758748015}
110 | {"timestamp": "2025-02-01T09:47:12.267248", "correction_factor_summer": 0.0046, "correction_factor_winter": -892.3147143888002, "quality_of_calculation": 9.6009174324746}
111 | {"timestamp": "2025-02-01T09:50:28.691253", "correction_factor_summer": 0.0046, "correction_factor_winter": -876.5146981327614, "quality_of_calculation": 9.601850653764732}
112 | {"timestamp": "2025-02-01T09:52:18.996695", "correction_factor_summer": 0.0046, "correction_factor_winter": -861.1886823644038, "quality_of_calculation": 9.602474041698816}
113 | {"timestamp": "2025-02-01T09:55:19.689397", "correction_factor_summer": 0.0046, "correction_factor_winter": -846.3224470690969, "quality_of_calculation": 9.603406807592874}
114 | {"timestamp": "2025-02-01T09:56:10.713106", "correction_factor_summer": 0.0046, "correction_factor_winter": -831.9021988326492, "quality_of_calculation": 9.603717577963389}
115 | {"timestamp": "2025-02-01T09:58:09.278894", "correction_factor_summer": 0.0046, "correction_factor_winter": -817.914558043295, "quality_of_calculation": 9.604337842814658}
116 | {"timestamp": "2025-02-01T10:05:24.985671", "correction_factor_summer": 0.0046, "correction_factor_winter": -804.4419778224593, "quality_of_calculation": 9.60715322346256}
117 | {"timestamp": "2025-02-01T10:35:29.158119", "correction_factor_summer": 0.0046, "correction_factor_winter": -791.3735750082487, "quality_of_calculation": 9.618023362527294}
118 | {"timestamp": "2025-02-01T10:39:13.977329", "correction_factor_summer": 0.0046, "correction_factor_winter": -778.6972242784645, "quality_of_calculation": 9.619273999719502}
119 | {"timestamp": "2025-02-01T10:56:30.688498", "correction_factor_summer": 0.0046, "correction_factor_winter": -766.4011640705738, "quality_of_calculation": 9.62382679931707}
120 | {"timestamp": "2025-02-02T11:23:21.881412", "correction_factor_summer": 0.0046, "correction_factor_winter": -751.7099611882975, "quality_of_calculation": 9.992048128658315}
121 | {"timestamp": "2025-02-02T11:24:59.764532", "correction_factor_summer": 0.0046, "correction_factor_winter": -737.4594943924894, "quality_of_calculation": 9.992086272451727}
122 | {"timestamp": "2025-02-02T11:30:02.579479", "correction_factor_summer": 0.0046, "correction_factor_winter": -723.6365416005556, "quality_of_calculation": 9.992209994091649}
123 | {"timestamp": "2025-02-02T11:31:52.319981", "correction_factor_summer": 0.0046, "correction_factor_winter": -710.2282773923798, "quality_of_calculation": 9.992259455642982}
124 | {"timestamp": "2025-02-02T11:33:18.189443", "correction_factor_summer": 0.0046, "correction_factor_winter": -697.2222611104493, "quality_of_calculation": 9.992296958825786}
125 | {"timestamp": "2025-02-02T11:36:06.385308", "correction_factor_summer": 0.0046, "correction_factor_winter": -684.6064253169767, "quality_of_calculation": 9.992374575101504}
126 | {"timestamp": "2025-02-02T11:44:53.399352", "correction_factor_summer": 0.0046, "correction_factor_winter": -672.3690645973082, "quality_of_calculation": 9.992588138811776}
127 | {"timestamp": "2025-02-02T12:32:05.513319", "correction_factor_summer": 0.0046, "correction_factor_winter": -660.4988246992299, "quality_of_calculation": 10.017027023698777}
128 | {"timestamp": "2025-02-02T12:37:29.914968", "correction_factor_summer": 0.0046, "correction_factor_winter": -648.9846919980938, "quality_of_calculation": 10.021413046469851}
129 | {"timestamp": "2025-02-02T12:44:10.233916", "correction_factor_summer": 0.0046, "correction_factor_winter": -637.8159832779919, "quality_of_calculation": 10.027353406246908}
130 | {"timestamp": "2025-02-02T12:46:51.632643", "correction_factor_summer": 0.0046, "correction_factor_winter": -626.982335819493, "quality_of_calculation": 10.029506169390068}
131 | {"timestamp": "2025-02-02T12:51:05.305248", "correction_factor_summer": 0.0046, "correction_factor_winter": -616.473697784749, "quality_of_calculation": 10.03348032989999}
132 | {"timestamp": "2025-02-02T13:00:42.063779", "correction_factor_summer": 0.0046, "correction_factor_winter": -606.2803188910474, "quality_of_calculation": 10.042069509095498}
133 | {"timestamp": "2025-02-02T13:02:23.937914", "correction_factor_summer": 0.0046, "correction_factor_winter": -596.3927413641568, "quality_of_calculation": 10.043405267524353}
134 | {"timestamp": "2025-02-02T13:04:43.002608", "correction_factor_summer": 0.0046, "correction_factor_winter": -586.801791163073, "quality_of_calculation": 10.04570890309905}
135 | {"timestamp": "2025-02-02T13:05:26.228814", "correction_factor_summer": 0.0046, "correction_factor_winter": -577.4985694680216, "quality_of_calculation": 10.046180376665049}
136 | {"timestamp": "2025-02-02T13:06:10.745343", "correction_factor_summer": 0.0046, "correction_factor_winter": -568.4744444238219, "quality_of_calculation": 10.047113081859893}
137 |
--------------------------------------------------------------------------------
/backend/smartCharge.py:
--------------------------------------------------------------------------------
1 | # smartCharge.py
2 |
3 | # This project is licensed under the MIT License.
4 |
5 | # Disclaimer: This code has been created with the help of AI (ChatGPT) and may not be suitable for
6 | # AI-Training. This code is Alpha-Stage
7 |
8 |
9 |
10 | # Important
11 | # TODO: are grid_feedin and future_grid_feedin the same?
12 | # TODO: check API post to block heatpump - multiple commands necessary due to possible evcc bug?
13 |
14 | # Good to have
15 | # TODO: unify cache folder to /backend cache and not /cache
16 |
17 | # TODO: Unimportant / Nice to have
18 | # add MQTT temperature source (via external script and cache)
19 | # implement finer resolutions for the api data - finest resolution determined by the worst resolution of all data sources. solcast hobbyist minimum and maximum is 30 minutes
20 | # add barchart into each trip with the energy compsotion (solar, grid)
21 | # add savings information for each trip (in the index.html) compared to average price
22 | # add pre-heating and pre-cooling using sg ready. evcc now has a virtual sg ready charger as well
23 | # add additional (electric) heating (by timetable)
24 |
25 | # Quality check:
26 | # ✓ heatpump id is correct
27 | # ✓ check loadpoint for car is not necessary as we set for car and not loadpoint
28 | # ✓ fake_loadpint_id is correct as well
29 |
30 |
31 |
32 | import requests
33 | import logging
34 | import datetime
35 | import time
36 | import os
37 | import math
38 | import utils
39 | import solarweather
40 | import vehicle
41 | import home
42 | import initialize_smartcharge
43 | import evcc
44 | import socGuard
45 |
46 |
47 | current_version = "v0.0.4-alpha"
48 | # Logging configuration with color scheme for debug information
49 | # DEBUG, INFO, WARNING, ERROR, CRITICAL
50 | logging.basicConfig(level=logging.INFO)
51 | RESET = "\033[0m"
52 | RED = "\033[91m"
53 | GREEN = "\033[92m"
54 | YELLOW = "\033[93m"
55 | BLUE = "\033[94m"
56 | CYAN = "\033[96m"
57 | GREY = "\033[37m"
58 |
59 | settings = initialize_smartcharge.load_settings()
60 |
61 | # Zugriff auf die Einstellungen
62 | # User-Config
63 | # OneCall API 3.0 von OpenWeather
64 | API_KEY = settings['OneCallAPI']['API_KEY']
65 | LATITUDE = settings['OneCallAPI']['LATITUDE']
66 | LONGITUDE = settings['OneCallAPI']['LONGITUDE']
67 |
68 | # Energy: SOLCAST und TIBBER API-URLs und Header
69 | SOLCAST_API_URL1 = settings['EnergyAPIs']['SOLCAST_API_URL1']
70 | SOLCAST_API_URL2 = settings['EnergyAPIs']['SOLCAST_API_URL2']
71 | TIBBER_API_URL = settings['EnergyAPIs']['TIBBER_API_URL']
72 | TIBBER_HEADERS = settings['EnergyAPIs']['TIBBER_HEADERS']
73 |
74 | # InfluxDB
75 | INFLUX_BASE_URL = settings['InfluxDB']['INFLUX_BASE_URL']
76 | INFLUX_ORGANIZATION = settings['InfluxDB']['INFLUX_ORGANIZATION']
77 | INFLUX_BUCKET = settings['InfluxDB']['INFLUX_BUCKET']
78 | INFLUX_ACCESS_TOKEN = settings['InfluxDB']['INFLUX_ACCESS_TOKEN']
79 | TIMESPAN_WEEKS = settings['InfluxDB']['TIMESPAN_WEEKS']
80 |
81 |
82 | # Hauptprogramm
83 | if __name__ == "__main__":
84 |
85 | # Check if the OS is windows. If yes set the mode to debug
86 | if os.name == 'nt':
87 | logging.getLogger().setLevel(logging.DEBUG)
88 | logging.info(f"{YELLOW}Windows detected. Setting logging level to DEBUG. Assuming you are developing and not using the program in normal mode{RESET}")
89 |
90 |
91 | logging.info(f"{GREEN}Starting the main program...{RESET}")
92 | current_hour_start = datetime.datetime.now().hour
93 | # we loop the main program till infinity
94 | while True:
95 | if settings['HolidayMode']['HOLIDAY_MODE']:
96 | logging.info(f"{GREEN}Holiday mode is active. Skipping all calculations.{RESET}")
97 | continue
98 | else:
99 | # wait till the next full hour to start the program as the electricity prices, weather, etc. change exactly to the hour
100 | logging.info(f"{GREEN}Waiting for the next full hour to start the program...{RESET}")
101 | while logging.getLogger().level != logging.DEBUG: # wait only on normal mode not debug
102 | logging.info(f"{GREEN}... still waiting!{RESET}")
103 | time.sleep(10)
104 | current_hour = datetime.datetime.now().hour
105 | if current_hour == (current_hour_start + 1) % 24:
106 | current_hour_start = current_hour
107 | break
108 |
109 |
110 |
111 | # Initialilzing: Getting all the data
112 | print(f"{GREEN}╔══════════════════════════════════════════════════╗{RESET}")
113 | print(f"{GREEN}║{CYAN} Starting the EV charging optimization program... {GREEN}{RESET}")
114 | print(f"{GREEN}╚══════════════════════════════════════════════════╝{RESET}")
115 |
116 |
117 | cars = initialize_smartcharge.load_cars()
118 |
119 | # I will need this as I need more than /api/state
120 | evcc_base_url = initialize_smartcharge.load_settings()
121 |
122 | github_check_new_version = initialize_smartcharge.github_check_new_version(current_version)
123 |
124 | usage_plan = initialize_smartcharge.read_usage_plan()
125 |
126 | # API-Abrufe
127 | weather_forecast, sunrise, sunset = solarweather.get_weather_forecast()
128 | solar_forecast = solarweather.get_solar_forecast(SOLCAST_API_URL1, SOLCAST_API_URL2)
129 | # electricity_prices = utils.get_electricity_prices(TIBBER_API_URL, TIBBER_HEADERS)
130 | electricity_prices = evcc.get_electricity_prices()
131 | logging.debug(f"{GREY}Electricity prices: {electricity_prices}{RESET}")
132 | evcc_state = initialize_smartcharge.get_evcc_state()
133 |
134 | # Create our "smartCharge4evcc" bucket in InfluxDB if it does not exist
135 | initialize_smartcharge.create_influxdb_bucket()
136 |
137 | logging.info("Alle Daten wurden erfolgreich geladen.")
138 | ########################################################################################################################
139 |
140 | #Before we start the calculations for the cars we need to know the available energy
141 |
142 | # Energy-Balance: Incoming Energy
143 | # --> we have solar_forecast
144 | print(f"{GREEN}╔══════════════════════════════════════════════════╗{RESET}")
145 | print(f"{GREEN}║{CYAN} Calculate the energy balance... {GREEN}║{RESET}")
146 | print(f"{GREEN}╚══════════════════════════════════════════════════╝{RESET}")
147 |
148 |
149 | usable_energy = solar_forecast
150 |
151 | # Energy-Balance: Outgoing Energy
152 | hourly_climate_energy = home.calculate_hourly_house_energy_consumption(solar_forecast, weather_forecast)
153 | # Write the corrected energy consumption to InfluxDB
154 | utils.write_corrected_energy_consumption(hourly_climate_energy)
155 |
156 |
157 | # the correction factor is used to correct the energy consumption of the house
158 | # however, it is an estimate at the beginning - so we update it bit by bit
159 | # within time the correction factor will be more accurate
160 |
161 | # Check if the correction factor was updated today
162 | cache_folder = "cache"
163 | os.makedirs(cache_folder, exist_ok=True)
164 | last_update_file = os.path.join(cache_folder, "last_correction_update.txt")
165 | today_date = datetime.date.today().isoformat()
166 |
167 | if os.path.exists(last_update_file):
168 | with open(last_update_file, "r") as file:
169 | last_update_date = file.read().strip()
170 | else:
171 | last_update_date = ""
172 |
173 |
174 | if logging.getLogger().level == logging.DEBUG:
175 | last_update_date = "2022-01-01" # for testing
176 |
177 | if last_update_date != today_date:
178 | utils.update_correction_factor()
179 | utils.update_correction_factor_nominal()
180 | with open(last_update_file, "w") as file:
181 | file.write(today_date)
182 |
183 |
184 |
185 |
186 | # Calculate hourly energy surplus
187 | hourly_energy_surplus = utils.calculate_hourly_energy_surplus(hourly_climate_energy, solar_forecast)
188 |
189 | ########################################################################################################################
190 | # now we can start the calculations for the cars
191 | ########################################################################################################################
192 |
193 | print(f"{GREEN}╔══════════════════════════════════════════════════╗{RESET}")
194 | print(f"{GREEN}║{CYAN} Apply the energy balance to each trip... {GREEN}║{RESET}")
195 | print(f"{GREEN}╚══════════════════════════════════════════════════╝{RESET}")
196 |
197 | initialize_smartcharge.delete_deprecated_trips()
198 | usage_plan = initialize_smartcharge.get_usage_plan_from_json()
199 | sorted_trips = vehicle.sort_trips_by_earliest_departure_time(usage_plan)
200 |
201 | # which car is assigned to which loadpoint - we need to know that as the loadpoint determines the charging speed
202 | # here we just load the cars and loadpoints and assign later in the loop
203 | cars = initialize_smartcharge.load_cars()
204 | evcc_base_url = initialize_smartcharge.settings['EVCC']['EVCC_API_BASE_URL']
205 |
206 | # iterate over all trips
207 | for trip in sorted_trips:
208 | # get the car name and the loadpoint id from assignments
209 | car_name = trip['car_name']
210 | logging.info(f"\033[42m\033[93mEnergy calculation for {car_name}{RESET}")
211 | loadpoint_id = initialize_smartcharge.get_loadpoint_id_for_car(car_name, evcc_state) # + 1 already done in the function
212 | if loadpoint_id is None:
213 | logging.error(f"{RED}No loadpoint assigned to car {car_name}. Skipping this trip.{RESET}")
214 | continue
215 | # we have departure_datetime and return_datetime and distance in the trip
216 | departure_time = trip['departure_datetime']
217 | return_time = trip['return_datetime']
218 | total_distance = trip['distance']
219 | # match car_name to the car in cars and get the consumption and buffer_distance from cars
220 | car_info = next((car for car in cars.values() if car['CAR_NAME'] == car_name), None)
221 | consumption = car_info['CONSUMPTION']
222 | buffer_distance = car_info['BUFFER_DISTANCE']
223 |
224 | # get departure_temperature and return_temperature from solarweather.get_temperatures
225 | temperatures = solarweather.get_temperature_for_times(weather_forecast, departure_time, return_time)
226 | if temperatures is None:
227 | logging.error(f"{RED}Temperature data is missing for the trip of {car_name}. Skipping this trip.{RESET}")
228 | continue
229 | departure_temperature, return_temperature, outside_temperatures_till_departure = temperatures
230 |
231 | # calculate the energy requirements for the car
232 | ev_energy_for_trip = vehicle.calculate_ev_energy_consumption(departure_temperature, return_temperature, total_distance, consumption, buffer_distance, car_name, evcc_state, loadpoint_id)
233 | current_soc = vehicle.get_evcc_soc(loadpoint_id, evcc_state) # yes, SoC is under loadpoint_id in evcc
234 |
235 | # here we calculate the final required SoC and the energy gap
236 | trip_name = trip['description']
237 | required_soc_final = current_soc + vehicle.calculate_required_soc_topup(ev_energy_for_trip, car_name, evcc_state, loadpoint_id, trip_name)
238 | ev_energy_gap_Wh = vehicle.calculate_energy_gap(required_soc_final, current_soc, car_name, evcc_state, loadpoint_id)
239 |
240 | # remaining hours till departure to find best charging window
241 | remaining_hours = utils.calculate_remaining_hours(departure_time)
242 |
243 | # from here we take care of the loading process
244 |
245 |
246 | # how much regenerative energy can we use? (we do not need usable_energy for now)
247 | regenerative_energy_surplus, usable_energy = utils.get_usable_charging_energy_surplus(usable_energy, departure_time, ev_energy_gap_Wh, evcc_state, car_name, load_car=True)
248 | logging.debug(f"{GREY}Regenerativer Energieüberschuss der genutzt werden KANN: {regenerative_energy_surplus/1000:.2f} kWh{RESET}")
249 |
250 | # Update ev_energy_gap_Wh: till departure we can use regenerative_energy_surplus
251 | logging.debug(f"{GREY}regenerative_energy_surplus: {regenerative_energy_surplus}{RESET}")
252 | logging.debug(f"{GREY}ev_energy_gap_Wh: {ev_energy_gap_Wh}{RESET}")
253 | ev_energy_gap_Wh -= regenerative_energy_surplus
254 | if ev_energy_gap_Wh < 0:
255 | ev_energy_gap_Wh = 0
256 | logging.debug(f"{GREY}ev_energy_gap_Wh nach Abzug des regenerativen Energieüberschusses: {ev_energy_gap_Wh/1000:.2f} kWh{RESET}")
257 | logging.debug(f"{GREY}Energiebedarf für {car_name} nach regenerativer Energie: {ev_energy_gap_Wh:.2f} kWh{RESET}")
258 | # this required energy to be topped up with grid energy in Wh
259 | # so using vehicle.calculate_required_soc again is correct
260 | required_soc_grid_topup = vehicle.calculate_required_soc_topup(ev_energy_gap_Wh, car_name, evcc_state, loadpoint_id, trip_name)
261 | logging.info(f"{GREEN}Required SoC with addon from grid: {required_soc_grid_topup:.2f}%{RESET}")
262 |
263 | # required_soc_initial = required_soc_final - required_soc_grid_topup
264 | logging.info(f"{GREEN}Required SoC to start charging: required trip soc excluding solar energy)): {required_soc_grid_topup:.2f}%{RESET}")
265 |
266 | # round it up as the api does not accept decimal places
267 | required_soc_grid_topup = math.ceil(required_soc_grid_topup)
268 |
269 | # using evcc's internal API to set the required SoC
270 | post_url = f"{evcc_base_url}/api/vehicles/{car_name}/plan/soc/{required_soc_grid_topup}/{departure_time.isoformat()}Z"
271 | logging.debug(f"{GREY}Post URL: {post_url}{RESET}")
272 | response = requests.post(post_url)
273 | if response.status_code == 200:
274 | logging.info(f"{GREEN}Successfully posted required SoC for {car_name} to the API.{RESET}")
275 | else:
276 | logging.error(f"{RED}Failed to post required SoC for {car_name} to the API. Status code: {response.status_code}{RESET}")
277 | logging.error(f"{RED}the url called was: {post_url}{RESET}")
278 | logging.error(f"{RED}This is most likely because you do not own this car or have assigned a different name for instance opel instead of Opel{RESET}")
279 | logging.error(f"{RED}The name here must be the same as in evcc!{RESET}")
280 |
281 | ########################################################################################################################
282 | # Calculations for the heat pump
283 | print(f"{GREEN}╔══════════════════════════════════════════════════╗{RESET}")
284 | print(f"{GREEN}║{CYAN} Now we optimize the heating... {GREEN}║{RESET}")
285 | print(f"{GREEN}╚══════════════════════════════════════════════════╝{RESET}")
286 | season = utils.get_season()
287 | if season == "summer" or season == "interim":
288 | logging.info(f"{GREEN}It is summer or interim season. Skipping the heating optimization.{RESET}")
289 | # it can happen that from one round of the program the season changes and SG Ready is still on. So we have to switch it to pv mode
290 | home.switch_heatpump_to_mode(heatpump_id, "pv")
291 | else:
292 | # Calculate different time parameters
293 | # BUG: [high prio] block price is higher than boost price. calculation boost is wrong
294 | price_limit_blocking = utils.calculate_price_limit_blocktime(weather_forecast, electricity_prices, settings)
295 | price_limit_boostmode = utils.calculate_price_limit_boostmode(settings, hourly_climate_energy, electricity_prices)
296 | current_price = utils.get_current_electricity_price(electricity_prices)
297 | # get basic parameters for the heating and blocking logic
298 | heatpump_id = utils.get_heatpump_id(settings) # +1 already in get_heatpump_id() IDs in the api POST begin with 1 however in the settings and /api/state with 0
299 |
300 | # Decision: force heating / normal mode / blocking mode
301 | # price limit for boostig can be set via evcc api
302 | # FIXME: [medium prio] off and price limit can be set in the same round
303 |
304 | logging.debug(f"{GREY}Heatpump ID: {heatpump_id}{RESET}")
305 | logging.debug(f"{GREY}Price limit for blocking: {price_limit_blocking}{RESET}")
306 | logging.debug(f"{GREY}Price limit for boosting: {price_limit_boostmode}{RESET}")
307 |
308 | # BOOST MODE - we can always set this mode as it does not interfere with anything else
309 | post_url = f"{evcc_base_url}/api/loadpoints/{heatpump_id}/smartcostlimit/{price_limit_boostmode}"
310 | response = requests.post(post_url)
311 | cache_folder = 'cache'
312 | cache_file = os.path.join(cache_folder, 'heatpump.log')
313 | if response.status_code == 200:
314 | logging.info(f"{GREEN}Successfully set smart cost limit for heat pump.{RESET}")
315 | with open(cache_file, "a") as log_file:
316 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
317 | log_file.write(f"{timestamp} - SG: price limit {price_limit_boostmode} \n")
318 | else:
319 | logging.error(f"{RED}Failed to set smart cost limit for heat pump. Status code: {response.status_code}{RESET}")
320 |
321 | # NORMAL MODE - also does not interfere with anything else
322 | if price_limit_boostmode <= current_price < price_limit_blocking:
323 | home.switch_heatpump_to_mode(heatpump_id, "pv")
324 | with open(cache_file, "a") as log_file:
325 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
326 | log_file.write(f"{timestamp} - SG: pv \n")
327 |
328 |
329 | # blocking is more tricky - when the current price is in the blocking range --> block
330 | # TODO:[medium prio] Think about logic. Is made sure that blocking is only for x hours in y hours?
331 | if current_price >= price_limit_blocking:
332 | for _ in range(8): # there seems to be a bug in evcc - we have to send the command multiple times
333 | home.switch_heatpump_to_mode(heatpump_id, "off")
334 | time.sleep(0.2)
335 | with open(cache_file, "a") as log_file:
336 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
337 | log_file.write(f"{timestamp} - SG: off \n")
338 |
339 | # we have a maximum blocktime - as we get new priceds at 1300hrs the blocktime might be to long
340 | # therefore here is a
341 | # TODO [low prio]: global variable counting the blocked hours and then manual override
342 |
343 |
344 |
345 | ########################################################################################################################
346 | # Calculations for the house batteries
347 | print(f"{GREEN}╔══════════════════════════════════════════════════╗{RESET}")
348 | print(f"{GREEN}║{CYAN} Now we optimize the home battery... {GREEN}║{RESET}")
349 | print(f"{GREEN}╚══════════════════════════════════════════════════╝{RESET}")
350 | logging.info(f"{GREEN}Calculations for home batteries{RESET}")
351 | home_battery_json_data = initialize_smartcharge.get_home_battery_data_from_json()
352 |
353 |
354 | home_battery_api_data = initialize_smartcharge.get_home_battery_data_from_api(evcc_state)
355 | if home_battery_json_data is None or home_battery_api_data == [{'battery_id': 0, 'battery_soc': 0, 'battery_capacity': 0}]:
356 | logging.error(f"{RED}Home battery data could not be loaded. Skipping the home battery optimization.{RESET}")
357 | potential_home_battery_energy_forecast, grid_feedin, required_charge, charging_plan, grid_feedin = None, None, None, None, None
358 | else:
359 | battery_data = home.process_battery_data(home_battery_json_data, home_battery_api_data)
360 | home_batteries_capacity = home.get_total_home_batteries_capacity() # this is the total usable capacity of all batteries
361 | home_batteries_SoC = home.get_home_battery_soc() # we refresh it every 4 minutes, so evcc_state is not suitable as it is static
362 | remaining_home_battery_capacity = home.calculate_remaining_home_battery_capacity(home_batteries_capacity, home_batteries_SoC)
363 |
364 |
365 |
366 | # this is the additional cost due to wear of battery and inverter
367 | additional_home_battery_charging_cost_per_kWh = home.get_home_battery_charging_cost_per_Wh(battery_data, evcc_state) * 1000
368 |
369 | home_battery_efficiency = home.calculate_average_battery_efficiency(battery_data, evcc_state)
370 |
371 | # Here we forcast the energy of the home battery in hourly increments
372 | # BUG [high prio]: check complete logic - acceptable price always is the highest price
373 | home_battery_energy_forecast, grid_feedin, required_charge = home.calculate_homebattery_soc_forcast_in_Wh(home_batteries_capacity, remaining_home_battery_capacity, usable_energy, hourly_climate_energy, home_battery_efficiency)
374 |
375 |
376 | # the real charging plan is done by evcc - we just set the price
377 | charging_plan = home.calculate_charging_plan(home_battery_energy_forecast, electricity_prices, additional_home_battery_charging_cost_per_kWh, battery_data, required_charge, evcc_state)
378 | maximum_acceptable_price = charging_plan
379 |
380 | # TODO: uncomment when logic is working
381 | evcc.set_upper_price_limit(maximum_acceptable_price)
382 |
383 |
384 | # first thought: we do not need to lock the battery when using grid energy is cheaper than battery energy as the battery
385 | # will be locked indirectly by the minimum price - yes but there is a price gap in between:
386 | # to expensive to charge but to cheap to use it - so we have to lock the battery
387 | # get fake loadpoint id from settings
388 |
389 |
390 | # here we handley the battery lock to minimize the grid feedin
391 | # TODO: uncomment when logic is working
392 | home.minimize_future_grid_feedin(settings, electricity_prices, usable_energy, home_battery_energy_forecast, evcc_state, maximum_acceptable_price, maximum_acceptable_price)
393 |
394 | # we guard the soc of the home battery every 4 minutes. If the current minute is 0, we exit the loop to run the main program
395 | # error handling in case there is no battery
396 | if 'home_battery_energy_forecast' not in locals() or 'grid_feedin' not in locals() or 'required_charge' not in locals() or 'home_battery_charging_cost_per_kWh' not in locals():
397 | home_battery_energy_forecast, grid_feedin, required_charge, home_battery_charging_cost_per_kWh = [], [], [], 0
398 |
399 | # even without guard we "guard" to slow down the program
400 | socGuard.initiate_guarding(GREEN, RESET, settings, home_battery_energy_forecast, home_battery_charging_cost_per_kWh)
401 |
402 | # TODO: check if there is a future_grid_feedin or just grid_feedin
403 | # data for the WebUI, might all be correct as the battery guard stops the program till the next full hour
404 | utils.json_dump_all_time_series_data(weather_forecast, hourly_climate_energy, hourly_energy_surplus, electricity_prices, home_battery_energy_forecast, grid_feedin, required_charge, charging_plan, usable_energy, solar_forecast)
405 | logging.info(f"{GREEN}EV charging optimization program completed.{RESET}")
406 |
--------------------------------------------------------------------------------