emhass 0.10.6__py3-none-any.whl → 0.11.0__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 +61 -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 +871 -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 +620 -303
- emhass/web_server.py +323 -213
- {emhass-0.10.6.dist-info → emhass-0.11.0.dist-info}/METADATA +207 -169
- emhass-0.11.0.dist-info/RECORD +32 -0
- {emhass-0.10.6.dist-info → emhass-0.11.0.dist-info}/WHEEL +1 -1
- emhass-0.10.6.dist-info/RECORD +0 -26
- {emhass-0.10.6.dist-info → emhass-0.11.0.dist-info}/LICENSE +0 -0
- {emhass-0.10.6.dist-info → emhass-0.11.0.dist-info}/entry_points.txt +0 -0
- {emhass-0.10.6.dist-info → emhass-0.11.0.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,205 @@ 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
|
+
app.logger.info(params)
|
275
|
+
|
276
|
+
# Save params with updated config
|
277
|
+
if os.path.exists(emhass_conf['data_path']):
|
278
|
+
with open(str(emhass_conf['data_path'] / 'params.pkl'), "wb") as fid:
|
279
|
+
pickle.dump((config_path, build_params(emhass_conf,params_secrets,config,app.logger)), fid)
|
280
|
+
else:
|
281
|
+
return make_response(["Unable to save params file, missing data_path"],500)
|
282
|
+
|
283
|
+
app.logger.info("Saved parameters from webserver")
|
284
|
+
return make_response({},201)
|
97
285
|
|
98
|
-
#post actions
|
99
286
|
@app.route('/action/<action_name>', methods=['POST'])
|
100
287
|
def action_call(action_name):
|
288
|
+
"""
|
289
|
+
Receive Post action, run action according to passed slug(action_name) (e.g. /action/publish-data)
|
290
|
+
|
291
|
+
:param action_name: Slug/Action string corresponding to which action to take
|
292
|
+
:type action_name: String
|
293
|
+
|
294
|
+
"""
|
101
295
|
# Setting up parameters
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
296
|
+
# Params
|
297
|
+
if (emhass_conf['data_path'] / 'params.pkl').exists():
|
298
|
+
with open(str(emhass_conf['data_path'] / 'params.pkl'), "rb") as fid:
|
299
|
+
emhass_conf['config_path'], params = pickle.load(fid)
|
300
|
+
params = json.dumps(params)
|
301
|
+
else:
|
302
|
+
app.logger.error("Unable to find params.pkl file")
|
303
|
+
return make_response(grabLog(ActionStr), 400)
|
304
|
+
# Runtime
|
305
|
+
runtimeparams = request.get_json(force=True,silent=True)
|
306
|
+
if runtimeparams is not None:
|
307
|
+
if runtimeparams != '{}':
|
308
|
+
app.logger.info("Passed runtime parameters: " + str(runtimeparams))
|
309
|
+
else:
|
310
|
+
app.logger.warning("Unable to parse runtime parameters")
|
311
|
+
runtimeparams = {}
|
108
312
|
runtimeparams = json.dumps(runtimeparams)
|
109
|
-
|
110
|
-
#
|
313
|
+
|
314
|
+
# weather-forecast-cache (check before set_input_data_dict)
|
111
315
|
if action_name == 'weather-forecast-cache':
|
112
316
|
ActionStr = " >> Performing weather forecast, try to caching result"
|
113
317
|
app.logger.info(ActionStr)
|
@@ -125,13 +329,15 @@ def action_call(action_name):
|
|
125
329
|
return make_response(grabLog(ActionStr), 400)
|
126
330
|
|
127
331
|
# 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(
|
332
|
+
if len(continual_publish_thread) == 0 and input_data_dict['retrieve_hass_conf'].get('continual_publish',False):
|
129
333
|
# Start Thread
|
130
|
-
continualLoop = threading.Thread(name=
|
334
|
+
continualLoop = threading.Thread(name='continual_publish',target=continual_publish,args=[input_data_dict,entity_path,app.logger])
|
131
335
|
continualLoop.start()
|
132
336
|
continual_publish_thread.append(continualLoop)
|
133
|
-
|
134
|
-
#
|
337
|
+
|
338
|
+
# Run action based on POST request
|
339
|
+
# 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)
|
340
|
+
# publish-data
|
135
341
|
if action_name == 'publish-data':
|
136
342
|
ActionStr = " >> Publishing data..."
|
137
343
|
app.logger.info(ActionStr)
|
@@ -140,6 +346,7 @@ def action_call(action_name):
|
|
140
346
|
if not checkFileLog(ActionStr):
|
141
347
|
return make_response(msg, 201)
|
142
348
|
return make_response(grabLog(ActionStr), 400)
|
349
|
+
# perfect-optim
|
143
350
|
elif action_name == 'perfect-optim':
|
144
351
|
ActionStr = " >> Performing perfect optimization..."
|
145
352
|
app.logger.info(ActionStr)
|
@@ -151,6 +358,7 @@ def action_call(action_name):
|
|
151
358
|
if not checkFileLog(ActionStr):
|
152
359
|
return make_response(msg, 201)
|
153
360
|
return make_response(grabLog(ActionStr), 400)
|
361
|
+
# dayahead-optim
|
154
362
|
elif action_name == 'dayahead-optim':
|
155
363
|
ActionStr = " >> Performing dayahead optimization..."
|
156
364
|
app.logger.info(ActionStr)
|
@@ -162,6 +370,7 @@ def action_call(action_name):
|
|
162
370
|
if not checkFileLog(ActionStr):
|
163
371
|
return make_response(msg, 201)
|
164
372
|
return make_response(grabLog(ActionStr), 400)
|
373
|
+
# naive-mpc-optim
|
165
374
|
elif action_name == 'naive-mpc-optim':
|
166
375
|
ActionStr = " >> Performing naive MPC optimization..."
|
167
376
|
app.logger.info(ActionStr)
|
@@ -173,6 +382,7 @@ def action_call(action_name):
|
|
173
382
|
if not checkFileLog(ActionStr):
|
174
383
|
return make_response(msg, 201)
|
175
384
|
return make_response(grabLog(ActionStr), 400)
|
385
|
+
# forecast-model-fit
|
176
386
|
elif action_name == 'forecast-model-fit':
|
177
387
|
ActionStr = " >> Performing a machine learning forecast model fit..."
|
178
388
|
app.logger.info(ActionStr)
|
@@ -185,6 +395,7 @@ def action_call(action_name):
|
|
185
395
|
if not checkFileLog(ActionStr):
|
186
396
|
return make_response(msg, 201)
|
187
397
|
return make_response(grabLog(ActionStr), 400)
|
398
|
+
# forecast-model-predict
|
188
399
|
elif action_name == 'forecast-model-predict':
|
189
400
|
ActionStr = " >> Performing a machine learning forecast model predict..."
|
190
401
|
app.logger.info(ActionStr)
|
@@ -202,6 +413,7 @@ def action_call(action_name):
|
|
202
413
|
if not checkFileLog(ActionStr):
|
203
414
|
return make_response(msg, 201)
|
204
415
|
return make_response(grabLog(ActionStr), 400)
|
416
|
+
# forecast-model-tune
|
205
417
|
elif action_name == 'forecast-model-tune':
|
206
418
|
ActionStr = " >> Performing a machine learning forecast model tune..."
|
207
419
|
app.logger.info(ActionStr)
|
@@ -216,6 +428,7 @@ def action_call(action_name):
|
|
216
428
|
if not checkFileLog(ActionStr):
|
217
429
|
return make_response(msg, 201)
|
218
430
|
return make_response(grabLog(ActionStr), 400)
|
431
|
+
# regressor-model-fit
|
219
432
|
elif action_name == 'regressor-model-fit':
|
220
433
|
ActionStr = " >> Performing a machine learning regressor fit..."
|
221
434
|
app.logger.info(ActionStr)
|
@@ -224,6 +437,7 @@ def action_call(action_name):
|
|
224
437
|
if not checkFileLog(ActionStr):
|
225
438
|
return make_response(msg, 201)
|
226
439
|
return make_response(grabLog(ActionStr), 400)
|
440
|
+
# regressor-model-predict
|
227
441
|
elif action_name == 'regressor-model-predict':
|
228
442
|
ActionStr = " >> Performing a machine learning regressor predict..."
|
229
443
|
app.logger.info(ActionStr)
|
@@ -232,6 +446,7 @@ def action_call(action_name):
|
|
232
446
|
if not checkFileLog(ActionStr):
|
233
447
|
return make_response(msg, 201)
|
234
448
|
return make_response(grabLog(ActionStr), 400)
|
449
|
+
# Else return error
|
235
450
|
else:
|
236
451
|
app.logger.error("ERROR: passed action is not valid")
|
237
452
|
msg = f'EMHASS >> ERROR: Passed action is not valid... \n'
|
@@ -242,82 +457,74 @@ if __name__ == "__main__":
|
|
242
457
|
parser = argparse.ArgumentParser()
|
243
458
|
parser.add_argument('--url', type=str, help='The URL to your Home Assistant instance, ex the external_url in your hass configuration')
|
244
459
|
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
460
|
parser.add_argument('--no_response', type=strtobool, default='False', help='This is set if json response errors occur')
|
247
461
|
args = parser.parse_args()
|
462
|
+
|
463
|
+
# Pre formatted config parameters
|
464
|
+
config = {}
|
465
|
+
# Secrets
|
466
|
+
params_secrets = {}
|
467
|
+
# Built parameters (formatted config + secrets)
|
468
|
+
params = None
|
248
469
|
|
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")
|
470
|
+
# Find env's, not not set defaults
|
262
471
|
DATA_PATH = os.getenv("DATA_PATH", default="/app/data/")
|
263
472
|
ROOT_PATH = os.getenv("ROOT_PATH", default=str(Path(__file__).parent))
|
264
|
-
|
265
|
-
|
266
|
-
|
473
|
+
CONFIG_PATH = os.getenv('CONFIG_PATH', default="/share/config.json")
|
474
|
+
OPTIONS_PATH = os.getenv('OPTIONS_PATH', default="/data/options.json")
|
475
|
+
DEFAULTS_PATH = os.getenv('DEFAULTS_PATH', default=ROOT_PATH +"/data/config_defaults.json")
|
476
|
+
ASSOCIATIONS_PATH = os.getenv('ASSOCIATIONS_PATH', default=ROOT_PATH + "/data/associations.csv")
|
477
|
+
LEGACY_CONFIG_PATH = os.getenv("LEGACY_CONFIG_PATH", default="/app/config_emhass.yaml")
|
267
478
|
|
268
479
|
# 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
480
|
config_path = Path(CONFIG_PATH)
|
481
|
+
options_path = Path(OPTIONS_PATH)
|
482
|
+
defaults_path = Path(DEFAULTS_PATH)
|
483
|
+
associations_path = Path(ASSOCIATIONS_PATH)
|
484
|
+
legacy_config_path = Path(LEGACY_CONFIG_PATH)
|
298
485
|
data_path = Path(DATA_PATH)
|
299
486
|
root_path = Path(ROOT_PATH)
|
300
|
-
|
487
|
+
# Add paths to emhass_conf
|
301
488
|
emhass_conf['config_path'] = config_path
|
489
|
+
emhass_conf['options_path'] = options_path
|
490
|
+
emhass_conf['defaults_path'] = defaults_path
|
491
|
+
emhass_conf['associations_path'] = associations_path
|
492
|
+
emhass_conf['legacy_config_path'] = legacy_config_path
|
302
493
|
emhass_conf['data_path'] = data_path
|
303
494
|
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
495
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
496
|
+
# Combine parameters from configuration sources (if exists)
|
497
|
+
config.update(build_config(emhass_conf,app.logger,defaults_path,config_path,legacy_config_path))
|
498
|
+
if type(config) is bool and not config:
|
499
|
+
raise Exception("Failed to find default config")
|
500
|
+
|
501
|
+
costfun = os.getenv('LOCAL_COSTFUN', config.get('costfun', 'profit'))
|
502
|
+
logging_level = os.getenv('LOGGING_LEVEL', config.get('logging_level','INFO'))
|
503
|
+
# Temporary set logging level if debug
|
504
|
+
if logging_level == "DEBUG":
|
505
|
+
app.logger.setLevel(logging.DEBUG)
|
506
|
+
|
507
|
+
## Secrets
|
508
|
+
argument = {}
|
509
|
+
if args.url:
|
510
|
+
argument['url'] = args.url
|
511
|
+
if args.key:
|
512
|
+
argument['key'] = args.key
|
513
|
+
# 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)
|
514
|
+
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))
|
515
|
+
params_secrets.update(secrets)
|
516
|
+
|
517
|
+
server_ip = params_secrets.get("server_ip","0.0.0.0")
|
518
|
+
|
519
|
+
# Check if data path exists
|
520
|
+
if not os.path.isdir(emhass_conf['data_path']):
|
521
|
+
app.logger.warning("Unable to find data_path: " + str(emhass_conf['data_path']))
|
522
|
+
if os.path.isdir(Path("/app/data/")):
|
523
|
+
emhass_conf['data_path'] = Path("/app/data/")
|
524
|
+
else:
|
525
|
+
Path(root_path / "data/").mkdir(parents=True, exist_ok=True)
|
526
|
+
emhass_conf['data_path'] = root_path / "data/"
|
527
|
+
app.logger.info("data_path has been set to " + str(emhass_conf['data_path']))
|
321
528
|
|
322
529
|
# Initialize this global dict
|
323
530
|
if (emhass_conf['data_path'] / 'injection_dict.pkl').exists():
|
@@ -325,123 +532,23 @@ if __name__ == "__main__":
|
|
325
532
|
injection_dict = pickle.load(fid)
|
326
533
|
else:
|
327
534
|
injection_dict = None
|
535
|
+
|
536
|
+
# Build params from config and param_secrets (migrate params to correct config catagories), save result to params.pkl
|
537
|
+
params = build_params(emhass_conf, params_secrets, config, app.logger)
|
538
|
+
if type(params) is bool:
|
539
|
+
raise Exception("A error has occurred while building params")
|
328
540
|
|
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
541
|
if os.path.exists(str(emhass_conf['data_path'])):
|
434
542
|
with open(str(emhass_conf['data_path'] / 'params.pkl'), "wb") as fid:
|
435
543
|
pickle.dump((config_path, params), fid)
|
436
544
|
else:
|
437
545
|
raise Exception("missing: " + str(emhass_conf['data_path']))
|
438
546
|
|
439
|
-
# Define
|
440
|
-
#stream logger
|
547
|
+
# Define loggers
|
441
548
|
ch = logging.StreamHandler()
|
442
549
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
443
550
|
ch.setFormatter(formatter)
|
444
|
-
#Action
|
551
|
+
# Action file logger
|
445
552
|
fileLogger = logging.FileHandler(str(emhass_conf['data_path'] / 'actionLogs.txt'))
|
446
553
|
formatter = logging.Formatter('%(levelname)s - %(name)s - %(message)s')
|
447
554
|
fileLogger.setFormatter(formatter) # add format to Handler
|
@@ -468,7 +575,8 @@ if __name__ == "__main__":
|
|
468
575
|
app.logger.propagate = False
|
469
576
|
app.logger.addHandler(ch)
|
470
577
|
app.logger.addHandler(fileLogger)
|
471
|
-
|
578
|
+
# Clear Action File logger file, ready for new instance
|
579
|
+
clearFileLog()
|
472
580
|
|
473
581
|
# If entity_path exists, remove any entity/metadata files
|
474
582
|
entity_path = emhass_conf['data_path'] / "entities"
|
@@ -483,11 +591,13 @@ if __name__ == "__main__":
|
|
483
591
|
|
484
592
|
# Launch server
|
485
593
|
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)
|
594
|
+
app.logger.info("Launching the emhass webserver at: http://"+server_ip+":"+str(port))
|
595
|
+
app.logger.info("Home Assistant data fetch will be performed using url: "+params_secrets['hass_url'])
|
488
596
|
app.logger.info("The data path is: "+str(emhass_conf['data_path']))
|
597
|
+
app.logger.info("The logging is: "+str(logging_level))
|
489
598
|
try:
|
490
599
|
app.logger.info("Using core emhass version: "+version('emhass'))
|
491
600
|
except PackageNotFoundError:
|
492
601
|
app.logger.info("Using development emhass version")
|
493
|
-
serve(app, host=
|
602
|
+
serve(app, host=server_ip, port=port, threads=8)
|
603
|
+
|