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.
- {emhass-0.7.7 → emhass-0.8.0}/CHANGELOG.md +16 -1
- {emhass-0.7.7 → emhass-0.8.0}/PKG-INFO +2 -2
- {emhass-0.7.7 → emhass-0.8.0}/README.md +1 -1
- {emhass-0.7.7 → emhass-0.8.0}/setup.py +3 -2
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass/command_line.py +21 -10
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass/forecast.py +6 -4
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass/retrieve_hass.py +26 -7
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass/static/style.css +12 -4
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass/templates/index.html +87 -28
- emhass-0.8.0/src/emhass/templates/template.html +5 -0
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass/utils.py +36 -1
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass/web_server.py +117 -22
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass.egg-info/PKG-INFO +2 -2
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass.egg-info/SOURCES.txt +1 -0
- {emhass-0.7.7 → emhass-0.8.0}/tests/test_retrieve_hass.py +1 -1
- {emhass-0.7.7 → emhass-0.8.0}/CODE_OF_CONDUCT.md +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/CONTRIBUTING.md +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/LICENSE +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/MANIFEST.in +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/data_load_cost_forecast.csv +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/data_load_forecast.csv +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/data_prod_price_forecast.csv +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/data_train_load_clustering.pkl +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/data_train_load_forecast.pkl +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/data_weather_forecast.csv +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/logger_emhass.log +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/opt_res_latest.csv +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/opt_res_perfect_optim_cost.csv +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/opt_res_perfect_optim_profit.csv +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/opt_res_perfect_optim_self-consumption.csv +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/test_df_final.pkl +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/test_response_get_data_get_method.pbz2 +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/test_response_scrapper_get_method.pbz2 +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/test_response_solarforecast_get_method.pbz2 +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/data/test_response_solcast_get_method.pbz2 +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/pyproject.toml +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/setup.cfg +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass/__init__.py +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass/machine_learning_forecaster.py +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass/optimization.py +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass/static/img/emhass_icon.png +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass/static/img/emhass_logo_short.svg +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass.egg-info/dependency_links.txt +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass.egg-info/entry_points.txt +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass.egg-info/requires.txt +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/src/emhass.egg-info/top_level.txt +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/tests/test_command_line_utils.py +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/tests/test_forecast.py +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/tests/test_machine_learning_forecaster.py +0 -0
- {emhass-0.7.7 → emhass-0.8.0}/tests/test_optimization.py +0 -0
- {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.
|
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.
|
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](
|
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](
|
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.
|
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','
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
135
|
-
|
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
|
-
|
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> <!--
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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;">© 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
|
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
|
-
})
|
95
|
-
|
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
|
105
|
-
if (status === "remove") { //
|
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");
|
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>✓</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>⤬</p>`
|
130
|
+
loading.innerHTML = `<p class=cross>⤬</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
|
-
|
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>
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
103
|
-
|
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
|
-
|
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="/
|
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="/
|
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("
|
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="/
|
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
|
-
|
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.
|
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](
|
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.
|
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
|
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
|
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
|