emhass 0.10.0__py3-none-any.whl → 0.10.2__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 +86 -21
- emhass/forecast.py +94 -37
- emhass/machine_learning_forecaster.py +20 -20
- emhass/optimization.py +46 -50
- emhass/utils.py +59 -34
- emhass/web_server.py +14 -4
- {emhass-0.10.0.dist-info → emhass-0.10.2.dist-info}/METADATA +16 -10
- {emhass-0.10.0.dist-info → emhass-0.10.2.dist-info}/RECORD +12 -12
- {emhass-0.10.0.dist-info → emhass-0.10.2.dist-info}/WHEEL +1 -1
- {emhass-0.10.0.dist-info → emhass-0.10.2.dist-info}/LICENSE +0 -0
- {emhass-0.10.0.dist-info → emhass-0.10.2.dist-info}/entry_points.txt +0 -0
- {emhass-0.10.0.dist-info → emhass-0.10.2.dist-info}/top_level.txt +0 -0
emhass/command_line.py
CHANGED
@@ -52,7 +52,7 @@ def set_input_data_dict(emhass_conf: dict, costfun: str,
|
|
52
52
|
logger.info("Setting up needed data")
|
53
53
|
# Parsing yaml
|
54
54
|
retrieve_hass_conf, optim_conf, plant_conf = utils.get_yaml_parse(
|
55
|
-
emhass_conf, use_secrets=not
|
55
|
+
emhass_conf, use_secrets=not(get_data_from_file), params=params)
|
56
56
|
# Treat runtimeparams
|
57
57
|
params, retrieve_hass_conf, optim_conf, plant_conf = utils.treat_runtimeparams(
|
58
58
|
runtimeparams, params, retrieve_hass_conf, optim_conf, plant_conf, set_type, logger)
|
@@ -97,6 +97,8 @@ def set_input_data_dict(emhass_conf: dict, costfun: str,
|
|
97
97
|
# Get PV and load forecasts
|
98
98
|
df_weather = fcst.get_weather_forecast(
|
99
99
|
method=optim_conf["weather_forecast_method"])
|
100
|
+
if isinstance(df_weather, bool) and not df_weather:
|
101
|
+
return False
|
100
102
|
P_PV_forecast = fcst.get_power_from_weather(df_weather)
|
101
103
|
P_load_forecast = fcst.get_load_forecast(
|
102
104
|
method=optim_conf['load_forecast_method'])
|
@@ -142,6 +144,8 @@ def set_input_data_dict(emhass_conf: dict, costfun: str,
|
|
142
144
|
# Get PV and load forecasts
|
143
145
|
df_weather = fcst.get_weather_forecast(
|
144
146
|
method=optim_conf['weather_forecast_method'])
|
147
|
+
if isinstance(df_weather, bool) and not df_weather:
|
148
|
+
return False
|
145
149
|
P_PV_forecast = fcst.get_power_from_weather(
|
146
150
|
df_weather, set_mix_forecast=True, df_now=df_input_data)
|
147
151
|
P_load_forecast = fcst.get_load_forecast(
|
@@ -243,6 +247,50 @@ def set_input_data_dict(emhass_conf: dict, costfun: str,
|
|
243
247
|
}
|
244
248
|
return input_data_dict
|
245
249
|
|
250
|
+
def weather_forecast_cache(emhass_conf: dict, params: str,
|
251
|
+
runtimeparams: str, logger: logging.Logger) -> bool:
|
252
|
+
"""
|
253
|
+
Perform a call to get forecast function, intend to save results to cache.
|
254
|
+
|
255
|
+
:param emhass_conf: Dictionary containing the needed emhass paths
|
256
|
+
:type emhass_conf: dict
|
257
|
+
:param params: Configuration parameters passed from data/options.json
|
258
|
+
:type params: str
|
259
|
+
:param runtimeparams: Runtime optimization parameters passed as a dictionary
|
260
|
+
:type runtimeparams: str
|
261
|
+
:param logger: The passed logger object
|
262
|
+
:type logger: logging object
|
263
|
+
:return: A bool for function completion
|
264
|
+
:rtype: bool
|
265
|
+
|
266
|
+
"""
|
267
|
+
|
268
|
+
# Parsing yaml
|
269
|
+
retrieve_hass_conf, optim_conf, plant_conf = utils.get_yaml_parse(
|
270
|
+
emhass_conf, use_secrets=True, params=params)
|
271
|
+
|
272
|
+
# Treat runtimeparams
|
273
|
+
params, retrieve_hass_conf, optim_conf, plant_conf = utils.treat_runtimeparams(
|
274
|
+
runtimeparams, params, retrieve_hass_conf, optim_conf, plant_conf, "forecast", logger)
|
275
|
+
|
276
|
+
# Make sure weather_forecast_cache is true
|
277
|
+
if (params != None) and (params != "null"):
|
278
|
+
params = json.loads(params)
|
279
|
+
else:
|
280
|
+
params = {}
|
281
|
+
params["passed_data"]["weather_forecast_cache"] = True
|
282
|
+
params = json.dumps(params)
|
283
|
+
|
284
|
+
# Create Forecast object
|
285
|
+
fcst = Forecast(retrieve_hass_conf, optim_conf, plant_conf,
|
286
|
+
params, emhass_conf, logger)
|
287
|
+
|
288
|
+
result = fcst.get_weather_forecast(optim_conf["weather_forecast_method"])
|
289
|
+
if isinstance(result, bool) and not result:
|
290
|
+
return False
|
291
|
+
|
292
|
+
return True
|
293
|
+
|
246
294
|
|
247
295
|
def perfect_forecast_optim(input_data_dict: dict, logger: logging.Logger,
|
248
296
|
save_data_to_file: Optional[bool] = True,
|
@@ -683,8 +731,6 @@ def regressor_model_predict(input_data_dict: dict, logger: logging.Logger,
|
|
683
731
|
type_var="mlregressor")
|
684
732
|
return prediction
|
685
733
|
|
686
|
-
|
687
|
-
|
688
734
|
def publish_data(input_data_dict: dict, logger: logging.Logger,
|
689
735
|
save_data_to_file: Optional[bool] = False,
|
690
736
|
opt_res_latest: Optional[pd.DataFrame] = None,
|
@@ -708,12 +754,10 @@ def publish_data(input_data_dict: dict, logger: logging.Logger,
|
|
708
754
|
|
709
755
|
"""
|
710
756
|
logger.info("Publishing data to HASS instance")
|
711
|
-
|
712
757
|
if not isinstance(input_data_dict["params"],dict):
|
713
758
|
params = json.loads(input_data_dict["params"])
|
714
759
|
else:
|
715
760
|
params = input_data_dict["params"]
|
716
|
-
|
717
761
|
# Check if a day ahead optimization has been performed (read CSV file)
|
718
762
|
if save_data_to_file:
|
719
763
|
today = datetime.now(timezone.utc).replace(
|
@@ -726,7 +770,6 @@ def publish_data(input_data_dict: dict, logger: logging.Logger,
|
|
726
770
|
opt_res_list_names = []
|
727
771
|
publish_prefix = params["passed_data"]["publish_prefix"]
|
728
772
|
entity_path = input_data_dict['emhass_conf']['data_path'] / "entities"
|
729
|
-
|
730
773
|
# Check if items in entity_path
|
731
774
|
if os.path.exists(entity_path) and len(os.listdir(entity_path)) > 0:
|
732
775
|
# Obtain all files in entity_path
|
@@ -805,6 +848,36 @@ def publish_data(input_data_dict: dict, logger: logging.Logger,
|
|
805
848
|
dont_post=dont_post
|
806
849
|
)
|
807
850
|
cols_published = ["P_PV", "P_Load"]
|
851
|
+
# Publish PV curtailment
|
852
|
+
if input_data_dict["fcst"].plant_conf['compute_curtailment']:
|
853
|
+
custom_pv_curtailment_id = params["passed_data"]["custom_pv_curtailment_id"]
|
854
|
+
input_data_dict["rh"].post_data(
|
855
|
+
opt_res_latest["P_PV_curtailment"],
|
856
|
+
idx_closest,
|
857
|
+
custom_pv_curtailment_id["entity_id"],
|
858
|
+
custom_pv_curtailment_id["unit_of_measurement"],
|
859
|
+
custom_pv_curtailment_id["friendly_name"],
|
860
|
+
type_var="power",
|
861
|
+
publish_prefix=publish_prefix,
|
862
|
+
save_entities=entity_save,
|
863
|
+
dont_post=dont_post
|
864
|
+
)
|
865
|
+
cols_published = cols_published + ["P_PV_curtailment"]
|
866
|
+
# Publish P_hybrid_inverter
|
867
|
+
if input_data_dict["fcst"].plant_conf['inverter_is_hybrid']:
|
868
|
+
custom_hybrid_inverter_id = params["passed_data"]["custom_hybrid_inverter_id"]
|
869
|
+
input_data_dict["rh"].post_data(
|
870
|
+
opt_res_latest["P_hybrid_inverter"],
|
871
|
+
idx_closest,
|
872
|
+
custom_hybrid_inverter_id["entity_id"],
|
873
|
+
custom_hybrid_inverter_id["unit_of_measurement"],
|
874
|
+
custom_hybrid_inverter_id["friendly_name"],
|
875
|
+
type_var="power",
|
876
|
+
publish_prefix=publish_prefix,
|
877
|
+
save_entities=entity_save,
|
878
|
+
dont_post=dont_post
|
879
|
+
)
|
880
|
+
cols_published = cols_published + ["P_hybrid_inverter"]
|
808
881
|
# Publish deferrable loads
|
809
882
|
custom_deferrable_forecast_id = params["passed_data"][
|
810
883
|
"custom_deferrable_forecast_id"
|
@@ -944,7 +1017,7 @@ def publish_data(input_data_dict: dict, logger: logging.Logger,
|
|
944
1017
|
opt_res_latest.index[idx_closest]]]
|
945
1018
|
return opt_res
|
946
1019
|
|
947
|
-
def continual_publish(input_data_dict,entity_path,logger):
|
1020
|
+
def continual_publish(input_data_dict: dict, entity_path: pathlib.Path, logger: logging.Logger):
|
948
1021
|
"""
|
949
1022
|
If continual_publish is true and a entity file saved in /data_path/entities, continually publish sensor on freq rate, updating entity current state value based on timestamp
|
950
1023
|
|
@@ -959,23 +1032,22 @@ def continual_publish(input_data_dict,entity_path,logger):
|
|
959
1032
|
logger.info("Continual publish thread service started")
|
960
1033
|
freq = input_data_dict['retrieve_hass_conf'].get("freq", pd.to_timedelta(1, "minutes"))
|
961
1034
|
entity_path_contents = []
|
962
|
-
|
963
1035
|
while True:
|
964
1036
|
# Sleep for x seconds (using current time as a reference for time left)
|
965
1037
|
time.sleep(max(0,freq.total_seconds() - (datetime.now(input_data_dict["retrieve_hass_conf"]["time_zone"]).timestamp() % 60)))
|
966
|
-
|
967
1038
|
# Loop through all saved entity files
|
968
1039
|
if os.path.exists(entity_path) and len(os.listdir(entity_path)) > 0:
|
969
1040
|
entity_path_contents = os.listdir(entity_path)
|
970
1041
|
for entity in entity_path_contents:
|
971
1042
|
if entity != "metadata.json":
|
972
1043
|
# Call publish_json with entity file, build entity, and publish
|
973
|
-
publish_json(entity,input_data_dict,entity_path,logger,"continual_publish")
|
1044
|
+
publish_json(entity, input_data_dict, entity_path, logger, "continual_publish")
|
974
1045
|
pass
|
975
1046
|
# This function should never return
|
976
1047
|
return False
|
977
1048
|
|
978
|
-
def publish_json(entity,input_data_dict,entity_path
|
1049
|
+
def publish_json(entity: dict, input_data_dict: dict, entity_path: pathlib.Path,
|
1050
|
+
logger: logging.Logger, reference: Optional[str] = ""):
|
979
1051
|
"""
|
980
1052
|
Extract saved entity data from .json (in data_path/entities), build entity, post results to post_data
|
981
1053
|
|
@@ -989,9 +1061,8 @@ def publish_json(entity,input_data_dict,entity_path,logger,reference: Optional[s
|
|
989
1061
|
:type logger: logging.Logger
|
990
1062
|
:param reference: String for identifying who ran the function
|
991
1063
|
:type reference: str, optional
|
992
|
-
|
1064
|
+
|
993
1065
|
"""
|
994
|
-
|
995
1066
|
# Retrieve entity metadata from file
|
996
1067
|
if os.path.isfile(entity_path / "metadata.json"):
|
997
1068
|
with open(entity_path / "metadata.json", "r") as file:
|
@@ -1001,16 +1072,12 @@ def publish_json(entity,input_data_dict,entity_path,logger,reference: Optional[s
|
|
1001
1072
|
else:
|
1002
1073
|
logger.error("unable to located metadata.json in:" + entity_path)
|
1003
1074
|
return False
|
1004
|
-
|
1005
1075
|
# Round current timecode (now)
|
1006
1076
|
now_precise = datetime.now(input_data_dict["retrieve_hass_conf"]["time_zone"]).replace(second=0, microsecond=0)
|
1007
|
-
|
1008
1077
|
# Retrieve entity data from file
|
1009
1078
|
entity_data = pd.read_json(entity_path / entity , orient='index')
|
1010
|
-
|
1011
1079
|
# Remove ".json" from string for entity_id
|
1012
1080
|
entity_id = entity.replace(".json", "")
|
1013
|
-
|
1014
1081
|
# Adjust Dataframe from received entity json file
|
1015
1082
|
entity_data.columns = [metadata[entity_id]["name"]]
|
1016
1083
|
entity_data.index.name = "timestamp"
|
@@ -1025,15 +1092,13 @@ def publish_json(entity,input_data_dict,entity_path,logger,reference: Optional[s
|
|
1025
1092
|
idx_closest = entity_data.index.get_indexer([now_precise], method="bfill")[0]
|
1026
1093
|
if idx_closest == -1:
|
1027
1094
|
idx_closest = entity_data.index.get_indexer([now_precise], method="nearest")[0]
|
1028
|
-
|
1029
1095
|
# Call post data
|
1030
1096
|
if reference == "continual_publish":
|
1031
1097
|
logger.debug("Auto Published sensor:")
|
1032
1098
|
logger_levels = "DEBUG"
|
1033
1099
|
else:
|
1034
1100
|
logger_levels = "INFO"
|
1035
|
-
|
1036
|
-
#post/save entity
|
1101
|
+
# post/save entity
|
1037
1102
|
input_data_dict["rh"].post_data(
|
1038
1103
|
data_df=entity_data[metadata[entity_id]["name"]],
|
1039
1104
|
idx=idx_closest,
|
@@ -1125,7 +1190,7 @@ def main():
|
|
1125
1190
|
logger.error("Could not find emhass/src foulder in: " + str(root_path))
|
1126
1191
|
logger.error("Try setting emhass root path with --root")
|
1127
1192
|
return False
|
1128
|
-
#
|
1193
|
+
# Additional argument
|
1129
1194
|
try:
|
1130
1195
|
parser.add_argument(
|
1131
1196
|
"--version",
|
emhass/forecast.py
CHANGED
@@ -182,6 +182,7 @@ class Forecast(object):
|
|
182
182
|
|
183
183
|
"""
|
184
184
|
csv_path = self.emhass_conf['data_path'] / csv_path
|
185
|
+
w_forecast_cache_path = os.path.abspath(self.emhass_conf['data_path'] / "weather_forecast_data.pkl")
|
185
186
|
|
186
187
|
self.logger.info("Retrieving weather forecast data using method = "+method)
|
187
188
|
self.weather_forecast_method = method # Saving this attribute for later use to identify csv method usage
|
@@ -223,40 +224,97 @@ class Forecast(object):
|
|
223
224
|
data['relative_humidity'] = raw_data['Relative Humidity (%)']
|
224
225
|
data['precipitable_water'] = pvlib.atmosphere.gueymard94_pw(
|
225
226
|
data['temp_air'], data['relative_humidity'])
|
226
|
-
elif method == 'solcast': # using
|
227
|
-
#
|
228
|
-
if
|
229
|
-
|
230
|
-
self.
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
227
|
+
elif method == 'solcast': # using Solcast API
|
228
|
+
# Check if weather_forecast_cache is true or if forecast_data file does not exist
|
229
|
+
if self.params["passed_data"]["weather_forecast_cache"] or not os.path.isfile(w_forecast_cache_path):
|
230
|
+
# Check if weather_forecast_cache_only is true, if so produce error for not finding cache file
|
231
|
+
if not self.params["passed_data"]["weather_forecast_cache_only"]:
|
232
|
+
# Retrieve data from the Solcast API
|
233
|
+
if 'solcast_api_key' not in self.retrieve_hass_conf:
|
234
|
+
self.logger.error("The solcast_api_key parameter was not defined")
|
235
|
+
return False
|
236
|
+
if 'solcast_rooftop_id' not in self.retrieve_hass_conf:
|
237
|
+
self.logger.error("The solcast_rooftop_id parameter was not defined")
|
238
|
+
return False
|
239
|
+
headers = {
|
240
|
+
'User-Agent': 'EMHASS',
|
241
|
+
"Authorization": "Bearer " + self.retrieve_hass_conf['solcast_api_key'],
|
242
|
+
"content-type": "application/json",
|
243
|
+
}
|
244
|
+
days_solcast = int(len(self.forecast_dates)*self.freq.seconds/3600)
|
245
|
+
# If weather_forecast_cache, set request days as twice as long to avoid length issues (add a buffer)
|
246
|
+
if self.params["passed_data"]["weather_forecast_cache"]:
|
247
|
+
days_solcast = min((days_solcast * 2), 336)
|
248
|
+
url = "https://api.solcast.com.au/rooftop_sites/"+self.retrieve_hass_conf['solcast_rooftop_id']+"/forecasts?hours="+str(days_solcast)
|
249
|
+
response = get(url, headers=headers)
|
250
|
+
'''import bz2 # Uncomment to save a serialized data for tests
|
251
|
+
import _pickle as cPickle
|
252
|
+
with bz2.BZ2File("data/test_response_solcast_get_method.pbz2", "w") as f:
|
253
|
+
cPickle.dump(response, f)'''
|
254
|
+
# Verify the request passed
|
255
|
+
if int(response.status_code) == 200:
|
256
|
+
data = response.json()
|
257
|
+
elif int(response.status_code) == 402 or int(response.status_code) == 429:
|
258
|
+
self.logger.error("Solcast error: May have exceeded your subscription limit.")
|
259
|
+
return False
|
260
|
+
elif int(response.status_code) >= 400 or int(response.status_code) >= 202:
|
261
|
+
self.logger.error("Solcast error: There was a issue with the solcast request, check solcast API key and rooftop ID.")
|
262
|
+
self.logger.error("Solcast error: Check that your subscription is valid and your network can connect to Solcast.")
|
263
|
+
return False
|
264
|
+
data_list = []
|
265
|
+
for elm in data['forecasts']:
|
266
|
+
data_list.append(elm['pv_estimate']*1000) # Converting kW to W
|
267
|
+
# Check if the retrieved data has the correct length
|
268
|
+
if len(data_list) < len(self.forecast_dates):
|
269
|
+
self.logger.error("Not enough data retried from Solcast service, try increasing the time step or use MPC.")
|
270
|
+
else:
|
271
|
+
# If runtime weather_forecast_cache is true save forecast result to file as cache
|
272
|
+
if self.params["passed_data"]["weather_forecast_cache"]:
|
273
|
+
# Add x2 forecast periods for cached results. This adds a extra delta_forecast amount of days for a buffer
|
274
|
+
cached_forecast_dates = self.forecast_dates.union(pd.date_range(self.forecast_dates[-1], periods=(len(self.forecast_dates) +1), freq=self.freq)[1:])
|
275
|
+
cache_data_list = data_list[0:len(cached_forecast_dates)]
|
276
|
+
cache_data_dict = {'ts':cached_forecast_dates, 'yhat':cache_data_list}
|
277
|
+
data_cache = pd.DataFrame.from_dict(cache_data_dict)
|
278
|
+
data_cache.set_index('ts', inplace=True)
|
279
|
+
with open(w_forecast_cache_path, "wb") as file:
|
280
|
+
cPickle.dump(data_cache, file)
|
281
|
+
if not os.path.isfile(w_forecast_cache_path):
|
282
|
+
self.logger.warning("Solcast forecast data could not be saved to file.")
|
283
|
+
else:
|
284
|
+
self.logger.info("Saved the Solcast results to cache, for later reference.")
|
285
|
+
# Trim request results to forecast_dates
|
286
|
+
data_list = data_list[0:len(self.forecast_dates)]
|
287
|
+
data_dict = {'ts':self.forecast_dates, 'yhat':data_list}
|
288
|
+
# Define DataFrame
|
289
|
+
data = pd.DataFrame.from_dict(data_dict)
|
290
|
+
# Define index
|
291
|
+
data.set_index('ts', inplace=True)
|
292
|
+
# Else, notify user to update cache
|
293
|
+
else:
|
294
|
+
self.logger.error("Unable to obtain Solcast cache file.")
|
295
|
+
self.logger.error("Try running optimization again with 'weather_forecast_cache_only': false")
|
296
|
+
self.logger.error("Optionally, obtain new Solcast cache with runtime parameter 'weather_forecast_cache': true in an optimization, or run the `forecast-cache` action, to pull new data from Solcast and cache.")
|
297
|
+
return False
|
298
|
+
# Else, open stored weather_forecast_data.pkl file for previous forecast data (cached data)
|
252
299
|
else:
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
300
|
+
with open(w_forecast_cache_path, "rb") as file:
|
301
|
+
data = cPickle.load(file)
|
302
|
+
if not isinstance(data, pd.DataFrame) or len(data) < len(self.forecast_dates):
|
303
|
+
self.logger.error("There has been a error obtaining cached Solcast forecast data.")
|
304
|
+
self.logger.error("Try running optimization again with 'weather_forecast_cache': true, or run action `forecast-cache`, to pull new data from Solcast and cache.")
|
305
|
+
self.logger.warning("Removing old Solcast cache file. Next optimization will pull data from Solcast, unless 'weather_forecast_cache_only': true")
|
306
|
+
os.remove(w_forecast_cache_path)
|
307
|
+
return False
|
308
|
+
# Filter cached forecast data to match current forecast_dates start-end range (reduce forecast Dataframe size to appropriate length)
|
309
|
+
if self.forecast_dates[0] in data.index and self.forecast_dates[-1] in data.index:
|
310
|
+
data = data.loc[self.forecast_dates[0]:self.forecast_dates[-1]]
|
311
|
+
self.logger.info("Retrieved Solcast data from the previously saved cache.")
|
312
|
+
else:
|
313
|
+
self.logger.error("Unable to obtain cached Solcast forecast data within the requested timeframe range.")
|
314
|
+
self.logger.error("Try running optimization again (not using cache). Optionally, add runtime parameter 'weather_forecast_cache': true to pull new data from Solcast and cache.")
|
315
|
+
self.logger.warning("Removing old Solcast cache file. Next optimization will pull data from Solcast, unless 'weather_forecast_cache_only': true")
|
316
|
+
os.remove(w_forecast_cache_path)
|
317
|
+
return False
|
260
318
|
elif method == 'solar.forecast': # using the solar.forecast API
|
261
319
|
# Retrieve data from the solar.forecast API
|
262
320
|
if 'solar_forecast_kwp' not in self.retrieve_hass_conf:
|
@@ -423,9 +481,9 @@ class Forecast(object):
|
|
423
481
|
# Setting the main parameters of the PV plant
|
424
482
|
location = Location(latitude=self.lat, longitude=self.lon)
|
425
483
|
temp_params = TEMPERATURE_MODEL_PARAMETERS['sapm']['close_mount_glass_glass']
|
426
|
-
cec_modules = bz2.BZ2File(self.emhass_conf['root_path'] / 'data/cec_modules.pbz2', "rb")
|
484
|
+
cec_modules = bz2.BZ2File(self.emhass_conf['root_path'] / 'data' / 'cec_modules.pbz2', "rb")
|
427
485
|
cec_modules = cPickle.load(cec_modules)
|
428
|
-
cec_inverters = bz2.BZ2File(self.emhass_conf['root_path'] / 'data/cec_inverters.pbz2', "rb")
|
486
|
+
cec_inverters = bz2.BZ2File(self.emhass_conf['root_path'] / 'data' / 'cec_inverters.pbz2', "rb")
|
429
487
|
cec_inverters = cPickle.load(cec_inverters)
|
430
488
|
if type(self.plant_conf['module_model']) == list:
|
431
489
|
P_PV_forecast = pd.Series(0, index=df_weather.index)
|
@@ -838,5 +896,4 @@ class Forecast(object):
|
|
838
896
|
else:
|
839
897
|
self.logger.error("Passed method is not valid")
|
840
898
|
return False
|
841
|
-
return df_final
|
842
|
-
|
899
|
+
return df_final
|
@@ -221,51 +221,52 @@ class MLForecaster:
|
|
221
221
|
:return: The DataFrame with the forecasts using the optimized model.
|
222
222
|
:rtype: pd.DataFrame
|
223
223
|
"""
|
224
|
-
# Bayesian search hyperparameter and lags with skforecast/optuna
|
225
|
-
# Lags used as predictors
|
226
|
-
if debug:
|
227
|
-
lags_grid = [3]
|
228
|
-
refit = False
|
229
|
-
num_lags = 3
|
230
|
-
else:
|
231
|
-
lags_grid = [6, 12, 24, 36, 48, 60, 72]
|
232
|
-
refit = True
|
233
|
-
num_lags = self.num_lags
|
234
224
|
# Regressor hyperparameters search space
|
235
225
|
if self.sklearn_model == 'LinearRegression':
|
236
226
|
if debug:
|
237
227
|
def search_space(trial):
|
238
|
-
search_space = {'fit_intercept': trial.suggest_categorical('fit_intercept', [True])
|
228
|
+
search_space = {'fit_intercept': trial.suggest_categorical('fit_intercept', [True]),
|
229
|
+
'lags': trial.suggest_categorical('lags', [3])}
|
239
230
|
return search_space
|
240
231
|
else:
|
241
232
|
def search_space(trial):
|
242
|
-
search_space = {'fit_intercept': trial.suggest_categorical('fit_intercept', [True, False])
|
233
|
+
search_space = {'fit_intercept': trial.suggest_categorical('fit_intercept', [True, False]),
|
234
|
+
'lags': trial.suggest_categorical('lags', [6, 12, 24, 36, 48, 60, 72])}
|
243
235
|
return search_space
|
244
236
|
elif self.sklearn_model == 'ElasticNet':
|
245
237
|
if debug:
|
246
238
|
def search_space(trial):
|
247
|
-
search_space = {'selection': trial.suggest_categorical('selection', ['random'])
|
239
|
+
search_space = {'selection': trial.suggest_categorical('selection', ['random']),
|
240
|
+
'lags': trial.suggest_categorical('lags', [3])}
|
248
241
|
return search_space
|
249
242
|
else:
|
250
243
|
def search_space(trial):
|
251
244
|
search_space = {'alpha': trial.suggest_float('alpha', 0.0, 2.0),
|
252
245
|
'l1_ratio': trial.suggest_float('l1_ratio', 0.0, 1.0),
|
253
|
-
'selection': trial.suggest_categorical('selection', ['cyclic', 'random'])
|
254
|
-
}
|
246
|
+
'selection': trial.suggest_categorical('selection', ['cyclic', 'random']),
|
247
|
+
'lags': trial.suggest_categorical('lags', [6, 12, 24, 36, 48, 60, 72])}
|
255
248
|
return search_space
|
256
249
|
elif self.sklearn_model == 'KNeighborsRegressor':
|
257
250
|
if debug:
|
258
251
|
def search_space(trial):
|
259
|
-
search_space = {'weights': trial.suggest_categorical('weights', ['uniform'])
|
252
|
+
search_space = {'weights': trial.suggest_categorical('weights', ['uniform']),
|
253
|
+
'lags': trial.suggest_categorical('lags', [3])}
|
260
254
|
return search_space
|
261
255
|
else:
|
262
256
|
def search_space(trial):
|
263
257
|
search_space = {'n_neighbors': trial.suggest_int('n_neighbors', 2, 20),
|
264
258
|
'leaf_size': trial.suggest_int('leaf_size', 20, 40),
|
265
|
-
'weights': trial.suggest_categorical('weights', ['uniform', 'distance'])
|
266
|
-
}
|
259
|
+
'weights': trial.suggest_categorical('weights', ['uniform', 'distance']),
|
260
|
+
'lags': trial.suggest_categorical('lags', [6, 12, 24, 36, 48, 60, 72])}
|
267
261
|
return search_space
|
268
|
-
|
262
|
+
# Bayesian search hyperparameter and lags with skforecast/optuna
|
263
|
+
# Lags used as predictors
|
264
|
+
if debug:
|
265
|
+
refit = False
|
266
|
+
num_lags = 3
|
267
|
+
else:
|
268
|
+
refit = True
|
269
|
+
num_lags = self.num_lags
|
269
270
|
# The optimization routine call
|
270
271
|
self.logger.info("Bayesian hyperparameter optimization with backtesting")
|
271
272
|
start_time = time.time()
|
@@ -273,7 +274,6 @@ class MLForecaster:
|
|
273
274
|
forecaster = self.forecaster,
|
274
275
|
y = self.data_train[self.var_model],
|
275
276
|
exog = self.data_train.drop(self.var_model, axis=1),
|
276
|
-
lags_grid = lags_grid,
|
277
277
|
search_space = search_space,
|
278
278
|
steps = num_lags,
|
279
279
|
metric = MLForecaster.neg_r2_score,
|
emhass/optimization.py
CHANGED
@@ -272,12 +272,20 @@ class Optimization:
|
|
272
272
|
rhs = 0)
|
273
273
|
for i in set_I}
|
274
274
|
else:
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
275
|
+
if self.plant_conf['compute_curtailment']:
|
276
|
+
constraints = {"constraint_main2_{}".format(i) :
|
277
|
+
plp.LpConstraint(
|
278
|
+
e = P_PV[i] - P_PV_curtailment[i] - P_def_sum[i] - P_load[i] + P_grid_neg[i] + P_grid_pos[i] + P_sto_pos[i] + P_sto_neg[i],
|
279
|
+
sense = plp.LpConstraintEQ,
|
280
|
+
rhs = 0)
|
281
|
+
for i in set_I}
|
282
|
+
else:
|
283
|
+
constraints = {"constraint_main3_{}".format(i) :
|
284
|
+
plp.LpConstraint(
|
285
|
+
e = P_PV[i] - P_def_sum[i] - P_load[i] + P_grid_neg[i] + P_grid_pos[i] + P_sto_pos[i] + P_sto_neg[i],
|
286
|
+
sense = plp.LpConstraintEQ,
|
287
|
+
rhs = 0)
|
288
|
+
for i in set_I}
|
281
289
|
|
282
290
|
# Constraint for hybrid inverter and curtailment cases
|
283
291
|
if type(self.plant_conf['module_model']) == list:
|
@@ -312,12 +320,13 @@ class Optimization:
|
|
312
320
|
rhs = 0)
|
313
321
|
for i in set_I})
|
314
322
|
else:
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
323
|
+
if self.plant_conf['compute_curtailment']:
|
324
|
+
constraints.update({"constraint_curtailment_{}".format(i) :
|
325
|
+
plp.LpConstraint(
|
326
|
+
e = P_PV_curtailment[i] - max(P_PV[i],0),
|
327
|
+
sense = plp.LpConstraintLE,
|
328
|
+
rhs = 0)
|
329
|
+
for i in set_I})
|
321
330
|
|
322
331
|
# Constraint for sequence of deferrable
|
323
332
|
# WARNING: This is experimental, formulation seems correct but feasibility problems.
|
@@ -363,13 +372,13 @@ class Optimization:
|
|
363
372
|
# Two special constraints just for a self-consumption cost function
|
364
373
|
if self.costfun == 'self-consumption':
|
365
374
|
if type_self_conso == 'maxmin': # maxmin linear problem
|
366
|
-
constraints.update({"
|
375
|
+
constraints.update({"constraint_selfcons_PV1_{}".format(i) :
|
367
376
|
plp.LpConstraint(
|
368
377
|
e = SC[i] - P_PV[i],
|
369
378
|
sense = plp.LpConstraintLE,
|
370
379
|
rhs = 0)
|
371
380
|
for i in set_I})
|
372
|
-
constraints.update({"
|
381
|
+
constraints.update({"constraint_selfcons_PV2_{}".format(i) :
|
373
382
|
plp.LpConstraint(
|
374
383
|
e = SC[i] - P_load[i] - P_def_sum[i],
|
375
384
|
sense = plp.LpConstraintLE,
|
@@ -439,41 +448,27 @@ class Optimization:
|
|
439
448
|
sense=plp.LpConstraintLE,
|
440
449
|
rhs=0)
|
441
450
|
for i in set_I})
|
442
|
-
# Treat the number of starts for a deferrable load
|
443
|
-
if self.optim_conf['set_def_constant'][k]:
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
# Treat deferrable load
|
463
|
-
if self.optim_conf['treat_def_as_semi_cont'][k]:
|
464
|
-
constraints.update({"constraint_pdef{}_semicont1_{}".format(k, i) :
|
465
|
-
plp.LpConstraint(
|
466
|
-
e=P_deferrable[k][i] - self.optim_conf['P_deferrable_nom'][k]*P_def_bin1[k][i],
|
467
|
-
sense=plp.LpConstraintGE,
|
468
|
-
rhs=0)
|
469
|
-
for i in set_I})
|
470
|
-
constraints.update({"constraint_pdef{}_semicont2_{}".format(k, i) :
|
471
|
-
plp.LpConstraint(
|
472
|
-
e=P_deferrable[k][i] - self.optim_conf['P_deferrable_nom'][k]*P_def_bin1[k][i],
|
473
|
-
sense=plp.LpConstraintLE,
|
474
|
-
rhs=0)
|
475
|
-
for i in set_I})
|
476
|
-
# Treat the number of starts for a deferrable load
|
451
|
+
# Treat the number of starts for a deferrable load (old method, kept here just in case)
|
452
|
+
# if self.optim_conf['set_def_constant'][k]:
|
453
|
+
# constraints.update({"constraint_pdef{}_start1_{}".format(k, i) :
|
454
|
+
# plp.LpConstraint(
|
455
|
+
# e=P_deferrable[k][i] - P_def_bin2[k][i]*M,
|
456
|
+
# sense=plp.LpConstraintLE,
|
457
|
+
# rhs=0)
|
458
|
+
# for i in set_I})
|
459
|
+
# constraints.update({"constraint_pdef{}_start2_{}".format(k, i):
|
460
|
+
# plp.LpConstraint(
|
461
|
+
# e=P_def_start[k][i] - P_def_bin2[k][i] + (P_def_bin2[k][i-1] if i-1 >= 0 else 0),
|
462
|
+
# sense=plp.LpConstraintGE,
|
463
|
+
# rhs=0)
|
464
|
+
# for i in set_I})
|
465
|
+
# constraints.update({"constraint_pdef{}_start3".format(k) :
|
466
|
+
# plp.LpConstraint(
|
467
|
+
# e = plp.lpSum(P_def_start[k][i] for i in set_I),
|
468
|
+
# sense = plp.LpConstraintEQ,
|
469
|
+
# rhs = 1)
|
470
|
+
# })
|
471
|
+
# Treat the number of starts for a deferrable load (new method considering current state)
|
477
472
|
current_state = 0
|
478
473
|
if ("def_current_state" in self.optim_conf and len(self.optim_conf["def_current_state"]) > k):
|
479
474
|
current_state = 1 if self.optim_conf["def_current_state"][k] else 0
|
@@ -644,7 +639,8 @@ class Optimization:
|
|
644
639
|
opt_tp["SOC_opt"] = SOC_opt
|
645
640
|
if self.plant_conf['inverter_is_hybrid']:
|
646
641
|
opt_tp["P_hybrid_inverter"] = [P_hybrid_inverter[i].varValue for i in set_I]
|
647
|
-
|
642
|
+
if self.plant_conf['compute_curtailment']:
|
643
|
+
opt_tp["P_PV_curtailment"] = [P_PV_curtailment[i].varValue for i in set_I]
|
648
644
|
opt_tp.index = data_opt.index
|
649
645
|
|
650
646
|
# Lets compute the optimal cost function
|
emhass/utils.py
CHANGED
@@ -11,6 +11,7 @@ import numpy as np
|
|
11
11
|
import pandas as pd
|
12
12
|
import yaml
|
13
13
|
import pytz
|
14
|
+
import ast
|
14
15
|
|
15
16
|
import plotly.express as px
|
16
17
|
|
@@ -161,6 +162,16 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic
|
|
161
162
|
"unit_of_measurement": "W",
|
162
163
|
"friendly_name": "Load Power Forecast",
|
163
164
|
},
|
165
|
+
"custom_pv_curtailment_id": {
|
166
|
+
"entity_id": "sensor.p_pv_curtailment",
|
167
|
+
"unit_of_measurement": "W",
|
168
|
+
"friendly_name": "PV Power Curtailment",
|
169
|
+
},
|
170
|
+
"custom_hybrid_inverter_id": {
|
171
|
+
"entity_id": "sensor.p_hybrid_inverter",
|
172
|
+
"unit_of_measurement": "W",
|
173
|
+
"friendly_name": "PV Hybrid Inverter",
|
174
|
+
},
|
164
175
|
"custom_batt_forecast_id": {
|
165
176
|
"entity_id": "sensor.p_batt_forecast",
|
166
177
|
"unit_of_measurement": "W",
|
@@ -242,7 +253,6 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic
|
|
242
253
|
if "target" in runtimeparams:
|
243
254
|
target = runtimeparams["target"]
|
244
255
|
params["passed_data"]["target"] = target
|
245
|
-
|
246
256
|
# Treating special data passed for MPC control case
|
247
257
|
if set_type == "naive-mpc-optim":
|
248
258
|
if "prediction_horizon" not in runtimeparams.keys():
|
@@ -298,6 +308,18 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic
|
|
298
308
|
# Treat passed forecast data lists
|
299
309
|
list_forecast_key = ['pv_power_forecast', 'load_power_forecast', 'load_cost_forecast', 'prod_price_forecast']
|
300
310
|
forecast_methods = ['weather_forecast_method', 'load_forecast_method', 'load_cost_forecast_method', 'prod_price_forecast_method']
|
311
|
+
# Param to save forecast cache (i.e. Solcast)
|
312
|
+
if "weather_forecast_cache" not in runtimeparams.keys():
|
313
|
+
weather_forecast_cache = False
|
314
|
+
else:
|
315
|
+
weather_forecast_cache = runtimeparams["weather_forecast_cache"]
|
316
|
+
params["passed_data"]["weather_forecast_cache"] = weather_forecast_cache
|
317
|
+
# Param to make sure optimization only uses cached data. (else produce error)
|
318
|
+
if "weather_forecast_cache_only" not in runtimeparams.keys():
|
319
|
+
weather_forecast_cache_only = False
|
320
|
+
else:
|
321
|
+
weather_forecast_cache_only = runtimeparams["weather_forecast_cache_only"]
|
322
|
+
params["passed_data"]["weather_forecast_cache_only"] = weather_forecast_cache_only
|
301
323
|
for method, forecast_key in enumerate(list_forecast_key):
|
302
324
|
if forecast_key in runtimeparams.keys():
|
303
325
|
if type(runtimeparams[forecast_key]) == list and len(runtimeparams[forecast_key]) >= len(forecast_dates):
|
@@ -352,14 +374,12 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic
|
|
352
374
|
if "perform_backtest" not in runtimeparams.keys():
|
353
375
|
perform_backtest = False
|
354
376
|
else:
|
355
|
-
perform_backtest =
|
377
|
+
perform_backtest = ast.literal_eval(str(runtimeparams["perform_backtest"]).capitalize())
|
356
378
|
params["passed_data"]["perform_backtest"] = perform_backtest
|
357
379
|
if "model_predict_publish" not in runtimeparams.keys():
|
358
380
|
model_predict_publish = False
|
359
381
|
else:
|
360
|
-
model_predict_publish =
|
361
|
-
str(runtimeparams["model_predict_publish"]).capitalize()
|
362
|
-
)
|
382
|
+
model_predict_publish = ast.literal_eval(str(runtimeparams["model_predict_publish"]).capitalize())
|
363
383
|
params["passed_data"]["model_predict_publish"] = model_predict_publish
|
364
384
|
if "model_predict_entity_id" not in runtimeparams.keys():
|
365
385
|
model_predict_entity_id = "sensor.p_load_forecast_custom_model"
|
@@ -416,12 +436,16 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic
|
|
416
436
|
optim_conf["def_current_state"] = [bool(s) for s in runtimeparams["def_current_state"]]
|
417
437
|
if "treat_def_as_semi_cont" in runtimeparams.keys():
|
418
438
|
optim_conf["treat_def_as_semi_cont"] = [
|
419
|
-
|
439
|
+
ast.literal_eval(str(k).capitalize())
|
420
440
|
for k in runtimeparams["treat_def_as_semi_cont"]
|
421
441
|
]
|
422
442
|
if "set_def_constant" in runtimeparams.keys():
|
423
443
|
optim_conf["set_def_constant"] = [
|
424
|
-
|
444
|
+
ast.literal_eval(str(k).capitalize()) for k in runtimeparams["set_def_constant"]
|
445
|
+
]
|
446
|
+
if "def_start_penalty" in runtimeparams.keys():
|
447
|
+
optim_conf["def_start_penalty"] = [
|
448
|
+
ast.literal_eval(str(k).capitalize()) for k in runtimeparams["def_start_penalty"]
|
425
449
|
]
|
426
450
|
if "solcast_api_key" in runtimeparams.keys():
|
427
451
|
retrieve_hass_conf["solcast_api_key"] = runtimeparams["solcast_api_key"]
|
@@ -447,8 +471,16 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic
|
|
447
471
|
if 'continual_publish' in runtimeparams.keys():
|
448
472
|
retrieve_hass_conf['continual_publish'] = bool(runtimeparams['continual_publish'])
|
449
473
|
# Treat plant configuration parameters passed at runtime
|
474
|
+
if "SOCmin" in runtimeparams.keys():
|
475
|
+
plant_conf["SOCmin"] = runtimeparams["SOCmin"]
|
476
|
+
if "SOCmax" in runtimeparams.keys():
|
477
|
+
plant_conf["SOCmax"] = runtimeparams["SOCmax"]
|
450
478
|
if "SOCtarget" in runtimeparams.keys():
|
451
479
|
plant_conf["SOCtarget"] = runtimeparams["SOCtarget"]
|
480
|
+
if "Pd_max" in runtimeparams.keys():
|
481
|
+
plant_conf["Pd_max"] = runtimeparams["Pd_max"]
|
482
|
+
if "Pc_max" in runtimeparams.keys():
|
483
|
+
plant_conf["Pc_max"] = runtimeparams["Pc_max"]
|
452
484
|
# Treat custom entities id's and friendly names for variables
|
453
485
|
if "custom_pv_forecast_id" in runtimeparams.keys():
|
454
486
|
params["passed_data"]["custom_pv_forecast_id"] = runtimeparams[
|
@@ -458,6 +490,14 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic
|
|
458
490
|
params["passed_data"]["custom_load_forecast_id"] = runtimeparams[
|
459
491
|
"custom_load_forecast_id"
|
460
492
|
]
|
493
|
+
if "custom_pv_curtailment_id" in runtimeparams.keys():
|
494
|
+
params["passed_data"]["custom_pv_curtailment_id"] = runtimeparams[
|
495
|
+
"custom_pv_curtailment_id"
|
496
|
+
]
|
497
|
+
if "custom_hybrid_inverter_id" in runtimeparams.keys():
|
498
|
+
params["passed_data"]["custom_hybrid_inverter_id"] = runtimeparams[
|
499
|
+
"custom_hybrid_inverter_id"
|
500
|
+
]
|
461
501
|
if "custom_batt_forecast_id" in runtimeparams.keys():
|
462
502
|
params["passed_data"]["custom_batt_forecast_id"] = runtimeparams[
|
463
503
|
"custom_batt_forecast_id"
|
@@ -745,9 +785,7 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int,
|
|
745
785
|
params["retrieve_hass_conf"]["var_load"] = options.get("sensor_power_load_no_var_loads", params["retrieve_hass_conf"]["var_load"])
|
746
786
|
params["retrieve_hass_conf"]["load_negative"] = options.get("load_negative", params["retrieve_hass_conf"]["load_negative"])
|
747
787
|
params["retrieve_hass_conf"]["set_zero_min"] = options.get("set_zero_min", params["retrieve_hass_conf"]["set_zero_min"])
|
748
|
-
params["retrieve_hass_conf"]["var_replace_zero"] = [
|
749
|
-
options.get("sensor_power_photovoltaics", params["retrieve_hass_conf"]["var_replace_zero"])
|
750
|
-
]
|
788
|
+
params["retrieve_hass_conf"]["var_replace_zero"] = [options.get("sensor_power_photovoltaics", params["retrieve_hass_conf"]["var_replace_zero"])]
|
751
789
|
params["retrieve_hass_conf"]["var_interp"] = [
|
752
790
|
options.get("sensor_power_photovoltaics", params["retrieve_hass_conf"]["var_PV"]),
|
753
791
|
options.get("sensor_power_load_no_var_loads", params["retrieve_hass_conf"]["var_load"])
|
@@ -764,20 +802,11 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int,
|
|
764
802
|
params["optim_conf"]["set_use_battery"] = options.get("set_use_battery", params["optim_conf"]["set_use_battery"])
|
765
803
|
params["optim_conf"]["num_def_loads"] = options.get("number_of_deferrable_loads", params["optim_conf"]["num_def_loads"])
|
766
804
|
if options.get("list_nominal_power_of_deferrable_loads", None) != None:
|
767
|
-
params["optim_conf"]["P_deferrable_nom"] = [
|
768
|
-
i["nominal_power_of_deferrable_loads"]
|
769
|
-
for i in options.get("list_nominal_power_of_deferrable_loads")
|
770
|
-
]
|
805
|
+
params["optim_conf"]["P_deferrable_nom"] = [i["nominal_power_of_deferrable_loads"] for i in options.get("list_nominal_power_of_deferrable_loads")]
|
771
806
|
if options.get("list_operating_hours_of_each_deferrable_load", None) != None:
|
772
|
-
params["optim_conf"]["def_total_hours"] = [
|
773
|
-
i["operating_hours_of_each_deferrable_load"]
|
774
|
-
for i in options.get("list_operating_hours_of_each_deferrable_load")
|
775
|
-
]
|
807
|
+
params["optim_conf"]["def_total_hours"] = [i["operating_hours_of_each_deferrable_load"] for i in options.get("list_operating_hours_of_each_deferrable_load")]
|
776
808
|
if options.get("list_treat_deferrable_load_as_semi_cont", None) != None:
|
777
|
-
params["optim_conf"]["treat_def_as_semi_cont"] = [
|
778
|
-
i["treat_deferrable_load_as_semi_cont"]
|
779
|
-
for i in options.get("list_treat_deferrable_load_as_semi_cont")
|
780
|
-
]
|
809
|
+
params["optim_conf"]["treat_def_as_semi_cont"] = [i["treat_deferrable_load_as_semi_cont"] for i in options.get("list_treat_deferrable_load_as_semi_cont")]
|
781
810
|
params["optim_conf"]["weather_forecast_method"] = options.get("weather_forecast_method", params["optim_conf"]["weather_forecast_method"])
|
782
811
|
# Update optional param secrets
|
783
812
|
if params["optim_conf"]["weather_forecast_method"] == "solcast":
|
@@ -789,19 +818,14 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int,
|
|
789
818
|
params["optim_conf"]["delta_forecast"] = options.get("delta_forecast_daily", params["optim_conf"]["delta_forecast"])
|
790
819
|
params["optim_conf"]["load_cost_forecast_method"] = options.get("load_cost_forecast_method", params["optim_conf"]["load_cost_forecast_method"])
|
791
820
|
if options.get("list_set_deferrable_load_single_constant", None) != None:
|
792
|
-
params["optim_conf"]["set_def_constant"] = [
|
793
|
-
|
794
|
-
|
795
|
-
]
|
821
|
+
params["optim_conf"]["set_def_constant"] = [i["set_deferrable_load_single_constant"] for i in options.get("list_set_deferrable_load_single_constant")]
|
822
|
+
|
823
|
+
if options.get("list_set_deferrable_startup_penalty", None) != None:
|
824
|
+
params["optim_conf"]["def_start_penalty"] = [i["set_deferrable_startup_penalty"] for i in options.get("list_set_deferrable_startup_penalty")]
|
825
|
+
|
796
826
|
if (options.get("list_peak_hours_periods_start_hours", None) != None and options.get("list_peak_hours_periods_end_hours", None) != None):
|
797
|
-
start_hours_list = [
|
798
|
-
|
799
|
-
for i in options["list_peak_hours_periods_start_hours"]
|
800
|
-
]
|
801
|
-
end_hours_list = [
|
802
|
-
i["peak_hours_periods_end_hours"]
|
803
|
-
for i in options["list_peak_hours_periods_end_hours"]
|
804
|
-
]
|
827
|
+
start_hours_list = [i["peak_hours_periods_start_hours"] for i in options["list_peak_hours_periods_start_hours"]]
|
828
|
+
end_hours_list = [i["peak_hours_periods_end_hours"] for i in options["list_peak_hours_periods_end_hours"]]
|
805
829
|
num_peak_hours = len(start_hours_list)
|
806
830
|
list_hp_periods_list = [{'period_hp_'+str(i+1):[{'start':start_hours_list[i]},{'end':end_hours_list[i]}]} for i in range(num_peak_hours)]
|
807
831
|
params['optim_conf']['list_hp_periods'] = list_hp_periods_list
|
@@ -839,6 +863,7 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int,
|
|
839
863
|
if options.get('list_strings_per_inverter',None) != None:
|
840
864
|
params['plant_conf']['strings_per_inverter'] = [i['strings_per_inverter'] for i in options.get('list_strings_per_inverter')]
|
841
865
|
params["plant_conf"]["inverter_is_hybrid"] = options.get("inverter_is_hybrid", params["plant_conf"]["inverter_is_hybrid"])
|
866
|
+
params["plant_conf"]["compute_curtailment"] = options.get("compute_curtailment", params["plant_conf"]["compute_curtailment"])
|
842
867
|
params['plant_conf']['Pd_max'] = options.get('battery_discharge_power_max', params['plant_conf']['Pd_max'])
|
843
868
|
params['plant_conf']['Pc_max'] = options.get('battery_charge_power_max', params['plant_conf']['Pc_max'])
|
844
869
|
params['plant_conf']['eta_disch'] = options.get('battery_discharge_efficiency', params['plant_conf']['eta_disch'])
|
emhass/web_server.py
CHANGED
@@ -12,7 +12,7 @@ from distutils.util import strtobool
|
|
12
12
|
|
13
13
|
from emhass.command_line import set_input_data_dict
|
14
14
|
from emhass.command_line import perfect_forecast_optim, dayahead_forecast_optim, naive_mpc_optim
|
15
|
-
from emhass.command_line import forecast_model_fit, forecast_model_predict, forecast_model_tune
|
15
|
+
from emhass.command_line import forecast_model_fit, forecast_model_predict, forecast_model_tune, weather_forecast_cache
|
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, \
|
@@ -106,6 +106,17 @@ def action_call(action_name):
|
|
106
106
|
if runtimeparams is not None and runtimeparams != '{}':
|
107
107
|
app.logger.info("Passed runtime parameters: " + str(runtimeparams))
|
108
108
|
runtimeparams = json.dumps(runtimeparams)
|
109
|
+
|
110
|
+
# Run action if weather_forecast_cache
|
111
|
+
if action_name == 'weather-forecast-cache':
|
112
|
+
ActionStr = " >> Performing weather forecast, try to caching result"
|
113
|
+
app.logger.info(ActionStr)
|
114
|
+
weather_forecast_cache(emhass_conf, params, runtimeparams, app.logger)
|
115
|
+
msg = f'EMHASS >> Weather Forecast has run and results possibly cached... \n'
|
116
|
+
if not checkFileLog(ActionStr):
|
117
|
+
return make_response(msg, 201)
|
118
|
+
return make_response(grabLog(ActionStr), 400)
|
119
|
+
|
109
120
|
ActionStr = " >> Setting input data dict"
|
110
121
|
app.logger.info(ActionStr)
|
111
122
|
input_data_dict = set_input_data_dict(emhass_conf, costfun,
|
@@ -459,15 +470,14 @@ if __name__ == "__main__":
|
|
459
470
|
app.logger.addHandler(fileLogger)
|
460
471
|
clearFileLog() #Clear Action File logger file, ready for new instance
|
461
472
|
|
462
|
-
|
463
|
-
#If entity_path exists, remove any entity/metadata files
|
473
|
+
# If entity_path exists, remove any entity/metadata files
|
464
474
|
entity_path = emhass_conf['data_path'] / "entities"
|
465
475
|
if os.path.exists(entity_path):
|
466
476
|
entity_pathContents = os.listdir(entity_path)
|
467
477
|
if len(entity_pathContents) > 0:
|
468
478
|
for entity in entity_pathContents:
|
469
479
|
os.remove(entity_path / entity)
|
470
|
-
|
480
|
+
|
471
481
|
# Initialise continual publish thread list
|
472
482
|
continual_publish_thread = []
|
473
483
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: emhass
|
3
|
-
Version: 0.10.
|
3
|
+
Version: 0.10.2
|
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
|
@@ -28,7 +28,7 @@ Requires-Dist: h5py ==3.11.0
|
|
28
28
|
Requires-Dist: pulp >=2.4
|
29
29
|
Requires-Dist: pyyaml >=5.4.1
|
30
30
|
Requires-Dist: tables <=3.9.1
|
31
|
-
Requires-Dist: skforecast ==0.12.
|
31
|
+
Requires-Dist: skforecast ==0.12.1
|
32
32
|
Requires-Dist: flask >=2.0.3
|
33
33
|
Requires-Dist: waitress >=2.1.1
|
34
34
|
Requires-Dist: plotly >=5.6.0
|
@@ -179,13 +179,9 @@ docker run -it --restart always -p 5000:5000 -e TZ="Europe/Paris" -e LOCAL_COS
|
|
179
179
|
### Method 3) Legacy method using a Python virtual environment
|
180
180
|
|
181
181
|
With this method it is recommended to install on a virtual environment.
|
182
|
-
|
182
|
+
Create and activate a virtual environment:
|
183
183
|
```bash
|
184
|
-
|
185
|
-
```
|
186
|
-
Then create and activate the virtual environment:
|
187
|
-
```bash
|
188
|
-
virtualenv -p /usr/bin/python3 emhassenv
|
184
|
+
python3 -m venv emhassenv
|
189
185
|
cd emhassenv
|
190
186
|
source bin/activate
|
191
187
|
```
|
@@ -496,7 +492,7 @@ curl -i -H 'Content-Type:application/json' -X POST -d '{"publish_prefix":"all"}'
|
|
496
492
|
```
|
497
493
|
This action will publish the dayahead (_dh) and MPC (_mpc) optimization results from the optimizations above.
|
498
494
|
|
499
|
-
### Forecast data
|
495
|
+
### Forecast data at runtime
|
500
496
|
|
501
497
|
It is possible to provide EMHASS with your own forecast data. For this just add the data as list of values to a data dictionary during the call to `emhass` using the `runtimeparams` option.
|
502
498
|
|
@@ -519,7 +515,7 @@ The possible dictionary keys to pass data are:
|
|
519
515
|
|
520
516
|
- `prod_price_forecast` for the PV production selling price forecast.
|
521
517
|
|
522
|
-
### Passing other data
|
518
|
+
### Passing other data at runtime
|
523
519
|
|
524
520
|
It is possible to also pass other data during runtime in order to automate the energy management. For example, it could be useful to dynamically update the total number of hours for each deferrable load (`def_total_hours`) using for instance a correlation with the outdoor temperature (useful for water heater for example).
|
525
521
|
|
@@ -535,6 +531,8 @@ Here is the list of the other additional dictionary keys that can be passed at r
|
|
535
531
|
|
536
532
|
- `def_end_timestep` for the timestep before which each deferrable load should operate (if you don't want the deferrable load to use the whole optimization timewindow).
|
537
533
|
|
534
|
+
- `def_current_state` Pass this as a list of booleans (True/False) to indicate the current deferrable load state. This is used internally to avoid incorrectly penalizing a deferrable load start if a forecast is run when that load is already running.
|
535
|
+
|
538
536
|
- `treat_def_as_semi_cont` to define if we should treat each deferrable load as a semi-continuous variable.
|
539
537
|
|
540
538
|
- `set_def_constant` to define if we should set each deferrable load as a constant fixed value variable with just one startup for each optimization task.
|
@@ -545,8 +543,16 @@ Here is the list of the other additional dictionary keys that can be passed at r
|
|
545
543
|
|
546
544
|
- `solar_forecast_kwp` for the PV peak installed power in kW used for the solar.forecast API call.
|
547
545
|
|
546
|
+
- `SOCmin` the minimum possible SOC.
|
547
|
+
|
548
|
+
- `SOCmax` the maximum possible SOC.
|
549
|
+
|
548
550
|
- `SOCtarget` for the desired target value of initial and final SOC.
|
549
551
|
|
552
|
+
- `Pd_max` for the maximum battery discharge power.
|
553
|
+
|
554
|
+
- `Pc_max` for the maximum battery charge power.
|
555
|
+
|
550
556
|
- `publish_prefix` use this key to pass a common prefix to all published data. This will add a prefix to the sensor name but also to the forecasts attributes keys within the sensor.
|
551
557
|
|
552
558
|
## A naive Model Predictive Controller
|
@@ -1,12 +1,12 @@
|
|
1
1
|
emhass/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
emhass/command_line.py,sha256=
|
3
|
-
emhass/forecast.py,sha256=
|
4
|
-
emhass/machine_learning_forecaster.py,sha256=
|
2
|
+
emhass/command_line.py,sha256=jQib__atH07dEaTQuqTyvUk9irlVelvnipoc5Sw8CEg,60404
|
3
|
+
emhass/forecast.py,sha256=ZwTjZxodszGM_1VBfFz5zhlzx0gobCQAkHA3PLjTXnA,53141
|
4
|
+
emhass/machine_learning_forecaster.py,sha256=kUa5b5Ay3ZO7ceU7yUo1p5vu2DI6QbXMFmzX6IVLkzw,16027
|
5
5
|
emhass/machine_learning_regressor.py,sha256=WmR9ODWkY64RAniqLowwf5tZWzPTVp5ftCTKNtzcd6I,10407
|
6
|
-
emhass/optimization.py,sha256=
|
6
|
+
emhass/optimization.py,sha256=LTIXc42IlMdARkstiqIHtDB0sryq230EohqJJsggKqE,47120
|
7
7
|
emhass/retrieve_hass.py,sha256=k-BPZMqW-uQ95Q7Gzz93nPkLHStDCkI7-047GVYBGC0,22983
|
8
|
-
emhass/utils.py,sha256=
|
9
|
-
emhass/web_server.py,sha256=
|
8
|
+
emhass/utils.py,sha256=Q9X0pM3yMPVMoal6FVuUNft4RGZpLZYqeSWrwcg30AI,50380
|
9
|
+
emhass/web_server.py,sha256=Kwx3YmNugxMDHHWMiK-cb8qjLmM9ugKvaj6HWnI_NYc,24638
|
10
10
|
emhass/data/cec_inverters.pbz2,sha256=tK8FvAUDW0uYez8EPttdCJwHhpPofclYV6GhhNZL0Pk,168272
|
11
11
|
emhass/data/cec_modules.pbz2,sha256=8vEaysgYffXg3KUl8XSF36Mdywzi3LpEtUN_qenjO9s,1655747
|
12
12
|
emhass/static/advanced.html,sha256=15tYNw9ck_ds1zxQk0XXj7wmS9px_0x0GDx57cFx3vA,1970
|
@@ -18,9 +18,9 @@ emhass/static/img/emhass_logo_short.svg,sha256=yzMcqtBRCV8rH84-MwnigZh45_f9Eoqwh
|
|
18
18
|
emhass/static/img/feather-sprite.svg,sha256=VHjMJQg88wXa9CaeYrKGhNtyK0xdd47zCqwSIa-hxo8,60319
|
19
19
|
emhass/templates/index.html,sha256=_BsvUJ981uSQkx5H9tq_3es__x4WdPiOy7FjNoNYU9w,2744
|
20
20
|
emhass/templates/template.html,sha256=TkGgMecQEbFUZA4ymPwMUzNjKHsENvCgroUWbPt7G4Y,158
|
21
|
-
emhass-0.10.
|
22
|
-
emhass-0.10.
|
23
|
-
emhass-0.10.
|
24
|
-
emhass-0.10.
|
25
|
-
emhass-0.10.
|
26
|
-
emhass-0.10.
|
21
|
+
emhass-0.10.2.dist-info/LICENSE,sha256=1X3-S1yvOCBDBeox1aK3dq00m7dA8NDtcPrpKPISzbE,1077
|
22
|
+
emhass-0.10.2.dist-info/METADATA,sha256=4hytYJ1JCekiyvvaYSILPuE55e46ao9Iaf4KGFCB6XM,44679
|
23
|
+
emhass-0.10.2.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
|
24
|
+
emhass-0.10.2.dist-info/entry_points.txt,sha256=6Bp1NFOGNv_fSTxYl1ke3K3h3aqAcBxI-bgq5yq-i1M,52
|
25
|
+
emhass-0.10.2.dist-info/top_level.txt,sha256=L7fIX4awfmxQbAePtSdVg2e6x_HhghfReHfsKSpKr9I,7
|
26
|
+
emhass-0.10.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|