├── backend ├── data │ ├── maximum_soc_allowed.txt │ ├── chargingCostsHomeBattery.txt │ ├── usage_plan.json │ ├── settings_example.json │ ├── usage_plan_example.json │ ├── correction_factor_nominal_log.txt │ └── correction_factor_log.txt ├── test_environment.py ├── socGuard.py ├── evcc.py ├── vehicle.py ├── initialize_smartcharge.py ├── solarweather.py └── smartCharge.py ├── assets ├── web-ui.png ├── settings.png ├── datepicker.png ├── add-recurring.png └── project_graphic.jpg ├── www ├── promts.md ├── static │ ├── favicon.ico │ ├── Montserrat │ │ ├── Montserrat-Bold.ttf │ │ └── Montserrat-Regular.ttf │ ├── icons │ │ ├── car.svg │ │ ├── edit.svg │ │ ├── distance.svg │ │ ├── delete.svg │ │ ├── house_in.svg │ │ ├── settings.svg │ │ ├── house_out.svg │ │ └── description.svg │ ├── bootstrap-clockpicker.min.css │ ├── styles.css │ └── bootstrap-clockpicker.min.js ├── dokumentation.md ├── server.py └── templates │ ├── settings.html │ └── index.html ├── requirements.txt ├── .gitignore ├── LICENSE └── readme.md /backend/data/maximum_soc_allowed.txt: -------------------------------------------------------------------------------- 1 | 5620 -------------------------------------------------------------------------------- /backend/data/chargingCostsHomeBattery.txt: -------------------------------------------------------------------------------- 1 | 0.23153 -------------------------------------------------------------------------------- /assets/web-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coernel82/smartCharge4evcc/HEAD/assets/web-ui.png -------------------------------------------------------------------------------- /assets/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coernel82/smartCharge4evcc/HEAD/assets/settings.png -------------------------------------------------------------------------------- /www/promts.md: -------------------------------------------------------------------------------- 1 | Implement the following: 2 | 3 | The Load now button has to display a state of charge of -------------------------------------------------------------------------------- /assets/datepicker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coernel82/smartCharge4evcc/HEAD/assets/datepicker.png -------------------------------------------------------------------------------- /www/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coernel82/smartCharge4evcc/HEAD/www/static/favicon.ico -------------------------------------------------------------------------------- /assets/add-recurring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coernel82/smartCharge4evcc/HEAD/assets/add-recurring.png -------------------------------------------------------------------------------- /assets/project_graphic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coernel82/smartCharge4evcc/HEAD/assets/project_graphic.jpg -------------------------------------------------------------------------------- /www/static/Montserrat/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coernel82/smartCharge4evcc/HEAD/www/static/Montserrat/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /www/static/Montserrat/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coernel82/smartCharge4evcc/HEAD/www/static/Montserrat/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | paho-mqtt 3 | influxdb-client 4 | numpy 5 | pandas 6 | requests 7 | scipy 8 | scikit-learn 9 | # for the webserver 10 | flask_cors -------------------------------------------------------------------------------- /backend/test_environment.py: -------------------------------------------------------------------------------- 1 | import utils 2 | 3 | if __name__ == "__main__": 4 | # Call your test functions here 5 | for _ in range(1): 6 | utils.update_correction_factor() -------------------------------------------------------------------------------- /www/dokumentation.md: -------------------------------------------------------------------------------- 1 | pip install flask-cors 2 | 3 | 4 | # Idee 5 | Batterieladung 6 | floating average über 10 Tage aus Influx von Netzbezug und Einspeisung 7 | 8 | das durch 10 --> ergibt Unter bzw. Überdeckung der Batterie -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignoriere den __pycache__ Ordner 2 | __pycache__/ 3 | 4 | # Ignoriere das Verzeichnis dont publish 5 | dontpublish/ 6 | node_modules/ 7 | 8 | # Ignoriere die settings.json Datei 9 | backend/data/settings.json 10 | backend/data/usage_plan.json 11 | 12 | cache/ 13 | *cache* 14 | 15 | 16 | # Ignoriere die Datei Zugangsdaten.py 17 | Zugangsdaten.py 18 | 19 | 20 | SmartCharge.code-workspace 21 | settings.json 22 | Abhängigkeit Temperatur und realie Reichweite.ods 23 | 24 | 25 | # Files created from the program (but no cache) 26 | www/templates/time_series_data.json 27 | backend/data/correction_factor_nominal_log.txt 28 | 29 | -------------------------------------------------------------------------------- /www/static/icons/car.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /backend/data/usage_plan.json: -------------------------------------------------------------------------------- 1 | { 2 | "Opel": { 3 | "recurring": [ 4 | { 5 | "description": "another day", 6 | "departure_date": "Wednesday", 7 | "departure_time": "13:00", 8 | "distance": 75, 9 | "return_date": "Wednesday", 10 | "return_time": "19:00", 11 | "id": "2" 12 | }, 13 | { 14 | "description": "daily trip", 15 | "departure_date": "Friday", 16 | "departure_time": "13:00", 17 | "distance": 70, 18 | "return_date": "Friday", 19 | "return_time": "19:00", 20 | "id": "daily-7" 21 | } 22 | ] 23 | } 24 | } -------------------------------------------------------------------------------- /www/static/icons/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2024] [Cornelius Berger] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the *following* conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,** 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /www/static/icons/distance.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /www/static/icons/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /www/static/icons/house_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /www/static/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /www/static/icons/house_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /www/static/icons/description.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /www/static/bootstrap-clockpicker.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * ClockPicker v0.0.7 for Bootstrap (http://weareoutman.github.io/clockpicker/) 3 | * Copyright 2014 Wang Shenwei. 4 | * Licensed under MIT (https://github.com/weareoutman/clockpicker/blob/gh-pages/LICENSE) 5 | */.clockpicker .input-group-addon{cursor:pointer}.clockpicker-moving{cursor:move}.clockpicker-align-left.popover>.arrow{left:25px}.clockpicker-align-top.popover>.arrow{top:17px}.clockpicker-align-right.popover>.arrow{left:auto;right:25px}.clockpicker-align-bottom.popover>.arrow{top:auto;bottom:6px}.clockpicker-popover .popover-title{background-color:#fff;color:#999;font-size:24px;font-weight:700;line-height:30px;text-align:center}.clockpicker-popover .popover-title span{cursor:pointer}.clockpicker-popover .popover-content{background-color:#f8f8f8;padding:12px}.popover-content:last-child{border-bottom-left-radius:5px;border-bottom-right-radius:5px}.clockpicker-plate{background-color:#fff;border:1px solid #ccc;border-radius:50%;width:200px;height:200px;overflow:visible;position:relative;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.clockpicker-canvas,.clockpicker-dial{width:200px;height:200px;position:absolute;left:-1px;top:-1px}.clockpicker-minutes{visibility:hidden}.clockpicker-tick{border-radius:50%;color:#666;line-height:26px;text-align:center;width:26px;height:26px;position:absolute;cursor:pointer}.clockpicker-tick.active,.clockpicker-tick:hover{background-color:#c0e5f7;background-color:rgba(0,149,221,.25)}.clockpicker-button{background-image:none;background-color:#fff;border-width:1px 0 0;border-top-left-radius:0;border-top-right-radius:0;margin:0;padding:10px 0}.clockpicker-button:hover{background-image:none;background-color:#ebebeb}.clockpicker-button:focus{outline:0!important}.clockpicker-dial{-webkit-transition:-webkit-transform 350ms,opacity 350ms;-moz-transition:-moz-transform 350ms,opacity 350ms;-ms-transition:-ms-transform 350ms,opacity 350ms;-o-transition:-o-transform 350ms,opacity 350ms;transition:transform 350ms,opacity 350ms}.clockpicker-dial-out{opacity:0}.clockpicker-hours.clockpicker-dial-out{-webkit-transform:scale(1.2,1.2);-moz-transform:scale(1.2,1.2);-ms-transform:scale(1.2,1.2);-o-transform:scale(1.2,1.2);transform:scale(1.2,1.2)}.clockpicker-minutes.clockpicker-dial-out{-webkit-transform:scale(.8,.8);-moz-transform:scale(.8,.8);-ms-transform:scale(.8,.8);-o-transform:scale(.8,.8);transform:scale(.8,.8)}.clockpicker-canvas{-webkit-transition:opacity 175ms;-moz-transition:opacity 175ms;-ms-transition:opacity 175ms;-o-transition:opacity 175ms;transition:opacity 175ms}.clockpicker-canvas-out{opacity:.25}.clockpicker-canvas-bearing,.clockpicker-canvas-fg{stroke:none;fill:#0095dd}.clockpicker-canvas-bg{stroke:none;fill:#c0e5f7}.clockpicker-canvas-bg-trans{fill:rgba(0,149,221,.25)}.clockpicker-canvas line{stroke:#0095dd;stroke-width:1;stroke-linecap:round}.clockpicker-button.am-button{margin:1px;padding:5px;border:1px solid rgba(0,0,0,.2);border-radius:4px}.clockpicker-button.pm-button{margin:1px 1px 1px 136px;padding:5px;border:1px solid rgba(0,0,0,.2);border-radius:4px} -------------------------------------------------------------------------------- /backend/data/settings_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "Cars": { 3 | "0": { 4 | "BATTERY_CAPACITY": 50000, 5 | "BATTERY_YEAR": 2021, 6 | "BUFFER_DISTANCE": 20, 7 | "CAR_NAME": "Opel", 8 | "CONSUMPTION": 16000, 9 | "DEGRADATION": 0.02, 10 | "default": "true" 11 | }, 12 | "1": { 13 | "BATTERY_CAPACITY": 77000, 14 | "BATTERY_YEAR": 2020, 15 | "BUFFER_DISTANCE": 20, 16 | "CAR_NAME": "VW", 17 | "CONSUMPTION": 18000, 18 | "DEGRADATION": 0.015 19 | } 20 | }, 21 | "EVCC": { 22 | "EVCC_API_BASE_URL": "http://192.168.178.28:7070" 23 | }, 24 | "EnergyAPIs": { 25 | "SOLCAST_API_URL1": "https://api.solcast.com.au/rooftop_sites/YOUR_SITE_ID1/forecasts?format=json&api_key=YOUR_API_KEY", 26 | "SOLCAST_API_URL2": "https://api.solcast.com.au/rooftop_sites/YOUR_SITE_ID2/forecasts?format=json&api_key=YOUR_API_KEY", 27 | "info": "The free plan of solcast support two sites = two urls. If you just have one url leave the second one empty", 28 | "TIBBER_API_URL": "https://api.tibber.com/v1-beta/gql", 29 | "TIBBER_HEADERS": { 30 | "Authorization": "Bearer YOUR_TIBBER_API_KEY", 31 | "Content-Type": "application/json" 32 | } 33 | }, 34 | "HolidayMode": { 35 | "HOLIDAY_MODE": false, 36 | "info": "if you are on holiday set the value to true" 37 | }, 38 | "Home": { 39 | "HomeBatteries": { 40 | "0": { 41 | "BATTERYSYSTEM_EFFICIENCY": 0.93, 42 | "BATTERY_DEGRADATION": 0.02, 43 | "BATTERY_INVERTER_LIFETIME_YEARS": 20, 44 | "BATTERY_INVERTER_PRICE": 5000, 45 | "BATTERY_LOADING_ENERGY": 3300, 46 | "BATTERY_MAXIMUM_LOADING_CYCLES_LIFETIME": 10000, 47 | "BATTERY_PURCHASE_PRICE": 5000, 48 | "BATTERY_PURCHASE_YEAR": 2024, 49 | "BATTERY_RESIDUAL_SOC": 25, 50 | "info": "if you have more than one battery go to /backend/data/settings.json and add a new battery", 51 | "info3": "if you do not have a battery set the values to 0, except cycles and efficiency as that results in division by zero" 52 | } 53 | } 54 | }, 55 | "House": { 56 | "CURTAILMENT_THRESHOLD": 0.6, 57 | "ENERGY_CERTIFICATE": 7000, 58 | "FAKE_LOADPOINT_ID": 2, 59 | "HEATED_AREA": 222, 60 | "INDOOR_TEMPERATURE": 21, 61 | "MAXIMUM_PV": 7200, 62 | "SUMMER_THRESHOLD": 15, 63 | "SUMMER_THRESHOLD_HYSTERESIS": 1.5, 64 | "adjustment_rate": 0.2, 65 | "correction_factor_radiation": 0.8, 66 | "correction_factor_summer": 0.0046, 67 | "correction_factor_winter": -87.47498154483097, 68 | "info1": "the hysteresis will be applied as + and - to the threshold", 69 | "info2": "adjustment rate must be between 0 and 1", 70 | "info3": "do not use inverted commas for the values otherwise the calculation will not work", 71 | "integrated_devices": { 72 | "heatpump": { 73 | "COP": 4.5, 74 | "POWER": 4000 75 | } 76 | } 77 | }, 78 | "InfluxDB": { 79 | "INFLUX_ACCESS_TOKEN": "YOUR_INFLUX_ACCESS_TOKEN", 80 | "INFLUX_BASE_URL": "http://192.168.178.28:8086/", 81 | "INFLUX_BUCKET": "evcc", 82 | "INFLUX_LOADPOINT": "Wärmepumpe", 83 | "INFLUX_ORGANIZATION": "zu Hause", 84 | "TIMESPAN_WEEKS": 4, 85 | "TIMESPAN_WEEKS_BASELOAD": 4, 86 | "info": "your evcc bucket" 87 | }, 88 | "OneCallAPI": { 89 | "API_KEY": "YOUR_ONECALL_API_KEY", 90 | "LATITUDE": "51.434", 91 | "LONGITUDE": "7.114", 92 | "info": "Get your api key here: OpenWeatherMap" 93 | } 94 | } -------------------------------------------------------------------------------- /backend/data/usage_plan_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "Opel": { 3 | "recurring": [ 4 | { 5 | "description": "another day", 6 | "departure_date": "Thursday", 7 | "departure_time": "14:00", 8 | "distance": 100, 9 | "return_date": "Thursday", 10 | "return_time": "17:00", 11 | "id": "2" 12 | }, 13 | { 14 | "description": "another day", 15 | "departure_date": "Thursday", 16 | "departure_time": "19:00", 17 | "distance": 120, 18 | "return_date": "Thursday", 19 | "return_time": "22:00", 20 | "id": "23dadadd-1b1b-4b7b-8b7b-1b1b1b1b1b1b" 21 | } 22 | , 23 | { 24 | "description": "daily trip", 25 | "departure_date": "Monday", 26 | "departure_time": "08:00", 27 | "distance": 50, 28 | "return_date": "Monday", 29 | "return_time": "09:00", 30 | "id": "daily-1" 31 | }, 32 | { 33 | "description": "daily trip", 34 | "departure_date": "Tuesday", 35 | "departure_time": "08:00", 36 | "distance": 50, 37 | "return_date": "Tuesday", 38 | "return_time": "09:00", 39 | "id": "daily-2" 40 | }, 41 | { 42 | "description": "daily trip", 43 | "departure_date": "Wednesday", 44 | "departure_time": "08:00", 45 | "distance": 50, 46 | "return_date": "Wednesday", 47 | "return_time": "09:00", 48 | "id": "daily-3" 49 | }, 50 | { 51 | "description": "daily trip", 52 | "departure_date": "Thursday", 53 | "departure_time": "08:00", 54 | "distance": 50, 55 | "return_date": "Thursday", 56 | "return_time": "09:00", 57 | "id": "daily-4" 58 | }, 59 | { 60 | "description": "daily trip", 61 | "departure_date": "Friday", 62 | "departure_time": "08:00", 63 | "distance": 50, 64 | "return_date": "Friday", 65 | "return_time": "09:00", 66 | "id": "daily-5" 67 | }, 68 | { 69 | "description": "daily trip", 70 | "departure_date": "Saturday", 71 | "departure_time": "08:00", 72 | "distance": 50, 73 | "return_date": "Saturday", 74 | "return_time": "09:00", 75 | "id": "daily-6" 76 | }, 77 | { 78 | "description": "daily trip", 79 | "departure_date": "Sunday", 80 | "departure_time": "08:00", 81 | "distance": 50, 82 | "return_date": "Sunday", 83 | "return_time": "09:00", 84 | "id": "daily-7" 85 | } 86 | ], 87 | "non_recurring": [ 88 | { 89 | "departure_date": "2024-12-29", 90 | "departure_time": "10:00", 91 | "return_date": "2024-12-29", 92 | "return_time": "14:00", 93 | "distance": 100, 94 | "description": "Testfahrt", 95 | "id": "nrt-517338af-896d-4da1-a621-0c8709e3494a" 96 | } 97 | ] 98 | }, 99 | "VW": { 100 | "recurring": [ 101 | { 102 | "description": "To Space", 103 | "departure_date": "Monday", 104 | "departure_time": "12:05", 105 | "distance": 60, 106 | "return_date": "Monday", 107 | "return_time": "17:00", 108 | "id": "6" 109 | }, 110 | { 111 | "description": "To the moon", 112 | "departure_date": "Tuesday", 113 | "departure_time": "12:59", 114 | "distance": 12, 115 | "return_date": "Tuesday", 116 | "return_time": "12:05", 117 | "id": "7" 118 | } 119 | ], 120 | "non_recurring": [] 121 | } 122 | } -------------------------------------------------------------------------------- /www/static/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Montserrat'; 3 | src: url('/static/Montserrat/Montserrat-Regular.ttf') format('truetype'); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: 'Montserrat'; 10 | src: url('/static/Montserrat/Montserrat-Bold.ttf') format('truetype'); 11 | font-weight: bold; 12 | font-style: normal; 13 | } 14 | 15 | :root { 16 | --primary-color: #333; 17 | --secondary-color: #93949e; 18 | --background-color: #f3f3f7; 19 | --subtle: #e9eef5; 20 | --bs-primary-bg-subtle: white!important; 21 | --color: #ccc; 22 | } 23 | 24 | 25 | 26 | /* Apply Montserrat Bold to Headings */ 27 | h1, h2, h3, h4, h5, h6 { 28 | font-family: 'Montserrat', sans-serif; 29 | font-weight: 700; /* Bold */ 30 | } 31 | 32 | 33 | h1 { 34 | color: var(--primary-color); 35 | 36 | } 37 | 38 | 39 | 40 | h2 { 41 | color: var(--secondary-color); 42 | } 43 | 44 | body { 45 | background: var(--background-color); 46 | font-family: 'Montserrat', sans-serif; 47 | margin: 0; 48 | padding: 0; 49 | } 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | /* Bootstrap modifications */ 59 | .container { 60 | margin-top: 3em; 61 | padding-bottom: 6em; 62 | } 63 | 64 | .trip-actions { 65 | display: flex; 66 | justify-content: flex-end; 67 | } 68 | 69 | /* FIXME: trip-actions hover must probably be other class */ 70 | .trip-actions:hover { 71 | color: darken(var(--secondary-color), 10%); 72 | } 73 | 74 | 75 | .alert { 76 | border-radius: 2rem !important; 77 | } 78 | 79 | .alert-primary { 80 | background-color: white; 81 | border-color: white; 82 | --bs-alert-color: var(--primary-color); 83 | 84 | } 85 | 86 | .spacer-right { 87 | margin-right: 0.5em; 88 | } 89 | 90 | .close { 91 | border: none; 92 | color: var(--primary-color); 93 | background-color: white; 94 | border-radius: 50%; 95 | width: 2cm; 96 | height: 2cm; 97 | text-align: center; 98 | line-height: 30px; 99 | display: flex; 100 | align-items: center; 101 | justify-content: center; 102 | cursor: pointer; 103 | position: absolute; 104 | top: 18px; 105 | right: 10px; 106 | } 107 | 108 | 109 | .close:hover { 110 | color: darken(var(--primary-color), 10%); 111 | } 112 | 113 | .btn-close:click { 114 | display: none; 115 | } 116 | 117 | #open-settings-btn:hover, 118 | #add-weekly:hover, 119 | #add-non-recurring:hover { 120 | filter: brightness(0.8); 121 | cursor: pointer; 122 | background-color: var(--subtle); 123 | } 124 | 125 | .modal { 126 | backdrop-filter: blur(5px); 127 | } 128 | 129 | 130 | .modal-dialog { 131 | width: 98%; 132 | } 133 | 134 | @media (max-width: 1024px) { 135 | .container { 136 | margin: 0.2em; 137 | } 138 | 139 | } 140 | 141 | /* Increase font size on mobile devices */ 142 | @media (max-width: 767px) { 143 | body p .alert .alert-primary { 144 | font-size: 5.2rem!important; 145 | } 146 | 147 | .bottom-bar a, 148 | .bottom-bar button { 149 | font-size: 1rem; 150 | } 151 | } 152 | 153 | /* Toggle switch */ 154 | /* Toggle Switch Styles */ 155 | .switch { 156 | position: relative; 157 | display: inline-block; 158 | width: 60px; 159 | height: 34px; 160 | } 161 | 162 | .switch input { 163 | opacity: 0; 164 | width: 0; 165 | height: 0; 166 | } 167 | 168 | .slider { 169 | position: absolute; 170 | cursor: pointer; 171 | top: 0; 172 | left: 0; 173 | right: 0; 174 | bottom: 0; 175 | background-color: var(--subtle); 176 | transition: 0.4s; 177 | } 178 | 179 | .slider:before { 180 | position: absolute; 181 | content: ""; 182 | height: 26px; 183 | width: 26px; 184 | left: 4px; 185 | bottom: 4px; 186 | background-color: white; 187 | transition: 0.4s; 188 | } 189 | 190 | input:checked + .slider { 191 | background-color: var(--primary-color); 192 | } 193 | 194 | input:focus + .slider { 195 | box-shadow: 0 0 1px var(--primary-color); 196 | } 197 | 198 | input:checked + .slider:before { 199 | transform: translateX(26px); 200 | } 201 | 202 | /* Rounded sliders */ 203 | .slider.round { 204 | border-radius: 34px; 205 | } 206 | 207 | .slider.round:before { 208 | border-radius: 50%; 209 | } 210 | -------------------------------------------------------------------------------- /backend/socGuard.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import requests 4 | import logging 5 | import home 6 | import datetime 7 | 8 | # Farbige Konsole 9 | class ColorFormatter(logging.Formatter): 10 | GREEN = '\033[92m' 11 | RED = '\033[91m' 12 | RESET = '\033[0m' 13 | 14 | def format(self, record): 15 | if record.levelno == logging.INFO: 16 | record.msg = f"{self.GREEN}{record.msg}{self.RESET}" 17 | elif record.levelno == logging.ERROR: 18 | record.msg = f"{self.RED}{record.msg}{self.RESET}" 19 | return super().format(record) 20 | 21 | # Logger konfigurieren 22 | logger = logging.getLogger() 23 | logger.setLevel(logging.DEBUG) 24 | handler = logging.StreamHandler() 25 | formatter = ColorFormatter('%(message)s') 26 | handler.setFormatter(formatter) 27 | logger.addHandler(handler) 28 | 29 | # BUG: [from 2025-01-10 high prio] home_battery_energy_forecast has always the same value 30 | def guard_home_battery_soc(settings, home_battery_energy_forecast, chargingCosts): 31 | EVCC_API_BASE_URL = settings['EVCC']['EVCC_API_BASE_URL'] 32 | 33 | # get maximum_soc_allowed from list home_battery_energy_forecast 34 | # it always is index 0 as we need it for the current hour 35 | maximum_soc_allowed = home_battery_energy_forecast[0]['energy'] 36 | 37 | # get current SoC from home battery - as we need it every 4 minutes it really comes from the api and not from evcc_state 38 | currentSoC = home.get_home_battery_soc() 39 | 40 | # feedin abrufen 41 | try: 42 | response = requests.get(f"{EVCC_API_BASE_URL}/api/tariff/feedin") 43 | response.raise_for_status() 44 | feedin_data = response.json() 45 | now = datetime.datetime.now() 46 | feedin = 0.0 47 | for rate in feedin_data['result']['rates']: 48 | start = datetime.datetime.fromisoformat(rate['start']).replace(tzinfo=None) 49 | end = datetime.datetime.fromisoformat(rate['end']).replace(tzinfo=None) 50 | if start <= now < end: 51 | feedin = rate['price'] 52 | break 53 | except Exception as e: 54 | logger.error(f"Fehler beim Abrufen von feedin: {e}") 55 | return 56 | 57 | # Vergleichen und Aktion durchführen 58 | if currentSoC >= maximum_soc_allowed: 59 | cost = feedin - chargingCosts 60 | 61 | 62 | # TODO: negative value. also -40.058.... results in -4.005.8 cents 63 | # could be correct as there can be negative energy costs 64 | # Guarding home battery charge 65 | # Still guarding! 66 | # Retrieving home battery SoC from http://192.168.178.28:7070/api/state 67 | # Current home battery SoC: 51% 68 | # POST zu /api/batterygridchargelimit/-40.05805630386612 erfolgreich. 69 | # Serverantwort: {"result":-40.05805630386612} 70 | 71 | 72 | 73 | # POST Request ausführen 74 | try: 75 | response = requests.post(f"{EVCC_API_BASE_URL}/api/batterygridchargelimit/{cost}") 76 | response.raise_for_status() 77 | logger.info(f"POST zu /api/batterygridchargelimit/{cost} erfolgreich.") 78 | logger.info(f"Serverantwort: {response.text}") 79 | except Exception as e: 80 | logger.error(f"Fehler beim POST Request: {e}") 81 | return 82 | else: 83 | logger.info("currentSoC ist kleiner als maximum_soc_allowed. Keine Aktion erforderlich.") 84 | 85 | def initiate_guarding(GREEN, RESET, settings, home_battery_energy_forecast, home_battery_charging_cost_per_kWh): 86 | # Guarde the home battery every 4 minutes and break the loop just before the full hour 87 | import time 88 | end_time = datetime.datetime.now() + datetime.timedelta(minutes=4) 89 | while datetime.datetime.now() < end_time: 90 | logging.info(f"{GREEN}Guarding home battery charge / slowing down the program (when there is no home battery){RESET}") 91 | current_time = datetime.datetime.now() 92 | # Calculate the remaining time until the next full hour 93 | seconds_until_full_hour = (60 - current_time.minute) * 60 - current_time.second 94 | if seconds_until_full_hour <= 0: 95 | # If it's already past the full hour, break the loop 96 | break 97 | 98 | while seconds_until_full_hour > 0: 99 | sleep_duration = min(60, seconds_until_full_hour) 100 | time.sleep(sleep_duration) 101 | logging.info(f"{GREEN}Still guarding!{RESET}") 102 | seconds_until_full_hour -= sleep_duration 103 | if seconds_until_full_hour <= 4 * 60: 104 | break 105 | guard_home_battery_soc(settings, home_battery_energy_forecast, home_battery_charging_cost_per_kWh) -------------------------------------------------------------------------------- /backend/evcc.py: -------------------------------------------------------------------------------- 1 | # evcc.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 datetime 9 | import logging 10 | import requests 11 | import json 12 | import os 13 | import initialize_smartcharge 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 | 25 | EVCC_API_BASE_URL = initialize_smartcharge.settings['EVCC']['EVCC_API_BASE_URL'] 26 | 27 | def get_evcc_state(): 28 | logging.debug(f"{GREEN}Retrieving EVCC state from {EVCC_API_BASE_URL}/api/state{RESET}") 29 | try: 30 | # we must query the api in this case as we need fresh data every 4 minutes 31 | response = requests.get(f"{EVCC_API_BASE_URL}/api/state") 32 | response.raise_for_status() # Check for HTTP errors 33 | evcc_state = response.json() 34 | # logging.debug(f"{GREY}Response from EVCC API: {evcc_state}{RESET}") 35 | return evcc_state 36 | except requests.RequestException as e: 37 | logging.critical(f"{RED}Error retrieving the EVCC state: {e}{RESET}") 38 | # stop the whole program if the EVCC state cannot be retrieved as this is vital 39 | raise SystemExit 40 | 41 | def get_evcc_minsoc(car_name, evcc_state): 42 | logging.debug(f"{GREEN}Retrieving EVCC minSoC for {car_name} from evcc_state") 43 | cache_file = "evcc_minsoc_cache.json" 44 | lock_file = "evcc_minsoc_cache.lock" 45 | 46 | # Wenn Lockfile existiert, nicht überschreiben 47 | if os.path.exists(lock_file): 48 | logging.debug(f"{YELLOW}Lockfile {lock_file} exists. Not overwriting minSoC cache.{RESET}") 49 | return None 50 | 51 | 52 | 53 | # Suche nach dem Fahrzeug mit dem Namen car_name in der API-Antwort 54 | vehicles = evcc_state.get('result', {}).get('vehicles', {}) 55 | logging.debug(f"{GREEN}Vehicles retrieved: {vehicles}{RESET}") 56 | 57 | # Überprüfe, ob car_name in vehicles vorhanden ist 58 | if car_name in vehicles: 59 | vehicle = vehicles[car_name] 60 | min_soc = vehicle.get('minSoc') 61 | logging.debug(f"{GREEN}Retrieved minSoC value for {car_name}: {min_soc}{RESET}") 62 | 63 | if min_soc is not None: 64 | logging.debug(f"{GREEN}Caching vehicle minSoC: {min_soc}{RESET}") 65 | try: 66 | with open(cache_file, "w") as f: 67 | json.dump({"min_soc": min_soc}, f) 68 | logging.debug(f"{GREEN}minSoC successfully cached in {cache_file}{RESET}") 69 | # Lockfile erstellen 70 | with open(lock_file, 'w') as f: 71 | f.write('locked') 72 | logging.debug(f"{GREEN}Created lockfile {lock_file}{RESET}") 73 | except Exception as file_error: 74 | logging.error(f"{RED}Error writing to cache file: {file_error}{RESET}") 75 | return min_soc 76 | else: 77 | logging.error(f"{RED}The minSoC could not be found for vehicle {car_name}{RESET}") 78 | return None 79 | else: 80 | logging.error(f"{RED}No vehicle with the name {car_name} found in the API response{RESET}") 81 | return None 82 | 83 | 84 | def set_upper_price_limit(upper_limit_price_battery): 85 | """ 86 | Sets the upper price limit for battery charging via API. 87 | """ 88 | if upper_limit_price_battery is not None: 89 | post_url = f"{EVCC_API_BASE_URL}/api/batterygridchargelimit/{upper_limit_price_battery}" 90 | logging.debug(f"{GREY}Setting upper price limit via URL: {post_url}{RESET}") 91 | try: 92 | response = requests.post(post_url) 93 | response.raise_for_status() 94 | logging.info(f"{GREEN}Successfully set upper price limit: {upper_limit_price_battery:.4f} Euro{RESET}") 95 | except Exception as e: 96 | logging.error(f"{RED}Failed to set upper price limit: {e}{RESET}") 97 | else: 98 | logging.warning("Upper limit price is None. Cannot set the price limit.") 99 | 100 | def lock_battery(fake_loadpoint_id, lock): 101 | """ 102 | Locks the battery to prevent discharging. This is done by a trick: There is no direct locking option in evcc, however 103 | setting a loadpoint to quick charge ("now") will lock the home battery. For this you just need to 104 | set up a fake loadpoint: 105 | https://github.com/evcc-io/evcc/wiki/aaa-Lifehacks#entladung-eines-steuerbaren-hausspeicher-preisgesteuert-oder-manuell-sperren 106 | """ 107 | if lock == True: 108 | post_url_dischargecontrol = f"{EVCC_API_BASE_URL}/api/batterydischargecontrol/true" 109 | post_url_chargemode = f"{EVCC_API_BASE_URL}/api/loadpoints/{fake_loadpoint_id}/mode/now" 110 | logging.debug(f"{GREY}Locking battery via URL: {post_url_dischargecontrol}{RESET}") 111 | else: 112 | post_url_dischargecontrol = f"{EVCC_API_BASE_URL}/api/batterydischargecontrol/false" 113 | post_url_chargemode = f"{EVCC_API_BASE_URL}/api/loadpoints/{fake_loadpoint_id}/mode/off" 114 | logging.debug(f"{GREY}Unlocking battery via URL: {post_url_chargemode}{RESET}") 115 | 116 | 117 | try: 118 | response = requests.post(post_url_chargemode) 119 | response.raise_for_status() 120 | logging.info(f"{GREEN}Successfully locked battery{RESET}") 121 | except Exception as e: 122 | logging.error(f"{RED}Failed to lock battery. Refer to the readme - this is likely due to not having set up a fake loadpoint: {e}{RESET}") 123 | 124 | try: 125 | response = requests.post(post_url_dischargecontrol) 126 | response.raise_for_status() 127 | logging.debug(f"Successfully activated battery discharge control") 128 | except Exception as e: 129 | logging.error(f"{RED}Failed to activate battery discharge control: {e}{RESET}") 130 | 131 | 132 | def set_dischargecontrol(is_active): 133 | if is_active == True: 134 | is_active = "true" 135 | else: 136 | is_active = "false" 137 | post_url = f"{EVCC_API_BASE_URL}/api/batterydischargecontrol/{is_active}" 138 | logging.debug(f"{GREY}Setting discharge control via URL: {post_url}{RESET}") 139 | try: 140 | response = requests.post(post_url) 141 | response.raise_for_status() 142 | logging.info(f"{GREEN}Successfully set discharge control: {False}{RESET}") 143 | except Exception as e: 144 | logging.error(f"{RED}Failed to set discharge control: {e}{RESET}") 145 | 146 | def get_electricity_prices(): 147 | """ 148 | Get the electricity prices from the EVCC API 149 | """ 150 | logging.debug(f"{GREEN}Retrieving electricity prices from {EVCC_API_BASE_URL}/api/tariff/grid{RESET}") 151 | try: 152 | response = requests.get(f"{EVCC_API_BASE_URL}/api/tariff/grid") 153 | response.raise_for_status() # Check for HTTP errors 154 | prices = response.json() 155 | logging.debug(f"{GREY}Response from EVCC API: {prices}{RESET}") 156 | 157 | # transform the new JSON (prices["result"]["rates"]) to match the old format ("total": x, "startsAt": y, ...) 158 | new_rates = prices.get("result", {}).get("rates", []) 159 | old_format_prices = [] 160 | for rate in new_rates: 161 | old_format_prices.append({ 162 | "total": rate["price"], 163 | "startsAt": rate["start"] 164 | }) 165 | prices = old_format_prices 166 | 167 | return prices 168 | except requests.RequestException as e: 169 | logging.critical(f"{RED}Error retrieving the electricity prices: {e}{RESET}") 170 | # stop the whole program if the EVCC state cannot be retrieved as this is vital 171 | raise SystemExit -------------------------------------------------------------------------------- /www/server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, request, render_template, send_from_directory 2 | import json 3 | import os 4 | from flask_cors import CORS 5 | import uuid 6 | 7 | app = Flask(__name__) 8 | 9 | # Enable CORS for all routes 10 | # CORS(app, resources={r"/*": {"origins": "*"}}) 11 | CORS(app) 12 | 13 | # Load data from JSON file 14 | def load_data(): 15 | with open('usage_plan.json', 'r') as f: 16 | return json.load(f) 17 | 18 | # Save data to JSON file 19 | def save_data(data): 20 | with open('usage_plan.json', 'w') as f: 21 | json.dump(data, f, indent=4) 22 | 23 | # Serve the main HTML file 24 | @app.route('/') 25 | def index(): 26 | return render_template('index.html') 27 | 28 | # Serve the settings HTML file 29 | @app.route('/settings.html') 30 | def settings(): 31 | return render_template('settings.html') 32 | 33 | # App route for favicon 34 | @app.route('/favicon.ico') 35 | def favicon(): 36 | return send_from_directory('static', 'favicon.ico') 37 | 38 | # Serve static files 39 | @app.route('/static/') 40 | def send_static(path): 41 | return send_from_directory('static', path) 42 | 43 | @app.route('/templates/time_series_data.json') 44 | def serve_time_series_data(): 45 | return send_from_directory('templates', 'time_series_data.json') 46 | 47 | # Path to the usage_plan.json file 48 | USAGE_PLAN = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'backend', 'data', 'usage_plan.json')) 49 | # USAGE_PLAN = os.path.join(os.path.dirname(__file__), 'usage_plan.json') 50 | 51 | SETTINGS = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'backend', 'data', 'settings.json')) 52 | 53 | def load_data(): 54 | """Load data from the JSON file""" 55 | with open(USAGE_PLAN, 'r') as f: 56 | return json.load(f) 57 | 58 | 59 | def save_data(data): 60 | """Save data to the JSON file""" 61 | with open(USAGE_PLAN, 'w') as f: 62 | json.dump(data, f, indent=4) 63 | 64 | @app.route('/get_data') 65 | def get_data(): 66 | """API endpoint to return the data from usage_plan.json""" 67 | data = load_data() 68 | return jsonify(data) 69 | 70 | @app.route('/load_settings') 71 | def load_settings(): 72 | try: 73 | settings_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'backend', 'data', 'settings.json')) 74 | with open(settings_path, 'r') as f: 75 | data = json.load(f) 76 | return jsonify(data) 77 | except Exception as e: 78 | return jsonify({'status': 'error', 'message': str(e)}), 500 79 | 80 | @app.route('/add_non_recurring_trip', methods=['POST']) 81 | def add_non_recurring_trip(): 82 | """API endpoint to add a non-recurring trip""" 83 | try: 84 | data = load_data() # Load current JSON data 85 | new_trip = request.json # Get the new trip data from the request 86 | 87 | # Extrahieren Sie die Marke aus den neuen Fahrtdaten 88 | brand = new_trip.get('brand') 89 | if not brand: 90 | return jsonify({'error': 'Brand not specified'}), 400 91 | 92 | if brand not in data: 93 | return jsonify({'error': f'Brand "{brand}" does not exist'}), 400 94 | 95 | # Entfernen Sie die Marke aus den Fahrtdaten, da sie nicht innerhalb der Fahrt benötigt wird 96 | new_trip.pop('brand', None) 97 | 98 | # Add a unique id to the trip 99 | new_trip['id'] = 'nrt-' + str(uuid.uuid4()) 100 | 101 | 102 | # Fügen Sie die neue Fahrt zur non_recurring Liste der entsprechenden Marke hinzu 103 | data[brand]['non_recurring'].append(new_trip) 104 | save_data(data) 105 | 106 | return jsonify({'message': 'Trip added successfully'}), 201 107 | except Exception as e: 108 | print(f"Error adding trip: {e}") 109 | return jsonify({'error': str(e)}), 500 110 | 111 | @app.route('/add_recurring_trip', methods=['POST']) 112 | def add_recurring_trip(): 113 | """API endpoint to add a recurring trip""" 114 | try: 115 | data = load_data() # Load current JSON data 116 | new_trip = request.json # Get the new recurring trip data from the request 117 | 118 | # Extrahieren Sie die Marke aus den neuen Fahrtdaten 119 | brand = new_trip.get('brand') 120 | if not brand: 121 | return jsonify({'error': 'Brand not specified'}), 400 122 | 123 | if brand not in data: 124 | return jsonify({'error': f'Brand "{brand}" does not exist'}), 400 125 | 126 | # Entfernen Sie die Marke aus den Fahrtdaten, da sie nicht innerhalb der Fahrt benötigt wird 127 | new_trip.pop('brand', None) 128 | 129 | # Add a unique id to the trip 130 | new_trip['id'] = 'rt-' + str(uuid.uuid4()) 131 | 132 | # Add the new trip to the recurring list 133 | data[brand]['recurring'].append(new_trip) 134 | 135 | # Save the updated data to the JSON file 136 | save_data(data) 137 | 138 | return jsonify({'status': 'success', 'message': 'Recurring trip added successfully'}), 200 139 | except Exception as e: 140 | return jsonify({'status': 'error', 'message': str(e)}), 500 141 | 142 | 143 | @app.route('/test_json') 144 | def test_json(): 145 | """Test route to check if the JSON file is being read correctly""" 146 | try: 147 | data = load_data() 148 | return jsonify({"status": "success", "data": data}), 200 149 | except Exception as e: 150 | return jsonify({"status": "error", "message": str(e)}), 500 151 | 152 | # app route save settings to ../backend/data/settings.json 153 | @app.route('/save_settings', methods=['POST']) 154 | def save_settings(): 155 | """API endpoint to save settings to settings.json""" 156 | try: 157 | settings = request.json 158 | settings_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'settings.json') 159 | with open(settings_path, 'w') as f: 160 | json.dump(settings, f, indent=4) 161 | return jsonify({'message': 'Settings saved successfully'}), 200 162 | except Exception as e: 163 | return jsonify({'error': str(e)}), 500 164 | 165 | # editing and deleting trips 166 | @app.route('/get_trip/', methods=['GET']) 167 | def get_trip(trip_id): 168 | """API endpoint to get a specific trip by ID""" 169 | try: 170 | data = load_data() 171 | for brand in data: 172 | for trip_type in ['non_recurring', 'recurring']: 173 | for trip in data[brand][trip_type]: 174 | if trip['id'] == trip_id: 175 | return jsonify({'status': 'success', 'trip': trip}), 200 176 | return jsonify({'status': 'error', 'message': 'Trip not found'}), 404 177 | except Exception as e: 178 | return jsonify({'status': 'error', 'message': str(e)}), 500 179 | 180 | @app.route('/edit_trip', methods=['POST']) 181 | def edit_trip(): 182 | """API endpoint to edit a trip""" 183 | try: 184 | data = load_data() 185 | updated_trip = request.json 186 | trip_id = updated_trip.get('id') 187 | if not trip_id: 188 | return jsonify({'error': 'Trip ID not specified'}), 400 189 | for brand in data: 190 | for trip_type in ['non_recurring', 'recurring']: 191 | for trip in data[brand][trip_type]: 192 | if trip['id'] == trip_id: 193 | trip.update(updated_trip) 194 | save_data(data) 195 | return jsonify({'message': 'Trip updated successfully'}), 200 196 | return jsonify({'error': 'Trip not found'}), 404 197 | except Exception as e: 198 | return jsonify({'error': str(e)}), 500 199 | 200 | @app.route('/delete_trip', methods=['POST']) 201 | def delete_trip(): 202 | """API endpoint to delete a trip""" 203 | try: 204 | data = load_data() 205 | trip_id = request.json.get('id') 206 | if not trip_id: 207 | return jsonify({'error': 'Trip ID not specified'}), 400 208 | for brand in data: 209 | for trip_type in ['non_recurring', 'recurring']: 210 | data[brand][trip_type] = [trip for trip in data[brand][trip_type] if trip['id'] != trip_id] 211 | save_data(data) 212 | return jsonify({'message': 'Trip deleted successfully'}), 200 213 | except Exception as e: 214 | return jsonify({'error': str(e)}), 500 215 | 216 | @app.route('/update_holiday_mode', methods=['POST']) 217 | def update_holiday_mode(): 218 | """API endpoint to update the holiday mode status""" 219 | # empty code 220 | try: 221 | data = request.get_json() 222 | holiday_mode = data.get('HOLIDAY_MODE', False) 223 | if isinstance(holiday_mode, str): 224 | holiday_mode = holiday_mode.lower() == 'true' 225 | if not isinstance(holiday_mode, bool): 226 | return jsonify({'error': 'HOLIDAY_MODE must be a boolean'}), 400 227 | settings = load_settings().json 228 | settings['HolidayMode']['HOLIDAY_MODE'] = holiday_mode 229 | settings_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'backend', 'data', 'settings.json')) 230 | with open(settings_path, 'w') as f: 231 | json.dump(settings, f, indent=4) 232 | except Exception as e: 233 | return jsonify({'error': str(e)}), 500 234 | print("Holiday mode updated") 235 | return 236 | 237 | 238 | 239 | if __name__ == '__main__': 240 | app.run(host='0.0.0.0', port=5000, debug=True) 241 | 242 | 243 | -------------------------------------------------------------------------------- /www/templates/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Settings 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | Back to dashboard 21 |

Settings

22 |
23 | 24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 | 170 | 171 | -------------------------------------------------------------------------------- /www/static/bootstrap-clockpicker.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ClockPicker v0.0.7 (http://weareoutman.github.io/clockpicker/) 3 | * Copyright 2014 Wang Shenwei. 4 | * Licensed under MIT (https://github.com/weareoutman/clockpicker/blob/gh-pages/LICENSE) 5 | */ 6 | !function(){function t(t){return document.createElementNS(p,t)}function i(t){return(10>t?"0":"")+t}function e(t){var i=++m+"";return t?t+i:i}function s(s,r){function p(t,i){var e=u.offset(),s=/^touch/.test(t.type),o=e.left+b,n=e.top+b,p=(s?t.originalEvent.touches[0]:t).pageX-o,h=(s?t.originalEvent.touches[0]:t).pageY-n,k=Math.sqrt(p*p+h*h),v=!1;if(!i||!(g-y>k||k>g+y)){t.preventDefault();var m=setTimeout(function(){c.addClass("clockpicker-moving")},200);l&&u.append(x.canvas),x.setHand(p,h,!i,!0),a.off(d).on(d,function(t){t.preventDefault();var i=/^touch/.test(t.type),e=(i?t.originalEvent.touches[0]:t).pageX-o,s=(i?t.originalEvent.touches[0]:t).pageY-n;(v||e!==p||s!==h)&&(v=!0,x.setHand(e,s,!1,!0))}),a.off(f).on(f,function(t){a.off(f),t.preventDefault();var e=/^touch/.test(t.type),s=(e?t.originalEvent.changedTouches[0]:t).pageX-o,l=(e?t.originalEvent.changedTouches[0]:t).pageY-n;(i||v)&&s===p&&l===h&&x.setHand(s,l),"hours"===x.currentView?x.toggleView("minutes",A/2):r.autoclose&&(x.minutesView.addClass("clockpicker-dial-out"),setTimeout(function(){x.done()},A/2)),u.prepend(j),clearTimeout(m),c.removeClass("clockpicker-moving"),a.off(d)})}}var h=n(V),u=h.find(".clockpicker-plate"),v=h.find(".clockpicker-hours"),m=h.find(".clockpicker-minutes"),T=h.find(".clockpicker-am-pm-block"),C="INPUT"===s.prop("tagName"),H=C?s:s.find("input"),P=s.find(".input-group-addon"),x=this;if(this.id=e("cp"),this.element=s,this.options=r,this.isAppended=!1,this.isShown=!1,this.currentView="hours",this.isInput=C,this.input=H,this.addon=P,this.popover=h,this.plate=u,this.hoursView=v,this.minutesView=m,this.amPmBlock=T,this.spanHours=h.find(".clockpicker-span-hours"),this.spanMinutes=h.find(".clockpicker-span-minutes"),this.spanAmPm=h.find(".clockpicker-span-am-pm"),this.amOrPm="PM",r.twelvehour){{var S=['
','",'","
"].join("");n(S)}n('').on("click",function(){x.amOrPm="AM",n(".clockpicker-span-am-pm").empty().append("AM")}).appendTo(this.amPmBlock),n('').on("click",function(){x.amOrPm="PM",n(".clockpicker-span-am-pm").empty().append("PM")}).appendTo(this.amPmBlock)}r.autoclose||n('").click(n.proxy(this.done,this)).appendTo(h),"top"!==r.placement&&"bottom"!==r.placement||"top"!==r.align&&"bottom"!==r.align||(r.align="left"),"left"!==r.placement&&"right"!==r.placement||"left"!==r.align&&"right"!==r.align||(r.align="top"),h.addClass(r.placement),h.addClass("clockpicker-align-"+r.align),this.spanHours.click(n.proxy(this.toggleView,this,"hours")),this.spanMinutes.click(n.proxy(this.toggleView,this,"minutes")),H.on("focus.clockpicker click.clockpicker",n.proxy(this.show,this)),P.on("click.clockpicker",n.proxy(this.toggle,this));var E,D,I,B,z=n('
');if(r.twelvehour)for(E=1;13>E;E+=1)D=z.clone(),I=E/6*Math.PI,B=g,D.css("font-size","120%"),D.css({left:b+Math.sin(I)*B-y,top:b-Math.cos(I)*B-y}),D.html(0===E?"00":E),v.append(D),D.on(k,p);else for(E=0;24>E;E+=1){D=z.clone(),I=E/6*Math.PI;var O=E>0&&13>E;B=O?w:g,D.css({left:b+Math.sin(I)*B-y,top:b-Math.cos(I)*B-y}),O&&D.css("font-size","120%"),D.html(0===E?"00":E),v.append(D),D.on(k,p)}for(E=0;60>E;E+=5)D=z.clone(),I=E/30*Math.PI,D.css({left:b+Math.sin(I)*g-y,top:b-Math.cos(I)*g-y}),D.css("font-size","120%"),D.html(i(E)),m.append(D),D.on(k,p);if(u.on(k,function(t){0===n(t.target).closest(".clockpicker-tick").length&&p(t,!0)}),l){var j=h.find(".clockpicker-canvas"),L=t("svg");L.setAttribute("class","clockpicker-svg"),L.setAttribute("width",M),L.setAttribute("height",M);var U=t("g");U.setAttribute("transform","translate("+b+","+b+")");var W=t("circle");W.setAttribute("class","clockpicker-canvas-bearing"),W.setAttribute("cx",0),W.setAttribute("cy",0),W.setAttribute("r",2);var N=t("line");N.setAttribute("x1",0),N.setAttribute("y1",0);var X=t("circle");X.setAttribute("class","clockpicker-canvas-bg"),X.setAttribute("r",y);var Y=t("circle");Y.setAttribute("class","clockpicker-canvas-fg"),Y.setAttribute("r",3.5),U.appendChild(N),U.appendChild(X),U.appendChild(Y),U.appendChild(W),L.appendChild(U),j.append(L),this.hand=N,this.bg=X,this.fg=Y,this.bearing=W,this.g=U,this.canvas=j}o(this.options.init)}function o(t){t&&"function"==typeof t&&t()}var c,n=window.jQuery,r=n(window),a=n(document),p="http://www.w3.org/2000/svg",l="SVGAngle"in window&&function(){var t,i=document.createElement("div");return i.innerHTML="",t=(i.firstChild&&i.firstChild.namespaceURI)==p,i.innerHTML="",t}(),h=function(){var t=document.createElement("div").style;return"transition"in t||"WebkitTransition"in t||"MozTransition"in t||"msTransition"in t||"OTransition"in t}(),u="ontouchstart"in window,k="mousedown"+(u?" touchstart":""),d="mousemove.clockpicker"+(u?" touchmove.clockpicker":""),f="mouseup.clockpicker"+(u?" touchend.clockpicker":""),v=navigator.vibrate?"vibrate":navigator.webkitVibrate?"webkitVibrate":null,m=0,b=100,g=80,w=54,y=13,M=2*b,A=h?350:1,V=['
','
','
',''," : ",'','',"
",'
','
','
','
','
',"
",'',"","
","
"].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 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 75 | 76 | 77 | 131 | 132 | 194 | 195 | 196 |
197 | 198 |
199 | 200 | 201 |
202 | 203 |
204 |
205 | 218 |
219 | 220 | 221 | 222 |
223 |
224 | 225 | 226 | 227 | 228 |
229 |
230 |
231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![a futuristic image of ev and solar panels](assets/project_graphic.jpg) 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 | ![](assets/add-recurring.png) 355 | ![](assets/datepicker.png) 356 | ![](assets/web-ui.png) 357 | ![](assets/settings.png) 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 | --------------------------------------------------------------------------------