emhass 0.7.7__tar.gz → 0.8.0__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 (51) hide show
  1. {emhass-0.7.7 → emhass-0.8.0}/CHANGELOG.md +16 -1
  2. {emhass-0.7.7 → emhass-0.8.0}/PKG-INFO +2 -2
  3. {emhass-0.7.7 → emhass-0.8.0}/README.md +1 -1
  4. {emhass-0.7.7 → emhass-0.8.0}/setup.py +3 -2
  5. {emhass-0.7.7 → emhass-0.8.0}/src/emhass/command_line.py +21 -10
  6. {emhass-0.7.7 → emhass-0.8.0}/src/emhass/forecast.py +6 -4
  7. {emhass-0.7.7 → emhass-0.8.0}/src/emhass/retrieve_hass.py +26 -7
  8. {emhass-0.7.7 → emhass-0.8.0}/src/emhass/static/style.css +12 -4
  9. {emhass-0.7.7 → emhass-0.8.0}/src/emhass/templates/index.html +87 -28
  10. emhass-0.8.0/src/emhass/templates/template.html +5 -0
  11. {emhass-0.7.7 → emhass-0.8.0}/src/emhass/utils.py +36 -1
  12. {emhass-0.7.7 → emhass-0.8.0}/src/emhass/web_server.py +117 -22
  13. {emhass-0.7.7 → emhass-0.8.0}/src/emhass.egg-info/PKG-INFO +2 -2
  14. {emhass-0.7.7 → emhass-0.8.0}/src/emhass.egg-info/SOURCES.txt +1 -0
  15. {emhass-0.7.7 → emhass-0.8.0}/tests/test_retrieve_hass.py +1 -1
  16. {emhass-0.7.7 → emhass-0.8.0}/CODE_OF_CONDUCT.md +0 -0
  17. {emhass-0.7.7 → emhass-0.8.0}/CONTRIBUTING.md +0 -0
  18. {emhass-0.7.7 → emhass-0.8.0}/LICENSE +0 -0
  19. {emhass-0.7.7 → emhass-0.8.0}/MANIFEST.in +0 -0
  20. {emhass-0.7.7 → emhass-0.8.0}/data/data_load_cost_forecast.csv +0 -0
  21. {emhass-0.7.7 → emhass-0.8.0}/data/data_load_forecast.csv +0 -0
  22. {emhass-0.7.7 → emhass-0.8.0}/data/data_prod_price_forecast.csv +0 -0
  23. {emhass-0.7.7 → emhass-0.8.0}/data/data_train_load_clustering.pkl +0 -0
  24. {emhass-0.7.7 → emhass-0.8.0}/data/data_train_load_forecast.pkl +0 -0
  25. {emhass-0.7.7 → emhass-0.8.0}/data/data_weather_forecast.csv +0 -0
  26. {emhass-0.7.7 → emhass-0.8.0}/data/logger_emhass.log +0 -0
  27. {emhass-0.7.7 → emhass-0.8.0}/data/opt_res_latest.csv +0 -0
  28. {emhass-0.7.7 → emhass-0.8.0}/data/opt_res_perfect_optim_cost.csv +0 -0
  29. {emhass-0.7.7 → emhass-0.8.0}/data/opt_res_perfect_optim_profit.csv +0 -0
  30. {emhass-0.7.7 → emhass-0.8.0}/data/opt_res_perfect_optim_self-consumption.csv +0 -0
  31. {emhass-0.7.7 → emhass-0.8.0}/data/test_df_final.pkl +0 -0
  32. {emhass-0.7.7 → emhass-0.8.0}/data/test_response_get_data_get_method.pbz2 +0 -0
  33. {emhass-0.7.7 → emhass-0.8.0}/data/test_response_scrapper_get_method.pbz2 +0 -0
  34. {emhass-0.7.7 → emhass-0.8.0}/data/test_response_solarforecast_get_method.pbz2 +0 -0
  35. {emhass-0.7.7 → emhass-0.8.0}/data/test_response_solcast_get_method.pbz2 +0 -0
  36. {emhass-0.7.7 → emhass-0.8.0}/pyproject.toml +0 -0
  37. {emhass-0.7.7 → emhass-0.8.0}/setup.cfg +0 -0
  38. {emhass-0.7.7 → emhass-0.8.0}/src/emhass/__init__.py +0 -0
  39. {emhass-0.7.7 → emhass-0.8.0}/src/emhass/machine_learning_forecaster.py +0 -0
  40. {emhass-0.7.7 → emhass-0.8.0}/src/emhass/optimization.py +0 -0
  41. {emhass-0.7.7 → emhass-0.8.0}/src/emhass/static/img/emhass_icon.png +0 -0
  42. {emhass-0.7.7 → emhass-0.8.0}/src/emhass/static/img/emhass_logo_short.svg +0 -0
  43. {emhass-0.7.7 → emhass-0.8.0}/src/emhass.egg-info/dependency_links.txt +0 -0
  44. {emhass-0.7.7 → emhass-0.8.0}/src/emhass.egg-info/entry_points.txt +0 -0
  45. {emhass-0.7.7 → emhass-0.8.0}/src/emhass.egg-info/requires.txt +0 -0
  46. {emhass-0.7.7 → emhass-0.8.0}/src/emhass.egg-info/top_level.txt +0 -0
  47. {emhass-0.7.7 → emhass-0.8.0}/tests/test_command_line_utils.py +0 -0
  48. {emhass-0.7.7 → emhass-0.8.0}/tests/test_forecast.py +0 -0
  49. {emhass-0.7.7 → emhass-0.8.0}/tests/test_machine_learning_forecaster.py +0 -0
  50. {emhass-0.7.7 → emhass-0.8.0}/tests/test_optimization.py +0 -0
  51. {emhass-0.7.7 → emhass-0.8.0}/tests/test_utils.py +0 -0
@@ -1,8 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.0] - 2024-02-25
4
+ ### Improvement
5
+ - Thanks to the great work from @GeoDerp we now have a unified/centralized Dockerfile that allows for testing different installation configuration methods in one place. This greatly helps testing, notably emulating the add-on environment. This will improve overall testing for both teh core code and the add-on. Again many thanks!
6
+ - There were also a lot of nice improveements from @GeoDerp to the webui, namely: styling, dynamic table, optimization feedback after button press, logging, a new clear button, etc.
7
+ - From now on we will unify the semantic versioning for both the main core code and the add-on.
8
+
9
+ ## [0.7.8] - 2024-02-18
10
+ ### Improvement
11
+ Added some nice logging functionalities and responsiveness on the webui.
12
+ Thanks to @GeoDerp for this great work!
13
+ - new actionLogs.txt is generated in datapath storing sessions app.logger info
14
+ - on successful html button press, fetch is called to get html containing latest table data
15
+ - on html button press, If app.logger ERROR is present, send action log back and present on page.
16
+
3
17
  ## [0.7.7] - 2024-02-10
4
18
  ### Improvement
5
- - Bumped the webui. Index/style added runtime parameter dictionary inputs. Thanks to @GeoDerp
19
+ - Bumped the webui. Some great new features and styling. Now it is possible to pass data directly as lsit of values when using the buttons in the webui. Thanks to @GeoDerp
6
20
  - Added two additional testing environment options. Thanks to @GeoDerp
7
21
  ### Fix
8
22
  - Bump markupsafe from 2.1.4 to 2.1.5
@@ -565,6 +579,7 @@
565
579
  [0.7.5]: https://github.com/davidusb-geek/emhass/releases/tag/v0.7.5
566
580
  [0.7.6]: https://github.com/davidusb-geek/emhass/releases/tag/v0.7.6
567
581
  [0.7.7]: https://github.com/davidusb-geek/emhass/releases/tag/v0.7.7
582
+ [0.7.8]: https://github.com/davidusb-geek/emhass/releases/tag/v0.7.8
568
583
 
569
584
  # Notes
570
585
  All notable changes to this project will be documented in this file.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: emhass
3
- Version: 0.7.7
3
+ Version: 0.8.0
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
@@ -460,7 +460,7 @@ Check the dedicated section in the documentation here: [https://emhass.readthedo
460
460
 
461
461
  ## Development
462
462
 
463
- Pull request are very much accepted on this project. For development you can find some instructions here [Development](./docs/develop.md)
463
+ Pull request are very much accepted on this project. For development you can find some instructions here [Development](https://emhass.readthedocs.io/en/latest/develop.html)
464
464
 
465
465
  ## Troubleshooting
466
466
 
@@ -425,7 +425,7 @@ Check the dedicated section in the documentation here: [https://emhass.readthedo
425
425
 
426
426
  ## Development
427
427
 
428
- Pull request are very much accepted on this project. For development you can find some instructions here [Development](./docs/develop.md)
428
+ Pull request are very much accepted on this project. For development you can find some instructions here [Development](https://emhass.readthedocs.io/en/latest/develop.html)
429
429
 
430
430
  ## Troubleshooting
431
431
 
@@ -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.7.7', # Required
22
+ version='0.8.0', # 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)
@@ -62,5 +62,6 @@ setup(
62
62
  'emhass=emhass.command_line:main',
63
63
  ],
64
64
  },
65
- package_data={'emhass': ['templates/index.html','static/style.css','static/img/emhass_icon.png','static/img/emhass_logo_short.svg']},
65
+ package_data={'emhass': ['templates/index.html','templates/template.html',
66
+ 'static/style.css','static/img/emhass_icon.png','static/img/emhass_logo_short.svg']},
66
67
  )
@@ -75,12 +75,14 @@ def set_input_data_dict(config_path: pathlib.Path, base_path: str, costfun: str,
75
75
  else:
76
76
  days_list = utils.get_days_list(retrieve_hass_conf['days_to_retrieve'])
77
77
  var_list = [retrieve_hass_conf['var_load'], retrieve_hass_conf['var_PV']]
78
- rh.get_data(days_list, var_list,
79
- minimal_response=False, significant_changes_only=False)
80
- rh.prepare_data(retrieve_hass_conf['var_load'], load_negative = retrieve_hass_conf['load_negative'],
78
+ if not rh.get_data(days_list, var_list,
79
+ minimal_response=False, significant_changes_only=False):
80
+ return False
81
+ if not rh.prepare_data(retrieve_hass_conf['var_load'], load_negative = retrieve_hass_conf['load_negative'],
81
82
  set_zero_min = retrieve_hass_conf['set_zero_min'],
82
83
  var_replace_zero = retrieve_hass_conf['var_replace_zero'],
83
- var_interp = retrieve_hass_conf['var_interp'])
84
+ var_interp = retrieve_hass_conf['var_interp']):
85
+ return False
84
86
  df_input_data = rh.df_final.copy()
85
87
  # What we don't need for this type of action
86
88
  P_PV_forecast, P_load_forecast, df_input_data_dayahead = None, None, None
@@ -89,6 +91,9 @@ def set_input_data_dict(config_path: pathlib.Path, base_path: str, costfun: str,
89
91
  df_weather = fcst.get_weather_forecast(method=optim_conf['weather_forecast_method'])
90
92
  P_PV_forecast = fcst.get_power_from_weather(df_weather)
91
93
  P_load_forecast = fcst.get_load_forecast(method=optim_conf['load_forecast_method'])
94
+ if isinstance(P_load_forecast,bool) and not P_load_forecast:
95
+ logger.error("Unable to get sensor_power_photovoltaics or sensor_power_load_no_var_loads")
96
+ return False
92
97
  df_input_data_dayahead = pd.DataFrame(np.transpose(np.vstack([P_PV_forecast.values,P_load_forecast.values])),
93
98
  index=P_PV_forecast.index,
94
99
  columns=['P_PV_forecast', 'P_load_forecast'])
@@ -107,12 +112,14 @@ def set_input_data_dict(config_path: pathlib.Path, base_path: str, costfun: str,
107
112
  else:
108
113
  days_list = utils.get_days_list(1)
109
114
  var_list = [retrieve_hass_conf['var_load'], retrieve_hass_conf['var_PV']]
110
- rh.get_data(days_list, var_list,
111
- minimal_response=False, significant_changes_only=False)
112
- rh.prepare_data(retrieve_hass_conf['var_load'], load_negative = retrieve_hass_conf['load_negative'],
115
+ if not rh.get_data(days_list, var_list,
116
+ minimal_response=False, significant_changes_only=False):
117
+ return False
118
+ if not rh.prepare_data(retrieve_hass_conf['var_load'], load_negative = retrieve_hass_conf['load_negative'],
113
119
  set_zero_min = retrieve_hass_conf['set_zero_min'],
114
120
  var_replace_zero = retrieve_hass_conf['var_replace_zero'],
115
- var_interp = retrieve_hass_conf['var_interp'])
121
+ var_interp = retrieve_hass_conf['var_interp']):
122
+ return False
116
123
  df_input_data = rh.df_final.copy()
117
124
  # Get PV and load forecasts
118
125
  df_weather = fcst.get_weather_forecast(method=optim_conf['weather_forecast_method'])
@@ -143,7 +150,8 @@ def set_input_data_dict(config_path: pathlib.Path, base_path: str, costfun: str,
143
150
  else:
144
151
  days_list = utils.get_days_list(days_to_retrieve)
145
152
  var_list = [var_model]
146
- rh.get_data(days_list, var_list)
153
+ if not rh.get_data(days_list, var_list):
154
+ return False
147
155
  df_input_data = rh.df_final.copy()
148
156
  elif set_type == "publish-data":
149
157
  df_input_data, df_input_data_dayahead = None, None
@@ -415,7 +423,7 @@ def forecast_model_tune(input_data_dict: dict, logger: logging.Logger,
415
423
  mlf = pickle.load(inp)
416
424
  else:
417
425
  logger.error("The ML forecaster file was not found, please run a model fit method before this tune method")
418
- return
426
+ return None, None
419
427
  # Tune the model
420
428
  df_pred_optim = mlf.tune(debug=debug)
421
429
  # Save model
@@ -540,6 +548,9 @@ def publish_data(input_data_dict: dict, logger: logging.Logger,
540
548
  publish_prefix = publish_prefix)
541
549
  # Publish the optimization status
542
550
  custom_cost_fun_id = params['passed_data']['custom_optim_status_id']
551
+ if "optim_status" not in opt_res_latest:
552
+ opt_res_latest["optim_status"] = 'Optimal'
553
+ logger.warning("no optim_status in opt_res_latest, run an optimization task first")
543
554
  input_data_dict['rh'].post_data(opt_res_latest['optim_status'], idx_closest,
544
555
  custom_cost_fun_id["entity_id"],
545
556
  custom_cost_fun_id["unit_of_measurement"],
@@ -585,12 +585,14 @@ class Forecast(object):
585
585
  with open(pathlib.Path(self.root) / 'data' / 'test_df_final.pkl', 'rb') as inp:
586
586
  rh.df_final, days_list, _ = pickle.load(inp)
587
587
  else:
588
- days_list = get_days_list(days_min_load_forecast)
589
- rh.get_data(days_list, var_list)
590
- rh.prepare_data(self.retrieve_hass_conf['var_load'], load_negative = self.retrieve_hass_conf['load_negative'],
588
+ days_list = get_days_list(days_min_load_forecast)
589
+ if not rh.get_data(days_list, var_list):
590
+ return False
591
+ if not rh.prepare_data(self.retrieve_hass_conf['var_load'], load_negative = self.retrieve_hass_conf['load_negative'],
591
592
  set_zero_min = self.retrieve_hass_conf['set_zero_min'],
592
593
  var_replace_zero = var_replace_zero,
593
- var_interp = var_interp)
594
+ var_interp = var_interp):
595
+ return False
594
596
  df = rh.df_final.copy()[[self.var_load_new]]
595
597
  if method == 'naive': # using a naive approach
596
598
  mask_forecast_out = (df.index > days_list[-1] - self.optim_conf['delta_forecast'])
@@ -92,6 +92,7 @@ class RetrieveHass:
92
92
  """
93
93
  self.logger.info("Retrieve hass get data method initiated...")
94
94
  self.df_final = pd.DataFrame()
95
+ x = 0 #iterate based on days
95
96
  # Looping on each day from days list
96
97
  for day in days_list:
97
98
 
@@ -115,8 +116,14 @@ class RetrieveHass:
115
116
  try:
116
117
  response = get(url, headers=headers)
117
118
  except Exception:
118
- return "Request Get Error"
119
+ self.logger.error("Unable to access Home Assistance instance, check URL")
120
+ self.logger.error("If using addon, try setting url and token to 'empty'")
121
+ return False
119
122
  else:
123
+ if response.status_code == 401:
124
+ self.logger.error("Unable to access Home Assistance instance, TOKEN/KEY")
125
+ self.logger.error("If using addon, try setting url and token to 'empty'")
126
+ return False
120
127
  if response.status_code > 299:
121
128
  return f"Request Get Error: {response.status_code}"
122
129
  '''import bz2 # Uncomment to save a serialized data for tests
@@ -126,13 +133,18 @@ class RetrieveHass:
126
133
  try: # Sometimes when there are connection problems we need to catch empty retrieved json
127
134
  data = response.json()[0]
128
135
  except IndexError:
129
- self.logger.error("The retrieved JSON is empty, check that correct day or variable names are passed")
130
- self.logger.error("Either the names of the passed variables are not correct or days_to_retrieve is larger than the recorded history of your sensor (check your recorder settings)")
131
- break
136
+ if x is 0:
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
+ else:
139
+ self.logger.error("The retrieved JSON is empty, days_to_retrieve may be larger than the recorded history of sensor:" + var + " (check your recorder settings)")
140
+ return False
132
141
  df_raw = pd.DataFrame.from_dict(data)
133
142
  if len(df_raw) == 0:
134
- self.logger.error("Retrieved empty Dataframe, check that correct day or variable names are passed")
135
- self.logger.error("Either the names of the passed variables are not correct or days_to_retrieve is larger than the recorded history of your sensor (check your recorder settings)")
143
+ if x is 0:
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
+ else:
146
+ self.logger.error("Retrieved empty Dataframe, days_to_retrieve may be larger than the recorded history of sensor:" + var + " (check your recorder settings)")
147
+ return False
136
148
  if i == 0: # Defining the DataFrame container
137
149
  from_date = pd.to_datetime(df_raw['last_changed'], format="ISO8601").min()
138
150
  to_date = pd.to_datetime(df_raw['last_changed'], format="ISO8601").max()
@@ -147,11 +159,13 @@ class RetrieveHass:
147
159
  df_tp = df_tp.resample(self.freq).mean()
148
160
  df_day = pd.concat([df_day, df_tp], axis=1)
149
161
 
162
+ x += 1
150
163
  self.df_final = pd.concat([self.df_final, df_day], axis=0)
151
164
  self.df_final = set_df_index_freq(self.df_final)
152
165
  if self.df_final.index.freq != self.freq:
153
166
  self.logger.error("The inferred freq from data is not equal to the defined freq in passed parameters")
154
-
167
+ return False
168
+ return True
155
169
 
156
170
  def prepare_data(self, var_load: str, load_negative: Optional[bool] = False, set_zero_min: Optional[bool] = True,
157
171
  var_replace_zero: Optional[list] = None, var_interp: Optional[list] = None) -> None:
@@ -185,6 +199,10 @@ class RetrieveHass:
185
199
  self.df_final.drop([var_load], inplace=True, axis=1)
186
200
  except KeyError:
187
201
  self.logger.error("Variable "+var_load+" was not found. This is typically because no data could be retrieved from Home Assistant")
202
+ return False
203
+ except ValueError:
204
+ self.logger.error("sensor.power_photovoltaics and sensor.power_load_no_var_loads should not be the same")
205
+ return False
188
206
  if set_zero_min: # Apply minimum values
189
207
  self.df_final.clip(lower=0.0, inplace=True, axis=1)
190
208
  self.df_final.replace(to_replace=0.0, value=np.nan, inplace=True)
@@ -215,6 +233,7 @@ class RetrieveHass:
215
233
  self.df_final.index = self.df_final.index.tz_convert(self.time_zone)
216
234
  # Drop datetimeindex duplicates on final DF
217
235
  self.df_final = self.df_final[~self.df_final.index.duplicated(keep='first')]
236
+ return True
218
237
 
219
238
  @staticmethod
220
239
  def get_attr_data_dict(data_df: pd.DataFrame, idx: int, entity_id: str,
@@ -568,6 +568,10 @@ button {
568
568
  overflow-x: auto !important;
569
569
  margin: 0 auto;
570
570
  margin-bottom: 10px;
571
+ animation-name: fadeInOpacity;
572
+ animation-iteration-count: 1;
573
+ animation-timing-function: ease-in-out;
574
+ animation-duration: .8s;
571
575
  }
572
576
 
573
577
  /* Set min size for diagrams */
@@ -688,7 +692,7 @@ th {
688
692
  font-size: 4.0em;
689
693
  animation-name: fadeInOpacity;
690
694
  animation-iteration-count: 1;
691
- animation-timing-function: ease-in;
695
+ animation-timing-function: ease-in-out;
692
696
  animation-duration: .5s;
693
697
  }
694
698
 
@@ -699,7 +703,7 @@ th {
699
703
  font-size: 4.0em;
700
704
  animation-name: fadeInOpacity;
701
705
  animation-iteration-count: 1;
702
- animation-timing-function: ease-in;
706
+ animation-timing-function: ease-in-out;
703
707
  animation-duration: .5s;
704
708
  }
705
709
 
@@ -741,7 +745,10 @@ th {
741
745
  margin: 0 auto;
742
746
  width: fit-content;
743
747
  height: fit-content;
744
- transition: 0.3s;
748
+ animation-name: fadeInOpacity;
749
+ animation-iteration-count: 1;
750
+ animation-timing-function: ease-in-out;
751
+ animation-duration: .8s;
745
752
  }
746
753
 
747
754
  #alert-text {
@@ -807,7 +814,8 @@ th {
807
814
  text-align: center;
808
815
  }
809
816
 
810
- #input-select {
817
+ #input-select,
818
+ #input-clear {
811
819
  width: 77px;
812
820
  }
813
821
 
@@ -18,7 +18,7 @@
18
18
  <!-- action button elements section -->
19
19
  <div class="loading-div">
20
20
  <h4>Use the buttons below to manually launch different optimization tasks</h4>
21
- <div id=loader></div> <!-- post status dynamic element -->
21
+ <div id=loader></div> <!-- status dynamic element -->
22
22
  </div>
23
23
  <button type="button" id="perfect-optim" class="button button1">Perfect Optimization</button>
24
24
  <button type="button" id="dayahead-optim" class="button button2">Day-ahead Optimization</button>
@@ -44,6 +44,7 @@
44
44
  <option value="List" selected>List</option>
45
45
  <option value="Box">Box</option>
46
46
  </select>
47
+ <button type="button" id="input-clear">Clear</button>
47
48
  </div>
48
49
  </div>
49
50
  <div id="input-container"> <!-- (Box/List) dynamic input elements will be added here -->
@@ -57,18 +58,22 @@
57
58
  <p id="alert-text"></p>
58
59
  </div>
59
60
  </div>
61
+ <!-- -->
60
62
  <br><br><br>
61
63
  <!-- dynamic table/diagram elements section -->
62
64
  </div>
63
- {% for plot in injection_dict %} <!-- diagrams/tables elements will be added here -->
64
- <div class="table_div">
65
- {{injection_dict[plot]}}
66
- </div>
67
- {% endfor %}
68
- <!-- -->
69
- <footer class="footer">
70
- <p style="margin-top:10px; text-align:center;">&copy; MIT License | Copyright (c) 2021-2023 David HERNANDEZ</p>
71
- </footer>
65
+ <div id="template"> <!-- table/diagram container element -->
66
+ {% for plot in injection_dict %} <!-- diagrams/tables elements will be added here -->
67
+ <div class="table_div">
68
+ {{injection_dict[plot]}}
69
+ </div>
70
+ {% endfor %}
71
+ <div>
72
+ <!-- -->
73
+ <footer class="footer">
74
+ <p style="margin-top:10px; text-align:center;">&copy; MIT License | Copyright (c) 2021-2023 David
75
+ HERNANDEZ</p>
76
+ </footer>
72
77
  </body>
73
78
 
74
79
 
@@ -83,16 +88,20 @@
83
88
  //function pushing data via post, triggered by button action
84
89
  async function formAction(action) {
85
90
  var data = inputToJson()
86
- if (data !== 0) { //don't run if there is an error in the data Json
87
- showChangeStatus("loading") // show loading div for status
88
- response = await fetch(`{{ basename }}/action/${action}`, {
91
+ if (data !== 0) { //don't run if there is an error in the input (box/list) Json data
92
+ showChangeStatus("loading", {}) // show loading div for status
93
+ const response = await fetch(`{{ basename }}/action/${action}`, { //fetch data from webserver.py
89
94
  method: "POST",
90
95
  headers: {
91
96
  "Content-Type": "application/json",
92
97
  },
93
- body: JSON.stringify(data), //post can only send data via strings
94
- }).then(saveStorage()) //save to storage if successful
95
- showChangeStatus(response.status) //replace loading, show tick or cross
98
+ body: JSON.stringify(data), //note that post can only send data via strings
99
+ })
100
+ if (response.status == 201) {
101
+ showChangeStatus(response.status, {})
102
+ saveStorage() //save to storage if successful
103
+ } //if successful
104
+ else { showChangeStatus(response.status, await response.json()) } // else get Log data from response
96
105
  }
97
106
  else {
98
107
  showChangeStatus("remove") //replace loading, show tick or cross with none
@@ -100,27 +109,52 @@
100
109
  }
101
110
 
102
111
  //function in control of status icons of post above
103
- function showChangeStatus(status) {
104
- var loading = document.getElementById("loader") //element showing status
105
- if (status === "remove") { //show loading logo
112
+ async function showChangeStatus(status, logJson) {
113
+ var loading = document.getElementById("loader") //element showing statuses
114
+ if (status === "remove") { //remove all
106
115
  loading.innerHTML = "";
107
- loading.classList.remove("loading"); //append class with loading animation styling
116
+ loading.classList.remove("loading");
108
117
  }
109
118
  else if (status === "loading") { //show loading logo
110
119
  loading.innerHTML = "";
111
120
  loading.classList.add("loading"); //append class with loading animation styling
112
121
  }
113
- else if (status === 201) { //then show a tick
122
+ else if (status === 201) { //if status is 201, then show a tick
114
123
  loading.classList.remove("loading")
115
124
  loading.innerHTML = `<p class=tick>&#x2713;</p>`
125
+ getTemplate() //get updated templates
116
126
 
117
127
  }
118
128
  else { //then show a cross
119
129
  loading.classList.remove("loading")
120
- loading.innerHTML = `<p class=cross>&#x292C;</p>`
130
+ loading.innerHTML = `<p class=cross>&#x292C;</p>` //show cross icon to indicate an error
131
+ if (logJson.length != 0) {
132
+ document.getElementById("alert-text").textContent = "\r\n\u2022 " + logJson.join("\r\n\u2022 ") //show received log data in alert box
133
+ document.getElementById("alert").style.display = "block";
134
+ document.getElementById("alert").style.textAlign = "left";
135
+ }
121
136
  }
122
137
  }
123
138
 
139
+ //get rendered html template with containing new table data
140
+ async function getTemplate() { //fetch data from webserver.py
141
+ let htmlTemplateData = ""
142
+ response = await fetch(`{{ basename }}/template/table-template`, {
143
+ method: "GET"
144
+ })
145
+ blob = await response.blob() //get data blob
146
+ htmlTemplateData = await new Response(blob).text() //obtain html from blob
147
+ templateDiv = document.getElementById("template") //get template container element to override
148
+ templateDiv.innerHTML = htmlTemplateData //override container inner html with new data
149
+ var scripts = Array.from(templateDiv.getElementsByTagName('script')); //replace script tags manually
150
+ for (const script of scripts) {
151
+ var TempScript = document.createElement("script");
152
+ TempScript.innerHTML = script.innerHTML
153
+ script.parentElement.appendChild(TempScript)
154
+ }
155
+ }
156
+
157
+
124
158
  //test localStorage support
125
159
  function testStorage() {
126
160
  try {
@@ -142,7 +176,7 @@
142
176
  if (testStorage()) { //if local storage exists and works
143
177
  let selectElement = document.getElementById('input-select') // select button element
144
178
  var input_container = document.getElementById('input-container'); // container div containing all dynamic input elements (Box/List)
145
- if (localStorage.getItem("input_container_content")) { //If items already stored in local storage, then override default
179
+ if (localStorage.getItem("input_container_content") && localStorage.getItem("input_container_content") !== "{}" ) { //If items already stored in local storage, then override default
146
180
  if (selectElement.value == "Box") { //if Box is selected, show saved json data into box
147
181
  document.getElementById("text-area").value = localStorage.getItem("input_container_content");
148
182
  }
@@ -177,7 +211,7 @@
177
211
  input_container_child = input_container.firstElementChild; //work out which element is first inside container div
178
212
  var jsonReturnData = {};
179
213
 
180
- if (input_container_child == null){ //if no elements in container then return empty
214
+ if (input_container_child == null) { //if no elements in container then return empty
181
215
  return (jsonReturnData)
182
216
  };
183
217
  //if List return box json
@@ -200,6 +234,7 @@
200
234
  catch (error) { //if json error, show in alert box
201
235
  document.getElementById("alert-text").textContent = "\r\n" + error + "\r\n" + "JSON Error: String values may not be wrapped in quotes"
202
236
  document.getElementById("alert").style.display = "block";
237
+ document.getElementById("alert").style.textAlign = "center";
203
238
  return (0)
204
239
  }
205
240
  }
@@ -265,10 +300,7 @@
265
300
  //if box is selected, remove input-list elements and replace (with text-area)
266
301
  if (selectElement.value == "Box") {
267
302
  if (input_container_child_name == "input-list" || input_container_child === null) { // if input list exists or no Box element
268
- inputListArr = input_container.getElementsByClassName('input-list');
269
- while (inputListArr[0]) { //while there is still input-list elements remove
270
- inputListArr[0].parentNode.removeChild(inputListArr[0]);
271
- };
303
+ input_container.innerHTML = ""; //remove input-list list elements via erasing container innerHTML
272
304
  let div = document.createElement('div'); //add input-box element
273
305
  div.className = "input-box";
274
306
  div.innerHTML = `
@@ -291,6 +323,30 @@
291
323
  }
292
324
  }
293
325
 
326
+ //clear stored input data from localStorage (if any), clear input elements
327
+ async function ClearInputData(id) {
328
+ if (testStorage() && localStorage.getItem("input_container_content") !== null) {
329
+ localStorage.setItem("input_container_content", "{}")
330
+ }
331
+ ClearInputElements();
332
+
333
+ }
334
+
335
+ //clear input elements
336
+ async function ClearInputElements() {
337
+ let selectElement = document.getElementById('input-select')
338
+ var input_container = document.getElementById('input-container');
339
+ if (selectElement.value == "Box") {
340
+ document.getElementById("text-area").value = "{}";
341
+ }
342
+ if (selectElement.value == "List") {
343
+ input_container.innerHTML = "";
344
+ }
345
+
346
+ }
347
+
348
+
349
+
294
350
  //add listeners to buttons
295
351
  [
296
352
  "dayahead-optim",
@@ -310,6 +366,9 @@
310
366
  [
311
367
  "input-select"
312
368
  ].forEach((id) => document.getElementById(id).addEventListener('change', () => getSavedData(id)));
369
+ [
370
+ "input-clear"
371
+ ].forEach((id) => document.getElementById(id).addEventListener('click', () => ClearInputData(id)));
313
372
  </script>
314
373
 
315
374
  </html>
@@ -0,0 +1,5 @@
1
+ {% for plot in injection_dict %} <!-- diagrams/tables elements will be added here -->
2
+ <div class="table_div">
3
+ {{injection_dict[plot]}}
4
+ </div>
5
+ {% endfor %}
@@ -610,7 +610,7 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int,
610
610
  params['optim_conf']['load_cost_forecast_method'] = options.get('load_cost_forecast_method',params['optim_conf']['load_cost_forecast_method'])
611
611
  if options.get('list_set_deferrable_load_single_constant',None) != None:
612
612
  params['optim_conf']['set_def_constant'] = [i['set_deferrable_load_single_constant'] for i in options.get('list_set_deferrable_load_single_constant')]
613
- if options.get('list_peak_hours_periods_start_hours',None) != None:
613
+ if options.get('list_peak_hours_periods_start_hours',None) != None and options.get('list_peak_hours_periods_end_hours',None) != None:
614
614
  start_hours_list = [i['peak_hours_periods_start_hours'] for i in options['list_peak_hours_periods_start_hours']]
615
615
  end_hours_list = [i['peak_hours_periods_end_hours'] for i in options['list_peak_hours_periods_end_hours']]
616
616
  num_peak_hours = len(start_hours_list)
@@ -656,6 +656,41 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int,
656
656
  params['plant_conf']['SOCmin'] = options.get('battery_minimum_state_of_charge',params['plant_conf']['SOCmin'])
657
657
  params['plant_conf']['SOCmax'] = options.get('battery_maximum_state_of_charge',params['plant_conf']['SOCmax'])
658
658
  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
661
+ # If not, set defaults it fill in gaps
662
+ if params['optim_conf']['num_def_loads'] is not len(params['optim_conf']['def_start_timestep']):
663
+ 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")
664
+ for x in range(len(params['optim_conf']['def_start_timestep']), params['optim_conf']['num_def_loads']):
665
+ params['optim_conf']['def_start_timestep'].append(0)
666
+ if params['optim_conf']['num_def_loads'] is not len(params['optim_conf']['def_end_timestep']):
667
+ logger.warning("def_end_timestep / list_end_timesteps_of_each_deferrable_load does not match number in num_def_loads, adding default values to parameter")
668
+ for x in range(len(params['optim_conf']['def_end_timestep']), params['optim_conf']['num_def_loads']):
669
+ params['optim_conf']['def_end_timestep'].append(0)
670
+ if params['optim_conf']['num_def_loads'] is not len(params['optim_conf']['set_def_constant']):
671
+ logger.warning("set_def_constant / list_set_deferrable_load_single_constant does not match number in num_def_loads, adding default values to parameter")
672
+ for x in range(len(params['optim_conf']['set_def_constant']), params['optim_conf']['num_def_loads']):
673
+ params['optim_conf']['set_def_constant'].append(False)
674
+ if params['optim_conf']['num_def_loads'] is not len(params['optim_conf']['treat_def_as_semi_cont']):
675
+ logger.warning("treat_def_as_semi_cont / list_treat_deferrable_load_as_semi_cont does not match number in num_def_loads, adding default values to parameter")
676
+ for x in range(len(params['optim_conf']['treat_def_as_semi_cont']), params['optim_conf']['num_def_loads']):
677
+ params['optim_conf']['treat_def_as_semi_cont'].append(True)
678
+ if params['optim_conf']['num_def_loads'] is not len(params['optim_conf']['def_total_hours']):
679
+ logger.warning("def_total_hours / list_operating_hours_of_each_deferrable_load does not match number in num_def_loads, adding default values to parameter")
680
+ for x in range(len(params['optim_conf']['def_total_hours']), params['optim_conf']['num_def_loads']):
681
+ params['optim_conf']['def_total_hours'].append(0)
682
+ if params['optim_conf']['num_def_loads'] is not len(params['optim_conf']['P_deferrable_nom']):
683
+ 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
+ for x in range(len(params['optim_conf']['P_deferrable_nom']), params['optim_conf']['num_def_loads']):
685
+ 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
+ # days_to_retrieve should be no less then 2
691
+ if params['retrieve_hass_conf']['days_to_retrieve'] < 2:
692
+ params['retrieve_hass_conf']['days_to_retrieve'] = 2
693
+ logger.warning("days_to_retrieve should not be lower then 2, setting days_to_retrieve to 2. Make sure your sensors also have at least 2 days of history")
659
694
  else:
660
695
  params['params_secrets'] = params_secrets
661
696
  # The params dict
@@ -7,7 +7,7 @@ from requests import get
7
7
  from waitress import serve
8
8
  from importlib.metadata import version, PackageNotFoundError
9
9
  from pathlib import Path
10
- import os, json, argparse, pickle, yaml, logging
10
+ import os, json, argparse, pickle, yaml, logging, re
11
11
  from distutils.util import strtobool
12
12
  import pandas as pd
13
13
 
@@ -21,7 +21,41 @@ from emhass.utils import get_injection_dict, get_injection_dict_forecast_model_f
21
21
  # Define the Flask instance
22
22
  app = Flask(__name__)
23
23
 
24
+ #check logfile for error, anything after string match if provided
25
+ def checkFileLog(refString=None):
26
+ if (refString is not None):
27
+ logArray = grabLog(refString) #grab reduced log array
28
+ else:
29
+ if ((data_path / 'actionLogs.txt')).exists():
30
+ with open(str(data_path / 'actionLogs.txt'), "r") as fp:
31
+ logArray = fp.readlines()
32
+ for logString in logArray:
33
+ if (logString.split(' ', 1)[0] == "ERROR"):
34
+ return True
35
+ return False
24
36
 
37
+ #find string in logs, append all lines after to return
38
+ def grabLog(refString):
39
+ isFound = []
40
+ output = []
41
+ if ((data_path / 'actionLogs.txt')).exists():
42
+ with open(str(data_path / 'actionLogs.txt'), "r") as fp:
43
+ logArray = fp.readlines()
44
+ for x in range(len(logArray)-1): #find all matches and log key in isFound
45
+ if (re.search(refString,logArray[x])):
46
+ isFound.append(x)
47
+ if len(isFound) != 0:
48
+ for x in range(isFound[-1],len(logArray)): #use isFound to extract last related action logs
49
+ output.append(logArray[x])
50
+ return output
51
+
52
+ #clear the log file
53
+ def clearFileLog():
54
+ if ((data_path / 'actionLogs.txt')).exists():
55
+ with open(str(data_path / 'actionLogs.txt'), "w") as fp:
56
+ fp.truncate()
57
+
58
+ #initial index page render
25
59
  @app.route('/')
26
60
  def index():
27
61
  app.logger.info("EMHASS server online, serving index.html...")
@@ -39,6 +73,24 @@ def index():
39
73
  basename = request.headers.get("X-Ingress-Path", "")
40
74
  return make_response(template.render(injection_dict=injection_dict, basename=basename))
41
75
 
76
+ #get actions
77
+ @app.route('/template/<action_name>', methods=['GET'])
78
+ def template_action(action_name):
79
+ app.logger.info(" >> Sending rendered template table data")
80
+ if action_name == 'table-template':
81
+ file_loader = PackageLoader('emhass', 'templates')
82
+ env = Environment(loader=file_loader)
83
+ template = env.get_template('template.html')
84
+ if (data_path / 'injection_dict.pkl').exists():
85
+ with open(str(data_path / 'injection_dict.pkl'), "rb") as fid:
86
+ injection_dict = pickle.load(fid)
87
+ else:
88
+ app.logger.warning("The data container dictionary is empty... Please launch an optimization task")
89
+ injection_dict={}
90
+ basename = request.headers.get("X-Ingress-Path", "")
91
+ return make_response(template.render(injection_dict=injection_dict, basename=basename))
92
+
93
+ #post actions
42
94
  @app.route('/action/<action_name>', methods=['POST'])
43
95
  def action_call(action_name):
44
96
  with open(str(data_path / 'params.pkl'), "rb") as fid:
@@ -46,49 +98,71 @@ def action_call(action_name):
46
98
  runtimeparams = request.get_json(force=True)
47
99
  params = json.dumps(params)
48
100
  runtimeparams = json.dumps(runtimeparams)
101
+ ActionStr = " >> Setting input data dict"
102
+ app.logger.info(ActionStr)
49
103
  input_data_dict = set_input_data_dict(config_path, str(data_path), costfun,
50
104
  params, runtimeparams, action_name, app.logger)
105
+ if not input_data_dict:
106
+ return make_response(grabLog(ActionStr), 400)
51
107
  if action_name == 'publish-data':
52
- app.logger.info(" >> Publishing data...")
108
+ ActionStr = " >> Publishing data..."
109
+ app.logger.info(ActionStr)
53
110
  _ = publish_data(input_data_dict, app.logger)
54
111
  msg = f'EMHASS >> Action publish-data executed... \n'
55
- return make_response(msg, 201)
112
+ if not checkFileLog(ActionStr):
113
+ return make_response(msg, 201)
114
+ return make_response(grabLog(ActionStr), 400)
56
115
  elif action_name == 'perfect-optim':
57
- app.logger.info(" >> Performing perfect optimization...")
116
+ ActionStr = " >> Performing perfect optimization..."
117
+ app.logger.info(ActionStr)
58
118
  opt_res = perfect_forecast_optim(input_data_dict, app.logger)
59
119
  injection_dict = get_injection_dict(opt_res)
60
120
  with open(str(data_path / 'injection_dict.pkl'), "wb") as fid:
61
121
  pickle.dump(injection_dict, fid)
62
122
  msg = f'EMHASS >> Action perfect-optim executed... \n'
63
- return make_response(msg, 201)
123
+ if not checkFileLog(ActionStr):
124
+ return make_response(msg, 201)
125
+ return make_response(grabLog(ActionStr), 400)
64
126
  elif action_name == 'dayahead-optim':
65
- app.logger.info(" >> Performing dayahead optimization...")
127
+ ActionStr = " >> Performing dayahead optimization..."
128
+ app.logger.info(ActionStr)
66
129
  opt_res = dayahead_forecast_optim(input_data_dict, app.logger)
67
130
  injection_dict = get_injection_dict(opt_res)
68
131
  with open(str(data_path / 'injection_dict.pkl'), "wb") as fid:
69
132
  pickle.dump(injection_dict, fid)
70
133
  msg = f'EMHASS >> Action dayahead-optim executed... \n'
71
- return make_response(msg, 201)
134
+ if not checkFileLog(ActionStr):
135
+ return make_response(msg, 201)
136
+ return make_response(grabLog(ActionStr), 400)
72
137
  elif action_name == 'naive-mpc-optim':
73
- app.logger.info(" >> Performing naive MPC optimization...")
138
+ ActionStr = " >> Performing naive MPC optimization..."
139
+ app.logger.info(ActionStr)
74
140
  opt_res = naive_mpc_optim(input_data_dict, app.logger)
75
141
  injection_dict = get_injection_dict(opt_res)
76
142
  with open(str(data_path / 'injection_dict.pkl'), "wb") as fid:
77
143
  pickle.dump(injection_dict, fid)
78
144
  msg = f'EMHASS >> Action naive-mpc-optim executed... \n'
79
- return make_response(msg, 201)
145
+ if not checkFileLog(ActionStr):
146
+ return make_response(msg, 201)
147
+ return make_response(grabLog(ActionStr), 400)
80
148
  elif action_name == 'forecast-model-fit':
81
- app.logger.info(" >> Performing a machine learning forecast model fit...")
149
+ ActionStr = " >> Performing a machine learning forecast model fit..."
150
+ app.logger.info(ActionStr)
82
151
  df_fit_pred, _, mlf = forecast_model_fit(input_data_dict, app.logger)
83
152
  injection_dict = get_injection_dict_forecast_model_fit(
84
153
  df_fit_pred, mlf)
85
154
  with open(str(data_path / 'injection_dict.pkl'), "wb") as fid:
86
155
  pickle.dump(injection_dict, fid)
87
156
  msg = f'EMHASS >> Action forecast-model-fit executed... \n'
88
- return make_response(msg, 201)
157
+ if not checkFileLog(ActionStr):
158
+ return make_response(msg, 201)
159
+ return make_response(grabLog(ActionStr), 400)
89
160
  elif action_name == 'forecast-model-predict':
90
- app.logger.info(" >> Performing a machine learning forecast model predict...")
161
+ ActionStr = " >> Performing a machine learning forecast model predict..."
162
+ app.logger.info(ActionStr)
91
163
  df_pred = forecast_model_predict(input_data_dict, app.logger)
164
+ if df_pred is None:
165
+ return make_response(grabLog(ActionStr), 400)
92
166
  table1 = df_pred.reset_index().to_html(classes='mystyle', index=False)
93
167
  injection_dict = {}
94
168
  injection_dict['title'] = '<h2>Custom machine learning forecast model predict</h2>'
@@ -97,16 +171,23 @@ def action_call(action_name):
97
171
  with open(str(data_path / 'injection_dict.pkl'), "wb") as fid:
98
172
  pickle.dump(injection_dict, fid)
99
173
  msg = f'EMHASS >> Action forecast-model-predict executed... \n'
100
- return make_response(msg, 201)
174
+ if not checkFileLog(ActionStr):
175
+ return make_response(msg, 201)
176
+ return make_response(grabLog(ActionStr), 400)
101
177
  elif action_name == 'forecast-model-tune':
102
- app.logger.info(" >> Performing a machine learning forecast model tune...")
103
- df_pred_optim, mlf = forecast_model_tune(input_data_dict, app.logger)
178
+ ActionStr = " >> Performing a machine learning forecast model tune..."
179
+ app.logger.info(ActionStr)
180
+ df_pred_optim, mlf = forecast_model_tune(input_data_dict, app.logger)
181
+ if df_pred_optim is None or mlf is None:
182
+ return make_response(grabLog(ActionStr), 400)
104
183
  injection_dict = get_injection_dict_forecast_model_tune(
105
184
  df_pred_optim, mlf)
106
185
  with open(str(data_path / 'injection_dict.pkl'), "wb") as fid:
107
186
  pickle.dump(injection_dict, fid)
108
187
  msg = f'EMHASS >> Action forecast-model-tune executed... \n'
109
- return make_response(msg, 201)
188
+ if not checkFileLog(ActionStr):
189
+ return make_response(msg, 201)
190
+ return make_response(grabLog(ActionStr), 400)
110
191
  else:
111
192
  app.logger.error("ERROR: passed action is not valid")
112
193
  msg = f'EMHASS >> ERROR: Passed action is not valid... \n'
@@ -124,12 +205,14 @@ if __name__ == "__main__":
124
205
  use_options = os.getenv('USE_OPTIONS', default=False)
125
206
  # Define the paths
126
207
  if args.addon==1:
127
- OPTIONS_PATH = os.getenv('OPTIONS_PATH', default="/data/options.json")
208
+ OPTIONS_PATH = os.getenv('OPTIONS_PATH', default="/app/options.json")
128
209
  options_json = Path(OPTIONS_PATH)
129
- CONFIG_PATH = os.getenv("CONFIG_PATH", default="/usr/src/config_emhass.yaml")
210
+ CONFIG_PATH = os.getenv("CONFIG_PATH", default="/app/config_emhass.yaml")
130
211
  #Obtain url and key from ENV or ARG
131
212
  hass_url = os.getenv("EMHASS_URL", default=args.url)
132
- key = os.getenv("EMHASS_KEY", default=args.key)
213
+ key = os.getenv("SUPERVISOR_TOKEN", default=args.key)
214
+ if hass_url != "http://supervisor/core/api":
215
+ key = os.getenv("EMHASS_KEY", key)
133
216
  #If url or key is None, Set as empty string to reduce NoneType errors bellow
134
217
  if key is None: key = ""
135
218
  if hass_url is None: hass_url = ""
@@ -139,7 +222,7 @@ if __name__ == "__main__":
139
222
  options = json.load(data)
140
223
  else:
141
224
  app.logger.error("options.json does not exists")
142
- DATA_PATH = os.getenv("DATA_PATH", default="/share/")
225
+ DATA_PATH = os.getenv("DATA_PATH", default="/app/data/")
143
226
  else:
144
227
  if use_options:
145
228
  OPTIONS_PATH = os.getenv('OPTIONS_PATH', default="/app/options.json")
@@ -256,27 +339,39 @@ if __name__ == "__main__":
256
339
  pickle.dump((config_path, params), fid)
257
340
 
258
341
  # Define logger
259
- ch = logging.StreamHandler()
342
+ #stream logger
343
+ ch = logging.StreamHandler()
260
344
  formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
261
345
  ch.setFormatter(formatter)
346
+ #Action File logger
347
+ fileLogger = logging.FileHandler(str(data_path / 'actionLogs.txt'))
348
+ formatter = logging.Formatter('%(levelname)s - %(name)s - %(message)s')
349
+ fileLogger.setFormatter(formatter) # add format to Handler
262
350
  if logging_level == "DEBUG":
263
351
  app.logger.setLevel(logging.DEBUG)
264
352
  ch.setLevel(logging.DEBUG)
353
+ fileLogger.setLevel(logging.DEBUG)
265
354
  elif logging_level == "INFO":
266
355
  app.logger.setLevel(logging.INFO)
267
356
  ch.setLevel(logging.INFO)
357
+ fileLogger.setLevel(logging.INFO)
268
358
  elif logging_level == "WARNING":
269
359
  app.logger.setLevel(logging.WARNING)
270
360
  ch.setLevel(logging.WARNING)
361
+ fileLogger.setLevel(logging.WARNING)
271
362
  elif logging_level == "ERROR":
272
363
  app.logger.setLevel(logging.ERROR)
273
364
  ch.setLevel(logging.ERROR)
365
+ fileLogger.setLevel(logging.ERROR)
274
366
  else:
275
367
  app.logger.setLevel(logging.DEBUG)
276
368
  ch.setLevel(logging.DEBUG)
369
+ fileLogger.setLevel(logging.DEBUG)
277
370
  app.logger.propagate = False
278
371
  app.logger.addHandler(ch)
279
-
372
+ app.logger.addHandler(fileLogger)
373
+ clearFileLog() #Clear Action File logger file, ready for new instance
374
+
280
375
  # Launch server
281
376
  port = int(os.environ.get('PORT', 5000))
282
377
  app.logger.info("Launching the emhass webserver at: http://"+web_ui_url+":"+str(port))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: emhass
3
- Version: 0.7.7
3
+ Version: 0.8.0
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
@@ -460,7 +460,7 @@ Check the dedicated section in the documentation here: [https://emhass.readthedo
460
460
 
461
461
  ## Development
462
462
 
463
- Pull request are very much accepted on this project. For development you can find some instructions here [Development](./docs/develop.md)
463
+ Pull request are very much accepted on this project. For development you can find some instructions here [Development](https://emhass.readthedocs.io/en/latest/develop.html)
464
464
 
465
465
  ## Troubleshooting
466
466
 
@@ -40,6 +40,7 @@ src/emhass/static/style.css
40
40
  src/emhass/static/img/emhass_icon.png
41
41
  src/emhass/static/img/emhass_logo_short.svg
42
42
  src/emhass/templates/index.html
43
+ src/emhass/templates/template.html
43
44
  tests/test_command_line_utils.py
44
45
  tests/test_forecast.py
45
46
  tests/test_machine_learning_forecaster.py
@@ -84,7 +84,7 @@ class TestRetrieveHass(unittest.TestCase):
84
84
  days_list = get_days_list(1)
85
85
  var_list = [self.retrieve_hass_conf['var_load']]
86
86
  response = self.rh.get_data(days_list, var_list)
87
- self.assertEqual(response, "Request Get Error")
87
+ self.assertFalse(response)
88
88
 
89
89
  def test_get_data_mock(self):
90
90
  with requests_mock.mock() as m:
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
File without changes
File without changes