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 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 (get_data_from_file), params=params)
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,logger,reference: Optional[str] = ""):
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
- # Additionnal argument
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 solcast API
227
- # Retrieve data from the solcast API
228
- if 'solcast_api_key' not in self.retrieve_hass_conf:
229
- self.logger.warning("The solcast_api_key parameter was not defined, using dummy values for testing")
230
- self.retrieve_hass_conf['solcast_api_key'] = "123456"
231
- if 'solcast_rooftop_id' not in self.retrieve_hass_conf:
232
- self.logger.warning("The solcast_rooftop_id parameter was not defined, using dummy values for testing")
233
- self.retrieve_hass_conf['solcast_rooftop_id'] = "123456"
234
- headers = {
235
- "Authorization": "Bearer " + self.retrieve_hass_conf['solcast_api_key'],
236
- "content-type": "application/json",
237
- }
238
- days_solcast = int(len(self.forecast_dates)*self.freq.seconds/3600)
239
- url = "https://api.solcast.com.au/rooftop_sites/"+self.retrieve_hass_conf['solcast_rooftop_id']+"/forecasts?hours="+str(days_solcast)
240
- response = get(url, headers=headers)
241
- '''import bz2 # Uncomment to save a serialized data for tests
242
- import _pickle as cPickle
243
- with bz2.BZ2File("data/test_response_solcast_get_method.pbz2", "w") as f:
244
- cPickle.dump(response, f)'''
245
- data = response.json()
246
- data_list = []
247
- for elm in data['forecasts']:
248
- data_list.append(elm['pv_estimate']*1000) # Converting kW to W
249
- # Check if the retrieved data has the correct length
250
- if len(data_list) < len(self.forecast_dates):
251
- self.logger.error("Not enough data retrived from SolCast service, try increasing the time step or use MPC")
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
- # Ensure correct length
254
- data_list = data_list[0:len(self.forecast_dates)]
255
- # Define DataFrame
256
- data_dict = {'ts':self.forecast_dates, 'yhat':data_list}
257
- data = pd.DataFrame.from_dict(data_dict)
258
- # Define index
259
- data.set_index('ts', inplace=True)
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
- constraints = {"constraint_main1_{}".format(i) :
276
- plp.LpConstraint(
277
- 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],
278
- sense = plp.LpConstraintEQ,
279
- rhs = 0)
280
- for i in set_I}
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
- constraints.update({"constraint_curtailment_{}".format(i) :
316
- plp.LpConstraint(
317
- e = P_PV[i] - P_PV_curtailment[i] - P_nom_inverter,
318
- sense = plp.LpConstraintLE,
319
- rhs = 0)
320
- for i in set_I})
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({"constraint_selfcons_PV_{}".format(i) :
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({"constraint_selfcons_PV_{}".format(i) :
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
- constraints.update({"constraint_pdef{}_start1_{}".format(k, i) :
445
- plp.LpConstraint(
446
- e=P_deferrable[k][i] - P_def_bin2[k][i]*M,
447
- sense=plp.LpConstraintLE,
448
- rhs=0)
449
- for i in set_I})
450
- constraints.update({"constraint_pdef{}_start2_{}".format(k, i):
451
- plp.LpConstraint(
452
- e=P_def_start[k][i] - P_def_bin2[k][i] + (P_def_bin2[k][i-1] if i-1 >= 0 else 0),
453
- sense=plp.LpConstraintGE,
454
- rhs=0)
455
- for i in set_I})
456
- constraints.update({"constraint_pdef{}_start3".format(k) :
457
- plp.LpConstraint(
458
- e = plp.lpSum(P_def_start[k][i] for i in set_I),
459
- sense = plp.LpConstraintEQ,
460
- rhs = 1)
461
- })
462
- # Treat deferrable load as a semi-continuous variable
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
- opt_tp["P_PV_curtailment"] = [P_PV_curtailment[i].varValue for i in set_I]
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 = eval(str(runtimeparams["perform_backtest"]).capitalize())
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 = eval(
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
- eval(str(k).capitalize())
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
- eval(str(k).capitalize()) for k in runtimeparams["set_def_constant"]
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
- i["set_deferrable_load_single_constant"]
794
- for i in options.get("list_set_deferrable_load_single_constant")
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
- i["peak_hours_periods_start_hours"]
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.0
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.0
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
- For this you will need `virtualenv`, install it using:
182
+ Create and activate a virtual environment:
183
183
  ```bash
184
- sudo apt install python3-virtualenv
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=RPMFS86McukgzoGmeq-DEMwyCZrpFx6Ko27Ndj_A0Fc,57211
3
- emhass/forecast.py,sha256=VdB8O6V4LYXdfIg7fHNRIVdvj2Y8Ow8Z7r1Ck3hvvh8,47631
4
- emhass/machine_learning_forecaster.py,sha256=az8cYRCckmR-WEdXyigbe8udtbj82yfahPmow4gue4s,15655
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=bNn3txHurUNrnW-FFmY9MRokn1X1V50pjK9TNH3z-Es,47305
6
+ emhass/optimization.py,sha256=LTIXc42IlMdARkstiqIHtDB0sryq230EohqJJsggKqE,47120
7
7
  emhass/retrieve_hass.py,sha256=k-BPZMqW-uQ95Q7Gzz93nPkLHStDCkI7-047GVYBGC0,22983
8
- emhass/utils.py,sha256=uc2yrcvA-GwJPsGOQIc_tEf3MxtGu_tMNf1PyyEx3Aw,48112
9
- emhass/web_server.py,sha256=8hzVjDTCSMdPwpgzstGiH11IArIYiaYGSYkAiMq4S78,24095
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.0.dist-info/LICENSE,sha256=1X3-S1yvOCBDBeox1aK3dq00m7dA8NDtcPrpKPISzbE,1077
22
- emhass-0.10.0.dist-info/METADATA,sha256=602w53NAKX6TNa7sQcTVpyZlUrlgcJn1yxX2oz9Hn_M,44352
23
- emhass-0.10.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
24
- emhass-0.10.0.dist-info/entry_points.txt,sha256=6Bp1NFOGNv_fSTxYl1ke3K3h3aqAcBxI-bgq5yq-i1M,52
25
- emhass-0.10.0.dist-info/top_level.txt,sha256=L7fIX4awfmxQbAePtSdVg2e6x_HhghfReHfsKSpKr9I,7
26
- emhass-0.10.0.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (70.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5