diff --git a/CHANGELOG.md b/CHANGELOG.md index 97cf25b0..733006c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.10.0 - Unreleased +### Improvement +- Added support for hybrid inverters +- Implemented a new `continual_publish` service that avoid the need of setting a special automation for data publish. Thanks to @GeoDerp +- Implement a deferrable load start penalty functionality. Thanks to @werdnum + - This feature also implement a `def_current_state` that can be passed at runtime to let the optimization consider that a deferrable load is currently scheduled or under operation when launching the optimization task +### Fix +- Fixed forecast methods to treat delta_forecast higher than 1 day +- Fixed solar.forecast wrong interpolation of nan values + ## 0.9.1 - 2024-05-13 ### Fix - Fix patch for issue with paths to modules and inverters database diff --git a/config_emhass.yaml b/config_emhass.yaml index 6d306e51..bc2ac8d9 100644 --- a/config_emhass.yaml +++ b/config_emhass.yaml @@ -77,6 +77,7 @@ plant_conf: - 16 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 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/options.json b/options.json index 0b154ed1..7e6aa849 100644 --- a/options.json +++ b/options.json @@ -134,6 +134,7 @@ "strings_per_inverter": 1 } ], + "inverter_is_hybrid": 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 d5f4dcc1..57383c5b 100644 --- a/scripts/script_debug_optim.py +++ b/scripts/script_debug_optim.py @@ -73,14 +73,14 @@ 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({'P_deferrable_nom': [[500.0, 100.0, 100.0, 500.0], 750.0]}) + # optim_conf.update({'P_deferrable_nom': [[500.0, 100.0, 100.0, 500.0], 750.0]}) optim_conf.update({'set_use_battery': False}) optim_conf.update({'set_nocharge_from_grid': False}) optim_conf.update({'set_battery_dynamic': True}) optim_conf.update({'set_nodischarge_to_grid': True}) - optim_conf.update({'inverter_is_hybrid': False}) + plant_conf.update({'inverter_is_hybrid': True}) df_input_data.loc[df_input_data.index[25:30],'unit_prod_price'] = -0.07 df_input_data['P_PV_forecast'] = df_input_data['P_PV_forecast']*2 @@ -102,7 +102,7 @@ fig_inputs_dah.show() vars_to_plot = ['P_deferrable0', 'P_deferrable1','P_grid', 'P_PV', 'P_PV_curtailment'] - if optim_conf['inverter_is_hybrid']: + if plant_conf['inverter_is_hybrid']: vars_to_plot = vars_to_plot + ['P_hybrid_inverter'] if optim_conf['set_use_battery']: vars_to_plot = vars_to_plot + ['P_batt'] diff --git a/src/emhass/optimization.py b/src/emhass/optimization.py index 03fbf0f2..1ffdd5a6 100644 --- a/src/emhass/optimization.py +++ b/src/emhass/optimization.py @@ -212,7 +212,7 @@ def perform_optimization(self, data_opt: pd.DataFrame, P_PV: np.array, P_load: n if self.costfun == 'self-consumption': SC = {(i):plp.LpVariable(cat='Continuous', name="SC_{}".format(i)) for i in set_I} - if self.optim_conf['inverter_is_hybrid']: + if self.plant_conf['inverter_is_hybrid']: P_hybrid_inverter = {(i):plp.LpVariable(cat='Continuous', name="P_hybrid_inverter{}".format(i)) for i in set_I} P_PV_curtailment = {(i):plp.LpVariable(cat='Continuous', lowBound=0, @@ -264,7 +264,7 @@ def perform_optimization(self, data_opt: pd.DataFrame, P_PV: np.array, P_load: n ## Setting constraints # The main constraint: power balance - if self.optim_conf['inverter_is_hybrid']: + if self.plant_conf['inverter_is_hybrid']: constraints = {"constraint_main1_{}".format(i) : plp.LpConstraint( e = P_hybrid_inverter[i] - P_def_sum[i] - P_load[i] + P_grid_neg[i] + P_grid_pos[i] , @@ -298,7 +298,7 @@ def perform_optimization(self, data_opt: pd.DataFrame, P_PV: np.array, P_load: n P_nom_inverter = inverter.Paco else: P_nom_inverter = self.plant_conf['inverter_model'] - if self.optim_conf['inverter_is_hybrid']: + if self.plant_conf['inverter_is_hybrid']: constraints.update({"constraint_hybrid_inverter1_{}".format(i) : plp.LpConstraint( e = P_PV[i] - P_PV_curtailment[i] + P_sto_pos[i] + P_sto_neg[i] - P_nom_inverter, @@ -642,7 +642,7 @@ def create_matrix(input_list, n): SOC_opt.append(SOCinit - SOC_opt_delta[i]) SOCinit = SOC_opt[i] opt_tp["SOC_opt"] = SOC_opt - if self.optim_conf['inverter_is_hybrid']: + 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] opt_tp.index = data_opt.index diff --git a/src/emhass/utils.py b/src/emhass/utils.py index edfcf04c..3dcedbf7 100644 --- a/src/emhass/utils.py +++ b/src/emhass/utils.py @@ -739,63 +739,30 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int, """ if addon == 1: # Updating variables in retrieve_hass_conf - params["retrieve_hass_conf"]["freq"] = options.get( - "optimization_time_step", params["retrieve_hass_conf"]["freq"] - ) - params["retrieve_hass_conf"]["days_to_retrieve"] = options.get( - "historic_days_to_retrieve", - params["retrieve_hass_conf"]["days_to_retrieve"], - ) - params["retrieve_hass_conf"]["var_PV"] = options.get( - "sensor_power_photovoltaics", params["retrieve_hass_conf"]["var_PV"] - ) - 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"]["freq"] = options.get("optimization_time_step", params["retrieve_hass_conf"]["freq"]) + params["retrieve_hass_conf"]["days_to_retrieve"] = options.get("historic_days_to_retrieve", params["retrieve_hass_conf"]["days_to_retrieve"]) + params["retrieve_hass_conf"]["var_PV"] = options.get("sensor_power_photovoltaics", params["retrieve_hass_conf"]["var_PV"]) + 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"], - ) + 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"], - ), + 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"]) ] - params["retrieve_hass_conf"]["method_ts_round"] = options.get( - "method_ts_round", params["retrieve_hass_conf"]["method_ts_round"] - ) - params["retrieve_hass_conf"]["continual_publish"] = options.get( - "continual_publish", params["retrieve_hass_conf"]["continual_publish"] - ) + params["retrieve_hass_conf"]["method_ts_round"] = options.get("method_ts_round", params["retrieve_hass_conf"]["method_ts_round"]) + params["retrieve_hass_conf"]["continual_publish"] = options.get("continual_publish", params["retrieve_hass_conf"]["continual_publish"]) # Update params Secrets if specified params["params_secrets"] = params_secrets - params["params_secrets"]["time_zone"] = options.get( - "time_zone", params_secrets["time_zone"] - ) + params["params_secrets"]["time_zone"] = options.get("time_zone", params_secrets["time_zone"]) params["params_secrets"]["lat"] = options.get("Latitude", params_secrets["lat"]) - params["params_secrets"]["lon"] = options.get( - "Longitude", params_secrets["lon"] - ) + params["params_secrets"]["lon"] = options.get("Longitude", params_secrets["lon"]) params["params_secrets"]["alt"] = options.get("Altitude", params_secrets["alt"]) # Updating variables in optim_conf - 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"] - ) + 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"] @@ -811,43 +778,22 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int, 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"] - ) + 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": - params["params_secrets"]["solcast_api_key"] = options.get( - "optional_solcast_api_key", - params_secrets.get("solcast_api_key", "123456"), - ) - params["params_secrets"]["solcast_rooftop_id"] = options.get( - "optional_solcast_rooftop_id", - params_secrets.get("solcast_rooftop_id", "123456"), - ) + params["params_secrets"]["solcast_api_key"] = options.get("optional_solcast_api_key", params_secrets.get("solcast_api_key", "123456")) + params["params_secrets"]["solcast_rooftop_id"] = options.get("optional_solcast_rooftop_id", params_secrets.get("solcast_rooftop_id", "123456")) elif params["optim_conf"]["weather_forecast_method"] == "solar.forecast": - params["params_secrets"]["solar_forecast_kwp"] = options.get( - "optional_solar_forecast_kwp", - params_secrets.get("solar_forecast_kwp", 5), - ) - params["optim_conf"]["load_forecast_method"] = options.get( - "load_forecast_method", params["optim_conf"]["load_forecast_method"] - ) - 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"], - ) + params["params_secrets"]["solar_forecast_kwp"] = options.get("optional_solar_forecast_kwp", params_secrets.get("solar_forecast_kwp", 5)) + params["optim_conf"]["load_forecast_method"] = options.get("load_forecast_method", params["optim_conf"]["load_forecast_method"]) + 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") ] - if ( - options.get("list_peak_hours_periods_start_hours", None) != None - and options.get("list_peak_hours_periods_end_hours", None) != None - ): + 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"] @@ -859,27 +805,27 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int, 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 - params['optim_conf']['load_cost_hp'] = options.get('load_peak_hours_cost',params['optim_conf']['load_cost_hp']) + params['optim_conf']['load_cost_hp'] = options.get('load_peak_hours_cost', params['optim_conf']['load_cost_hp']) params['optim_conf']['load_cost_hc'] = options.get('load_offpeak_hours_cost', params['optim_conf']['load_cost_hc']) params['optim_conf']['prod_price_forecast_method'] = options.get('production_price_forecast_method', params['optim_conf']['prod_price_forecast_method']) - params['optim_conf']['prod_sell_price'] = options.get('photovoltaic_production_sell_price',params['optim_conf']['prod_sell_price']) - params['optim_conf']['set_total_pv_sell'] = options.get('set_total_pv_sell',params['optim_conf']['set_total_pv_sell']) - params['optim_conf']['lp_solver'] = options.get('lp_solver',params['optim_conf']['lp_solver']) - params['optim_conf']['lp_solver_path'] = options.get('lp_solver_path',params['optim_conf']['lp_solver_path']) - params['optim_conf']['set_nocharge_from_grid'] = options.get('set_nocharge_from_grid',params['optim_conf']['set_nocharge_from_grid']) - params['optim_conf']['set_nodischarge_to_grid'] = options.get('set_nodischarge_to_grid',params['optim_conf']['set_nodischarge_to_grid']) - params['optim_conf']['set_battery_dynamic'] = options.get('set_battery_dynamic',params['optim_conf']['set_battery_dynamic']) - params['optim_conf']['battery_dynamic_max'] = options.get('battery_dynamic_max',params['optim_conf']['battery_dynamic_max']) - params['optim_conf']['battery_dynamic_min'] = options.get('battery_dynamic_min',params['optim_conf']['battery_dynamic_min']) - params['optim_conf']['weight_battery_discharge'] = options.get('weight_battery_discharge',params['optim_conf']['weight_battery_discharge']) - params['optim_conf']['weight_battery_charge'] = options.get('weight_battery_charge',params['optim_conf']['weight_battery_charge']) + params['optim_conf']['prod_sell_price'] = options.get('photovoltaic_production_sell_price', params['optim_conf']['prod_sell_price']) + params['optim_conf']['set_total_pv_sell'] = options.get('set_total_pv_sell', params['optim_conf']['set_total_pv_sell']) + params['optim_conf']['lp_solver'] = options.get('lp_solver', params['optim_conf']['lp_solver']) + params['optim_conf']['lp_solver_path'] = options.get('lp_solver_path', params['optim_conf']['lp_solver_path']) + params['optim_conf']['set_nocharge_from_grid'] = options.get('set_nocharge_from_grid', params['optim_conf']['set_nocharge_from_grid']) + params['optim_conf']['set_nodischarge_to_grid'] = options.get('set_nodischarge_to_grid', params['optim_conf']['set_nodischarge_to_grid']) + params['optim_conf']['set_battery_dynamic'] = options.get('set_battery_dynamic', params['optim_conf']['set_battery_dynamic']) + params['optim_conf']['battery_dynamic_max'] = options.get('battery_dynamic_max', params['optim_conf']['battery_dynamic_max']) + params['optim_conf']['battery_dynamic_min'] = options.get('battery_dynamic_min', params['optim_conf']['battery_dynamic_min']) + params['optim_conf']['weight_battery_discharge'] = options.get('weight_battery_discharge', params['optim_conf']['weight_battery_discharge']) + params['optim_conf']['weight_battery_charge'] = options.get('weight_battery_charge', params['optim_conf']['weight_battery_charge']) if options.get('list_start_timesteps_of_each_deferrable_load',None) != None: params['optim_conf']['def_start_timestep'] = [i['start_timesteps_of_each_deferrable_load'] for i in options.get('list_start_timesteps_of_each_deferrable_load')] if options.get('list_end_timesteps_of_each_deferrable_load',None) != None: params['optim_conf']['def_end_timestep'] = [i['end_timesteps_of_each_deferrable_load'] for i in options.get('list_end_timesteps_of_each_deferrable_load')] # Updating variables in plant_conf - params['plant_conf']['P_from_grid_max'] = options.get('maximum_power_from_grid',params['plant_conf']['P_from_grid_max']) - params['plant_conf']['P_to_grid_max'] = options.get('maximum_power_to_grid',params['plant_conf']['P_to_grid_max']) + params['plant_conf']['P_from_grid_max'] = options.get('maximum_power_from_grid', params['plant_conf']['P_from_grid_max']) + params['plant_conf']['P_to_grid_max'] = options.get('maximum_power_to_grid', params['plant_conf']['P_to_grid_max']) if options.get('list_pv_module_model',None) != None: params['plant_conf']['module_model'] = [i['pv_module_model'] for i in options.get('list_pv_module_model')] if options.get('list_pv_inverter_model',None) != None: @@ -892,14 +838,15 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int, params['plant_conf']['modules_per_string'] = [i['modules_per_string'] for i in options.get('list_modules_per_string')] 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']['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']) - params['plant_conf']['eta_ch'] = options.get('battery_charge_efficiency',params['plant_conf']['eta_ch']) - params['plant_conf']['Enom'] = options.get('battery_nominal_energy_capacity',params['plant_conf']['Enom']) - params['plant_conf']['SOCmin'] = options.get('battery_minimum_state_of_charge',params['plant_conf']['SOCmin']) - params['plant_conf']['SOCmax'] = options.get('battery_maximum_state_of_charge',params['plant_conf']['SOCmax']) - params['plant_conf']['SOCtarget'] = options.get('battery_target_state_of_charge',params['plant_conf']['SOCtarget']) + params["plant_conf"]["inverter_is_hybrid"] = options.get("inverter_is_hybrid", params["plant_conf"]["inverter_is_hybrid"]) + 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']) + params['plant_conf']['eta_ch'] = options.get('battery_charge_efficiency', params['plant_conf']['eta_ch']) + params['plant_conf']['Enom'] = options.get('battery_nominal_energy_capacity', params['plant_conf']['Enom']) + params['plant_conf']['SOCmin'] = options.get('battery_minimum_state_of_charge', params['plant_conf']['SOCmin']) + params['plant_conf']['SOCmax'] = options.get('battery_maximum_state_of_charge', params['plant_conf']['SOCmax']) + params['plant_conf']['SOCtarget'] = options.get('battery_target_state_of_charge', params['plant_conf']['SOCtarget']) # Check parameter lists have the same amounts as deferrable loads # If not, set defaults it fill in gaps if params['optim_conf']['num_def_loads'] is not len(params['optim_conf']['def_start_timestep']): @@ -929,9 +876,7 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int, # days_to_retrieve should be no less then 2 if params["retrieve_hass_conf"]["days_to_retrieve"] < 2: params["retrieve_hass_conf"]["days_to_retrieve"] = 2 - logger.warning( - "days_to_retrieve should not be lower then 2, setting days_to_retrieve to 2. Make sure your sensors also have at least 2 days of history" - ) + logger.warning("days_to_retrieve should not be lower then 2, setting days_to_retrieve to 2. Make sure your sensors also have at least 2 days of history") else: params["params_secrets"] = params_secrets # The params dict