emhass 0.10.6__py3-none-any.whl → 0.11.1__py3-none-any.whl
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/command_line.py +178 -85
- emhass/data/associations.csv +63 -0
- emhass/data/config_defaults.json +117 -0
- emhass/forecast.py +38 -36
- emhass/machine_learning_forecaster.py +2 -1
- emhass/machine_learning_regressor.py +7 -2
- emhass/optimization.py +62 -62
- emhass/retrieve_hass.py +9 -4
- emhass/static/advanced.html +2 -1
- emhass/static/basic.html +4 -2
- emhass/static/configuration_list.html +44 -0
- emhass/static/configuration_script.js +872 -0
- emhass/static/data/param_definitions.json +424 -0
- emhass/static/script.js +345 -322
- emhass/static/style.css +267 -8
- emhass/templates/configuration.html +75 -0
- emhass/templates/index.html +15 -8
- emhass/utils.py +626 -302
- emhass/web_server.py +322 -213
- {emhass-0.10.6.dist-info → emhass-0.11.1.dist-info}/METADATA +207 -169
- emhass-0.11.1.dist-info/RECORD +32 -0
- {emhass-0.10.6.dist-info → emhass-0.11.1.dist-info}/WHEEL +1 -1
- emhass-0.10.6.dist-info/RECORD +0 -26
- {emhass-0.10.6.dist-info → emhass-0.11.1.dist-info}/LICENSE +0 -0
- {emhass-0.10.6.dist-info → emhass-0.11.1.dist-info}/entry_points.txt +0 -0
- {emhass-0.10.6.dist-info → emhass-0.11.1.dist-info}/top_level.txt +0 -0
emhass/web_server.py
CHANGED
@@ -16,60 +16,94 @@ from emhass.command_line import forecast_model_fit, forecast_model_predict, fore
|
|
16
16
|
from emhass.command_line import regressor_model_fit, regressor_model_predict
|
17
17
|
from emhass.command_line import publish_data, continual_publish
|
18
18
|
from emhass.utils import get_injection_dict, get_injection_dict_forecast_model_fit, \
|
19
|
-
get_injection_dict_forecast_model_tune, build_params
|
19
|
+
get_injection_dict_forecast_model_tune, build_config, build_secrets, build_params, \
|
20
|
+
param_to_config, build_legacy_config_params
|
20
21
|
|
21
22
|
# Define the Flask instance
|
22
23
|
app = Flask(__name__)
|
24
|
+
emhass_conf = {}
|
23
25
|
|
24
|
-
|
25
|
-
|
26
|
+
def checkFileLog(refString=None) -> bool:
|
27
|
+
"""
|
28
|
+
Check logfile for error, anything after string match if provided.
|
29
|
+
|
30
|
+
:param refString: String to reduce log area to check for errors. Use to reduce log to check anything after string match (ie. an action).
|
31
|
+
:type refString: str
|
32
|
+
:return: Boolean return if error was found in logs
|
33
|
+
:rtype: bool
|
34
|
+
|
35
|
+
"""
|
26
36
|
if (refString is not None):
|
27
|
-
logArray = grabLog(refString) #grab reduced log array
|
37
|
+
logArray = grabLog(refString) #grab reduced log array (everything after string match)
|
28
38
|
else:
|
29
39
|
if ((emhass_conf['data_path'] / 'actionLogs.txt')).exists():
|
30
40
|
with open(str(emhass_conf['data_path'] / 'actionLogs.txt'), "r") as fp:
|
31
41
|
logArray = fp.readlines()
|
42
|
+
else:
|
43
|
+
app.logger.debug("Unable to obtain actionLogs.txt")
|
32
44
|
for logString in logArray:
|
33
45
|
if (logString.split(' ', 1)[0] == "ERROR"):
|
34
46
|
return True
|
35
47
|
return False
|
36
48
|
|
37
|
-
|
38
|
-
|
49
|
+
def grabLog(refString) -> list:
|
50
|
+
"""
|
51
|
+
Find string in logs, append all lines after into list to return.
|
52
|
+
|
53
|
+
:param refString: String used to string match log.
|
54
|
+
:type refString: str
|
55
|
+
:return: List of lines in log after string match.
|
56
|
+
:rtype: list
|
57
|
+
|
58
|
+
"""
|
39
59
|
isFound = []
|
40
60
|
output = []
|
41
61
|
if ((emhass_conf['data_path'] / 'actionLogs.txt')).exists():
|
42
62
|
with open(str(emhass_conf['data_path'] / 'actionLogs.txt'), "r") as fp:
|
43
63
|
logArray = fp.readlines()
|
44
|
-
|
64
|
+
# Find all string matches, log key (line Number) in isFound
|
65
|
+
for x in range(len(logArray)-1):
|
45
66
|
if (re.search(refString,logArray[x])):
|
46
67
|
isFound.append(x)
|
47
68
|
if len(isFound) != 0:
|
48
|
-
|
69
|
+
# Use last item in isFound to extract action logs
|
70
|
+
for x in range(isFound[-1],len(logArray)):
|
49
71
|
output.append(logArray[x])
|
50
72
|
return output
|
51
73
|
|
52
74
|
# Clear the log file
|
53
|
-
def clearFileLog():
|
75
|
+
def clearFileLog():
|
76
|
+
"""
|
77
|
+
Clear the contents of the log file (actionLogs.txt)
|
78
|
+
|
79
|
+
"""
|
54
80
|
if ((emhass_conf['data_path'] / 'actionLogs.txt')).exists():
|
55
81
|
with open(str(emhass_conf['data_path'] / 'actionLogs.txt'), "w") as fp:
|
56
82
|
fp.truncate()
|
57
83
|
|
58
|
-
|
59
|
-
# Initial index page render
|
60
84
|
@app.route('/')
|
85
|
+
@app.route('/index')
|
61
86
|
def index():
|
87
|
+
"""
|
88
|
+
Render initial index page and serve to web server.
|
89
|
+
Appends plot tables saved from previous optimization into index.html, then serves.
|
90
|
+
|
91
|
+
"""
|
62
92
|
app.logger.info("EMHASS server online, serving index.html...")
|
63
93
|
# Load HTML template
|
64
94
|
file_loader = PackageLoader('emhass', 'templates')
|
65
95
|
env = Environment(loader=file_loader)
|
96
|
+
#check if index.html exists
|
97
|
+
if 'index.html' not in env.list_templates():
|
98
|
+
app.logger.error("Unable to find index.html in emhass module")
|
99
|
+
return make_response(["ERROR: unable to find index.html in emhass module"],404)
|
66
100
|
template = env.get_template('index.html')
|
67
|
-
# Load
|
101
|
+
# Load cached dict (if exists), to present generated plot tables
|
68
102
|
if (emhass_conf['data_path'] / 'injection_dict.pkl').exists():
|
69
103
|
with open(str(emhass_conf['data_path'] / 'injection_dict.pkl'), "rb") as fid:
|
70
104
|
injection_dict = pickle.load(fid)
|
71
105
|
else:
|
72
|
-
app.logger.
|
106
|
+
app.logger.info("The data container dictionary is empty... Please launch an optimization task")
|
73
107
|
injection_dict={}
|
74
108
|
|
75
109
|
# replace {{basename}} in html template html with path root
|
@@ -79,35 +113,204 @@ def index():
|
|
79
113
|
return make_response(template.render(injection_dict=injection_dict))
|
80
114
|
|
81
115
|
|
82
|
-
|
83
|
-
|
84
|
-
|
116
|
+
@app.route('/configuration')
|
117
|
+
def configuration():
|
118
|
+
"""
|
119
|
+
Configuration page actions:
|
120
|
+
Render and serve configuration page html
|
121
|
+
|
122
|
+
"""
|
123
|
+
app.logger.info("serving configuration.html...")
|
124
|
+
# Load HTML template
|
125
|
+
file_loader = PackageLoader('emhass', 'templates')
|
126
|
+
env = Environment(loader=file_loader)
|
127
|
+
#check if configuration.html exists
|
128
|
+
if 'configuration.html' not in env.list_templates():
|
129
|
+
app.logger.error("Unable to find configuration.html in emhass module")
|
130
|
+
return make_response(["ERROR: unable to find configuration.html in emhass module"],404)
|
131
|
+
template = env.get_template('configuration.html')
|
132
|
+
return make_response(template.render(config=params))
|
133
|
+
|
134
|
+
|
135
|
+
@app.route('/template', methods=['GET'])
|
136
|
+
def template_action():
|
137
|
+
"""
|
138
|
+
template page actions:
|
139
|
+
Render and serve template html
|
140
|
+
|
141
|
+
"""
|
85
142
|
app.logger.info(" >> Sending rendered template table data")
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
injection_dict=
|
96
|
-
|
143
|
+
file_loader = PackageLoader('emhass', 'templates')
|
144
|
+
env = Environment(loader=file_loader)
|
145
|
+
# Check if template.html exists
|
146
|
+
if 'template.html' not in env.list_templates():
|
147
|
+
app.logger.error("Unable to find template.html in emhass module")
|
148
|
+
return make_response(["WARNING: unable to find template.html in emhass module"],404)
|
149
|
+
template = env.get_template('template.html')
|
150
|
+
if (emhass_conf['data_path'] / 'injection_dict.pkl').exists():
|
151
|
+
with open(str(emhass_conf['data_path'] / 'injection_dict.pkl'), "rb") as fid:
|
152
|
+
injection_dict = pickle.load(fid)
|
153
|
+
else:
|
154
|
+
app.logger.warning("Unable to obtain plot data from injection_dict.pkl")
|
155
|
+
app.logger.warning("Try running an launch an optimization task")
|
156
|
+
injection_dict={}
|
157
|
+
return make_response(template.render(injection_dict=injection_dict))
|
158
|
+
|
159
|
+
@app.route('/get-config', methods=['GET'])
|
160
|
+
def parameter_get():
|
161
|
+
"""
|
162
|
+
Get request action that builds, formats and sends config as json (config.json format)
|
163
|
+
|
164
|
+
"""
|
165
|
+
app.logger.debug("Obtaining current saved parameters as config")
|
166
|
+
# Build config from all possible sources (inc. legacy yaml config)
|
167
|
+
config = build_config(emhass_conf,app.logger,emhass_conf["defaults_path"],emhass_conf["config_path"],emhass_conf["legacy_config_path"])
|
168
|
+
if type(config) is bool and not config:
|
169
|
+
return make_response(["failed to retrieve default config file"],500)
|
170
|
+
# Format parameters in config with params (converting legacy json parameters from options.json if any)
|
171
|
+
params = build_params(emhass_conf,{},config,app.logger)
|
172
|
+
if type(params) is bool and not params:
|
173
|
+
return make_response(["Unable to obtain associations file"],500)
|
174
|
+
# Covert formatted parameters from params back into config.json format
|
175
|
+
return_config = param_to_config(params,app.logger)
|
176
|
+
# Send config
|
177
|
+
return make_response(return_config,201)
|
178
|
+
|
179
|
+
|
180
|
+
# Get default Config
|
181
|
+
@app.route('/get-config/defaults', methods=['GET'])
|
182
|
+
def config_get():
|
183
|
+
"""
|
184
|
+
Get request action, retrieves and sends default configuration
|
185
|
+
|
186
|
+
"""
|
187
|
+
app.logger.debug("Obtaining default parameters")
|
188
|
+
# Build config, passing only default file
|
189
|
+
config = build_config(emhass_conf,app.logger,emhass_conf["defaults_path"])
|
190
|
+
if type(config) is bool and not config:
|
191
|
+
return make_response(["failed to retrieve default config file"],500)
|
192
|
+
# Format parameters in config with params
|
193
|
+
params = build_params(emhass_conf,{},config,app.logger)
|
194
|
+
if type(params) is bool and not params:
|
195
|
+
return make_response(["Unable to obtain associations file"],500)
|
196
|
+
# Covert formatted parameters from params back into config.json format
|
197
|
+
return_config = param_to_config(params,app.logger)
|
198
|
+
# Send params
|
199
|
+
return make_response(return_config,201)
|
200
|
+
|
201
|
+
|
202
|
+
# Get YAML-to-JSON config
|
203
|
+
@app.route('/get-json', methods=['POST'])
|
204
|
+
def json_convert():
|
205
|
+
"""
|
206
|
+
Post request action, receives yaml config (config_emhass.yaml or EMHASS-Add-on config page) and converts to config json format.
|
207
|
+
|
208
|
+
"""
|
209
|
+
app.logger.info("Attempting to convert YAML to JSON")
|
210
|
+
data = request.get_data()
|
211
|
+
yaml_config = yaml.safe_load(data)
|
212
|
+
|
213
|
+
# If filed to Parse YAML
|
214
|
+
if yaml_config is None:
|
215
|
+
return make_response(["failed to Parse YAML from data"],400)
|
216
|
+
# Test YAML is legacy config format (from config_emhass.yaml)
|
217
|
+
test_legacy_config = build_legacy_config_params(emhass_conf,yaml_config, app.logger)
|
218
|
+
if test_legacy_config:
|
219
|
+
yaml_config = test_legacy_config
|
220
|
+
# Format YAML to params (format params. check if params match legacy option.json format)
|
221
|
+
params = build_params(emhass_conf,{},yaml_config,app.logger)
|
222
|
+
if type(params) is bool and not params:
|
223
|
+
return make_response(["Unable to obtain associations file"],500)
|
224
|
+
# Covert formatted parameters from params back into config.json format
|
225
|
+
config = param_to_config(params,app.logger)
|
226
|
+
# convert json to str
|
227
|
+
config = json.dumps(config)
|
228
|
+
|
229
|
+
# Send params
|
230
|
+
return make_response(config,201)
|
231
|
+
|
232
|
+
@app.route('/set-config', methods=['POST'])
|
233
|
+
def parameter_set():
|
234
|
+
"""
|
235
|
+
Receive JSON config, and save config to file (config.json and param.pkl)
|
236
|
+
|
237
|
+
"""
|
238
|
+
config = {}
|
239
|
+
if not emhass_conf['defaults_path']:
|
240
|
+
return make_response(["Unable to Obtain defaults_path from emhass_conf"],500)
|
241
|
+
if not emhass_conf['config_path']:
|
242
|
+
return make_response(["Unable to Obtain config_path from emhass_conf"],500)
|
243
|
+
|
244
|
+
# Load defaults as a reference point (for sorting) and a base to override
|
245
|
+
if os.path.exists(emhass_conf['defaults_path']) and Path(emhass_conf['defaults_path']).is_file():
|
246
|
+
with emhass_conf['defaults_path'].open('r') as data:
|
247
|
+
config = json.load(data)
|
248
|
+
else:
|
249
|
+
app.logger.warning("Unable to obtain default config. only parameters passed from request will be saved to config.json")
|
250
|
+
|
251
|
+
# Retrieve sent config json
|
252
|
+
request_data = request.get_json(force=True)
|
253
|
+
|
254
|
+
# check if data is empty
|
255
|
+
if len(request_data) == 0:
|
256
|
+
return make_response(["failed to retrieve config json"],400)
|
257
|
+
|
258
|
+
# Format config by converting to params (format params. check if params match legacy option.json format. If so format)
|
259
|
+
params = build_params(emhass_conf,params_secrets,request_data,app.logger)
|
260
|
+
if type(params) is bool and not params:
|
261
|
+
return make_response(["Unable to obtain associations file"],500)
|
262
|
+
|
263
|
+
# Covert formatted parameters from params back into config.json format.
|
264
|
+
# Overwrite existing default parameters in config
|
265
|
+
config.update(param_to_config(params,app.logger))
|
266
|
+
|
267
|
+
# Save config to config.json
|
268
|
+
if os.path.exists(emhass_conf['config_path'].parent):
|
269
|
+
with emhass_conf['config_path'].open('w') as f:
|
270
|
+
json.dump(config, f, indent=4)
|
271
|
+
else:
|
272
|
+
return make_response(["Unable to save config file"],500)
|
273
|
+
request_data
|
274
|
+
|
275
|
+
# Save params with updated config
|
276
|
+
if os.path.exists(emhass_conf['data_path']):
|
277
|
+
with open(str(emhass_conf['data_path'] / 'params.pkl'), "wb") as fid:
|
278
|
+
pickle.dump((config_path, build_params(emhass_conf,params_secrets,config,app.logger)), fid)
|
279
|
+
else:
|
280
|
+
return make_response(["Unable to save params file, missing data_path"],500)
|
281
|
+
|
282
|
+
app.logger.info("Saved parameters from webserver")
|
283
|
+
return make_response({},201)
|
97
284
|
|
98
|
-
#post actions
|
99
285
|
@app.route('/action/<action_name>', methods=['POST'])
|
100
286
|
def action_call(action_name):
|
287
|
+
"""
|
288
|
+
Receive Post action, run action according to passed slug(action_name) (e.g. /action/publish-data)
|
289
|
+
|
290
|
+
:param action_name: Slug/Action string corresponding to which action to take
|
291
|
+
:type action_name: String
|
292
|
+
|
293
|
+
"""
|
101
294
|
# Setting up parameters
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
295
|
+
# Params
|
296
|
+
if (emhass_conf['data_path'] / 'params.pkl').exists():
|
297
|
+
with open(str(emhass_conf['data_path'] / 'params.pkl'), "rb") as fid:
|
298
|
+
emhass_conf['config_path'], params = pickle.load(fid)
|
299
|
+
params = json.dumps(params)
|
300
|
+
else:
|
301
|
+
app.logger.error("Unable to find params.pkl file")
|
302
|
+
return make_response(grabLog(ActionStr), 400)
|
303
|
+
# Runtime
|
304
|
+
runtimeparams = request.get_json(force=True,silent=True)
|
305
|
+
if runtimeparams is not None:
|
306
|
+
if runtimeparams != '{}':
|
307
|
+
app.logger.info("Passed runtime parameters: " + str(runtimeparams))
|
308
|
+
else:
|
309
|
+
app.logger.warning("Unable to parse runtime parameters")
|
310
|
+
runtimeparams = {}
|
108
311
|
runtimeparams = json.dumps(runtimeparams)
|
109
|
-
|
110
|
-
#
|
312
|
+
|
313
|
+
# weather-forecast-cache (check before set_input_data_dict)
|
111
314
|
if action_name == 'weather-forecast-cache':
|
112
315
|
ActionStr = " >> Performing weather forecast, try to caching result"
|
113
316
|
app.logger.info(ActionStr)
|
@@ -125,13 +328,15 @@ def action_call(action_name):
|
|
125
328
|
return make_response(grabLog(ActionStr), 400)
|
126
329
|
|
127
330
|
# If continual_publish is True, start thread with loop function
|
128
|
-
if len(continual_publish_thread) == 0 and input_data_dict['retrieve_hass_conf'].get(
|
331
|
+
if len(continual_publish_thread) == 0 and input_data_dict['retrieve_hass_conf'].get('continual_publish',False):
|
129
332
|
# Start Thread
|
130
|
-
continualLoop = threading.Thread(name=
|
333
|
+
continualLoop = threading.Thread(name='continual_publish',target=continual_publish,args=[input_data_dict,entity_path,app.logger])
|
131
334
|
continualLoop.start()
|
132
335
|
continual_publish_thread.append(continualLoop)
|
133
|
-
|
134
|
-
#
|
336
|
+
|
337
|
+
# Run action based on POST request
|
338
|
+
# If error in log when running action, return actions log (list) as response. (Using ActionStr as a reference of the action start in the log)
|
339
|
+
# publish-data
|
135
340
|
if action_name == 'publish-data':
|
136
341
|
ActionStr = " >> Publishing data..."
|
137
342
|
app.logger.info(ActionStr)
|
@@ -140,6 +345,7 @@ def action_call(action_name):
|
|
140
345
|
if not checkFileLog(ActionStr):
|
141
346
|
return make_response(msg, 201)
|
142
347
|
return make_response(grabLog(ActionStr), 400)
|
348
|
+
# perfect-optim
|
143
349
|
elif action_name == 'perfect-optim':
|
144
350
|
ActionStr = " >> Performing perfect optimization..."
|
145
351
|
app.logger.info(ActionStr)
|
@@ -151,6 +357,7 @@ def action_call(action_name):
|
|
151
357
|
if not checkFileLog(ActionStr):
|
152
358
|
return make_response(msg, 201)
|
153
359
|
return make_response(grabLog(ActionStr), 400)
|
360
|
+
# dayahead-optim
|
154
361
|
elif action_name == 'dayahead-optim':
|
155
362
|
ActionStr = " >> Performing dayahead optimization..."
|
156
363
|
app.logger.info(ActionStr)
|
@@ -162,6 +369,7 @@ def action_call(action_name):
|
|
162
369
|
if not checkFileLog(ActionStr):
|
163
370
|
return make_response(msg, 201)
|
164
371
|
return make_response(grabLog(ActionStr), 400)
|
372
|
+
# naive-mpc-optim
|
165
373
|
elif action_name == 'naive-mpc-optim':
|
166
374
|
ActionStr = " >> Performing naive MPC optimization..."
|
167
375
|
app.logger.info(ActionStr)
|
@@ -173,6 +381,7 @@ def action_call(action_name):
|
|
173
381
|
if not checkFileLog(ActionStr):
|
174
382
|
return make_response(msg, 201)
|
175
383
|
return make_response(grabLog(ActionStr), 400)
|
384
|
+
# forecast-model-fit
|
176
385
|
elif action_name == 'forecast-model-fit':
|
177
386
|
ActionStr = " >> Performing a machine learning forecast model fit..."
|
178
387
|
app.logger.info(ActionStr)
|
@@ -185,6 +394,7 @@ def action_call(action_name):
|
|
185
394
|
if not checkFileLog(ActionStr):
|
186
395
|
return make_response(msg, 201)
|
187
396
|
return make_response(grabLog(ActionStr), 400)
|
397
|
+
# forecast-model-predict
|
188
398
|
elif action_name == 'forecast-model-predict':
|
189
399
|
ActionStr = " >> Performing a machine learning forecast model predict..."
|
190
400
|
app.logger.info(ActionStr)
|
@@ -202,6 +412,7 @@ def action_call(action_name):
|
|
202
412
|
if not checkFileLog(ActionStr):
|
203
413
|
return make_response(msg, 201)
|
204
414
|
return make_response(grabLog(ActionStr), 400)
|
415
|
+
# forecast-model-tune
|
205
416
|
elif action_name == 'forecast-model-tune':
|
206
417
|
ActionStr = " >> Performing a machine learning forecast model tune..."
|
207
418
|
app.logger.info(ActionStr)
|
@@ -216,6 +427,7 @@ def action_call(action_name):
|
|
216
427
|
if not checkFileLog(ActionStr):
|
217
428
|
return make_response(msg, 201)
|
218
429
|
return make_response(grabLog(ActionStr), 400)
|
430
|
+
# regressor-model-fit
|
219
431
|
elif action_name == 'regressor-model-fit':
|
220
432
|
ActionStr = " >> Performing a machine learning regressor fit..."
|
221
433
|
app.logger.info(ActionStr)
|
@@ -224,6 +436,7 @@ def action_call(action_name):
|
|
224
436
|
if not checkFileLog(ActionStr):
|
225
437
|
return make_response(msg, 201)
|
226
438
|
return make_response(grabLog(ActionStr), 400)
|
439
|
+
# regressor-model-predict
|
227
440
|
elif action_name == 'regressor-model-predict':
|
228
441
|
ActionStr = " >> Performing a machine learning regressor predict..."
|
229
442
|
app.logger.info(ActionStr)
|
@@ -232,6 +445,7 @@ def action_call(action_name):
|
|
232
445
|
if not checkFileLog(ActionStr):
|
233
446
|
return make_response(msg, 201)
|
234
447
|
return make_response(grabLog(ActionStr), 400)
|
448
|
+
# Else return error
|
235
449
|
else:
|
236
450
|
app.logger.error("ERROR: passed action is not valid")
|
237
451
|
msg = f'EMHASS >> ERROR: Passed action is not valid... \n'
|
@@ -242,82 +456,74 @@ if __name__ == "__main__":
|
|
242
456
|
parser = argparse.ArgumentParser()
|
243
457
|
parser.add_argument('--url', type=str, help='The URL to your Home Assistant instance, ex the external_url in your hass configuration')
|
244
458
|
parser.add_argument('--key', type=str, help='Your access key. If using EMHASS in standalone this should be a Long-Lived Access Token')
|
245
|
-
parser.add_argument('--addon', type=strtobool, default='False', help='Define if we are usinng EMHASS with the add-on or in standalone mode')
|
246
459
|
parser.add_argument('--no_response', type=strtobool, default='False', help='This is set if json response errors occur')
|
247
460
|
args = parser.parse_args()
|
461
|
+
|
462
|
+
# Pre formatted config parameters
|
463
|
+
config = {}
|
464
|
+
# Secrets
|
465
|
+
params_secrets = {}
|
466
|
+
# Built parameters (formatted config + secrets)
|
467
|
+
params = None
|
248
468
|
|
249
|
-
#
|
250
|
-
hass_url = os.getenv("EMHASS_URL", default=args.url)
|
251
|
-
key = os.getenv("SUPERVISOR_TOKEN", default=args.key)
|
252
|
-
if hass_url != "http://supervisor/core/api":
|
253
|
-
key = os.getenv("EMHASS_KEY", key)
|
254
|
-
#If url or key is None, Set as empty string to reduce NoneType errors bellow
|
255
|
-
if key is None: key = ""
|
256
|
-
if hass_url is None: hass_url = ""
|
257
|
-
|
258
|
-
#find env's, not not set defaults
|
259
|
-
use_options = os.getenv('USE_OPTIONS', default=False)
|
260
|
-
CONFIG_PATH = os.getenv("CONFIG_PATH", default="/app/config_emhass.yaml")
|
261
|
-
OPTIONS_PATH = os.getenv('OPTIONS_PATH', default="/app/options.json")
|
469
|
+
# Find env's, not not set defaults
|
262
470
|
DATA_PATH = os.getenv("DATA_PATH", default="/app/data/")
|
263
471
|
ROOT_PATH = os.getenv("ROOT_PATH", default=str(Path(__file__).parent))
|
264
|
-
|
265
|
-
|
266
|
-
|
472
|
+
CONFIG_PATH = os.getenv('CONFIG_PATH', default="/share/config.json")
|
473
|
+
OPTIONS_PATH = os.getenv('OPTIONS_PATH', default="/data/options.json")
|
474
|
+
DEFAULTS_PATH = os.getenv('DEFAULTS_PATH', default=ROOT_PATH +"/data/config_defaults.json")
|
475
|
+
ASSOCIATIONS_PATH = os.getenv('ASSOCIATIONS_PATH', default=ROOT_PATH + "/data/associations.csv")
|
476
|
+
LEGACY_CONFIG_PATH = os.getenv("LEGACY_CONFIG_PATH", default="/app/config_emhass.yaml")
|
267
477
|
|
268
478
|
# Define the paths
|
269
|
-
if args.addon==1:
|
270
|
-
options_json = Path(OPTIONS_PATH)
|
271
|
-
# Read options info
|
272
|
-
if options_json.exists():
|
273
|
-
with options_json.open('r') as data:
|
274
|
-
options = json.load(data)
|
275
|
-
else:
|
276
|
-
app.logger.error("options.json does not exist")
|
277
|
-
raise Exception("options.json does not exist in path: "+str(options_json))
|
278
|
-
else:
|
279
|
-
if use_options:
|
280
|
-
options_json = Path(OPTIONS_PATH)
|
281
|
-
# Read options info
|
282
|
-
if options_json.exists():
|
283
|
-
with options_json.open('r') as data:
|
284
|
-
options = json.load(data)
|
285
|
-
else:
|
286
|
-
app.logger.error("options.json does not exist")
|
287
|
-
raise Exception("options.json does not exist in path: "+str(options_json))
|
288
|
-
else:
|
289
|
-
options = None
|
290
|
-
|
291
|
-
#if data path specified by options.json
|
292
|
-
if options is not None:
|
293
|
-
if options.get('data_path', None) != None and options.get('data_path', None) != "default":
|
294
|
-
DATA_PATH = options.get('data_path', None);
|
295
|
-
|
296
|
-
#save paths to dictionary
|
297
479
|
config_path = Path(CONFIG_PATH)
|
480
|
+
options_path = Path(OPTIONS_PATH)
|
481
|
+
defaults_path = Path(DEFAULTS_PATH)
|
482
|
+
associations_path = Path(ASSOCIATIONS_PATH)
|
483
|
+
legacy_config_path = Path(LEGACY_CONFIG_PATH)
|
298
484
|
data_path = Path(DATA_PATH)
|
299
485
|
root_path = Path(ROOT_PATH)
|
300
|
-
|
486
|
+
# Add paths to emhass_conf
|
301
487
|
emhass_conf['config_path'] = config_path
|
488
|
+
emhass_conf['options_path'] = options_path
|
489
|
+
emhass_conf['defaults_path'] = defaults_path
|
490
|
+
emhass_conf['associations_path'] = associations_path
|
491
|
+
emhass_conf['legacy_config_path'] = legacy_config_path
|
302
492
|
emhass_conf['data_path'] = data_path
|
303
493
|
emhass_conf['root_path'] = root_path
|
304
|
-
|
305
|
-
# Read the example default config file
|
306
|
-
if config_path.exists():
|
307
|
-
with open(config_path, 'r') as file:
|
308
|
-
config = yaml.load(file, Loader=yaml.FullLoader)
|
309
|
-
retrieve_hass_conf = config['retrieve_hass_conf']
|
310
|
-
optim_conf = config['optim_conf']
|
311
|
-
plant_conf = config['plant_conf']
|
312
|
-
else:
|
313
|
-
app.logger.error("Unable to open the default configuration yaml file")
|
314
|
-
raise Exception("Failed to open config file, config_path: "+str(config_path))
|
315
494
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
495
|
+
# Combine parameters from configuration sources (if exists)
|
496
|
+
config.update(build_config(emhass_conf,app.logger,defaults_path,config_path,legacy_config_path))
|
497
|
+
if type(config) is bool and not config:
|
498
|
+
raise Exception("Failed to find default config")
|
499
|
+
|
500
|
+
costfun = os.getenv('LOCAL_COSTFUN', config.get('costfun', 'profit'))
|
501
|
+
logging_level = os.getenv('LOGGING_LEVEL', config.get('logging_level','INFO'))
|
502
|
+
# Temporary set logging level if debug
|
503
|
+
if logging_level == "DEBUG":
|
504
|
+
app.logger.setLevel(logging.DEBUG)
|
505
|
+
|
506
|
+
## Secrets
|
507
|
+
argument = {}
|
508
|
+
if args.url:
|
509
|
+
argument['url'] = args.url
|
510
|
+
if args.key:
|
511
|
+
argument['key'] = args.key
|
512
|
+
# Combine secrets from ENV, Arguments/ARG, Secrets file (secrets_emhass.yaml), options (options.json from addon configuration file) and/or Home Assistant Standalone API (if exist)
|
513
|
+
emhass_conf, secrets = build_secrets(emhass_conf,app.logger,argument,options_path,os.getenv('SECRETS_PATH', default='/app/secrets_emhass.yaml'), bool(args.no_response))
|
514
|
+
params_secrets.update(secrets)
|
515
|
+
|
516
|
+
server_ip = params_secrets.get("server_ip","0.0.0.0")
|
517
|
+
|
518
|
+
# Check if data path exists
|
519
|
+
if not os.path.isdir(emhass_conf['data_path']):
|
520
|
+
app.logger.warning("Unable to find data_path: " + str(emhass_conf['data_path']))
|
521
|
+
if os.path.isdir(Path("/app/data/")):
|
522
|
+
emhass_conf['data_path'] = Path("/app/data/")
|
523
|
+
else:
|
524
|
+
Path(root_path / "data/").mkdir(parents=True, exist_ok=True)
|
525
|
+
emhass_conf['data_path'] = root_path / "data/"
|
526
|
+
app.logger.info("data_path has been set to " + str(emhass_conf['data_path']))
|
321
527
|
|
322
528
|
# Initialize this global dict
|
323
529
|
if (emhass_conf['data_path'] / 'injection_dict.pkl').exists():
|
@@ -325,123 +531,23 @@ if __name__ == "__main__":
|
|
325
531
|
injection_dict = pickle.load(fid)
|
326
532
|
else:
|
327
533
|
injection_dict = None
|
534
|
+
|
535
|
+
# Build params from config and param_secrets (migrate params to correct config catagories), save result to params.pkl
|
536
|
+
params = build_params(emhass_conf, params_secrets, config, app.logger)
|
537
|
+
if type(params) is bool:
|
538
|
+
raise Exception("A error has occurred while building params")
|
328
539
|
|
329
|
-
if args.addon==1:
|
330
|
-
# The cost function
|
331
|
-
costfun = options.get('costfun', 'profit')
|
332
|
-
# Some data from options
|
333
|
-
logging_level = options.get('logging_level','INFO')
|
334
|
-
url_from_options = options.get('hass_url', 'empty')
|
335
|
-
if url_from_options == 'empty' or url_from_options == '' or url_from_options == "http://supervisor/core/api":
|
336
|
-
url = "http://supervisor/core/api/config"
|
337
|
-
else:
|
338
|
-
hass_url = url_from_options
|
339
|
-
url = hass_url+"api/config"
|
340
|
-
token_from_options = options.get('long_lived_token', 'empty')
|
341
|
-
if token_from_options == 'empty' or token_from_options == '':
|
342
|
-
long_lived_token = key
|
343
|
-
else:
|
344
|
-
long_lived_token = token_from_options
|
345
|
-
headers = {
|
346
|
-
"Authorization": "Bearer " + long_lived_token,
|
347
|
-
"content-type": "application/json"
|
348
|
-
}
|
349
|
-
if not args.no_response==1:
|
350
|
-
response = get(url, headers=headers)
|
351
|
-
config_hass = response.json()
|
352
|
-
params_secrets = {
|
353
|
-
'hass_url': hass_url,
|
354
|
-
'long_lived_token': long_lived_token,
|
355
|
-
'time_zone': config_hass['time_zone'],
|
356
|
-
'lat': config_hass['latitude'],
|
357
|
-
'lon': config_hass['longitude'],
|
358
|
-
'alt': config_hass['elevation']
|
359
|
-
}
|
360
|
-
else: #if no_response is set to true
|
361
|
-
costfun = os.getenv('LOCAL_COSTFUN', default='profit')
|
362
|
-
logging_level = os.getenv('LOGGING_LEVEL', default='INFO')
|
363
|
-
# check if secrets file exists
|
364
|
-
if Path(os.getenv('SECRETS_PATH', default='/app/secrets_emhass.yaml')).is_file():
|
365
|
-
with open(os.getenv('SECRETS_PATH', default='/app/secrets_emhass.yaml'), 'r') as file:
|
366
|
-
params_secrets = yaml.load(file, Loader=yaml.FullLoader)
|
367
|
-
app.logger.debug("Obtained secrets from secrets file")
|
368
|
-
#If cant find secrets_emhass file, use env
|
369
|
-
else:
|
370
|
-
app.logger.debug("Failed to find secrets file: "+str(os.getenv('SECRETS_PATH', default='/app/secrets_emhass.yaml')))
|
371
|
-
app.logger.debug("Setting location defaults")
|
372
|
-
params_secrets = {}
|
373
|
-
#If no secrets file try args, else set some defaults
|
374
|
-
params_secrets['time_zone'] = os.getenv("TIME_ZONE", default="Europe/Paris")
|
375
|
-
params_secrets['lat'] = float(os.getenv("LAT", default="45.83"))
|
376
|
-
params_secrets['lon'] = float(os.getenv("LON", default="6.86"))
|
377
|
-
params_secrets['alt'] = float(os.getenv("ALT", default="4807.8"))
|
378
|
-
#If ARG/ENV specify url and key, then override secrets file
|
379
|
-
if hass_url != "":
|
380
|
-
params_secrets['hass_url'] = hass_url
|
381
|
-
app.logger.debug("Using URL obtained from ARG/ENV")
|
382
|
-
else:
|
383
|
-
hass_url = params_secrets.get('hass_url',"http://localhost:8123/")
|
384
|
-
if long_lived_token != "":
|
385
|
-
params_secrets['long_lived_token'] = long_lived_token
|
386
|
-
app.logger.debug("Using Key obtained from ARG/ENV")
|
387
|
-
else: #If addon is false
|
388
|
-
costfun = os.getenv('LOCAL_COSTFUN', default='profit')
|
389
|
-
logging_level = os.getenv('LOGGING_LEVEL', default='INFO')
|
390
|
-
if Path(os.getenv('SECRETS_PATH', default='/app/secrets_emhass.yaml')).is_file():
|
391
|
-
with open(os.getenv('SECRETS_PATH', default='/app/secrets_emhass.yaml'), 'r') as file:
|
392
|
-
params_secrets = yaml.load(file, Loader=yaml.FullLoader)
|
393
|
-
#Check if URL and KEY are provided by file. If not attempt using values from ARG/ENV
|
394
|
-
if params_secrets.get("hass_url", "empty") == "empty" or params_secrets['hass_url'] == "":
|
395
|
-
app.logger.info("No specified Home Assistant URL in secrets_emhass.yaml. Attempting to get from ARG/ENV")
|
396
|
-
if hass_url != "":
|
397
|
-
params_secrets['hass_url'] = hass_url
|
398
|
-
else:
|
399
|
-
app.logger.error("Can not find Home Assistant URL from secrets_emhass.yaml or ARG/ENV")
|
400
|
-
raise Exception("Can not find Home Assistant URL from secrets_emhass.yaml or ARG/ENV")
|
401
|
-
else:
|
402
|
-
hass_url = params_secrets['hass_url']
|
403
|
-
if params_secrets.get("long_lived_token", "empty") == "empty" or params_secrets['long_lived_token'] == "":
|
404
|
-
app.logger.info("No specified Home Assistant KEY in secrets_emhass.yaml. Attempting to get from ARG/ENV")
|
405
|
-
if key != "":
|
406
|
-
params_secrets['long_lived_token'] = key
|
407
|
-
else:
|
408
|
-
app.logger.error("Can not find Home Assistant KEY from secrets_emhass.yaml or ARG/ENV")
|
409
|
-
raise Exception("Can not find Home Assistant KEY from secrets_emhass.yaml or ARG/ENV")
|
410
|
-
else: #If no secrets file try args, else set some defaults
|
411
|
-
app.logger.info("Failed to find secrets_emhass.yaml in directory:" + os.getenv('SECRETS_PATH', default='/app/secrets_emhass.yaml') )
|
412
|
-
app.logger.info("Attempting to use secrets from arguments or environment variables")
|
413
|
-
params_secrets = {}
|
414
|
-
params_secrets['time_zone'] = os.getenv("TIME_ZONE", default="Europe/Paris")
|
415
|
-
params_secrets['lat'] = float(os.getenv("LAT", default="45.83"))
|
416
|
-
params_secrets['lon'] = float(os.getenv("LON", default="6.86"))
|
417
|
-
params_secrets['alt'] = float(os.getenv("ALT", default="4807.8"))
|
418
|
-
if hass_url != "":
|
419
|
-
params_secrets['hass_url'] = hass_url
|
420
|
-
else: #If cant find secrets_emhass and passed url ENV/ARG, then send error
|
421
|
-
app.logger.error("No specified Home Assistant URL")
|
422
|
-
raise Exception("Can not find Home Assistant URL from secrets_emhass.yaml or ARG/ENV")
|
423
|
-
if key != "":
|
424
|
-
params_secrets['long_lived_token'] = key
|
425
|
-
else: #If cant find secrets_emhass and passed key ENV/ARG, then send error
|
426
|
-
app.logger.error("No specified Home Assistant KEY")
|
427
|
-
raise Exception("Can not find Home Assistant KEY from secrets_emhass.yaml or ARG/ENV")
|
428
|
-
# Build params
|
429
|
-
if use_options:
|
430
|
-
params = build_params(params, params_secrets, options, 1, app.logger)
|
431
|
-
else:
|
432
|
-
params = build_params(params, params_secrets, options, args.addon, app.logger)
|
433
540
|
if os.path.exists(str(emhass_conf['data_path'])):
|
434
541
|
with open(str(emhass_conf['data_path'] / 'params.pkl'), "wb") as fid:
|
435
542
|
pickle.dump((config_path, params), fid)
|
436
543
|
else:
|
437
544
|
raise Exception("missing: " + str(emhass_conf['data_path']))
|
438
545
|
|
439
|
-
# Define
|
440
|
-
#stream logger
|
546
|
+
# Define loggers
|
441
547
|
ch = logging.StreamHandler()
|
442
548
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
443
549
|
ch.setFormatter(formatter)
|
444
|
-
#Action
|
550
|
+
# Action file logger
|
445
551
|
fileLogger = logging.FileHandler(str(emhass_conf['data_path'] / 'actionLogs.txt'))
|
446
552
|
formatter = logging.Formatter('%(levelname)s - %(name)s - %(message)s')
|
447
553
|
fileLogger.setFormatter(formatter) # add format to Handler
|
@@ -468,7 +574,8 @@ if __name__ == "__main__":
|
|
468
574
|
app.logger.propagate = False
|
469
575
|
app.logger.addHandler(ch)
|
470
576
|
app.logger.addHandler(fileLogger)
|
471
|
-
|
577
|
+
# Clear Action File logger file, ready for new instance
|
578
|
+
clearFileLog()
|
472
579
|
|
473
580
|
# If entity_path exists, remove any entity/metadata files
|
474
581
|
entity_path = emhass_conf['data_path'] / "entities"
|
@@ -483,11 +590,13 @@ if __name__ == "__main__":
|
|
483
590
|
|
484
591
|
# Launch server
|
485
592
|
port = int(os.environ.get('PORT', 5000))
|
486
|
-
app.logger.info("Launching the emhass webserver at: http://"+
|
487
|
-
app.logger.info("Home Assistant data fetch will be performed using url: "+hass_url)
|
593
|
+
app.logger.info("Launching the emhass webserver at: http://"+server_ip+":"+str(port))
|
594
|
+
app.logger.info("Home Assistant data fetch will be performed using url: "+params_secrets['hass_url'])
|
488
595
|
app.logger.info("The data path is: "+str(emhass_conf['data_path']))
|
596
|
+
app.logger.info("The logging is: "+str(logging_level))
|
489
597
|
try:
|
490
598
|
app.logger.info("Using core emhass version: "+version('emhass'))
|
491
599
|
except PackageNotFoundError:
|
492
600
|
app.logger.info("Using development emhass version")
|
493
|
-
serve(app, host=
|
601
|
+
serve(app, host=server_ip, port=port, threads=8)
|
602
|
+
|