From 6eb460d95266d9f24af0f905d0ffc7af8f0910c5 Mon Sep 17 00:00:00 2001 From: davidusb-geek Date: Sun, 9 Jun 2024 22:20:09 +0200 Subject: [PATCH 01/11] Improved documentation added missing params from config file and runtimeparams, solved security issues with eval --- README.md | 10 ++++----- config_emhass.yaml | 3 +++ docs/config.md | 4 +++- src/emhass/utils.py | 53 +++++++++++++++++---------------------------- 4 files changed, 30 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index fa608b8d..c867aba4 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 ``` @@ -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. diff --git a/config_emhass.yaml b/config_emhass.yaml index bc2ac8d9..4607a28d 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 diff --git a/docs/config.md b/docs/config.md index c2fce240..127bfa18 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,8 @@ 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). If your system has a battery (set_use_battery=True), then you should define the following parameters: diff --git a/src/emhass/utils.py b/src/emhass/utils.py index 723daf23..69c03384 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 @@ -357,14 +358,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" @@ -421,12 +420,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"] @@ -754,9 +757,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"]) @@ -773,20 +774,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": @@ -798,19 +790,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 From dbf4e8e681472567a4e5f360d7622affc4594253 Mon Sep 17 00:00:00 2001 From: davidusb-geek Date: Sun, 9 Jun 2024 22:23:12 +0200 Subject: [PATCH 02/11] Added missing param to options.json --- options.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/options.json b/options.json index 7e6aa849..dd03943c 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", From d6db91ac4ef46173b27a3555113d6fa3cd6b92e5 Mon Sep 17 00:00:00 2001 From: davidusb-geek Date: Sun, 9 Jun 2024 22:20:09 +0200 Subject: [PATCH 03/11] Improved documentation added missing params from config file and runtimeparams, solved security issues with eval --- README.md | 10 ++++----- config_emhass.yaml | 3 +++ docs/config.md | 4 +++- src/emhass/utils.py | 53 +++++++++++++++++---------------------------- 4 files changed, 30 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index fa608b8d..c867aba4 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 ``` @@ -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. diff --git a/config_emhass.yaml b/config_emhass.yaml index bc2ac8d9..4607a28d 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 diff --git a/docs/config.md b/docs/config.md index c2fce240..127bfa18 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,8 @@ 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). If your system has a battery (set_use_battery=True), then you should define the following parameters: diff --git a/src/emhass/utils.py b/src/emhass/utils.py index 18ad4836..f08cca55 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 @@ -369,14 +370,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 +432,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"] @@ -766,9 +769,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 +786,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 +802,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 From a97b3bc90b95cb905548632854aff2ba1adadaaa Mon Sep 17 00:00:00 2001 From: davidusb-geek Date: Sun, 9 Jun 2024 22:23:12 +0200 Subject: [PATCH 04/11] Added missing param to options.json --- options.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/options.json b/options.json index 7e6aa849..dd03943c 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", From ff5675cc0d9dd8b4537a02ae0c29f21d85224959 Mon Sep 17 00:00:00 2001 From: davidusb-geek Date: Fri, 5 Jul 2024 00:27:14 +0200 Subject: [PATCH 05/11] Fixed a bunch of stuff and some improvements, see CHANGELOG --- CHANGELOG.md | 9 ++++ README.md | 12 ++++- config_emhass.yaml | 1 + docs/config.md | 1 + options.json | 1 + scripts/script_debug_optim.py | 8 +-- src/emhass/command_line.py | 42 +++++++++++----- src/emhass/forecast.py | 4 +- src/emhass/optimization.py | 93 +++++++++++++++++------------------ src/emhass/utils.py | 19 ++++++- 10 files changed, 120 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 105808f4..98af2ba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.10.2 - 2024-07-04 +### 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 c867aba4..4db9b81d 100644 --- a/README.md +++ b/README.md @@ -457,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. @@ -480,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). @@ -508,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 4607a28d..ca3a47d7 100644 --- a/config_emhass.yaml +++ b/config_emhass.yaml @@ -81,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/docs/config.md b/docs/config.md index 127bfa18..3ac0af52 100644 --- a/docs/config.md +++ b/docs/config.md @@ -109,6 +109,7 @@ Solution (2) would be to use SolCast and pass that data directly to emhass as a - `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). - `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 dd03943c..01f83b52 100644 --- a/options.json +++ b/options.json @@ -143,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/src/emhass/command_line.py b/src/emhass/command_line.py index 4138d4c0..f6b04994 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["cst"].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["cst"].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..de08c328 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'] / 'src' / 'emhass' / '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'] / 'src' / 'emhass' / '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..3bda6444 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 diff --git a/src/emhass/utils.py b/src/emhass/utils.py index f08cca55..c899b090 100644 --- a/src/emhass/utils.py +++ b/src/emhass/utils.py @@ -167,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", @@ -248,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(): @@ -467,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[ @@ -482,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" @@ -847,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']) From fdf41f62b31f6581340c9ec55ed84dfe9b11a394 Mon Sep 17 00:00:00 2001 From: davidusb-geek Date: Sun, 9 Jun 2024 22:20:09 +0200 Subject: [PATCH 06/11] Improved documentation added missing params from config file and runtimeparams, solved security issues with eval --- README.md | 10 ++++----- config_emhass.yaml | 3 +++ docs/config.md | 4 +++- src/emhass/utils.py | 53 +++++++++++++++++---------------------------- 4 files changed, 30 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index fa608b8d..c867aba4 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 ``` @@ -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. diff --git a/config_emhass.yaml b/config_emhass.yaml index bc2ac8d9..4607a28d 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 diff --git a/docs/config.md b/docs/config.md index c2fce240..127bfa18 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,8 @@ 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). If your system has a battery (set_use_battery=True), then you should define the following parameters: diff --git a/src/emhass/utils.py b/src/emhass/utils.py index 18ad4836..f08cca55 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 @@ -369,14 +370,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 +432,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"] @@ -766,9 +769,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 +786,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 +802,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 From 8e2a91c359f430ef35698fdf0e728690a85c6449 Mon Sep 17 00:00:00 2001 From: davidusb-geek Date: Sun, 9 Jun 2024 22:23:12 +0200 Subject: [PATCH 07/11] Added missing param to options.json --- options.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/options.json b/options.json index 7e6aa849..dd03943c 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", From 027757bab029f99f5380e68471402e786dbbe56b Mon Sep 17 00:00:00 2001 From: davidusb-geek Date: Fri, 5 Jul 2024 00:27:14 +0200 Subject: [PATCH 08/11] Fixed a bunch of stuff and some improvements, see CHANGELOG --- CHANGELOG.md | 9 ++++ README.md | 12 ++++- config_emhass.yaml | 1 + docs/config.md | 1 + options.json | 1 + scripts/script_debug_optim.py | 8 +-- src/emhass/command_line.py | 42 +++++++++++----- src/emhass/forecast.py | 4 +- src/emhass/optimization.py | 93 +++++++++++++++++------------------ src/emhass/utils.py | 19 ++++++- 10 files changed, 120 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 105808f4..98af2ba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.10.2 - 2024-07-04 +### 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 c867aba4..4db9b81d 100644 --- a/README.md +++ b/README.md @@ -457,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. @@ -480,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). @@ -508,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 4607a28d..ca3a47d7 100644 --- a/config_emhass.yaml +++ b/config_emhass.yaml @@ -81,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/docs/config.md b/docs/config.md index 127bfa18..3ac0af52 100644 --- a/docs/config.md +++ b/docs/config.md @@ -109,6 +109,7 @@ Solution (2) would be to use SolCast and pass that data directly to emhass as a - `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). - `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 dd03943c..01f83b52 100644 --- a/options.json +++ b/options.json @@ -143,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/src/emhass/command_line.py b/src/emhass/command_line.py index 4138d4c0..f6b04994 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["cst"].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["cst"].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..de08c328 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'] / 'src' / 'emhass' / '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'] / 'src' / 'emhass' / '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..3bda6444 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 diff --git a/src/emhass/utils.py b/src/emhass/utils.py index f08cca55..c899b090 100644 --- a/src/emhass/utils.py +++ b/src/emhass/utils.py @@ -167,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", @@ -248,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(): @@ -467,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[ @@ -482,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" @@ -847,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']) From 46a39b64df5fab35b0c93bcba28419e84c345729 Mon Sep 17 00:00:00 2001 From: davidusb-geek Date: Fri, 5 Jul 2024 23:55:48 +0200 Subject: [PATCH 09/11] Fixed root path on forecast.py --- src/emhass/forecast.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emhass/forecast.py b/src/emhass/forecast.py index de08c328..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'] / 'src' / 'emhass' / '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'] / 'src' / 'emhass' / '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) From 3fee7a9a090b55b571af9934d50744bbe4e42de5 Mon Sep 17 00:00:00 2001 From: davidusb-geek Date: Sat, 6 Jul 2024 00:23:13 +0200 Subject: [PATCH 10/11] Fixed final errorsin command line --- CHANGELOG.md | 2 +- data/test_df_final.pkl | Bin 10655 -> 3678 bytes docs/conf.py | 2 +- setup.py | 2 +- src/emhass/command_line.py | 4 ++-- src/emhass/optimization.py | 3 ++- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98af2ba3..b4c80028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.10.2 - 2024-07-04 +## 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 diff --git a/data/test_df_final.pkl b/data/test_df_final.pkl index 2f81e53902753d73037b1c0e3144f1185d815e88..5c07a96bf1fcaff2a344ead7a3a6de9d60cc7f7b 100644 GIT binary patch delta 2539 zcmZ|PcUV(d769-h6pMqmpac++qV$AB5)zU}6%|RWpdbbktU&=4 z5JeeBS78)c6cyB!q9UJ4Sw#mcgAHZSJ<-|y_K)3{@5}Gp`|itmdH0-mcx9ilnnbO1 zrFO9z*G`XM3fatbhj;;xmF~b3W^mGkOhLN5P$xc9NbnN*0!}*Hik>2&mr|bZ16eF|=KJwzw-e1$vGUo|>j2xMa`I46GHvR;|(* zRcrygo4A`jyi){!YPb*D2c&>;6nkO6uK?oJr{}W@Qoz)pkLWNE!pQb5&rVP9VM_+d zYRE+Zmd1Y;sC*TQK~wq5&Z$6d=!v;;QVL>e#hEKcY0wtZ%Y32O0)JOtshJ0gjbmEB zHH+Y~mJO|SGzDxI2F4116@Yuu6StyRAw2qO=EI2;!Xm5IpV+fffS&qUBh*#|<0FPy zYQ+Rd%;lXAlL#pI)=O{66v5WRc^*ymVgmT=Ov8`PX)xLq#F9cGFkUvbitiVJe{lW> z-SSg~QUboTySm*gCO$OOC3ttIiHS&%6+P>m3`r$Q-@sEtgnfmdX` z>qGJy*goIbReVegcHIMEj(P&%sm+`_)WL`8tc@IcjsPC<9V~*L@Sq?u=8Em6WO$?F z9F{dl09RP^JlC#C2CHnR<##7|kUJ^q-1SfV?|AwYd@>rIw^7W|aFXLtBG0Z}{6 zW=Ni(U`l}td4*jO#O!YQP%}~hhje$T=4538O-tm)owE+~ zORB;YN7BKl<&KZZ*>t#Q(uRoWwD7QKWbr~6b@!bxRwm>2KTv( zpS3I(!El4Q^;PE-DDa~+t!t3Ny{o@gc3xLNl;D)^xAe8Jx~=SafLSUGEqvLLktTw- zZ}3=(wbg&|K=%C%xSnc!JvC=7C~MojF&h*w^wi!|4ib=8N`C*u{0z9)XOx&@gS!i=n;&@Y zfoOTJnJhOOOy0I{yxWrq|6HYizLSW7=md+KWrzIX#~VRbNwtxcP@}{h_aa_7M*BnD z)j7qVhC(2`T&{I_MlASs?@20uz=Sb9MP9F3DwvXe)?3`(1QrUti#DoKcy~?1ihh3& zT+2J88MvhkuA0@S<*Qf1l-+8RE_5A*s@T2M<670wZ@0pAouUTJ4?Ne6)2)Sbkw*N} zlv-Go9AzV4SStpnz$#6@v^v zp(EK+He~2UrpYYmX5^_dU;1f-#f8)*%nVKjFNL#A$mV3@3wNX3hHER+GHQ&|VE=VW zUZ|m5v0fzP-~#Td!pX`Zn1u8oh-5ous-9}2J8qybdAY~ztZbl_O2-xJh{a%%V~Dv7&R_zdl6>~s7bHPipp zHH;GJI^wLC7&PCpHhhiJ?BL=XG$DUg{0LhAl%R~Fq(wW9y+cFFDwcjg%Re`~{D_W( zcNLAHaOTC_FQ`;o<=!|tkZhDafjV7xG5Ur|%mbT9{dfanHj{#yJ<3s4MF}@?X|);# z&GNc{Nhs})*4G+nLQtPn6Rkh~(M1a-sq6Gm(GZsPdL6X9$$Q2WbcDXDc`6EPiNxus zv^!s24;^rhQL_rlJQI`rTu@%eS-!+oB`=kN>bk!Q-Rc0hR94c5^}p-djDK zi#o0F&UHa0b&)fFMmr`GSLdN-Yx0xaP%+U`uDJk%W|M}xg(z)9XNV`7&@=qb3$1sd z>;;r0r7u{FhP-lq?1PpsWk`I{k)w&`{wR=ht}R8Sv2tDjI?!-ZD-d-VYG$jwKeS(8Od|5EJi>6I`H5G5I7)-a&5>w=LS(rbtsl#~5rvY%%lR>A z$S)VProq90_V1G&y#Yfz_$0fR}XWPajS9@-(>V9iI( zMr7@R{wcTzfhSXhIMG~ll}7aI%Uo@IDBi)c%{KANmvH^QHzPNI8x(LMCY&0qY#wgt z>`mXBG@Kj3jpQc$uZC0kKMiNe@-g^Nzx{jD|H>@Skh7HO2ySYj$`a-}WoMIjY{!g+ p>hX!pL@o!jD#J8Rx=7B@OwY`dv$)>DnaU-xc%cW^lanWR_!q5PEDHbt literal 10655 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 G_kREf)2prk 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/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 f6b04994..ee88a4fe 100644 --- a/src/emhass/command_line.py +++ b/src/emhass/command_line.py @@ -849,7 +849,7 @@ def publish_data(input_data_dict: dict, logger: logging.Logger, ) cols_published = ["P_PV", "P_Load"] # Publish PV curtailment - if input_data_dict["cst"].plant_conf['compute_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"], @@ -864,7 +864,7 @@ def publish_data(input_data_dict: dict, logger: logging.Logger, ) cols_published = cols_published + ["P_PV_curtailment"] # Publish P_hybrid_inverter - if input_data_dict["cst"].plant_conf['inverter_is_hybrid']: + 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"], diff --git a/src/emhass/optimization.py b/src/emhass/optimization.py index 3bda6444..67b21f74 100644 --- a/src/emhass/optimization.py +++ b/src/emhass/optimization.py @@ -639,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 From bbb8d5733497d7534c7cf4f50ab700b8fc2a7a06 Mon Sep 17 00:00:00 2001 From: davidusb-geek Date: Sat, 6 Jul 2024 00:38:20 +0200 Subject: [PATCH 11/11] Fixed retrieve hass test UTC --- tests/test_retrieve_hass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'],