From 51c0a298de9bb3e410081eb5e25d9030e2debb96 Mon Sep 17 00:00:00 2001 From: davidusb-geek Date: Fri, 12 Jul 2024 21:59:17 +0200 Subject: [PATCH] Added missing support for predicted temp publish, prepared new release --- CHANGELOG.md | 4 + docs/conf.py | 2 +- scripts/script_simple_thermal_model.py | 146 +++++++++++++++++++++++++ setup.py | 2 +- src/emhass/command_line.py | 44 ++++++-- src/emhass/retrieve_hass.py | 3 + src/emhass/utils.py | 13 +++ 7 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 scripts/script_simple_thermal_model.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d52ff828..4a20d0d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.10.5 - 2024-07-12 +### Improvement +- Added support for pubishing thermal load data, namely the predicted room temperature + ## 0.10.4 - 2024-07-10 ### Improvement - Added a new thermal modeling, see the new section in the documentation for help to implement this of model for thermal deferrable loads diff --git a/docs/conf.py b/docs/conf.py index 54b85493..bb2326ce 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.4' +release = '0.10.5' # -- General configuration --------------------------------------------------- diff --git a/scripts/script_simple_thermal_model.py b/scripts/script_simple_thermal_model.py new file mode 100644 index 00000000..2a94bac6 --- /dev/null +++ b/scripts/script_simple_thermal_model.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +import pickle +import random +import numpy as np +import pandas as pd +import pathlib +import plotly.express as px +import plotly.subplots as sp +import plotly.io as pio +pio.renderers.default = 'browser' +pd.options.plotting.backend = "plotly" + +from emhass.retrieve_hass import RetrieveHass +from emhass.optimization import Optimization +from emhass.forecast import Forecast +from emhass.utils import get_root, get_yaml_parse, get_days_list, get_logger + +# the root folder +root = str(get_root(__file__, num_parent=2)) +emhass_conf = {} +emhass_conf['config_path'] = pathlib.Path(root) / 'config_emhass.yaml' +emhass_conf['data_path'] = pathlib.Path(root) / 'data/' +emhass_conf['root_path'] = pathlib.Path(root) + +# create logger +logger, ch = get_logger(__name__, emhass_conf, save_to_file=False) + +if __name__ == '__main__': + get_data_from_file = True + params = None + show_figures = True + template = 'presentation' + + retrieve_hass_conf, optim_conf, plant_conf = get_yaml_parse(emhass_conf, use_secrets=False) + retrieve_hass_conf, optim_conf, plant_conf = \ + retrieve_hass_conf, optim_conf, plant_conf + rh = RetrieveHass(retrieve_hass_conf['hass_url'], retrieve_hass_conf['long_lived_token'], + retrieve_hass_conf['freq'], retrieve_hass_conf['time_zone'], + params, emhass_conf, logger) + if get_data_from_file: + with open(emhass_conf['data_path'] / 'test_df_final.pkl', 'rb') as inp: + rh.df_final, days_list, var_list = pickle.load(inp) + retrieve_hass_conf['var_load'] = str(var_list[0]) + retrieve_hass_conf['var_PV'] = str(var_list[1]) + retrieve_hass_conf['var_interp'] = [retrieve_hass_conf['var_PV'], retrieve_hass_conf['var_load']] + retrieve_hass_conf['var_replace_zero'] = [retrieve_hass_conf['var_PV']] + else: + days_list = get_days_list(retrieve_hass_conf['days_to_retrieve']) + var_list = [retrieve_hass_conf['var_load'], retrieve_hass_conf['var_PV']] + rh.get_data(days_list, var_list, + minimal_response=False, significant_changes_only=False) + rh.prepare_data(retrieve_hass_conf['var_load'], load_negative = retrieve_hass_conf['load_negative'], + set_zero_min = retrieve_hass_conf['set_zero_min'], + var_replace_zero = retrieve_hass_conf['var_replace_zero'], + var_interp = retrieve_hass_conf['var_interp']) + df_input_data = rh.df_final.copy() + + fcst = Forecast(retrieve_hass_conf, optim_conf, plant_conf, + params, emhass_conf, logger, get_data_from_file=get_data_from_file) + df_weather = fcst.get_weather_forecast(method='csv') + P_PV_forecast = fcst.get_power_from_weather(df_weather) + P_load_forecast = fcst.get_load_forecast(method=optim_conf['load_forecast_method']) + df_input_data = pd.concat([P_PV_forecast, P_load_forecast], axis=1) + df_input_data.columns = ['P_PV_forecast', 'P_load_forecast'] + + df_input_data = fcst.get_load_cost_forecast(df_input_data) + df_input_data = fcst.get_prod_price_forecast(df_input_data) + input_data_dict = {'retrieve_hass_conf': retrieve_hass_conf} + + # Set special debug cases + + # Solver configurations + 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 + + # Config for a single thermal model + optim_conf.update({'num_def_loads': 1}) + optim_conf.update({'P_deferrable_nom': [1000.0]}) + optim_conf.update({'def_total_hours': [0]}) + optim_conf.update({'def_start_timestep': [0]}) + optim_conf.update({'def_end_timestep': [0]}) + optim_conf.update({'treat_def_as_semi_cont': [False]}) + optim_conf.update({'set_def_constant': [False]}) + optim_conf.update({'def_start_penalty': [0.0]}) + + # Thermal modeling + df_input_data['outdoor_temperature_forecast'] = [random.normalvariate(10.0, 3.0) for _ in range(48)] + + runtimeparams = { + 'def_load_config': [ + {'thermal_config': { + 'heating_rate': 5.0, + 'cooling_constant': 0.1, + 'overshoot_temperature': 24.0, + 'start_temperature': 20, + 'desired_temperatures': [21]*48, + } + } + ] + } + if 'def_load_config' in runtimeparams: + optim_conf["def_load_config"] = runtimeparams['def_load_config'] + + costfun = 'profit' + opt = Optimization(retrieve_hass_conf, optim_conf, plant_conf, + fcst.var_load_cost, fcst.var_prod_price, + costfun, emhass_conf, logger) + P_PV_forecast.loc[:] = 0 + P_load_forecast.loc[:] = 0 + + df_input_data.loc[df_input_data.index[25:30],'unit_load_cost'] = 2.0 # A price peak + unit_load_cost = df_input_data[opt.var_load_cost].values # €/kWh + unit_prod_price = df_input_data[opt.var_prod_price].values # €/kWh + + + opt_res_dayahead = opt.perform_optimization(df_input_data, P_PV_forecast.values.ravel(), + P_load_forecast.values.ravel(), + unit_load_cost, unit_prod_price, debug=True) + + # Let's plot the input data + fig_inputs_dah = df_input_data.plot() + fig_inputs_dah.layout.template = template + fig_inputs_dah.update_yaxes(title_text = "Powers (W) and Costs(EUR)") + fig_inputs_dah.update_xaxes(title_text = "Time") + if show_figures: + fig_inputs_dah.show() + + vars_to_plot = ['P_deferrable0', 'unit_load_cost', 'predicted_temp_heater0', 'target_temp_heater0', 'P_def_start_0'] + 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' + fig_res_dah.layout.template = template + fig_res_dah.update_yaxes(title_text = "Powers (W)") + fig_res_dah.update_xaxes(title_text = "Time") + if show_figures: + fig_res_dah.show() + + print("System with: PV, two deferrable loads, dayahead optimization, profit >> total cost function sum: "+\ + str(opt_res_dayahead['cost_profit'].sum())+", Status: "+opt_res_dayahead['optim_status'].unique().item()) + + print(opt_res_dayahead[vars_to_plot]) + \ No newline at end of file diff --git a/setup.py b/setup.py index 6e32a064..6265f7d3 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name='emhass', # Required - version='0.10.4', # Required + version='0.10.5', # 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 ae7b4700..a714853e 100644 --- a/src/emhass/command_line.py +++ b/src/emhass/command_line.py @@ -896,6 +896,25 @@ def publish_data(input_data_dict: dict, logger: logging.Logger, dont_post=dont_post ) cols_published = cols_published + ["P_deferrable{}".format(k)] + # Publish thermal model data (predicted temperature) + custom_predicted_temperature_id = params["passed_data"][ + "custom_predicted_temperature_id" + ] + for k in range(input_data_dict["opt"].optim_conf["num_def_loads"]): + if "def_load_config" in input_data_dict["opt"].optim_conf.keys(): + if "thermal_config" in input_data_dict["opt"].optim_conf["def_load_config"][k]: + input_data_dict["rh"].post_data( + opt_res_latest["P_deferrable{}".format(k)], + idx_closest, + custom_predicted_temperature_id[k]["entity_id"], + custom_predicted_temperature_id[k]["unit_of_measurement"], + custom_predicted_temperature_id[k]["friendly_name"], + type_var="temperature", + publish_prefix=publish_prefix, + save_entities=entity_save, + dont_post=dont_post + ) + cols_published = cols_published + ["predicted_temp_heater{}".format(k)] # Publish battery power if input_data_dict["opt"].optim_conf["set_use_battery"]: if "P_batt" not in opt_res_latest.columns: @@ -967,18 +986,19 @@ def publish_data(input_data_dict: dict, logger: logging.Logger, logger.warning( "no optim_status in opt_res_latest, run an optimization task first", ) - input_data_dict["rh"].post_data( - opt_res_latest["optim_status"], - idx_closest, - custom_cost_fun_id["entity_id"], - custom_cost_fun_id["unit_of_measurement"], - custom_cost_fun_id["friendly_name"], - type_var="optim_status", - publish_prefix=publish_prefix, - save_entities=entity_save, - dont_post=dont_post - ) - cols_published = cols_published + ["optim_status"] + else: + input_data_dict["rh"].post_data( + opt_res_latest["optim_status"], + idx_closest, + custom_cost_fun_id["entity_id"], + custom_cost_fun_id["unit_of_measurement"], + custom_cost_fun_id["friendly_name"], + type_var="optim_status", + publish_prefix=publish_prefix, + save_entities=entity_save, + dont_post=dont_post + ) + cols_published = cols_published + ["optim_status"] # Publish unit_load_cost custom_unit_load_cost_id = params["passed_data"]["custom_unit_load_cost_id"] input_data_dict["rh"].post_data( diff --git a/src/emhass/retrieve_hass.py b/src/emhass/retrieve_hass.py index 9737bc81..d86b7512 100644 --- a/src/emhass/retrieve_hass.py +++ b/src/emhass/retrieve_hass.py @@ -370,6 +370,9 @@ def post_data(self, data_df: pd.DataFrame, idx: int, entity_id: str, unit_of_mea elif type_var == "deferrable": data = RetrieveHass.get_attr_data_dict(data_df, idx, entity_id, unit_of_measurement, friendly_name, "deferrables_schedule", state) + elif type_var == "temperature": + data = RetrieveHass.get_attr_data_dict(data_df, idx, entity_id, unit_of_measurement, + friendly_name, "predicted_temperatures", state) elif type_var == "batt": data = RetrieveHass.get_attr_data_dict(data_df, idx, entity_id, unit_of_measurement, friendly_name, "battery_scheduled_power", state) diff --git a/src/emhass/utils.py b/src/emhass/utils.py index 7869b8d0..3742a7e3 100644 --- a/src/emhass/utils.py +++ b/src/emhass/utils.py @@ -143,6 +143,7 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic params = {} # Some default data needed custom_deferrable_forecast_id = [] + custom_predicted_temperature_id = [] for k in range(optim_conf["num_def_loads"]): custom_deferrable_forecast_id.append( { @@ -151,6 +152,13 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic "friendly_name": "Deferrable Load {}".format(k), } ) + custom_predicted_temperature_id.append( + { + "entity_id": "sensor.temp_predicted{}".format(k), + "unit_of_measurement": "°C", + "friendly_name": "Predicted temperature {}".format(k), + } + ) default_passed_dict = { "custom_pv_forecast_id": { "entity_id": "sensor.p_pv_forecast", @@ -208,6 +216,7 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic "friendly_name": "Unit Prod Price", }, "custom_deferrable_forecast_id": custom_deferrable_forecast_id, + "custom_predicted_temperature_id": custom_predicted_temperature_id, "publish_prefix": "", } if "passed_data" in params.keys(): @@ -521,6 +530,10 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic params["passed_data"]["custom_deferrable_forecast_id"] = runtimeparams[ "custom_deferrable_forecast_id" ] + if "custom_predicted_temperature_id" in runtimeparams.keys(): + params["passed_data"]["custom_predicted_temperature_id"] = runtimeparams[ + "custom_predicted_temperature_id" + ] # A condition to put a prefix on all published data, or check for saved data under prefix name if "publish_prefix" not in runtimeparams.keys(): publish_prefix = ""