emhass 0.8.4__tar.gz → 0.8.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {emhass-0.8.4 → emhass-0.8.5}/CHANGELOG.md +13 -0
  2. {emhass-0.8.4 → emhass-0.8.5}/PKG-INFO +2 -4
  3. {emhass-0.8.4 → emhass-0.8.5}/README.md +0 -2
  4. {emhass-0.8.4 → emhass-0.8.5}/setup.py +4 -3
  5. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/command_line.py +2 -2
  6. emhass-0.8.5/src/emhass/data/cec_inverters.pbz2 +0 -0
  7. emhass-0.8.5/src/emhass/data/cec_modules.pbz2 +0 -0
  8. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/forecast.py +11 -7
  9. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/optimization.py +4 -4
  10. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/retrieve_hass.py +3 -3
  11. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/static/script.js +3 -34
  12. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/static/style.css +74 -39
  13. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/templates/index.html +7 -7
  14. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/utils.py +21 -64
  15. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/web_server.py +8 -4
  16. {emhass-0.8.4 → emhass-0.8.5}/src/emhass.egg-info/PKG-INFO +2 -4
  17. {emhass-0.8.4 → emhass-0.8.5}/src/emhass.egg-info/SOURCES.txt +2 -0
  18. {emhass-0.8.4 → emhass-0.8.5}/tests/test_command_line_utils.py +8 -8
  19. {emhass-0.8.4 → emhass-0.8.5}/tests/test_forecast.py +57 -16
  20. {emhass-0.8.4 → emhass-0.8.5}/CODE_OF_CONDUCT.md +0 -0
  21. {emhass-0.8.4 → emhass-0.8.5}/CONTRIBUTING.md +0 -0
  22. {emhass-0.8.4 → emhass-0.8.5}/LICENSE +0 -0
  23. {emhass-0.8.4 → emhass-0.8.5}/MANIFEST.in +0 -0
  24. {emhass-0.8.4 → emhass-0.8.5}/data/data_load_cost_forecast.csv +0 -0
  25. {emhass-0.8.4 → emhass-0.8.5}/data/data_load_forecast.csv +0 -0
  26. {emhass-0.8.4 → emhass-0.8.5}/data/data_prod_price_forecast.csv +0 -0
  27. {emhass-0.8.4 → emhass-0.8.5}/data/data_train_load_clustering.pkl +0 -0
  28. {emhass-0.8.4 → emhass-0.8.5}/data/data_train_load_forecast.pkl +0 -0
  29. {emhass-0.8.4 → emhass-0.8.5}/data/data_weather_forecast.csv +0 -0
  30. {emhass-0.8.4 → emhass-0.8.5}/data/opt_res_latest.csv +0 -0
  31. {emhass-0.8.4 → emhass-0.8.5}/data/opt_res_perfect_optim_cost.csv +0 -0
  32. {emhass-0.8.4 → emhass-0.8.5}/data/opt_res_perfect_optim_profit.csv +0 -0
  33. {emhass-0.8.4 → emhass-0.8.5}/data/opt_res_perfect_optim_self-consumption.csv +0 -0
  34. {emhass-0.8.4 → emhass-0.8.5}/data/test_df_final.pkl +0 -0
  35. {emhass-0.8.4 → emhass-0.8.5}/data/test_response_get_data_get_method.pbz2 +0 -0
  36. {emhass-0.8.4 → emhass-0.8.5}/data/test_response_scrapper_get_method.pbz2 +0 -0
  37. {emhass-0.8.4 → emhass-0.8.5}/data/test_response_solarforecast_get_method.pbz2 +0 -0
  38. {emhass-0.8.4 → emhass-0.8.5}/data/test_response_solcast_get_method.pbz2 +0 -0
  39. {emhass-0.8.4 → emhass-0.8.5}/pyproject.toml +0 -0
  40. {emhass-0.8.4 → emhass-0.8.5}/setup.cfg +0 -0
  41. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/__init__.py +0 -0
  42. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/machine_learning_forecaster.py +0 -0
  43. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/static/advanced.html +0 -0
  44. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/static/basic.html +0 -0
  45. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/static/img/emhass_icon.png +0 -0
  46. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/static/img/emhass_logo_short.svg +0 -0
  47. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/static/img/feather-sprite.svg +0 -0
  48. {emhass-0.8.4 → emhass-0.8.5}/src/emhass/templates/template.html +0 -0
  49. {emhass-0.8.4 → emhass-0.8.5}/src/emhass.egg-info/dependency_links.txt +0 -0
  50. {emhass-0.8.4 → emhass-0.8.5}/src/emhass.egg-info/entry_points.txt +0 -0
  51. {emhass-0.8.4 → emhass-0.8.5}/src/emhass.egg-info/requires.txt +0 -0
  52. {emhass-0.8.4 → emhass-0.8.5}/src/emhass.egg-info/top_level.txt +0 -0
  53. {emhass-0.8.4 → emhass-0.8.5}/tests/test_machine_learning_forecaster.py +0 -0
  54. {emhass-0.8.4 → emhass-0.8.5}/tests/test_optimization.py +0 -0
  55. {emhass-0.8.4 → emhass-0.8.5}/tests/test_retrieve_hass.py +0 -0
  56. {emhass-0.8.4 → emhass-0.8.5}/tests/test_utils.py +0 -0
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.5 - 2024-04-01
4
+ ### Improvement
5
+ - Simplified fetch urls to relatives
6
+ - Improved code for passed forecast data error handling in utils.py
7
+ - Added new tests for forecast longer than 24h by changing parameter `delta_forecast`
8
+ - Added new files for updated PV modules and inverters database for use with PVLib
9
+ - Added a new webapp to help configuring modules and inverters: [https://emhass-pvlib-database.streamlit.app/](https://emhass-pvlib-database.streamlit.app/)
10
+ - Added a new `P_to_grid_max` variable, different from the current `P_from_grid_max` option
11
+ ### Fix
12
+ - style.css auto format and adjusted table styling
13
+ - Changed pandas datetime rounding to nonexistent='shift_forward' to help survive DST change
14
+ - Dropped support for Python 3.9
15
+
3
16
  ## 0.8.4 - 2024-03-13
4
17
  ### Improvement
5
18
  - Improved documentation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: emhass
3
- Version: 0.8.4
3
+ Version: 0.8.5
4
4
  Summary: An Energy Management System for Home Assistant
5
5
  Home-page: https://github.com/davidusb-geek/emhass
6
6
  Author: David HERNANDEZ
@@ -12,7 +12,7 @@ Classifier: Topic :: Software Development :: Build Tools
12
12
  Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Operating System :: OS Independent
15
- Requires-Python: >=3.9, <3.12
15
+ Requires-Python: >=3.10, <3.12
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: wheel
@@ -466,8 +466,6 @@ curl -i -H 'Content-Type:application/json' -X POST -d '{"pv_power_forecast":[0,
466
466
  curl -i -H 'Content-Type:application/json' -X POST -d '{"pv_power_forecast":[0, 70, 141.22, 246.18, 513.5, 753.27, 1049.89, 1797.93, 1697.3, 3078.93], "prediction_horizon":10, "soc_init":0.5,"soc_final":0.6,"def_total_hours":[1,3],"def_start_timestep":[0,3],"def_end_timestep":[0,6]}' http://localhost:5000/action/naive-mpc-optim
467
467
  ```
468
468
 
469
-
470
-
471
469
  ## A machine learning forecaster
472
470
 
473
471
  Starting in v0.4.0 a new machine learning forecaster class was introduced.
@@ -431,8 +431,6 @@ curl -i -H 'Content-Type:application/json' -X POST -d '{"pv_power_forecast":[0,
431
431
  curl -i -H 'Content-Type:application/json' -X POST -d '{"pv_power_forecast":[0, 70, 141.22, 246.18, 513.5, 753.27, 1049.89, 1797.93, 1697.3, 3078.93], "prediction_horizon":10, "soc_init":0.5,"soc_final":0.6,"def_total_hours":[1,3],"def_start_timestep":[0,3],"def_end_timestep":[0,6]}' http://localhost:5000/action/naive-mpc-optim
432
432
  ```
433
433
 
434
-
435
-
436
434
  ## A machine learning forecaster
437
435
 
438
436
  Starting in v0.4.0 a new machine learning forecaster class was introduced.
@@ -19,7 +19,7 @@ long_description = (here / 'README.md').read_text(encoding='utf-8')
19
19
 
20
20
  setup(
21
21
  name='emhass', # Required
22
- version='0.8.4', # Required
22
+ version='0.8.5', # Required
23
23
  description='An Energy Management System for Home Assistant', # Optional
24
24
  long_description=long_description, # Optional
25
25
  long_description_content_type='text/markdown', # Optional (see note above)
@@ -37,7 +37,7 @@ setup(
37
37
  keywords='energy, management, optimization, hass', # Optional
38
38
  package_dir={'': 'src'}, # Optional
39
39
  packages=find_packages(where='src'), # Required
40
- python_requires='>=3.9, <3.12',
40
+ python_requires='>=3.10, <3.12',
41
41
  install_requires=[
42
42
  'wheel',
43
43
  'numpy==1.26.4',
@@ -63,5 +63,6 @@ setup(
63
63
  ],
64
64
  },
65
65
  package_data={'emhass': ['templates/index.html','templates/template.html','static/advanced.html','static/basic.html', 'static/script.js',
66
- 'static/style.css','static/img/emhass_icon.png','static/img/emhass_logo_short.svg', 'static/img/feather-sprite.svg']},
66
+ 'static/style.css','static/img/emhass_icon.png','static/img/emhass_logo_short.svg', 'static/img/feather-sprite.svg',
67
+ 'data/cec_modules.pbz2', 'data/cec_inverters.pbz2']},
67
68
  )
@@ -59,8 +59,8 @@ def set_input_data_dict(config_path: pathlib.Path, base_path: str, costfun: str,
59
59
  optim_conf, plant_conf, set_type, logger)
60
60
  # Define main objects
61
61
  rh = RetrieveHass(retrieve_hass_conf['hass_url'], retrieve_hass_conf['long_lived_token'],
62
- retrieve_hass_conf['freq'], retrieve_hass_conf['time_zone'],
63
- params, base_path, logger, get_data_from_file=get_data_from_file)
62
+ retrieve_hass_conf['freq'], retrieve_hass_conf['time_zone'],
63
+ params, base_path, logger, get_data_from_file=get_data_from_file)
64
64
  fcst = Forecast(retrieve_hass_conf, optim_conf, plant_conf,
65
65
  params, base_path, logger, get_data_from_file=get_data_from_file)
66
66
  opt = Optimization(retrieve_hass_conf, optim_conf, plant_conf,
@@ -7,6 +7,8 @@ import copy
7
7
  import logging
8
8
  import json
9
9
  from typing import Optional
10
+ import bz2
11
+ import pickle as cPickle
10
12
  import pandas as pd
11
13
  import numpy as np
12
14
  from datetime import datetime, timedelta
@@ -21,7 +23,7 @@ from pvlib.irradiance import disc
21
23
 
22
24
  from emhass.retrieve_hass import RetrieveHass
23
25
  from emhass.machine_learning_forecaster import MLForecaster
24
- from emhass.utils import get_days_list
26
+ from emhass.utils import get_days_list, get_root
25
27
 
26
28
 
27
29
  class Forecast(object):
@@ -133,7 +135,7 @@ class Forecast(object):
133
135
  self.time_zone = self.retrieve_hass_conf['time_zone']
134
136
  self.method_ts_round = self.retrieve_hass_conf['method_ts_round']
135
137
  self.timeStep = self.freq.seconds/3600 # in hours
136
- self.time_delta = pd.to_timedelta(opt_time_delta, "hours") # The period of optimization
138
+ self.time_delta = pd.to_timedelta(opt_time_delta, "hours")
137
139
  self.var_PV = self.retrieve_hass_conf['var_PV']
138
140
  self.var_load = self.retrieve_hass_conf['var_load']
139
141
  self.var_load_new = self.var_load+'_positive'
@@ -159,7 +161,7 @@ class Forecast(object):
159
161
  self.end_forecast = (self.start_forecast + self.optim_conf['delta_forecast']).replace(microsecond=0)
160
162
  self.forecast_dates = pd.date_range(start=self.start_forecast,
161
163
  end=self.end_forecast-self.freq,
162
- freq=self.freq).round(self.freq, ambiguous='infer', nonexistent=self.freq)
164
+ freq=self.freq).round(self.freq, ambiguous='infer', nonexistent='shift_forward')
163
165
  if params is not None:
164
166
  if 'prediction_horizon' in list(self.params['passed_data'].keys()):
165
167
  if self.params['passed_data']['prediction_horizon'] is not None:
@@ -184,7 +186,7 @@ class Forecast(object):
184
186
  freq_scrap = pd.to_timedelta(60, "minutes") # The scrapping time step is 60min
185
187
  forecast_dates_scrap = pd.date_range(start=self.start_forecast,
186
188
  end=self.end_forecast-freq_scrap,
187
- freq=freq_scrap).round(freq_scrap, ambiguous='infer', nonexistent=freq_scrap)
189
+ freq=freq_scrap).round(freq_scrap, ambiguous='infer', nonexistent='shift_forward')
188
190
  # Using the clearoutside webpage
189
191
  response = get("https://clearoutside.com/forecast/"+str(round(self.lat, 2))+"/"+str(round(self.lon, 2))+"?desktop=true")
190
192
  '''import bz2 # Uncomment to save a serialized data for tests
@@ -412,8 +414,10 @@ class Forecast(object):
412
414
  # Setting the main parameters of the PV plant
413
415
  location = Location(latitude=self.lat, longitude=self.lon)
414
416
  temp_params = TEMPERATURE_MODEL_PARAMETERS['sapm']['close_mount_glass_glass']
415
- cec_modules = pvlib.pvsystem.retrieve_sam('CECMod')
416
- cec_inverters = pvlib.pvsystem.retrieve_sam('cecinverter')
417
+ cec_modules = bz2.BZ2File(get_root(__file__, num_parent=2) / 'emhass/data/cec_modules.pbz2', "rb")
418
+ cec_modules = cPickle.load(cec_modules)
419
+ cec_inverters = bz2.BZ2File(get_root(__file__, num_parent=2) / 'emhass/data/cec_inverters.pbz2', "rb")
420
+ cec_inverters = cPickle.load(cec_inverters)
417
421
  if type(self.plant_conf['module_model']) == list:
418
422
  P_PV_forecast = pd.Series(0, index=df_weather.index)
419
423
  for i in range(len(self.plant_conf['module_model'])):
@@ -476,7 +480,7 @@ class Forecast(object):
476
480
  end_forecast_csv = (start_forecast_csv + self.optim_conf['delta_forecast']).replace(microsecond=0)
477
481
  forecast_dates_csv = pd.date_range(start=start_forecast_csv,
478
482
  end=end_forecast_csv+timedelta(days=timedelta_days)-self.freq,
479
- freq=self.freq).round(self.freq, ambiguous='infer', nonexistent=self.freq)
483
+ freq=self.freq).round(self.freq, ambiguous='infer', nonexistent='shift_forward')
480
484
  if self.params is not None:
481
485
  if 'prediction_horizon' in list(self.params['passed_data'].keys()):
482
486
  if self.params['passed_data']['prediction_horizon'] is not None:
@@ -162,10 +162,10 @@ class Optimization:
162
162
 
163
163
  ## Add decision variables
164
164
  P_grid_neg = {(i):plp.LpVariable(cat='Continuous',
165
- lowBound=-self.plant_conf['P_grid_max'], upBound=0,
165
+ lowBound=-self.plant_conf['P_to_grid_max'], upBound=0,
166
166
  name="P_grid_neg{}".format(i)) for i in set_I}
167
167
  P_grid_pos = {(i):plp.LpVariable(cat='Continuous',
168
- lowBound=0, upBound=self.plant_conf['P_grid_max'],
168
+ lowBound=0, upBound=self.plant_conf['P_from_grid_max'],
169
169
  name="P_grid_pos{}".format(i)) for i in set_I}
170
170
  P_deferrable = []
171
171
  P_def_bin1 = []
@@ -267,13 +267,13 @@ class Optimization:
267
267
  # Avoid injecting and consuming from grid at the same time
268
268
  constraints.update({"constraint_pgridpos_{}".format(i) :
269
269
  plp.LpConstraint(
270
- e = P_grid_pos[i] - self.plant_conf['P_grid_max']*D[i],
270
+ e = P_grid_pos[i] - self.plant_conf['P_from_grid_max']*D[i],
271
271
  sense = plp.LpConstraintLE,
272
272
  rhs = 0)
273
273
  for i in set_I})
274
274
  constraints.update({"constraint_pgridneg_{}".format(i) :
275
275
  plp.LpConstraint(
276
- e = -P_grid_neg[i] - self.plant_conf['P_grid_max']*(1-D[i]),
276
+ e = -P_grid_neg[i] - self.plant_conf['P_to_grid_max']*(1-D[i]),
277
277
  sense = plp.LpConstraintLE,
278
278
  rhs = 0)
279
279
  for i in set_I})
@@ -133,14 +133,14 @@ class RetrieveHass:
133
133
  try: # Sometimes when there are connection problems we need to catch empty retrieved json
134
134
  data = response.json()[0]
135
135
  except IndexError:
136
- if x is 0:
136
+ if x == 0:
137
137
  self.logger.error("The retrieved JSON is empty, A sensor:" + var + " may have 0 days of history or passed sensor may not be correct")
138
138
  else:
139
139
  self.logger.error("The retrieved JSON is empty for day:"+ str(day) +", days_to_retrieve may be larger than the recorded history of sensor:" + var + " (check your recorder settings)")
140
140
  return False
141
141
  df_raw = pd.DataFrame.from_dict(data)
142
142
  if len(df_raw) == 0:
143
- if x is 0:
143
+ if x == 0:
144
144
  self.logger.error("The retrieved Dataframe is empty, A sensor:" + var + " may have 0 days of history or passed sensor may not be correct")
145
145
  else:
146
146
  self.logger.error("Retrieved empty Dataframe for day:"+ str(day) +", days_to_retrieve may be larger than the recorded history of sensor:" + var + " (check your recorder settings)")
@@ -149,7 +149,7 @@ class RetrieveHass:
149
149
  from_date = pd.to_datetime(df_raw['last_changed'], format="ISO8601").min()
150
150
  to_date = pd.to_datetime(df_raw['last_changed'], format="ISO8601").max()
151
151
  ts = pd.to_datetime(pd.date_range(start=from_date, end=to_date, freq=self.freq),
152
- format='%Y-%d-%m %H:%M').round(self.freq, ambiguous='infer', nonexistent=self.freq)
152
+ format='%Y-%d-%m %H:%M').round(self.freq, ambiguous='infer', nonexistent='shift_forward')
153
153
  df_day = pd.DataFrame(index = ts)
154
154
  # Caution with undefined string data: unknown, unavailable, etc.
155
155
  df_tp = df_raw.copy()[['state']].replace(
@@ -1,8 +1,3 @@
1
- //before page load check for stylesheet
2
- document.onreadystatechange = async function() {
3
- checkStyleSheets()
4
- }
5
-
6
1
  //on page reload get saved data
7
2
  window.onload = async function () {
8
3
 
@@ -12,23 +7,6 @@ window.onload = async function () {
12
7
  document.getElementById("basicOrAdvanced").addEventListener("click", () => SwitchBasicOrAdvanced());
13
8
  };
14
9
 
15
- //check style sheet is loaded
16
- async function checkStyleSheets() {
17
- var styleHREF = getHTMLURL() + `static/style.css`
18
- var styles = document.styleSheets;
19
- for (var i = 0; i < styles.length; i++) {
20
- if (styles[i].href.match("style")["input"] == styleHREF) {
21
- return true
22
- }
23
- }
24
- //if could not find file
25
- var style = document.createElement("link");
26
- style.rel = "stylesheet";
27
- style.href = styleHREF;
28
- style.type = "text/css";
29
- document.getElementsByTagName("head")[0].appendChild(style);
30
- }
31
-
32
10
  //add listeners to buttons (based on page)
33
11
  function loadButtons(page) {
34
12
  switch (page) {
@@ -120,19 +98,10 @@ function SwitchBasicOrAdvanced() {
120
98
  }
121
99
  }
122
100
 
123
- //set current url
124
- function getHTMLURL() {
125
- var currentUrl
126
- if (window.location) {
127
- currentUrl = window.location.href; //get current url to append
128
- }
129
- else { currentUrl = "" }
130
- return currentUrl
131
- }
132
101
 
133
102
  //get html data from basic.html or advanced.html
134
103
  async function getHTMLData(htmlFile) {
135
- const response = await fetch(getHTMLURL() + `static/` + htmlFile);
104
+ const response = await fetch(`static/` + htmlFile);
136
105
  blob = await response.blob(); //get data blob
137
106
  htmlTemplateData = await new Response(blob).text(); //obtain html from blob
138
107
  return await htmlTemplateData;
@@ -148,7 +117,7 @@ async function formAction(action, page) {
148
117
 
149
118
  if (data !== 0) { //don't run if there is an error in the input (box/list) Json data
150
119
  showChangeStatus("loading", {}); // show loading div for status
151
- const response = await fetch(getHTMLURL() + `action/${action}`, {
120
+ const response = await fetch(`action/` + action, {
152
121
  //fetch data from webserver.py
153
122
  method: "POST",
154
123
  headers: {
@@ -206,7 +175,7 @@ async function showChangeStatus(status, logJson) {
206
175
  async function getTemplate() {
207
176
  //fetch data from webserver.py
208
177
  let htmlTemplateData = "";
209
- response = await fetch(getHTMLURL() + `template/table-template`, {
178
+ response = await fetch(`template/table-template`, {
210
179
  method: "GET",
211
180
  });
212
181
  blob = await response.blob(); //get data blob
@@ -550,16 +550,16 @@ https://github.com/feathericons/feather */
550
550
  stroke-linecap: round !important;
551
551
  stroke-linejoin: round !important;
552
552
  fill: none !important;
553
- filter: drop-shadow( #282928 .2px .2px) !important;
553
+ filter: drop-shadow(#282928 .2px .2px) !important;
554
554
  -webkit-text-size-adjust: none !important;
555
555
  -ms-text-size-adjust: none !important;
556
556
 
557
557
  }
558
558
 
559
- /* feather icons no background color */
559
+ /* feather icons no background color */
560
560
  #top-links a {
561
561
  background: none !important;
562
- }
562
+ }
563
563
 
564
564
  /* -------------- */
565
565
 
@@ -586,7 +586,8 @@ select {
586
586
  }
587
587
 
588
588
 
589
- .alert, .info {
589
+ .alert,
590
+ .info {
590
591
  max-width: 50%;
591
592
  }
592
593
 
@@ -600,7 +601,7 @@ h2 {
600
601
  margin-bottom: .3em;
601
602
  }
602
603
 
603
- .table_div h4{
604
+ .table_div h4 {
604
605
  margin-top: .5em;
605
606
  }
606
607
 
@@ -683,17 +684,49 @@ button {
683
684
 
684
685
  th {
685
686
  padding: 5px 7.77px;
687
+ text-align: center;
686
688
  }
687
689
 
688
- .mystyle tr:nth-child(even) {
690
+ .mystyle tr:nth-child(even) td,
691
+ th {
689
692
  background: #e1e1e1;
690
693
  }
691
694
 
692
- .mystyle tr:hover {
693
- background: silver;
695
+ .mystyle tr:nth-child(odd) td {
696
+ background: white;
697
+ }
698
+
699
+ .mystyle tr:hover td {
700
+ background-color: silver;
694
701
  cursor: pointer;
695
702
  }
696
703
 
704
+ th:last-child {
705
+ border-top-right-radius: 7px;
706
+ }
707
+
708
+ th:first-child {
709
+ border-top-left-radius: 7px;
710
+ }
711
+
712
+ tr:last-child td:first-child {
713
+ border-bottom-left-radius: 7px;
714
+ }
715
+
716
+ tr:last-child td:last-child {
717
+ border-bottom-right-radius: 7px;
718
+ }
719
+
720
+ tr:hover td:first-child {
721
+ border-top-left-radius: 7px;
722
+ border-bottom-left-radius: 7px;
723
+ }
724
+
725
+ tr:hover td:last-child {
726
+ border-top-right-radius: 7px;
727
+ border-bottom-right-radius: 7px;
728
+ }
729
+
697
730
  #top-links {
698
731
  display: flex;
699
732
  position: absolute;
@@ -795,14 +828,20 @@ th {
795
828
  }
796
829
 
797
830
  /* Basic and Advanced fade transitions */
798
- .TabSelection, #advance, #basic, button, select, .info {
831
+ .TabSelection,
832
+ #advance,
833
+ #basic,
834
+ button,
835
+ select,
836
+ .info {
799
837
  animation-name: fadeInOpacity;
800
838
  animation-iteration-count: 1;
801
839
  animation-timing-function: ease-in-out;
802
840
  animation-duration: .3s;
803
841
  }
804
842
 
805
- .input-list, .input-box {
843
+ .input-list,
844
+ .input-box {
806
845
  animation-name: fadeInOpacity;
807
846
  animation-iteration-count: 1;
808
847
  animation-timing-function: ease-in-out;
@@ -944,7 +983,8 @@ th {
944
983
  display: none !important;
945
984
  }
946
985
 
947
- .info, .alert {
986
+ .info,
987
+ .alert {
948
988
  max-width: 100%;
949
989
  }
950
990
  }
@@ -957,15 +997,17 @@ th {
957
997
  }
958
998
 
959
999
  img,
960
- figure, svg.main-svg{
1000
+ figure,
1001
+ svg.main-svg {
961
1002
  -webkit-filter: invert(.82);
962
1003
  filter: invert(.82);
963
1004
  }
964
-
965
- figure, svg.main-svg{
966
- border-color: #181818;
967
- border-style: solid;
968
- border-width: 1px;
1005
+
1006
+ figure,
1007
+ svg.main-svg {
1008
+ border-color: #181818;
1009
+ border-style: solid;
1010
+ border-width: 1px;
969
1011
  }
970
1012
 
971
1013
  button,
@@ -1008,10 +1050,12 @@ th {
1008
1050
  .modebar-btn svg path {
1009
1051
  fill: #111 !important;
1010
1052
  }
1053
+
1011
1054
  .modebar-btn svg {
1012
- filter:invert(100%) sepia(64%) saturate(2%) hue-rotate(294deg) brightness(85%) contrast(93%) !important
1055
+ filter: invert(100%) sepia(64%) saturate(2%) hue-rotate(294deg) brightness(85%) contrast(93%) !important
1013
1056
  }
1014
- .modebar-btn--logo svg{
1057
+
1058
+ .modebar-btn--logo svg {
1015
1059
  filter: None !important;
1016
1060
  /* filter: invert(100%) saturate(100%) brightness(87%) contrast(100%) !important */
1017
1061
  }
@@ -1024,37 +1068,28 @@ th {
1024
1068
  color: #e1e1e1;
1025
1069
  }
1026
1070
 
1027
- tr td:nth-child(even),
1028
- .mystyle tr td:nth-child(even) {
1029
- background-color: #3d3d3d;
1071
+ .mystyle tr {
1072
+ background: none;
1030
1073
  }
1031
1074
 
1032
- tr:nth-child(odd),
1033
- .mystyle tr:nth-child(odd) {
1034
- background-color: #111111;
1075
+ .mystyle tr:nth-child(even) td,
1076
+ th {
1077
+ background: #282928;
1035
1078
  }
1036
1079
 
1037
- tr:nth-child(even),
1038
- .mystyle tr:nth-child(even) {
1039
- background-color: #181818;
1080
+ .mystyle tr:nth-child(odd) td {
1081
+ background: #111111;
1040
1082
  }
1041
1083
 
1042
- tr:hover td,
1043
- th {
1084
+ .mystyle tr:hover td {
1044
1085
  background-color: #3f3f3f;
1045
1086
  }
1046
1087
 
1047
- tr:hover td,
1048
- th:last-child, th:first-child, td:first-child, td:last-child
1049
- {
1050
- border-radius: 7px
1051
- }
1052
-
1053
- .modebar-group{
1088
+ .modebar-group {
1054
1089
  background-color: #0000 !important;
1055
1090
  }
1056
1091
 
1057
- .modebar-btn{
1092
+ .modebar-btn {
1058
1093
  background: #3f3f3f;
1059
1094
  }
1060
1095
 
@@ -1076,4 +1111,4 @@ th {
1076
1111
  }
1077
1112
 
1078
1113
 
1079
- }
1114
+ }
@@ -4,9 +4,9 @@
4
4
  <head>
5
5
  <title>EMHASS: Energy Management Optimization for Home Assistant</title>
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <link rel="stylesheet" type="text/css" href="{{ basename }}/static/style.css">
8
- <link rel="icon" type="image/x-icon" href="{{ basename }}/static/img/emhass_logo_short.svg">
9
- <script src="{{ basename }}/static/script.js"></script>
7
+ <link rel="stylesheet" type="text/css" href="static/style.css?version=1"> <!-- change version on stylesheet changes -->
8
+ <link rel="icon" type="image/x-icon" href="static/img/emhass_logo_short.svg">
9
+ <script src="static/script.js"></script>
10
10
  </head>
11
11
 
12
12
  <body style="margin: auto; align-items:center; text-align:center;">
@@ -17,22 +17,22 @@
17
17
  <!-- advanced or basic page switch -->
18
18
  <a id="basicOrAdvanced" style="margin-right: 24px; cursor: pointer; z-index: 1">
19
19
  <svg class="feather">
20
- <use class="feather" href="{{ basename }}/static/img/feather-sprite.svg#tool" />
20
+ <use class="feather" href="static/img/feather-sprite.svg#tool" />
21
21
  </svg>
22
22
  </a>
23
23
  <a href="https://emhass.readthedocs.io/en/latest/">
24
24
  <svg class="feather" style="margin-right: 12px;";>
25
- <use class="feather" href="{{ basename }}/static/img/feather-sprite.svg#book" />
25
+ <use class="feather" href="static/img/feather-sprite.svg#book" />
26
26
  </svg>
27
27
  </a>
28
28
  <a href="https://github.com/davidusb-geek/emhass" target="_blank" rel="noopener noreferrer">
29
29
  <svg class="feather" style="margin-right: 0px;" >
30
- <use class="feather" href="{{ basename }}/static/img/feather-sprite.svg#git-branch" />
30
+ <use class="feather" href="static/img/feather-sprite.svg#git-branch" />
31
31
  </svg>
32
32
  </a>
33
33
  </div>
34
34
  <!-- Title -->
35
- <img src="{{ basename }}/static/img/emhass_icon.png" alt="">
35
+ <img src="static/img/emhass_icon.png" alt="">
36
36
  <h2>EMHASS: Energy Management Optimization for Home Assistant</h2>
37
37
  </div>
38
38
 
@@ -96,7 +96,7 @@ def get_forecast_dates(freq: int, delta_forecast: int,
96
96
  end_forecast = (start_forecast + pd.Timedelta(days=delta_forecast)).replace(microsecond=0)
97
97
  forecast_dates = pd.date_range(start=start_forecast,
98
98
  end=end_forecast+timedelta(days=timedelta_days)-freq,
99
- freq=freq).round(freq, ambiguous='infer', nonexistent=freq)
99
+ freq=freq).round(freq, ambiguous='infer', nonexistent='shift_forward')
100
100
  return forecast_dates
101
101
 
102
102
  def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dict, optim_conf: dict, plant_conf: dict,
@@ -208,62 +208,23 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic
208
208
  params['passed_data']['alpha'] = None
209
209
  params['passed_data']['beta'] = None
210
210
  # Treat passed forecast data lists
211
- if 'pv_power_forecast' in runtimeparams.keys():
212
- if type(runtimeparams['pv_power_forecast']) == list and len(runtimeparams['pv_power_forecast']) >= len(forecast_dates):
213
- params['passed_data']['pv_power_forecast'] = runtimeparams['pv_power_forecast']
214
- optim_conf['weather_forecast_method'] = 'list'
211
+ list_forecast_key = ['pv_power_forecast', 'load_power_forecast', 'load_cost_forecast', 'prod_price_forecast']
212
+ forecast_methods = ['weather_forecast_method', 'load_forecast_method', 'load_cost_forecast_method', 'prod_price_forecast_method']
213
+ for method, forecast_key in enumerate(list_forecast_key):
214
+ if forecast_key in runtimeparams.keys():
215
+ if type(runtimeparams[forecast_key]) == list and len(runtimeparams[forecast_key]) >= len(forecast_dates):
216
+ params['passed_data'][forecast_key] = runtimeparams[forecast_key]
217
+ optim_conf[forecast_methods[method]] = 'list'
218
+ else:
219
+ logger.error(f"ERROR: The passed data is either not a list or the length is not correct, length should be {str(len(forecast_dates))}")
220
+ logger.error(f"Passed type is {str(type(runtimeparams[forecast_key]))} and length is {str(len(runtimeparams[forecast_key]))}")
221
+ list_non_digits = [x for x in runtimeparams[forecast_key] if not (isinstance(x, int) or isinstance(x, float))]
222
+ if len(list_non_digits) > 0:
223
+ logger.warning(f"There are non numeric values on the passed data for {forecast_key}, check for missing values (nans, null, etc)")
224
+ for x in list_non_digits:
225
+ logger.warning(f"This value in {forecast_key} was detected as non digits: {str(x)}")
215
226
  else:
216
- logger.error("ERROR: The passed data is either not a list or the length is not correct, length should be "+str(len(forecast_dates)))
217
- logger.error("Passed type is "+str(type(runtimeparams['pv_power_forecast']))+" and length is "+str(len(runtimeparams['pv_power_forecast'])))
218
- list_non_digits = [x for x in runtimeparams['pv_power_forecast'] if not (isinstance(x, int) or isinstance(x, float))]
219
- if len(list_non_digits) > 0:
220
- logger.warning("There are non numeric values on the passed data for pv_power_forecast, check for missing values (nans, null, etc)")
221
- for x in list_non_digits:
222
- logger.warning("This value in pv_power_forecast was detected as non digits: "+str(x))
223
- else:
224
- params['passed_data']['pv_power_forecast'] = None
225
- if 'load_power_forecast' in runtimeparams.keys():
226
- if type(runtimeparams['load_power_forecast']) == list and len(runtimeparams['load_power_forecast']) >= len(forecast_dates):
227
- params['passed_data']['load_power_forecast'] = runtimeparams['load_power_forecast']
228
- optim_conf['load_forecast_method'] = 'list'
229
- else:
230
- logger.error("ERROR: The passed data is either not a list or the length is not correct, length should be "+str(len(forecast_dates)))
231
- logger.error("Passed type is "+str(type(runtimeparams['load_power_forecast']))+" and length is "+str(len(runtimeparams['load_power_forecast'])))
232
- list_non_digits = [x for x in runtimeparams['load_power_forecast'] if not (isinstance(x, int) or isinstance(x, float))]
233
- if len(list_non_digits) > 0:
234
- logger.warning("There are non numeric values on the passed data for load_power_forecast, check for missing values (nans, null, etc)")
235
- for x in list_non_digits:
236
- logger.warning("This value in load_power_forecast was detected as non digits: "+str(x))
237
- else:
238
- params['passed_data']['load_power_forecast'] = None
239
- if 'load_cost_forecast' in runtimeparams.keys():
240
- if type(runtimeparams['load_cost_forecast']) == list and len(runtimeparams['load_cost_forecast']) >= len(forecast_dates):
241
- params['passed_data']['load_cost_forecast'] = runtimeparams['load_cost_forecast']
242
- optim_conf['load_cost_forecast_method'] = 'list'
243
- else:
244
- logger.error("ERROR: The passed data is either not a list or the length is not correct, length should be "+str(len(forecast_dates)))
245
- logger.error("Passed type is "+str(type(runtimeparams['load_cost_forecast']))+" and length is "+str(len(runtimeparams['load_cost_forecast'])))
246
- list_non_digits = [x for x in runtimeparams['load_cost_forecast'] if not (isinstance(x, int) or isinstance(x, float))]
247
- if len(list_non_digits) > 0:
248
- logger.warning("There are non numeric values on the passed data or load_cost_forecast, check for missing values (nans, null, etc)")
249
- for x in list_non_digits:
250
- logger.warning("This value in load_cost_forecast was detected as non digits: "+str(x))
251
- else:
252
- params['passed_data']['load_cost_forecast'] = None
253
- if 'prod_price_forecast' in runtimeparams.keys():
254
- if type(runtimeparams['prod_price_forecast']) == list and len(runtimeparams['prod_price_forecast']) >= len(forecast_dates):
255
- params['passed_data']['prod_price_forecast'] = runtimeparams['prod_price_forecast']
256
- optim_conf['prod_price_forecast_method'] = 'list'
257
- else:
258
- logger.error("ERROR: The passed data is either not a list or the length is not correct, length should be "+str(len(forecast_dates)))
259
- logger.error("Passed type is "+str(type(runtimeparams['prod_price_forecast']))+" and length is "+str(len(runtimeparams['prod_price_forecast'])))
260
- list_non_digits = [x for x in runtimeparams['prod_price_forecast'] if not (isinstance(x, int) or isinstance(x, float))]
261
- if len(list_non_digits) > 0:
262
- logger.warning("There are non numeric values on the passed data for prod_price_forecast, check for missing values (nans, null, etc)")
263
- for x in list_non_digits:
264
- logger.warning("This value in prod_price_forecast was detected as non digits: "+str(x))
265
- else:
266
- params['passed_data']['prod_price_forecast'] = None
227
+ params['passed_data'][forecast_key] = None
267
228
  # Treat passed data for forecast model fit/predict/tune at runtime
268
229
  if 'days_to_retrieve' not in runtimeparams.keys():
269
230
  days_to_retrieve = 9
@@ -634,8 +595,9 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int,
634
595
  params['optim_conf']['def_start_timestep'] = [i['start_timesteps_of_each_deferrable_load'] for i in options.get('list_start_timesteps_of_each_deferrable_load')]
635
596
  if options.get('list_end_timesteps_of_each_deferrable_load',None) != None:
636
597
  params['optim_conf']['def_end_timestep'] = [i['end_timesteps_of_each_deferrable_load'] for i in options.get('list_end_timesteps_of_each_deferrable_load')]
637
- # Updating variables in plant_con
638
- params['plant_conf']['P_grid_max'] = options.get('maximum_power_from_grid',params['plant_conf']['P_grid_max'])
598
+ # Updating variables in plant_conf
599
+ params['plant_conf']['P_from_grid_max'] = options.get('maximum_power_from_grid',params['plant_conf']['P_from_grid_max'])
600
+ params['plant_conf']['P_to_grid_max'] = options.get('maximum_power_to_grid',params['plant_conf']['P_to_grid_max'])
639
601
  if options.get('list_pv_module_model',None) != None:
640
602
  params['plant_conf']['module_model'] = [i['pv_module_model'] for i in options.get('list_pv_module_model')]
641
603
  if options.get('list_pv_inverter_model',None) != None:
@@ -656,8 +618,7 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int,
656
618
  params['plant_conf']['SOCmin'] = options.get('battery_minimum_state_of_charge',params['plant_conf']['SOCmin'])
657
619
  params['plant_conf']['SOCmax'] = options.get('battery_maximum_state_of_charge',params['plant_conf']['SOCmax'])
658
620
  params['plant_conf']['SOCtarget'] = options.get('battery_target_state_of_charge',params['plant_conf']['SOCtarget'])
659
-
660
- # Check parameter lists have the same amounts as deferrable loads
621
+ # Check parameter lists have the same amounts as deferrable loads
661
622
  # If not, set defaults it fill in gaps
662
623
  if params['optim_conf']['num_def_loads'] is not len(params['optim_conf']['def_start_timestep']):
663
624
  logger.warning("def_start_timestep / list_start_timesteps_of_each_deferrable_load does not match number in num_def_loads, adding default values to parameter")
@@ -683,10 +644,6 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int,
683
644
  logger.warning("P_deferrable_nom / list_nominal_power_of_deferrable_loads does not match number in num_def_loads, adding default values to parameter")
684
645
  for x in range(len(params['optim_conf']['P_deferrable_nom']), params['optim_conf']['num_def_loads']):
685
646
  params['optim_conf']['P_deferrable_nom'].append(0)
686
- if params['optim_conf']['num_def_loads'] is not len(params['optim_conf']['list_hp_periods']):
687
- logger.warning("list_hp_periods / list_peak_hours_periods_(start&end)_hours does not match number in num_def_loads, adding default values to parameter")
688
- for x in range(len(params['optim_conf']['list_hp_periods']), params['optim_conf']['num_def_loads']):
689
- params['optim_conf']['list_hp_periods'].append({'period_hp_'+str(x+1):[{'start':'02:54'},{'end':'20:24'}]})
690
647
  # days_to_retrieve should be no less then 2
691
648
  if params['retrieve_hass_conf']['days_to_retrieve'] < 2:
692
649
  params['retrieve_hass_conf']['days_to_retrieve'] = 2
@@ -69,8 +69,13 @@ def index():
69
69
  else:
70
70
  app.logger.warning("The data container dictionary is empty... Please launch an optimization task")
71
71
  injection_dict={}
72
- basename = request.headers.get("X-Ingress-Path", "")
73
- return make_response(template.render(injection_dict=injection_dict, basename=basename))
72
+
73
+ # replace {{basename}} in html template html with path root
74
+ # basename = request.headers.get("X-Ingress-Path", "")
75
+ # return make_response(template.render(injection_dict=injection_dict, basename=basename))
76
+
77
+ return make_response(template.render(injection_dict=injection_dict))
78
+
74
79
 
75
80
  #get actions
76
81
  @app.route('/template/<action_name>', methods=['GET'])
@@ -86,8 +91,7 @@ def template_action(action_name):
86
91
  else:
87
92
  app.logger.warning("The data container dictionary is empty... Please launch an optimization task")
88
93
  injection_dict={}
89
- basename = request.headers.get("X-Ingress-Path", "")
90
- return make_response(template.render(injection_dict=injection_dict, basename=basename))
94
+ return make_response(template.render(injection_dict=injection_dict))
91
95
 
92
96
  #post actions
93
97
  @app.route('/action/<action_name>', methods=['POST'])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: emhass
3
- Version: 0.8.4
3
+ Version: 0.8.5
4
4
  Summary: An Energy Management System for Home Assistant
5
5
  Home-page: https://github.com/davidusb-geek/emhass
6
6
  Author: David HERNANDEZ
@@ -12,7 +12,7 @@ Classifier: Topic :: Software Development :: Build Tools
12
12
  Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Operating System :: OS Independent
15
- Requires-Python: >=3.9, <3.12
15
+ Requires-Python: >=3.10, <3.12
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: wheel
@@ -466,8 +466,6 @@ curl -i -H 'Content-Type:application/json' -X POST -d '{"pv_power_forecast":[0,
466
466
  curl -i -H 'Content-Type:application/json' -X POST -d '{"pv_power_forecast":[0, 70, 141.22, 246.18, 513.5, 753.27, 1049.89, 1797.93, 1697.3, 3078.93], "prediction_horizon":10, "soc_init":0.5,"soc_final":0.6,"def_total_hours":[1,3],"def_start_timestep":[0,3],"def_end_timestep":[0,6]}' http://localhost:5000/action/naive-mpc-optim
467
467
  ```
468
468
 
469
-
470
-
471
469
  ## A machine learning forecaster
472
470
 
473
471
  Starting in v0.4.0 a new machine learning forecaster class was introduced.
@@ -35,6 +35,8 @@ src/emhass.egg-info/dependency_links.txt
35
35
  src/emhass.egg-info/entry_points.txt
36
36
  src/emhass.egg-info/requires.txt
37
37
  src/emhass.egg-info/top_level.txt
38
+ src/emhass/data/cec_inverters.pbz2
39
+ src/emhass/data/cec_modules.pbz2
38
40
  src/emhass/static/advanced.html
39
41
  src/emhass/static/basic.html
40
42
  src/emhass/static/script.js
@@ -39,10 +39,10 @@ class TestCommandLineUtils(unittest.TestCase):
39
39
  def setUp(self):
40
40
  params = TestCommandLineUtils.get_test_params()
41
41
  runtimeparams = {
42
- 'pv_power_forecast':[1,2,3,4,5,6,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],
43
- 'load_power_forecast':[1,2,3,4,5,6,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],
44
- 'load_cost_forecast':[1,2,3,4,5,6,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],
45
- 'prod_price_forecast':[1,2,3,4,5,6,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]
42
+ 'pv_power_forecast':[i+1 for i in range(48)],
43
+ 'load_power_forecast':[i+1 for i in range(48)],
44
+ 'load_cost_forecast':[i+1 for i in range(48)],
45
+ 'prod_price_forecast':[i+1 for i in range(48)]
46
46
  }
47
47
  self.runtimeparams_json = json.dumps(runtimeparams)
48
48
  params['passed_data'] = runtimeparams
@@ -113,8 +113,8 @@ class TestCommandLineUtils(unittest.TestCase):
113
113
  action = 'dayahead-optim'
114
114
  params = TestCommandLineUtils.get_test_params()
115
115
  runtimeparams = {
116
- 'load_cost_forecast':[1,2,3,4,5,6,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],
117
- 'prod_price_forecast':[1,2,3,4,5,6,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]
116
+ 'load_cost_forecast':[i+1 for i in range(48)],
117
+ 'prod_price_forecast':[i+1 for i in range(48)]
118
118
  }
119
119
  runtimeparams_json = json.dumps(runtimeparams)
120
120
  params['passed_data'] = runtimeparams
@@ -154,8 +154,8 @@ class TestCommandLineUtils(unittest.TestCase):
154
154
  action = 'dayahead-optim'
155
155
  params = TestCommandLineUtils.get_test_params()
156
156
  runtimeparams = {
157
- 'load_cost_forecast':[1,2,3,4,5,6,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],
158
- 'prod_price_forecast':[1,2,3,4,5,6,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]
157
+ 'load_cost_forecast':[i+1 for i in range(48)],
158
+ 'prod_price_forecast':[i+1 for i in range(48)]
159
159
  }
160
160
  runtimeparams_json = json.dumps(runtimeparams)
161
161
  params['passed_data'] = runtimeparams
@@ -103,9 +103,6 @@ class TestForecast(unittest.TestCase):
103
103
  self.assertEqual(len(self.df_weather_csv), len(P_PV_forecast))
104
104
  df_weather_none = self.fcst.get_weather_forecast(method='none')
105
105
  self.assertTrue(df_weather_none == None)
106
-
107
- def test_get_weather_forecast_mlforecaster(self):
108
- pass
109
106
 
110
107
  def test_get_weather_forecast_scrapper_method_mock(self):
111
108
  with requests_mock.mock() as m:
@@ -155,7 +152,7 @@ class TestForecast(unittest.TestCase):
155
152
  self.assertEqual(len(df_weather_scrap),
156
153
  int(self.optim_conf['delta_forecast'].total_seconds()/3600/self.fcst.timeStep))
157
154
 
158
- def test_get_weather_forecast_solarforecast_method(self):
155
+ def test_get_weather_forecast_solarforecast_method_mock(self):
159
156
  with requests_mock.mock() as m:
160
157
  data = bz2.BZ2File(str(pathlib.Path(root+'/data/test_response_solarforecast_get_method.pbz2')), "rb")
161
158
  data = cPickle.load(data)
@@ -187,10 +184,10 @@ class TestForecast(unittest.TestCase):
187
184
  }
188
185
  })
189
186
  runtimeparams = {
190
- 'pv_power_forecast':[1,2,3,4,5,6,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],
191
- 'load_power_forecast':[1,2,3,4,5,6,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],
192
- 'load_cost_forecast':[1,2,3,4,5,6,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],
193
- 'prod_price_forecast':[1,2,3,4,5,6,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]
187
+ 'pv_power_forecast':[i+1 for i in range(48)],
188
+ 'load_power_forecast':[i+1 for i in range(48)],
189
+ 'load_cost_forecast':[i+1 for i in range(48)],
190
+ 'prod_price_forecast':[i+1 for i in range(48)]
194
191
  }
195
192
  runtimeparams_json = json.dumps(runtimeparams)
196
193
  params['passed_data'] = runtimeparams
@@ -217,7 +214,6 @@ class TestForecast(unittest.TestCase):
217
214
  var_replace_zero = retrieve_hass_conf['var_replace_zero'],
218
215
  var_interp = retrieve_hass_conf['var_interp'])
219
216
  df_input_data = rh.df_final.copy()
220
-
221
217
  fcst = Forecast(retrieve_hass_conf, optim_conf, plant_conf,
222
218
  params_json, root, logger, get_data_from_file=True)
223
219
  df_input_data = copy.deepcopy(df_input_data).iloc[-49:-1]
@@ -227,8 +223,8 @@ class TestForecast(unittest.TestCase):
227
223
  self.assertIsInstance(P_PV_forecast, type(pd.DataFrame()))
228
224
  self.assertIsInstance(P_PV_forecast.index, pd.core.indexes.datetimes.DatetimeIndex)
229
225
  self.assertIsInstance(P_PV_forecast.index.dtype, pd.core.dtypes.dtypes.DatetimeTZDtype)
230
- self.assertEqual(P_PV_forecast.index.tz, self.fcst.time_zone)
231
- self.assertTrue(self.fcst.start_forecast < ts for ts in P_PV_forecast.index)
226
+ self.assertEqual(P_PV_forecast.index.tz, fcst.time_zone)
227
+ self.assertTrue(fcst.start_forecast < ts for ts in P_PV_forecast.index)
232
228
  self.assertTrue(P_PV_forecast.values[0][0] == 1)
233
229
  self.assertTrue(P_PV_forecast.values[-1][0] == 48)
234
230
  P_load_forecast = fcst.get_load_forecast(method='list')
@@ -249,6 +245,52 @@ class TestForecast(unittest.TestCase):
249
245
  self.assertTrue(df_input_data.isnull().sum().sum()==0)
250
246
  self.assertTrue(df_input_data['unit_prod_price'].values[0] == 1)
251
247
  self.assertTrue(df_input_data['unit_prod_price'].values[-1] == 48)
248
+ # Test with longer lists
249
+ with open(root+'/config_emhass.yaml', 'r') as file:
250
+ params = yaml.load(file, Loader=yaml.FullLoader)
251
+ params.update({
252
+ 'params_secrets': {
253
+ 'hass_url': 'http://supervisor/core/api',
254
+ 'long_lived_token': '${SUPERVISOR_TOKEN}',
255
+ 'time_zone': 'Europe/Paris',
256
+ 'lat': 45.83,
257
+ 'lon': 6.86,
258
+ 'alt': 4807.8
259
+ }
260
+ })
261
+ runtimeparams = {
262
+ 'pv_power_forecast':[i+1 for i in range(2*48)],
263
+ 'load_power_forecast':[i+1 for i in range(2*48)],
264
+ 'load_cost_forecast':[i+1 for i in range(2*48)],
265
+ 'prod_price_forecast':[i+1 for i in range(2*48)]
266
+ }
267
+ runtimeparams_json = json.dumps(runtimeparams)
268
+ params['passed_data'] = runtimeparams
269
+ params_json = json.dumps(params)
270
+ retrieve_hass_conf, optim_conf, plant_conf = get_yaml_parse(pathlib.Path(root+'/config_emhass.yaml'),
271
+ use_secrets=False, params=params_json)
272
+ optim_conf['delta_forecast'] = pd.Timedelta(days=2)
273
+ params, retrieve_hass_conf, optim_conf, plant_conf = treat_runtimeparams(
274
+ runtimeparams_json, params_json, retrieve_hass_conf,
275
+ optim_conf, plant_conf, set_type, logger)
276
+ fcst = Forecast(retrieve_hass_conf, optim_conf, plant_conf,
277
+ params_json, root, logger, opt_time_delta=48, get_data_from_file=True)
278
+ P_PV_forecast = fcst.get_weather_forecast(method='list')
279
+ self.assertIsInstance(P_PV_forecast, type(pd.DataFrame()))
280
+ self.assertIsInstance(P_PV_forecast.index, pd.core.indexes.datetimes.DatetimeIndex)
281
+ self.assertIsInstance(P_PV_forecast.index.dtype, pd.core.dtypes.dtypes.DatetimeTZDtype)
282
+ self.assertEqual(P_PV_forecast.index.tz, fcst.time_zone)
283
+ self.assertTrue(fcst.start_forecast < ts for ts in P_PV_forecast.index)
284
+ self.assertTrue(P_PV_forecast.values[0][0] == 1)
285
+ self.assertTrue(P_PV_forecast.values[-1][0] == 2*48)
286
+ P_load_forecast = fcst.get_load_forecast(method='list')
287
+ self.assertIsInstance(P_load_forecast, pd.core.series.Series)
288
+ self.assertIsInstance(P_load_forecast.index, pd.core.indexes.datetimes.DatetimeIndex)
289
+ self.assertIsInstance(P_load_forecast.index.dtype, pd.core.dtypes.dtypes.DatetimeTZDtype)
290
+ self.assertEqual(P_load_forecast.index.tz, fcst.time_zone)
291
+ self.assertEqual(len(P_PV_forecast), len(P_load_forecast))
292
+ self.assertTrue(P_load_forecast.values[0] == 1)
293
+ self.assertTrue(P_load_forecast.values[-1] == 2*48)
252
294
 
253
295
  def test_get_forecasts_with_lists_special_case(self):
254
296
  with open(root+'/config_emhass.yaml', 'r') as file:
@@ -264,8 +306,8 @@ class TestForecast(unittest.TestCase):
264
306
  }
265
307
  })
266
308
  runtimeparams = {
267
- 'load_cost_forecast':[1,2,3,4,5,6,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],
268
- 'prod_price_forecast':[1,2,3,4,5,6,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]
309
+ 'load_cost_forecast':[i+1 for i in range(48)],
310
+ 'prod_price_forecast':[i+1 for i in range(48)]
269
311
  }
270
312
  runtimeparams_json = json.dumps(runtimeparams)
271
313
  params['passed_data'] = runtimeparams
@@ -292,7 +334,6 @@ class TestForecast(unittest.TestCase):
292
334
  var_replace_zero = retrieve_hass_conf['var_replace_zero'],
293
335
  var_interp = retrieve_hass_conf['var_interp'])
294
336
  df_input_data = rh.df_final.copy()
295
-
296
337
  fcst = Forecast(retrieve_hass_conf, optim_conf, plant_conf,
297
338
  params_json, root, logger, get_data_from_file=True)
298
339
  df_input_data = copy.deepcopy(df_input_data).iloc[-49:-1]
@@ -300,13 +341,13 @@ class TestForecast(unittest.TestCase):
300
341
  df_input_data.index = P_PV_forecast.index
301
342
  df_input_data.index.freq = rh.df_final.index.freq
302
343
  df_input_data = fcst.get_load_cost_forecast(
303
- df_input_data, method=fcst.optim_conf['load_cost_forecast_method'])
344
+ df_input_data, method='list')
304
345
  self.assertTrue(fcst.var_load_cost in df_input_data.columns)
305
346
  self.assertTrue(df_input_data.isnull().sum().sum()==0)
306
347
  self.assertTrue(df_input_data['unit_load_cost'].values[0] == 1)
307
348
  self.assertTrue(df_input_data['unit_load_cost'].values[-1] == 48)
308
349
  df_input_data = fcst.get_prod_price_forecast(
309
- df_input_data, method=fcst.optim_conf['prod_price_forecast_method'])
350
+ df_input_data, method='list')
310
351
  self.assertTrue(fcst.var_prod_price in df_input_data.columns)
311
352
  self.assertTrue(df_input_data.isnull().sum().sum()==0)
312
353
  self.assertTrue(df_input_data['unit_prod_price'].values[0] == 1)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes