diff --git a/CHANGELOG.md b/CHANGELOG.md index 105808f4..b4c80028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.10.2 - 2024-07-06 +### Improvement +- Weather forecast caching and Solcast method fix by @GeoDerp +- Added a new configuration parameter to control wether we compute PV curtailment or not +- Added hybrid inverter to data publish +- It is now possible to pass these battery parameters at runtime: `SOCmin`, `SOCmax`, `Pd_max` and `Pc_max` +### Fix +- Fixed problem with negative PV forecast values in optimization.py, by @GeoDerp + ## 0.10.1 - 2024-06-03 ### Fix - Fixed PV curtailment maximum possible value constraint diff --git a/README.md b/README.md index fa608b8d..4db9b81d 100644 --- a/README.md +++ b/README.md @@ -144,13 +144,9 @@ docker run -it --restart always -p 5000:5000 -e TZ="Europe/Paris" -e LOCAL_COS ### Method 3) Legacy method using a Python virtual environment With this method it is recommended to install on a virtual environment. -For this you will need `virtualenv`, install it using: +Create and activate a virtual environment: ```bash -sudo apt install python3-virtualenv -``` -Then create and activate the virtual environment: -```bash -virtualenv -p /usr/bin/python3 emhassenv +python3 -m venv emhassenv cd emhassenv source bin/activate ``` @@ -461,7 +457,7 @@ curl -i -H 'Content-Type:application/json' -X POST -d '{"publish_prefix":"all"}' ``` This action will publish the dayahead (_dh) and MPC (_mpc) optimization results from the optimizations above. -### Forecast data +### Forecast data at runtime 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. @@ -484,7 +480,7 @@ The possible dictionary keys to pass data are: - `prod_price_forecast` for the PV production selling price forecast. -### Passing other data +### Passing other data at runtime 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). @@ -500,6 +496,8 @@ Here is the list of the other additional dictionary keys that can be passed at r - `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). +- `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. + - `treat_def_as_semi_cont` to define if we should treat each deferrable load as a semi-continuous variable. - `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. @@ -510,8 +508,16 @@ Here is the list of the other additional dictionary keys that can be passed at r - `solar_forecast_kwp` for the PV peak installed power in kW used for the solar.forecast API call. +- `SOCmin` the minimum possible SOC. + +- `SOCmax` the maximum possible SOC. + - `SOCtarget` for the desired target value of initial and final SOC. +- `Pd_max` for the maximum battery discharge power. + +- `Pc_max` for the maximum battery charge power. + - `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. ## A naive Model Predictive Controller diff --git a/config_emhass.yaml b/config_emhass.yaml index bc2ac8d9..ca3a47d7 100644 --- a/config_emhass.yaml +++ b/config_emhass.yaml @@ -37,6 +37,9 @@ optim_conf: set_def_constant: # set as a constant fixed value variable with just one startup for each 24h - False - False + def_start_penalty: # Set a penalty for each start up of a deferrable load + - 0.0 + - 0.0 weather_forecast_method: 'scrapper' # options are 'scrapper', 'csv', 'list', 'solcast' and 'solar.forecast' load_forecast_method: 'naive' # options are 'csv' to load a custom load forecast from a CSV file or 'naive' for a persistance model load_cost_forecast_method: 'hp_hc_periods' # options are 'hp_hc_periods' for peak and non-peak hours contracts and 'csv' to load custom cost from CSV file @@ -78,6 +81,7 @@ plant_conf: strings_per_inverter: # The number of used strings per inverter - 1 inverter_is_hybrid: False # Set if it is a hybrid inverter (PV+batteries) or not + compute_curtailment: False # Compute a PV curtailment variable or not Pd_max: 1000 # If your system has a battery (set_use_battery=True), the maximum discharge power in Watts Pc_max: 1000 # If your system has a battery (set_use_battery=True), the maximum charge power in Watts eta_disch: 0.95 # If your system has a battery (set_use_battery=True), the discharge efficiency diff --git a/data/test_df_final.pkl b/data/test_df_final.pkl index 2f81e539..5c07a96b 100644 Binary files a/data/test_df_final.pkl and b/data/test_df_final.pkl differ diff --git a/docs/conf.py b/docs/conf.py index 54ed1f74..45d7baa4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ author = 'David HERNANDEZ' # The full version, including alpha/beta/rc tags -release = '0.10.1' +release = '0.10.2' # -- General configuration --------------------------------------------------- diff --git a/docs/config.md b/docs/config.md index c2fce240..3ac0af52 100644 --- a/docs/config.md +++ b/docs/config.md @@ -62,6 +62,7 @@ These are the parameters needed to properly define the optimization problem. - `set_def_constant`: Define if we should set each deferrable load as a constant fixed value variable with just one startup for each optimization task. For example: - False - False +- `def_start_penalty`: Set to a list of floats. For each deferrable load with a penalty `P`, each time the deferrable load turns on will incur an additional cost of `P * P_deferrable_nom * cost_of_electricity` at that time. - `weather_forecast_method`: This will define the weather forecast method that will be used. The options are 'scrapper' for a scrapping method for weather forecast from clearoutside.com and 'csv' to load a CSV file. When loading a CSV file this will be directly considered as the PV power forecast in Watts. The default CSV file path that will be used is '/data/data_weather_forecast.csv'. Defaults to 'scrapper' method. - `load_forecast_method`: The load forecast method that will be used. The options are 'csv' to load a CSV file or 'naive' for a simple 1-day persistance model. The default CSV file path that will be used is '/data/data_load_forecast.csv'. Defaults to 'naive'. - `load_cost_forecast_method`: Define the method that will be used for load cost forecast. The options are 'hp_hc_periods' for peak and non-peak hours contracts and 'csv' to load custom cost from CSV file. The default CSV file path that will be used is '/data/data_load_cost_forecast.csv'. @@ -106,7 +107,9 @@ Solution (2) would be to use SolCast and pass that data directly to emhass as a - `surface_tilt`: The tilt angle of your solar panels. Defaults to 30. This parameter can be a list of items to enable the simulation of mixed orientation systems, for example one east-facing array (azimuth=90) and one west-facing array (azimuth=270). - `surface_azimuth`: The azimuth of your PV installation. Defaults to 205. This parameter can be a list of items to enable the simulation of mixed orientation systems, for example one east-facing array (azimuth=90) and one west-facing array (azimuth=270). - `modules_per_string`: The number of modules per string. Defaults to 16. This parameter can be a list of items to enable the simulation of mixed orientation systems, for example one east-facing array (azimuth=90) and one west-facing array (azimuth=270). -- `strings_per_inverter`: The number of used strings per inverter. Defaults to 1. This parameter can be a list of items to enable the simulation of mixed orientation systems, for example one east-facing array (azimuth=90) and one west-facing array (azimuth=270). +- `strings_per_inverter`: The number of used strings per inverter. Defaults to 1. This parameter can be a list of items to enable the simulation of mixed orientation systems, for example one east-facing array (azimuth=90) and one west-facing array (azimuth=270). +- `inverter_is_hybrid`: Set to True to consider that the installation inverter is hybrid for PV and batteries (Default False). +- `compute_curtailment`: Set to True to compute a special PV curtailment variable (Default False). If your system has a battery (set_use_battery=True), then you should define the following parameters: diff --git a/options.json b/options.json index 7e6aa849..01f83b52 100644 --- a/options.json +++ b/options.json @@ -98,6 +98,14 @@ "set_deferrable_load_single_constant": false } ], + "list_set_deferrable_startup_penalty": [ + { + "set_deferrable_startup_penalty": 0.0 + }, + { + "set_deferrable_startup_penalty": 0.0 + } + ], "load_peak_hours_cost": 0.1907, "load_offpeak_hours_cost": 0.1419, "production_price_forecast_method": "constant", @@ -135,6 +143,7 @@ } ], "inverter_is_hybrid": false, + "compute_curtailment": false, "set_use_battery": false, "battery_discharge_power_max": 1000, "battery_charge_power_max": 1000, diff --git a/scripts/script_debug_optim.py b/scripts/script_debug_optim.py index c4ded0f1..44900fc1 100644 --- a/scripts/script_debug_optim.py +++ b/scripts/script_debug_optim.py @@ -71,8 +71,8 @@ # Set special debug cases optim_conf.update({'lp_solver': 'PULP_CBC_CMD'}) # set the name of the linear programming solver that will be used. Options are 'PULP_CBC_CMD', 'GLPK_CMD' and 'COIN_CMD'. optim_conf.update({'lp_solver_path': 'empty'}) # set the path to the LP solver, COIN_CMD default is /usr/bin/cbc - optim_conf.update({'treat_def_as_semi_cont': [True, True]}) - optim_conf.update({'set_def_constant': [False, False]}) + optim_conf.update({'treat_def_as_semi_cont': [True, False]}) + optim_conf.update({'set_def_constant': [True, False]}) # optim_conf.update({'P_deferrable_nom': [[500.0, 100.0, 100.0, 500.0], 750.0]}) optim_conf.update({'set_use_battery': False}) @@ -101,9 +101,11 @@ if show_figures: fig_inputs_dah.show() - vars_to_plot = ['P_deferrable0', 'P_deferrable1','P_grid', 'P_PV', 'P_PV_curtailment'] + vars_to_plot = ['P_deferrable0', 'P_deferrable1','P_grid', 'P_PV'] if plant_conf['inverter_is_hybrid']: vars_to_plot = vars_to_plot + ['P_hybrid_inverter'] + if plant_conf['compute_curtailment']: + vars_to_plot = vars_to_plot + ['P_PV_curtailment'] if optim_conf['set_use_battery']: vars_to_plot = vars_to_plot + ['P_batt'] + ['SOC_opt'] fig_res_dah = opt_res_dayahead[vars_to_plot].plot() # 'P_def_start_0', 'P_def_start_1', 'P_def_bin2_0', 'P_def_bin2_1' diff --git a/setup.py b/setup.py index 88426e5c..0f83546e 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name='emhass', # Required - version='0.10.1', # Required + version='0.10.2', # Required description='An Energy Management System for Home Assistant', # Optional long_description=long_description, # Optional long_description_content_type='text/markdown', # Optional (see note above) diff --git a/src/emhass/command_line.py b/src/emhass/command_line.py index 4138d4c0..ee88a4fe 100644 --- a/src/emhass/command_line.py +++ b/src/emhass/command_line.py @@ -849,19 +849,35 @@ def publish_data(input_data_dict: dict, logger: logging.Logger, ) cols_published = ["P_PV", "P_Load"] # Publish PV curtailment - custom_pv_curtailment_id = params["passed_data"]["custom_pv_curtailment_id"] - input_data_dict["rh"].post_data( - opt_res_latest["P_PV_curtailment"], - idx_closest, - custom_pv_curtailment_id["entity_id"], - custom_pv_curtailment_id["unit_of_measurement"], - custom_pv_curtailment_id["friendly_name"], - type_var="power", - publish_prefix=publish_prefix, - save_entities=entity_save, - dont_post=dont_post - ) - cols_published = cols_published + ["P_PV_curtailment"] + if input_data_dict["fcst"].plant_conf['compute_curtailment']: + custom_pv_curtailment_id = params["passed_data"]["custom_pv_curtailment_id"] + input_data_dict["rh"].post_data( + opt_res_latest["P_PV_curtailment"], + idx_closest, + custom_pv_curtailment_id["entity_id"], + custom_pv_curtailment_id["unit_of_measurement"], + custom_pv_curtailment_id["friendly_name"], + type_var="power", + publish_prefix=publish_prefix, + save_entities=entity_save, + dont_post=dont_post + ) + cols_published = cols_published + ["P_PV_curtailment"] + # Publish P_hybrid_inverter + if input_data_dict["fcst"].plant_conf['inverter_is_hybrid']: + custom_hybrid_inverter_id = params["passed_data"]["custom_hybrid_inverter_id"] + input_data_dict["rh"].post_data( + opt_res_latest["P_hybrid_inverter"], + idx_closest, + custom_hybrid_inverter_id["entity_id"], + custom_hybrid_inverter_id["unit_of_measurement"], + custom_hybrid_inverter_id["friendly_name"], + type_var="power", + publish_prefix=publish_prefix, + save_entities=entity_save, + dont_post=dont_post + ) + cols_published = cols_published + ["P_hybrid_inverter"] # Publish deferrable loads custom_deferrable_forecast_id = params["passed_data"][ "custom_deferrable_forecast_id" diff --git a/src/emhass/forecast.py b/src/emhass/forecast.py index 6e2aa591..7058c220 100644 --- a/src/emhass/forecast.py +++ b/src/emhass/forecast.py @@ -481,9 +481,9 @@ def get_power_from_weather(self, df_weather: pd.DataFrame, # Setting the main parameters of the PV plant location = Location(latitude=self.lat, longitude=self.lon) temp_params = TEMPERATURE_MODEL_PARAMETERS['sapm']['close_mount_glass_glass'] - cec_modules = bz2.BZ2File(self.emhass_conf['root_path'] / 'data/cec_modules.pbz2', "rb") + cec_modules = bz2.BZ2File(self.emhass_conf['root_path'] / 'data' / 'cec_modules.pbz2', "rb") cec_modules = cPickle.load(cec_modules) - cec_inverters = bz2.BZ2File(self.emhass_conf['root_path'] / 'data/cec_inverters.pbz2', "rb") + cec_inverters = bz2.BZ2File(self.emhass_conf['root_path'] / 'data' / 'cec_inverters.pbz2', "rb") cec_inverters = cPickle.load(cec_inverters) if type(self.plant_conf['module_model']) == list: P_PV_forecast = pd.Series(0, index=df_weather.index) diff --git a/src/emhass/optimization.py b/src/emhass/optimization.py index 31858d1a..67b21f74 100644 --- a/src/emhass/optimization.py +++ b/src/emhass/optimization.py @@ -272,12 +272,20 @@ def perform_optimization(self, data_opt: pd.DataFrame, P_PV: np.array, P_load: n rhs = 0) for i in set_I} else: - constraints = {"constraint_main1_{}".format(i) : - plp.LpConstraint( - 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], - sense = plp.LpConstraintEQ, - rhs = 0) - for i in set_I} + if self.plant_conf['compute_curtailment']: + constraints = {"constraint_main2_{}".format(i) : + plp.LpConstraint( + 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], + sense = plp.LpConstraintEQ, + rhs = 0) + for i in set_I} + else: + constraints = {"constraint_main3_{}".format(i) : + plp.LpConstraint( + 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], + sense = plp.LpConstraintEQ, + rhs = 0) + for i in set_I} # Constraint for hybrid inverter and curtailment cases if type(self.plant_conf['module_model']) == list: @@ -312,12 +320,13 @@ def perform_optimization(self, data_opt: pd.DataFrame, P_PV: np.array, P_load: n rhs = 0) for i in set_I}) else: - constraints.update({"constraint_curtailment_{}".format(i) : - plp.LpConstraint( - e = P_PV_curtailment[i] - max(P_PV[i],0), - sense = plp.LpConstraintLE, - rhs = 0) - for i in set_I}) + if self.plant_conf['compute_curtailment']: + constraints.update({"constraint_curtailment_{}".format(i) : + plp.LpConstraint( + e = P_PV_curtailment[i] - max(P_PV[i],0), + sense = plp.LpConstraintLE, + rhs = 0) + for i in set_I}) # Constraint for sequence of deferrable # WARNING: This is experimental, formulation seems correct but feasibility problems. @@ -363,13 +372,13 @@ def create_matrix(input_list, n): # Two special constraints just for a self-consumption cost function if self.costfun == 'self-consumption': if type_self_conso == 'maxmin': # maxmin linear problem - constraints.update({"constraint_selfcons_PV_{}".format(i) : + constraints.update({"constraint_selfcons_PV1_{}".format(i) : plp.LpConstraint( e = SC[i] - P_PV[i], sense = plp.LpConstraintLE, rhs = 0) for i in set_I}) - constraints.update({"constraint_selfcons_PV_{}".format(i) : + constraints.update({"constraint_selfcons_PV2_{}".format(i) : plp.LpConstraint( e = SC[i] - P_load[i] - P_def_sum[i], sense = plp.LpConstraintLE, @@ -439,41 +448,27 @@ def create_matrix(input_list, n): sense=plp.LpConstraintLE, rhs=0) for i in set_I}) - # Treat the number of starts for a deferrable load - if self.optim_conf['set_def_constant'][k]: - constraints.update({"constraint_pdef{}_start1_{}".format(k, i) : - plp.LpConstraint( - e=P_deferrable[k][i] - P_def_bin2[k][i]*M, - sense=plp.LpConstraintLE, - rhs=0) - for i in set_I}) - constraints.update({"constraint_pdef{}_start2_{}".format(k, i): - plp.LpConstraint( - e=P_def_start[k][i] - P_def_bin2[k][i] + (P_def_bin2[k][i-1] if i-1 >= 0 else 0), - sense=plp.LpConstraintGE, - rhs=0) - for i in set_I}) - constraints.update({"constraint_pdef{}_start3".format(k) : - plp.LpConstraint( - e = plp.lpSum(P_def_start[k][i] for i in set_I), - sense = plp.LpConstraintEQ, - rhs = 1) - }) - # Treat deferrable load as a semi-continuous variable - if self.optim_conf['treat_def_as_semi_cont'][k]: - constraints.update({"constraint_pdef{}_semicont1_{}".format(k, i) : - plp.LpConstraint( - e=P_deferrable[k][i] - self.optim_conf['P_deferrable_nom'][k]*P_def_bin1[k][i], - sense=plp.LpConstraintGE, - rhs=0) - for i in set_I}) - constraints.update({"constraint_pdef{}_semicont2_{}".format(k, i) : - plp.LpConstraint( - e=P_deferrable[k][i] - self.optim_conf['P_deferrable_nom'][k]*P_def_bin1[k][i], - sense=plp.LpConstraintLE, - rhs=0) - for i in set_I}) - # Treat the number of starts for a deferrable load + # Treat the number of starts for a deferrable load (old method, kept here just in case) + # if self.optim_conf['set_def_constant'][k]: + # constraints.update({"constraint_pdef{}_start1_{}".format(k, i) : + # plp.LpConstraint( + # e=P_deferrable[k][i] - P_def_bin2[k][i]*M, + # sense=plp.LpConstraintLE, + # rhs=0) + # for i in set_I}) + # constraints.update({"constraint_pdef{}_start2_{}".format(k, i): + # plp.LpConstraint( + # e=P_def_start[k][i] - P_def_bin2[k][i] + (P_def_bin2[k][i-1] if i-1 >= 0 else 0), + # sense=plp.LpConstraintGE, + # rhs=0) + # for i in set_I}) + # constraints.update({"constraint_pdef{}_start3".format(k) : + # plp.LpConstraint( + # e = plp.lpSum(P_def_start[k][i] for i in set_I), + # sense = plp.LpConstraintEQ, + # rhs = 1) + # }) + # Treat the number of starts for a deferrable load (new method considering current state) current_state = 0 if ("def_current_state" in self.optim_conf and len(self.optim_conf["def_current_state"]) > k): current_state = 1 if self.optim_conf["def_current_state"][k] else 0 @@ -644,7 +639,8 @@ def create_matrix(input_list, n): opt_tp["SOC_opt"] = SOC_opt if self.plant_conf['inverter_is_hybrid']: opt_tp["P_hybrid_inverter"] = [P_hybrid_inverter[i].varValue for i in set_I] - opt_tp["P_PV_curtailment"] = [P_PV_curtailment[i].varValue for i in set_I] + if self.plant_conf['compute_curtailment']: + opt_tp["P_PV_curtailment"] = [P_PV_curtailment[i].varValue for i in set_I] opt_tp.index = data_opt.index # Lets compute the optimal cost function diff --git a/src/emhass/utils.py b/src/emhass/utils.py index 18ad4836..c899b090 100644 --- a/src/emhass/utils.py +++ b/src/emhass/utils.py @@ -11,6 +11,7 @@ import pandas as pd import yaml import pytz +import ast import plotly.express as px @@ -166,6 +167,11 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic "unit_of_measurement": "W", "friendly_name": "PV Power Curtailment", }, + "custom_hybrid_inverter_id": { + "entity_id": "sensor.p_hybrid_inverter", + "unit_of_measurement": "W", + "friendly_name": "PV Hybrid Inverter", + }, "custom_batt_forecast_id": { "entity_id": "sensor.p_batt_forecast", "unit_of_measurement": "W", @@ -247,7 +253,6 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic if "target" in runtimeparams: target = runtimeparams["target"] params["passed_data"]["target"] = target - # Treating special data passed for MPC control case if set_type == "naive-mpc-optim": if "prediction_horizon" not in runtimeparams.keys(): @@ -369,14 +374,12 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic if "perform_backtest" not in runtimeparams.keys(): perform_backtest = False else: - perform_backtest = eval(str(runtimeparams["perform_backtest"]).capitalize()) + perform_backtest = ast.literal_eval(str(runtimeparams["perform_backtest"]).capitalize()) params["passed_data"]["perform_backtest"] = perform_backtest if "model_predict_publish" not in runtimeparams.keys(): model_predict_publish = False else: - model_predict_publish = eval( - str(runtimeparams["model_predict_publish"]).capitalize() - ) + model_predict_publish = ast.literal_eval(str(runtimeparams["model_predict_publish"]).capitalize()) params["passed_data"]["model_predict_publish"] = model_predict_publish if "model_predict_entity_id" not in runtimeparams.keys(): model_predict_entity_id = "sensor.p_load_forecast_custom_model" @@ -433,12 +436,16 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic optim_conf["def_current_state"] = [bool(s) for s in runtimeparams["def_current_state"]] if "treat_def_as_semi_cont" in runtimeparams.keys(): optim_conf["treat_def_as_semi_cont"] = [ - eval(str(k).capitalize()) + ast.literal_eval(str(k).capitalize()) for k in runtimeparams["treat_def_as_semi_cont"] ] if "set_def_constant" in runtimeparams.keys(): optim_conf["set_def_constant"] = [ - eval(str(k).capitalize()) for k in runtimeparams["set_def_constant"] + ast.literal_eval(str(k).capitalize()) for k in runtimeparams["set_def_constant"] + ] + if "def_start_penalty" in runtimeparams.keys(): + optim_conf["def_start_penalty"] = [ + ast.literal_eval(str(k).capitalize()) for k in runtimeparams["def_start_penalty"] ] if "solcast_api_key" in runtimeparams.keys(): retrieve_hass_conf["solcast_api_key"] = runtimeparams["solcast_api_key"] @@ -464,8 +471,16 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic if 'continual_publish' in runtimeparams.keys(): retrieve_hass_conf['continual_publish'] = bool(runtimeparams['continual_publish']) # Treat plant configuration parameters passed at runtime + if "SOCmin" in runtimeparams.keys(): + plant_conf["SOCmin"] = runtimeparams["SOCmin"] + if "SOCmax" in runtimeparams.keys(): + plant_conf["SOCmax"] = runtimeparams["SOCmax"] if "SOCtarget" in runtimeparams.keys(): plant_conf["SOCtarget"] = runtimeparams["SOCtarget"] + if "Pd_max" in runtimeparams.keys(): + plant_conf["Pd_max"] = runtimeparams["Pd_max"] + if "Pc_max" in runtimeparams.keys(): + plant_conf["Pc_max"] = runtimeparams["Pc_max"] # Treat custom entities id's and friendly names for variables if "custom_pv_forecast_id" in runtimeparams.keys(): params["passed_data"]["custom_pv_forecast_id"] = runtimeparams[ @@ -479,6 +494,10 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic params["passed_data"]["custom_pv_curtailment_id"] = runtimeparams[ "custom_pv_curtailment_id" ] + if "custom_hybrid_inverter_id" in runtimeparams.keys(): + params["passed_data"]["custom_hybrid_inverter_id"] = runtimeparams[ + "custom_hybrid_inverter_id" + ] if "custom_batt_forecast_id" in runtimeparams.keys(): params["passed_data"]["custom_batt_forecast_id"] = runtimeparams[ "custom_batt_forecast_id" @@ -766,9 +785,7 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int, params["retrieve_hass_conf"]["var_load"] = options.get("sensor_power_load_no_var_loads", params["retrieve_hass_conf"]["var_load"]) params["retrieve_hass_conf"]["load_negative"] = options.get("load_negative", params["retrieve_hass_conf"]["load_negative"]) params["retrieve_hass_conf"]["set_zero_min"] = options.get("set_zero_min", params["retrieve_hass_conf"]["set_zero_min"]) - params["retrieve_hass_conf"]["var_replace_zero"] = [ - options.get("sensor_power_photovoltaics", params["retrieve_hass_conf"]["var_replace_zero"]) - ] + params["retrieve_hass_conf"]["var_replace_zero"] = [options.get("sensor_power_photovoltaics", params["retrieve_hass_conf"]["var_replace_zero"])] params["retrieve_hass_conf"]["var_interp"] = [ options.get("sensor_power_photovoltaics", params["retrieve_hass_conf"]["var_PV"]), options.get("sensor_power_load_no_var_loads", params["retrieve_hass_conf"]["var_load"]) @@ -785,20 +802,11 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int, params["optim_conf"]["set_use_battery"] = options.get("set_use_battery", params["optim_conf"]["set_use_battery"]) params["optim_conf"]["num_def_loads"] = options.get("number_of_deferrable_loads", params["optim_conf"]["num_def_loads"]) if options.get("list_nominal_power_of_deferrable_loads", None) != None: - params["optim_conf"]["P_deferrable_nom"] = [ - i["nominal_power_of_deferrable_loads"] - for i in options.get("list_nominal_power_of_deferrable_loads") - ] + params["optim_conf"]["P_deferrable_nom"] = [i["nominal_power_of_deferrable_loads"] for i in options.get("list_nominal_power_of_deferrable_loads")] if options.get("list_operating_hours_of_each_deferrable_load", None) != None: - 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") - ] + 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")] if options.get("list_treat_deferrable_load_as_semi_cont", None) != None: - 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") - ] + 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")] params["optim_conf"]["weather_forecast_method"] = options.get("weather_forecast_method", params["optim_conf"]["weather_forecast_method"]) # Update optional param secrets if params["optim_conf"]["weather_forecast_method"] == "solcast": @@ -810,19 +818,14 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int, params["optim_conf"]["delta_forecast"] = options.get("delta_forecast_daily", params["optim_conf"]["delta_forecast"]) params["optim_conf"]["load_cost_forecast_method"] = options.get("load_cost_forecast_method", params["optim_conf"]["load_cost_forecast_method"]) if options.get("list_set_deferrable_load_single_constant", None) != None: - params["optim_conf"]["set_def_constant"] = [ - i["set_deferrable_load_single_constant"] - for i in options.get("list_set_deferrable_load_single_constant") - ] + params["optim_conf"]["set_def_constant"] = [i["set_deferrable_load_single_constant"] for i in options.get("list_set_deferrable_load_single_constant")] + + if options.get("list_set_deferrable_startup_penalty", None) != None: + params["optim_conf"]["def_start_penalty"] = [i["set_deferrable_startup_penalty"] for i in options.get("list_set_deferrable_startup_penalty")] + if (options.get("list_peak_hours_periods_start_hours", None) != None and options.get("list_peak_hours_periods_end_hours", None) != None): - start_hours_list = [ - i["peak_hours_periods_start_hours"] - for i in options["list_peak_hours_periods_start_hours"] - ] - end_hours_list = [ - i["peak_hours_periods_end_hours"] - for i in options["list_peak_hours_periods_end_hours"] - ] + start_hours_list = [i["peak_hours_periods_start_hours"] for i in options["list_peak_hours_periods_start_hours"]] + end_hours_list = [i["peak_hours_periods_end_hours"] for i in options["list_peak_hours_periods_end_hours"]] num_peak_hours = len(start_hours_list) 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)] params['optim_conf']['list_hp_periods'] = list_hp_periods_list @@ -860,6 +863,7 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int, if options.get('list_strings_per_inverter',None) != None: params['plant_conf']['strings_per_inverter'] = [i['strings_per_inverter'] for i in options.get('list_strings_per_inverter')] params["plant_conf"]["inverter_is_hybrid"] = options.get("inverter_is_hybrid", params["plant_conf"]["inverter_is_hybrid"]) + params["plant_conf"]["compute_curtailment"] = options.get("compute_curtailment", params["plant_conf"]["compute_curtailment"]) params['plant_conf']['Pd_max'] = options.get('battery_discharge_power_max', params['plant_conf']['Pd_max']) params['plant_conf']['Pc_max'] = options.get('battery_charge_power_max', params['plant_conf']['Pc_max']) params['plant_conf']['eta_disch'] = options.get('battery_discharge_efficiency', params['plant_conf']['eta_disch']) diff --git a/tests/test_retrieve_hass.py b/tests/test_retrieve_hass.py index 7b36899d..3526334c 100644 --- a/tests/test_retrieve_hass.py +++ b/tests/test_retrieve_hass.py @@ -122,7 +122,7 @@ def test_prepare_data(self): self.assertEqual(len(self.rh.df_final.columns), len(self.var_list)) self.assertEqual(self.rh.df_final.index.isin(self.days_list).sum(), len(self.days_list)) self.assertEqual(self.rh.df_final.index.freq, self.retrieve_hass_conf['freq']) - self.assertEqual(self.rh.df_final.index.tz, pytz.UTC) + self.assertEqual(self.rh.df_final.index.tz, datetime.timezone.utc) self.rh.prepare_data(self.retrieve_hass_conf['var_load'], load_negative = self.retrieve_hass_conf['load_negative'], set_zero_min = self.retrieve_hass_conf['set_zero_min'],