Repository: davidusb-geek/emhass (language: python)

Issue:
Add a function to predict a value from a csv file.
The story behind this pull request.
I keep a CSV file in which I store data from which I want to predict the number of heating hours.
I first tried to do this via a custom_component for home-assistant, but apparently it is [not possible to install](https://community.home-assistant.io/t/how-to-use-scikit-learn-with-a-custom-intigration/536939/6) `scikit-learn`.
Since the result of my prediction is to be used in emhass and the necessary dependencies are already installed in emhass, I decided to go this way.

This pull request contains a new method `csv-predict` with new parameters, here is an example of a rest command in home-assistant
```
predict_heating:
  url: http://localhost:5001/action/csv-predict
  method: POST
  content_type: "application/json"
  payload: >-
    {
      "csv_file": "prediction.csv",
      "independent_variables":["dd", "solar"],
      "dependent_variable": "hour",
      "sklearn_model": "LinearRegression",
      "csv_predict_entity_id": "sensor.voorspelde_uren",
      "csv_predict_unit_of_measurement": "h",
      "csv_predict_friendly_name": "Voorspelde uren",
      "new_values": [{{states("sensor.degree_day_prediction")}}, {{states("sensor.solcast_solaredge_forecast_today")}}]
    }
```

If you are open to accepting this pull request, I will also take the time to write some documentation. And if necessary, I would also like to try writing some tests.


Here is also a used CSV file.
[prediction.csv](https://github.com/davidusb-geek/emhass/files/13975642/prediction.csv)

Pass only 48 data items in list
[2022-04-25 14:55:37,863] ERROR in app_server: ERROR: The passed data is either not a list or the length is not correct, length should be 48
[2022-04-25 14:55:37,866] ERROR in app_server: Passed type is <class 'list'> and length is 49


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 5dc21af8..fa1f0d74 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,10 +7,10 @@ secrets_emhass.yaml
 .vscode/launch.json
 .vscode/settings.json
 .vscode/tasks.json
-*.csv
 *.html
 *.pkl
 data/actionLogs.txt
+**/app
 
 
 # Byte-compiled / optimized / DLL files
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 10313c97..f0ceae3a 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -14,14 +14,15 @@
       "request": "launch",
       "module": "emhass.web_server",
       "console": "integratedTerminal",
-      "purpose":["debug-in-terminal"],
+      "purpose": [
+        "debug-in-terminal"
+      ],
       "justMyCode": true,
       "env": {
         "CONFIG_PATH": "/workspaces/emhass/config_emhass.yaml",
         "OPTIONS_PATH": "/workspaces/emhass/options.json",
         "SECRETS_PATH": "/workspaces/emhass/secrets_emhass.yaml",
         "DATA_PATH": "/workspaces/emhass/data/",
-        "LOGGING_LEVEL": "DEBUG"
       }
     },
     {
@@ -30,8 +31,15 @@
       "request": "launch",
       "module": "emhass.web_server",
       "console": "integratedTerminal",
-      "args": ["--addon", "true",  "--no_response", "true"],
-      "purpose":["debug-in-terminal"],
+      "args": [
+        "--addon",
+        "true",
+        "--no_response",
+        "true"
+      ],
+      "purpose": [
+        "debug-in-terminal"
+      ],
       "justMyCode": true,
       "env": {
         "CONFIG_PATH": "/workspaces/emhass/config_emhass.yaml",
@@ -44,9 +52,7 @@
         "LAT": "45.83", //optional change
         "LON": "6.86", //optional change
         "ALT": "4807.8", //optional change
-        "LOGGING_LEVEL": "DEBUG" //optional change
       },
-      
-  }
+    }
   ]
 }
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index ffe440eb..ee23d121 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -9,7 +9,11 @@
                 "isDefault": true
             },
             "args": [
-                "install", "--no-deps", "--force-reinstall", "."
+                "install",
+                "--no-deps",
+                "--force-reinstall",
+                "--editable",
+                "."
             ],
             "presentation": {
                 "echo": true,
diff --git a/data/heating_prediction.csv b/data/heating_prediction.csv
new file mode 100644
index 00000000..f50a8a49
--- /dev/null
+++ b/data/heating_prediction.csv
@@ -0,0 +1,130 @@
+﻿timestamp,degreeday,solar,hour
+2023-11-10 23:59:32.458039+01:00,12.23,3.982,2.87
+2023-11-11 23:59:32.459778+01:00,12.94,13.723,3.14
+2023-11-12 23:59:32.462220+01:00,14.45,4.925,3.5
+2023-11-13 23:59:32.462167+01:00,8.49,2.138,3.19
+2023-11-14 23:59:32.338942+01:00,8.61,2.444,2.91
+2023-11-15 23:59:32.195198+01:00,9.83,10.685,2.88
+2023-11-16 23:59:32.501044+01:00,12.8,1.955,1.28
+2023-11-17 23:59:32.316366+01:00,13.35,8.742,2.97
+2023-11-18 23:59:32.082785+01:00,11.84,0.849,3.42
+2023-11-19 23:59:32.077198+01:00,7.3,10.85,1.9
+2023-11-20 23:59:32.431964+01:00,9.91,6.395,2.48
+2023-11-21 23:59:32.295705+01:00,11.44,2.678,2.91
+2023-11-22 23:59:32.377740+01:00,16.14,2.994,2.96
+2023-11-23 23:59:32.385890+01:00,9.31,5.346,2.91
+2023-11-24 23:59:32.376194+01:00,12.96,8.61,2.9
+2023-11-25 23:59:32.373666+01:00,14.91,12.31,3.47
+2023-11-26 23:59:32.373647+01:00,14.79,2.589,3.69
+2023-11-27 23:59:32.379920+01:00,14.92,0.322,6.05
+2023-11-28 23:59:32.213947+01:00,18.59,20.342,2.94
+2023-11-29 23:59:32.217384+01:00,19.05,5.393,5.41
+2023-11-30 23:59:32.222641+01:00,21.27,1.899,6.77
+2023-12-01 23:59:32.224533+01:00,21.3,1.233,5.75
+2023-12-02 23:59:32.107119+01:00,21.97,14.653,2.96
+2023-12-03 23:59:32.107436+01:00,20.61,4.766,8.89
+2023-12-04 23:59:32.116642+01:00,18.36,1.349,6.73
+2023-12-05 23:59:32.191254+01:00,16.93,0.869,6.17
+2023-12-06 23:59:32.176803+01:00,16.8,5.413,5.38
+2023-12-07 23:59:32.251031+01:00,17.67,8.089,5.98
+2023-12-08 23:59:32.255888+01:00,14.37,1.203,5.63
+2023-12-09 23:59:32.109040+01:00,11.94,0.814,5.08
+2023-12-10 23:59:32.103738+01:00,9.72,6.051,3.42
+2023-12-11 23:59:32.497717+01:00,9.83,1.459,3.87
+2023-12-12 23:59:32.502503+01:00,11.18,4.176,3.31
+2023-12-13 23:59:32.504794+01:00,11.09,2.91,3.1
+2023-12-14 23:59:32.177489+01:00,13.88,7.53,2.89
+2023-12-15 23:59:32.186292+01:00,12.18,2.129,5.68
+2023-12-16 23:59:32.176812+01:00,11.75,1.641,3.46
+2023-12-17 23:59:32.119874+01:00,12.18,14.868,3.46
+2023-12-18 23:59:32.120168+01:00,14.75,1.283,3.12
+2023-12-19 23:59:32.120101+01:00,12.82,0.09,5.07
+2023-12-20 23:59:32.249731+01:00,12.8,3.803,3.6
+2023-12-21 23:59:32.249135+01:00,8.73,2.096,3.55
+2023-12-22 23:59:32.385164+01:00,9.12,1.278,0.85
+2023-12-23 23:59:32.382910+01:00,8.99,1.848,0.0
+2023-12-24 23:59:32.382457+01:00,8.04,0.165,7.42
+2023-12-25 23:59:32.303520+01:00,7.56,1.028,2.93
+2023-12-26 23:59:32.105788+01:00,10.55,9.274,2.92
+2023-12-27 23:59:32.183107+01:00,11.78,2.026,3.39
+2023-12-28 23:59:32.183405+01:00,8.91,3.68,3.19
+2023-12-29 23:59:32.399740+01:00,9.35,2.464,2.95
+2023-12-30 23:59:32.091110+01:00,11.07,7.948,3.44
+2023-12-31 23:59:32.257530+01:00,10.51,3.5,3.48
+2024-01-01 23:59:32.106161+01:00,12.75,4.046,3.08
+2024-01-02 23:59:32.103187+01:00,8.81,0.562,4.46
+2024-01-03 23:59:32.429947+01:00,10.03,2.184,3.26
+2024-01-04 23:59:32.436773+01:00,11.22,5.662,2.97
+2024-01-05 23:59:32.165969+01:00,12.42,1.199,3.6
+2024-01-06 23:59:32.110208+01:00,15.35,0.295,4.32
+2024-01-07 23:59:32.147775+01:00,19.88,0.896,6.19
+2024-01-08 23:59:32.242815+01:00,22.74,6.468,5.82
+2024-01-09 23:59:32.201342+01:00,24.38,21.307,6.92
+2024-01-10 23:59:32.411136+01:00,24.84,18.89,1.53
+2024-01-11 23:59:32.399433+01:00,23.57,19.27,3.05
+2024-01-12 23:59:32.467622+01:00,18.22,1.977,13.98
+2024-01-13 23:59:32.077428+01:00,17.9,0.472,6.93
+2024-01-14 23:59:32.127844+01:00,19.65,1.346,6.95
+2024-01-15 23:59:32.125062+01:00,19.49,4.35,7.82
+2024-01-16 23:59:32.280474+01:00,21.21,9.238,5.7
+2024-01-17 23:59:32.283951+01:00,23.17,1.193,7.37
+2024-01-18 23:59:32.361241+01:00,21.61,17.307,6.67
+2024-01-19 23:59:32.341654+01:00,22.06,21.004,6.24
+2024-01-20 23:59:32.359151+01:00,21.95,12.912,6.43
+2024-01-21 23:59:32.126221+01:00,17.38,3.28,7.45
+2024-01-22 23:59:32.126346+01:00,9.47,7.645,6.1
+2024-01-23 23:59:32.417727+01:00,11.87,7.689,4.76
+2024-01-24 23:59:32.420933+01:00,8.15,10.052,3.62
+2024-01-25 23:59:32.419138+01:00,12.38,3.785,3.98
+2024-01-26 23:59:32.422066+01:00,11.4,11.94,3.1
+2024-01-27 23:59:32.176538+01:00,17.96,19.741,3.45
+2024-01-28 23:59:32.168328+01:00,16.72,20.366,4.85
+2024-01-29 23:59:32.173916+01:00,13.11,16.972,4.51
+2024-01-30 23:59:32.503034+01:00,11.21,4.013,3.99
+2024-01-31 23:59:32.179265+01:00,12.79,4.766,3.73
+2024-02-01 23:59:32.487147+01:00,12.74,23.924,2.98
+2024-02-02 23:59:32.570084+01:00,13.0,2.98,5.04
+2024-02-03 23:59:32.484878+01:00,9.26,1.413,3.48
+2024-02-04 23:59:32.472168+01:00,8.35,4.306,3.47
+2024-02-05 23:59:32.409856+01:00,9.78,5.704,0.0
+2024-02-06 23:59:32.439147+01:00,9.15,2.431,6.56
+2024-02-07 23:59:32.235231+01:00,14.42,3.839,3.07
+2024-02-08 23:59:32.441543+01:00,13.9,1.412,5.94
+2024-02-09 23:59:32.443230+01:00,8.2,7.246,2.96
+2024-02-10 23:59:32.504326+01:00,8.37,8.567,3.48
+2024-02-11 23:59:32.452959+01:00,10.44,5.304,0.0
+2024-02-12 23:59:32.450999+01:00,12.65,16.004,3.42
+2024-02-13 23:59:32.343162+01:00,13.84,19.809,3.16
+2024-02-14 23:59:32.339408+01:00,8.48,1.98,4.52
+2024-02-15 23:59:32.339971+01:00,6.13,9.952,2.98
+2024-02-16 23:59:32.455273+01:00,7.66,3.675,3.06
+2024-02-17 23:59:32.097937+01:00,8.56,12.269,3.48
+2024-02-18 23:59:32.126377+01:00,9.59,2.205,3.04
+2024-02-19 23:59:32.421243+01:00,10.22,3.731,2.97
+2024-02-20 23:59:32.421985+01:00,11.61,13.775,0.0
+2024-02-21 23:59:32.371300+01:00,10.52,4.856,3.02
+2024-02-22 23:59:32.373153+01:00,9.53,4.256,3.48
+2024-02-23 23:59:32.372545+01:00,13.66,8.743,4.09
+2024-02-24 23:59:32.197044+01:00,14.44,7.842,4.3
+2024-02-25 23:59:32.196386+01:00,12.41,16.235,3.48
+2024-02-26 23:59:32.409648+01:00,14.63,2.096,5.05
+2024-02-27 23:59:32.373347+01:00,14.5,29.437,3.21
+2024-02-28 23:59:32.407538+01:00,15.38,6.475,4.88
+2024-02-29 23:59:32.194724+01:00,11.83,3.238,4.68
+2024-03-01 23:59:32.084520+01:00,10.56,14.352,3.8
+2024-03-02 23:59:32.066434+01:00,9.94,25.356,3.49
+2024-03-03 23:59:32.270878+01:00,8.9,10.577,3.19
+2024-03-04 23:59:32.274918+01:00,10.67,28.096,2.08
+2024-03-05 23:59:32.315023+01:00,12.19,10.553,2.95
+2024-03-06 23:59:32.441001+01:00,11.38,32.597,2.91
+2024-03-07 23:59:32.440044+01:00,12.39,28.856,2.96
+2024-03-08 23:59:32.228265+01:00,12.01,37.395,2.96
+2024-03-09 23:59:32.081874+01:00,8.72,17.66,3.5
+2024-03-10 23:59:32.335321+01:00,8.0,12.207,3.47
+2024-03-11 23:59:32.139531+01:00,10.39,2.526,2.96
+2024-03-12 23:59:32.136709+01:00,10.24,8.211,2.98
+2024-03-13 23:59:32.407174+01:00,7.19,6.425,2.95
+2024-03-14 23:59:32.342436+01:00,6.06,33.389,1.64
+2024-03-15 23:59:32.266278+01:00,5.63,12.628,2.96
+2024-03-16 23:59:32.155245+01:00,9.57,12.103,3.0
+2024-03-17 23:59:32.366155+01:00,8.43,14.302,0.25
diff --git a/docs/develop.md b/docs/develop.md
index 6716f5ec..da81ef25 100644
--- a/docs/develop.md
+++ b/docs/develop.md
@@ -221,6 +221,11 @@ For those who wish to mount/sync the local `data` folder with the data folder fr
 docker run ... -v $(pwd)/data/:/app/data ...
 ```
 
+You can also mount data (ex .csv)  files separately
+```bash
+docker run... -v $(pwd)/data/heating_prediction.csv:/app/data/ ...
+```
+
 #### Issue with TARGETARCH
 If your docker build fails with an error related to `TARGETARCH`. It may be best to add your devices architecture manually:
 
@@ -301,7 +306,7 @@ git checkout $branch
 ```bash
 #testing addon (build and run)
 docker build -t emhass/docker --build-arg build_version=addon-local .
-docker run --rm -it -p 5000:5000 --name emhass-container -v $(pwd)/options.json:/app/options.json -e LAT="45.83" -e LON="6.86" -e ALT="4807.8" -e TIME_ZONE="Europe/Paris" emhass/docker --url $HAURL --key $HAKEY
+docker run --rm -it -p 5000:5000 --name emhass-container -v $(pwd)/data/heating_prediction.csv:/app/data/heating_prediction.csv -v $(pwd)/options.json:/app/options.json -e LAT="45.83" -e LON="6.86" -e ALT="4807.8" -e TIME_ZONE="Europe/Paris" emhass/docker --url $HAURL --key $HAKEY
 ```
 ```bash
 #run actions on a separate terminal
@@ -311,6 +316,8 @@ curl -i -H 'Content-Type:application/json' -X POST -d {} http://localhost:5000/a
 curl -i -H 'Content-Type:application/json' -X POST -d {} http://localhost:5000/action/forecast-model-fit
 curl -i -H 'Content-Type:application/json' -X POST -d {} http://localhost:5000/action/forecast-model-predict
 curl -i -H 'Content-Type:application/json' -X POST -d {} http://localhost:5000/action/forecast-model-tune
+curl -i -H "Content-Type:application/json" -X POST -d  '{"csv_file": "heating_prediction.csv", "features": ["degreeday", "solar"], "target": "hour", "regression_model": "RandomForestRegression", "model_type": "heating_hours_degreeday", "timestamp": "timestamp", "date_features": ["month", "day_of_week"], "new_values": [12.79, 4.766, 1, 2] }' http://localhost:5000/action/regressor-model-fit
+curl -i -H "Content-Type:application/json" -X POST -d  '{"mlr_predict_entity_id": "sensor.mlr_predict", "mlr_predict_unit_of_measurement": "h", "mlr_predict_friendly_name": "mlr predictor", "new_values": [8.2, 7.23, 2, 6], "model_type": "heating_hours_degreeday" }' http://localhost:5000/action/regressor-model-predict
 curl -i -H 'Content-Type:application/json' -X POST -d {} http://localhost:5000/action/publish-data
 ```
 
@@ -326,7 +333,7 @@ lat: 45.83
 lon: 6.86
 alt: 4807.8
 EOT
-docker run --rm -it -p 5000:5000 --name emhass-container -v $(pwd)/config_emhass.yaml:/app/config_emhass.yaml -v $(pwd)/secrets_emhass.yaml:/app/secrets_emhass.yaml emhass/docker 
+docker run --rm -it -p 5000:5000 --name emhass-container -v $(pwd)/data/heating_prediction.csv:/app/data/heating_prediction.csv -v $(pwd)/config_emhass.yaml:/app/config_emhass.yaml -v $(pwd)/secrets_emhass.yaml:/app/secrets_emhass.yaml emhass/docker 
 ```
 ```bash
 #run actions on a separate terminal
@@ -336,10 +343,12 @@ curl -i -H 'Content-Type:application/json' -X POST -d {} http://localhost:5000/a
 curl -i -H 'Content-Type:application/json' -X POST -d {} http://localhost:5000/action/forecast-model-fit
 curl -i -H 'Content-Type:application/json' -X POST -d {} http://localhost:5000/action/forecast-model-predict
 curl -i -H 'Content-Type:application/json' -X POST -d {} http://localhost:5000/action/forecast-model-tune
+curl -i -H "Content-Type:application/json" -X POST -d  '{"csv_file": "heating_prediction.csv", "features": ["degreeday", "solar"], "target": "hour", "regression_model": "RandomForestRegression", "model_type": "heating_hours_degreeday", "timestamp": "timestamp", "date_features": ["month", "day_of_week"], "new_values": [12.79, 4.766, 1, 2] }' http://localhost:5000/action/regressor-model-fit
+curl -i -H "Content-Type:application/json" -X POST -d  '{"mlr_predict_entity_id": "sensor.mlr_predict", "mlr_predict_unit_of_measurement": "h", "mlr_predict_friendly_name": "mlr predictor", "new_values": [8.2, 7.23, 2, 6], "model_type": "heating_hours_degreeday" }' http://localhost:5000/action/regressor-model-predict
 curl -i -H 'Content-Type:application/json' -X POST -d {} http://localhost:5000/action/publish-data
 ```
 
-User may wish to re-test with tweaked parameters such as `lp_solver` and `weather_forecast_method`, in `config_emhass.yaml` *(standalone)* or `options.json` *(addon)*, to broaden the testing scope. 
+User may wish to re-test with tweaked parameters such as `lp_solver`, `weather_forecast_method` and `load_forecast_method`, in `config_emhass.yaml` *(standalone)* or `options.json` *(addon)*, to broaden the testing scope. 
 *see [EMHASS & EMHASS-Add-on differences](https://emhass.readthedocs.io/en/latest/differences.html) for more information on how these config_emhass & options files differ*
 
 *Note: may need to set `--build-arg TARGETARCH=YOUR-ARCH` in docker build*
diff --git a/docs/index.md b/docs/index.md
index cf015a3f..cc9f33a8 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -6,6 +6,7 @@
 # EMHASS: Energy Management for Home Assistant
 
 ```{image} images/emhass_logo.png
+
 ```
 
 Welcome to the documentation of EMHASS. With this package written in Python you will be able to implement a real Energy Management System for your household. This software was designed to be easy configurable and with a fast integration with Home Assistant: <https://www.home-assistant.io/>
@@ -21,6 +22,7 @@ differences.md
 lpems.md
 forecasts.md
 mlforecaster.md
+mlregressor.md
 study_case.md
 config.md
 emhass.md
@@ -32,5 +34,3 @@ develop.md
 - {ref}`genindex`
 - {ref}`modindex`
 - {ref}`search`
-
-
diff --git a/docs/mlregressor.md b/docs/mlregressor.md
new file mode 100644
index 00000000..7746e985
--- /dev/null
+++ b/docs/mlregressor.md
@@ -0,0 +1,196 @@
+# The machine learning regressor
+
+Starting with v0.9.0, a new framework is proposed within EMHASS. It provides a machine learning module to predict values from a csv file using different regression models.
+
+This API provides two main methods:
+
+- **fit**: To train a model with the passed data. This method is exposed with the `regressor-model-fit` end point.
+
+- **predict**: To obtain a prediction from a pre-trained model. This method is exposed with the `regressor-model-predict` end point.
+
+## A basic model fit
+
+To train a model use the `regressor-model-fit` end point.
+
+Some paramters can be optionally defined at runtime:
+
+- `csv_file`: The name of the csv file containing your data.
+
+- `features`: A list of features, you can provide new values for this.
+
+- `target`: The target, the value that has to be predicted.
+
+- `model_type`: Define the name of the model regressor that this will be used for. For example: `heating_hours_degreeday`. This should be an unique name if you are using multiple custom regressor models.
+
+- `regression_model`: The regression model that will be used. For now only this options are possible: `LinearRegression`, `RidgeRegression`, `LassoRegression`, `RandomForestRegression`, `GradientBoostingRegression` and `AdaBoostRegression`.
+
+- `timestamp`: If defined, the column key that has to be used for timestamp.
+
+- `date_features`: A list of 'date_features' to take into account when fitting the model. Possibilities are `year`, `month`, `day_of_week` (monday=0, sunday=6), `day_of_year`, `day`(day_of_month) and `hour`
+
+### Examples: 
+```yaml
+runtimeparams = {
+    "csv_file": "heating_prediction.csv",
+    "features": ["degreeday", "solar"],
+    "target": "heating_hours",
+    "regression_model": "RandomForestRegression",
+    "model_type": "heating_hours_degreeday",
+    "timestamp": "timestamp",
+    "date_features": ["month", "day_of_week"]
+    }
+```
+
+A correct `curl` call to launch a model fit can look like this:
+
+```bash
+curl -i -H "Content-Type:application/json" -X POST -d '{"csv_file": "heating_prediction.csv", "features": ["degreeday", "solar"], "target": "heating_hours"}' http://localhost:5000/action/regressor-model-fit
+```
+or 
+```bash
+curl -i -H "Content-Type:application/json" -X POST -d  '{"csv_file": "heating_prediction.csv", "features": ["degreeday", "solar"], "target": "hour", "regression_model": "RandomForestRegression", "model_type": "heating_hours_degreeday", "timestamp": "timestamp", "date_features": ["month", "day_of_week"], "new_values": [12.79, 4.766, 1, 2] }' http://localhost:5000/action/regressor-model-fit
+```
+
+A Home Assistant `rest_command` can look like this:
+
+```yaml
+fit_heating_hours:
+  url: http://127.0.0.1:5000/action/regressor-model-fit
+  method: POST
+  content_type: "application/json"
+  payload: >-
+    {
+    "csv_file": "heating_prediction.csv",
+    "features": ["degreeday", "solar"],
+    "target": "hours",
+    "regression_model": "RandomForestRegression",
+    "model_type": "heating_hours_degreeday",
+    "timestamp": "timestamp",
+    "date_features": ["month", "day_of_week"]
+    }
+```
+After fitting the model the following information is logged by EMHASS:
+
+    2024-04-17 12:41:50,019 - web_server - INFO - Passed runtime parameters: {'csv_file': 'heating_prediction.csv', 'features': ['degreeday', 'solar'], 'target': 'heating_hours', 'regression_model': 'RandomForestRegression', 'model_type': 'heating_hours_degreeday', 'timestamp': 'timestamp', 'date_features': ['month', 'day_of_week']}
+    2024-04-17 12:41:50,020 - web_server - INFO -  >> Setting input data dict
+    2024-04-17 12:41:50,021 - web_server - INFO - Setting up needed data
+    2024-04-17 12:41:50,048 - web_server - INFO -  >> Performing a machine learning regressor fit...
+    2024-04-17 12:41:50,049 - web_server - INFO - Performing a MLRegressor fit for heating_hours_degreeday
+    2024-04-17 12:41:50,064 - web_server - INFO - Training a RandomForestRegression model
+    2024-04-17 12:41:57,852 - web_server - INFO - Elapsed time for model fit: 7.78800106048584
+    2024-04-17 12:41:57,862 - web_server - INFO - Prediction R2 score of fitted model on test data: -0.5667567505914477
+
+## The predict method
+
+To obtain a prediction using a previously trained model use the `regressor-model-predict` end point.
+
+The list of parameters needed to set the data publish task is:
+
+- `mlr_predict_entity_id`: The unique `entity_id` to be used.
+
+- `mlr_predict_unit_of_measurement`: The `unit_of_measurement` to be used.
+
+- `mlr_predict_friendly_name`: The `friendly_name` to be used.
+
+- `new_values`: The new values for the features (in the same order as the features list). Also when using date_features, add these to the new values.
+
+- `model_type`: The model type that has to be predicted
+
+### Examples: 
+```yaml
+runtimeparams = {
+    "mlr_predict_entity_id": "sensor.mlr_predict",
+    "mlr_predict_unit_of_measurement": None,
+    "mlr_predict_friendly_name": "mlr predictor",
+    "new_values": [8.2, 7.23, 2, 6],
+    "model_type": "heating_hours_degreeday"
+}
+```
+
+Pass the correct `model_type` like this:
+
+```bash
+curl -i -H "Content-Type:application/json" -X POST -d '{"model_type": "heating_hours_degreeday"}' http://localhost:5000/action/regressor-model-predict
+```
+or
+```bash
+curl -i -H "Content-Type:application/json" -X POST -d  '{"mlr_predict_entity_id": "sensor.mlr_predict", "mlr_predict_unit_of_measurement": "h", "mlr_predict_friendly_name": "mlr predictor", "new_values": [8.2, 7.23, 2, 6], "model_type": "heating_hours_degreeday" }' http://localhost:5000/action/regressor-model-predict
+```
+
+A Home Assistant `rest_command` can look like this:
+
+```yaml
+predict_heating_hours:
+  url: http://localhost:5001/action/regressor-model-predict
+  method: POST
+  content_type: "application/json"
+  payload: >-
+   {
+    "mlr_predict_entity_id": "sensor.predicted_hours",
+    "mlr_predict_unit_of_measurement": "h",
+    "mlr_predict_friendly_name": "Predicted hours",
+    "new_values": [8.2, 7.23, 2, 6],
+    "model_type": "heating_hours_degreeday"
+    }
+```
+After predicting the model the following information is logged by EMHASS:
+
+```
+2024-04-17 14:25:40,695 - web_server - INFO - Passed runtime parameters: {'mlr_predict_entity_id': 'sensor.predicted_hours', 'mlr_predict_unit_of_measurement': 'h', 'mlr_predict_friendly_name': 'Predicted hours', 'new_values': [8.2, 7.23, 2, 6], 'model_type': 'heating_hours_degreeday'}
+2024-04-17 14:25:40,696 - web_server - INFO -  >> Setting input data dict
+2024-04-17 14:25:40,696 - web_server - INFO - Setting up needed data
+2024-04-17 14:25:40,700 - web_server - INFO -  >> Performing a machine learning regressor predict...
+2024-04-17 14:25:40,715 - web_server - INFO - Performing a prediction for heating_hours_degreeday
+2024-04-17 14:25:40,750 - web_server - INFO - Successfully posted to sensor.predicted_hours = 3.716600000000001
+```
+The predict method will publish the result to a Home Assistant sensor.
+
+
+## Storing CSV files  
+
+### Standalone container - how to mount a .csv files in data_path folder
+If running EMHASS as Standalone container, you will need to volume mount a folder to be the `data_path`, or mount a single .csv file inside `data_path`
+
+Example of mounting a folder as data_path *(.csv files stored inside)*
+```bash
+docker run -it --restart always -p 5000:5000 -e LOCAL_COSTFUN="profit" -v $(pwd)/data:/app/data -v $(pwd)/config_emhass.yaml:/app/config_emhass.yaml -v $(pwd)/secrets_emhass.yaml:/app/secrets_emhass.yaml --name DockerEMHASS <REPOSITORY:TAG>
+```
+Example of mounting a single csv file
+```bash
+docker run -it --restart always -p 5000:5000 -e LOCAL_COSTFUN="profit" -v $(pwd)/data/heating_prediction.csv:/app/data/heating_prediction.csv -v $(pwd)/config_emhass.yaml:/app/config_emhass.yaml -v $(pwd)/secrets_emhass.yaml:/app/secrets_emhass.yaml --name DockerEMHASS <REPOSITORY:TAG>
+```
+
+### Add-on - How to store data in a csv file from Home Assistant
+
+#### Change data_path
+If running EMHASS-Add-On, you will likley need to change the `data_path` to a folder your Home Assistant can access. 
+To do this, set the `data_path` to `/share/` in the addon *Configuration* page. 
+
+#### Store sensor data to csv
+
+Notify to a file
+```yaml
+notify:
+  - platform: file
+    name: heating_hours_prediction
+    timestamp: false
+    filename: /share/heating_prediction.csv
+```
+Then you need an automation to notify to this file
+```yaml
+alias: "Heating csv"
+id: 157b1d57-73d9-4f39-82c6-13ce0cf42
+trigger:
+  - platform: time
+    at: "23:59:32"
+action:
+  - service: notify.heating_hours_prediction
+    data:
+      message: >
+        {% set degreeday = states('sensor.degree_day_daily') |float %}
+        {% set heating_hours = states('sensor.heating_hours_today') |float | round(2) %}
+        {% set solar = states('sensor.solar_daily') |float | round(3) %}
+        {% set time = now() %}
+
+          {{time}},{{degreeday}},{{solar}},{{heating_hours}}
+```
\ No newline at end of file
diff --git a/src/emhass/command_line.py b/src/emhass/command_line.py
index add45f71..77665f1d 100644
--- a/src/emhass/command_line.py
+++ b/src/emhass/command_line.py
@@ -8,18 +8,19 @@
 import json
 import copy
 import pickle
-import time
-import numpy as np
-import pandas as pd
 from datetime import datetime, timezone
 from typing import Optional, Tuple
+from importlib.metadata import version
+import numpy as np
+import pandas as pd
+
 from distutils.util import strtobool
 
-from importlib.metadata import version
 from emhass.retrieve_hass import RetrieveHass
 from emhass.forecast import Forecast
 from emhass.machine_learning_forecaster import MLForecaster
 from emhass.optimization import Optimization
+from emhass.machine_learning_regressor import MLRegressor
 from emhass import utils
 
 
@@ -53,8 +54,14 @@ def set_input_data_dict(emhass_conf: dict, costfun: str,
         emhass_conf, use_secrets=not(get_data_from_file), params=params)
     # Treat runtimeparams
     params, retrieve_hass_conf, optim_conf, plant_conf = utils.treat_runtimeparams(
-        runtimeparams, params, retrieve_hass_conf, 
-        optim_conf, plant_conf, set_type, logger)
+        runtimeparams,
+        params,
+        retrieve_hass_conf,
+        optim_conf,
+        plant_conf,
+        set_type,
+        logger,
+    )
     # Define main objects
     rh = RetrieveHass(retrieve_hass_conf['hass_url'], retrieve_hass_conf['long_lived_token'], 
                       retrieve_hass_conf['freq'], retrieve_hass_conf['time_zone'], 
@@ -75,35 +82,53 @@ def set_input_data_dict(emhass_conf: dict, costfun: str,
             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 = utils.get_days_list(retrieve_hass_conf['days_to_retrieve'])
-            var_list = [retrieve_hass_conf['var_load'], retrieve_hass_conf['var_PV']]
-            if not rh.get_data(days_list, var_list,
-                               minimal_response=False, significant_changes_only=False):
-                return False 
-        if not 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']):
+            days_list = utils.get_days_list(retrieve_hass_conf["days_to_retrieve"])
+            var_list = [retrieve_hass_conf["var_load"], retrieve_hass_conf["var_PV"]]
+            if not rh.get_data(
+                days_list,
+                var_list,
+                minimal_response=False,
+                significant_changes_only=False,
+            ):
+                return False
+        if not 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"],
+        ):
             return False
         df_input_data = rh.df_final.copy()
         # What we don't need for this type of action
         P_PV_forecast, P_load_forecast, df_input_data_dayahead = None, None, None
     elif set_type == "dayahead-optim":
         # Get PV and load forecasts
-        df_weather = fcst.get_weather_forecast(method=optim_conf['weather_forecast_method'])
+        df_weather = fcst.get_weather_forecast(
+            method=optim_conf["weather_forecast_method"]
+        )
         P_PV_forecast = fcst.get_power_from_weather(df_weather)
         P_load_forecast = fcst.get_load_forecast(method=optim_conf['load_forecast_method'])
         if isinstance(P_load_forecast,bool) and not P_load_forecast:
             logger.error("Unable to get sensor power photovoltaics, or sensor power load no var loads. Check HA sensors and their daily data")
             return False
-        df_input_data_dayahead = pd.DataFrame(np.transpose(np.vstack([P_PV_forecast.values,P_load_forecast.values])),
-                                              index=P_PV_forecast.index,
-                                              columns=['P_PV_forecast', 'P_load_forecast'])
+        df_input_data_dayahead = pd.DataFrame(
+            np.transpose(np.vstack([P_PV_forecast.values, P_load_forecast.values])),
+            index=P_PV_forecast.index,
+            columns=["P_PV_forecast", "P_load_forecast"],
+        )
         df_input_data_dayahead = utils.set_df_index_freq(df_input_data_dayahead)
         params = json.loads(params)
-        if 'prediction_horizon' in params['passed_data'] and params['passed_data']['prediction_horizon'] is not None:
-            prediction_horizon = params['passed_data']['prediction_horizon']
-            df_input_data_dayahead = copy.deepcopy(df_input_data_dayahead)[df_input_data_dayahead.index[0]:df_input_data_dayahead.index[prediction_horizon-1]]
+        if (
+            "prediction_horizon" in params["passed_data"]
+            and params["passed_data"]["prediction_horizon"] is not None
+        ):
+            prediction_horizon = params["passed_data"]["prediction_horizon"]
+            df_input_data_dayahead = copy.deepcopy(df_input_data_dayahead)[
+                df_input_data_dayahead.index[0] : df_input_data_dayahead.index[
+                    prediction_horizon - 1
+                ]
+            ]
         # What we don't need for this type of action
         df_input_data, days_list = None, None
     elif set_type == "naive-mpc-optim":
@@ -117,14 +142,21 @@ def set_input_data_dict(emhass_conf: dict, costfun: str,
             retrieve_hass_conf['var_replace_zero'] = [retrieve_hass_conf['var_PV']]
         else:
             days_list = utils.get_days_list(1)
-            var_list = [retrieve_hass_conf['var_load'], retrieve_hass_conf['var_PV']]
-            if not rh.get_data(days_list, var_list,
-                               minimal_response=False, significant_changes_only=False):
+            var_list = [retrieve_hass_conf["var_load"], retrieve_hass_conf["var_PV"]]
+            if not rh.get_data(
+                days_list,
+                var_list,
+                minimal_response=False,
+                significant_changes_only=False,
+            ):
                 return False
-        if not 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']):
+        if not 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"],
+        ):
             return False
         df_input_data = rh.df_final.copy()
         # Get PV and load forecasts
@@ -136,38 +168,97 @@ def set_input_data_dict(emhass_conf: dict, costfun: str,
             return False
         df_input_data_dayahead = pd.concat([P_PV_forecast, P_load_forecast], axis=1)
         df_input_data_dayahead = utils.set_df_index_freq(df_input_data_dayahead)
-        df_input_data_dayahead.columns = ['P_PV_forecast', 'P_load_forecast']
+        df_input_data_dayahead.columns = ["P_PV_forecast", "P_load_forecast"]
         params = json.loads(params)
-        if 'prediction_horizon' in params['passed_data'] and params['passed_data']['prediction_horizon'] is not None:
-            prediction_horizon = params['passed_data']['prediction_horizon']
-            df_input_data_dayahead = copy.deepcopy(df_input_data_dayahead)[df_input_data_dayahead.index[0]:df_input_data_dayahead.index[prediction_horizon-1]]
-    elif set_type == "forecast-model-fit" or set_type == "forecast-model-predict" or set_type == "forecast-model-tune":
+        if (
+            "prediction_horizon" in params["passed_data"]
+            and params["passed_data"]["prediction_horizon"] is not None
+        ):
+            prediction_horizon = params["passed_data"]["prediction_horizon"]
+            df_input_data_dayahead = copy.deepcopy(df_input_data_dayahead)[
+                df_input_data_dayahead.index[0] : df_input_data_dayahead.index[
+                    prediction_horizon - 1
+                ]
+            ]
+    elif (
+        set_type == "forecast-model-fit"
+        or set_type == "forecast-model-predict"
+        or set_type == "forecast-model-tune"
+    ):
         df_input_data_dayahead = None
         P_PV_forecast, P_load_forecast = None, None
         params = json.loads(params)
         # Retrieve data from hass
-        days_to_retrieve = params['passed_data']['days_to_retrieve']
-        model_type = params['passed_data']['model_type']
-        var_model = params['passed_data']['var_model']
+        days_to_retrieve = params["passed_data"]["days_to_retrieve"]
+        model_type = params["passed_data"]["model_type"]
+        var_model = params["passed_data"]["var_model"]
         if get_data_from_file:
             days_list = None
             filename = 'data_train_'+model_type+'.pkl'
             filename_path = emhass_conf['data_path'] / filename
             with open(filename_path, 'rb') as inp:
                 df_input_data, _ = pickle.load(inp)
-            df_input_data = df_input_data[df_input_data.index[-1] - pd.offsets.Day(days_to_retrieve):]
+            df_input_data = df_input_data[
+                df_input_data.index[-1] - pd.offsets.Day(days_to_retrieve) :
+            ]
         else:
             days_list = utils.get_days_list(days_to_retrieve)
             var_list = [var_model]
             if not rh.get_data(days_list, var_list):
                 return False
             df_input_data = rh.df_final.copy()
+
+    elif set_type == "regressor-model-fit" or set_type == "regressor-model-predict":
+
+        df_input_data, df_input_data_dayahead = None, None
+        P_PV_forecast, P_load_forecast = None, None
+        params = json.loads(params)
+        days_list = None
+        csv_file = params["passed_data"].get("csv_file", None)
+        if "features" in params["passed_data"]:
+            features = params["passed_data"]["features"]
+        if "target" in params["passed_data"]:
+            target = params["passed_data"]["target"]
+        if "timestamp" in params["passed_data"]:
+            timestamp = params["passed_data"]["timestamp"]
+        if csv_file:
+            if get_data_from_file:
+                base_path = emhass_conf["data_path"]  # + "/data"
+                filename_path = pathlib.Path(base_path) / csv_file
+
+            else:
+                filename_path = emhass_conf["data_path"] / csv_file
+
+            if filename_path.is_file():
+                df_input_data = pd.read_csv(filename_path, parse_dates=True)
+
+            else:
+                logger.error("The CSV file " + csv_file + " was not found in path: " + str(emhass_conf["data_path"]))
+                return False
+                #raise ValueError("The CSV file " + csv_file + " was not found.")
+            required_columns = []
+            required_columns.extend(features)
+            required_columns.append(target)
+            if timestamp is not None:
+                required_columns.append(timestamp)
+
+            if not set(required_columns).issubset(df_input_data.columns):
+                logger.error("The cvs file does not contain the required columns.")
+                msg = f"CSV file should contain the following columns: {', '.join(required_columns)}"
+                logger.error(msg)
+                return False
+                #raise ValueError(
+                #    msg,
+                #)
+
     elif set_type == "publish-data":
         df_input_data, df_input_data_dayahead = None, None
         P_PV_forecast, P_load_forecast = None, None
         days_list = None
     else:
-        logger.error("The passed action argument and hence the set_type parameter for setup is not valid")
+        logger.error(
+            "The passed action argument and hence the set_type parameter for setup is not valid",
+        )
         df_input_data, df_input_data_dayahead = None, None
         P_PV_forecast, P_load_forecast = None, None
         days_list = None
@@ -188,12 +279,17 @@ def set_input_data_dict(emhass_conf: dict, costfun: str,
         'days_list': days_list
     }
     return input_data_dict
-    
-def perfect_forecast_optim(input_data_dict: dict, logger: logging.Logger,
-    save_data_to_file: Optional[bool] = True, debug: Optional[bool] = False) -> pd.DataFrame:
+
+
+def perfect_forecast_optim(
+    input_data_dict: dict,
+    logger: logging.Logger,
+    save_data_to_file: Optional[bool] = True,
+    debug: Optional[bool] = False,
+) -> pd.DataFrame:
     """
     Perform a call to the perfect forecast optimization routine.
-    
+
     :param input_data_dict:  A dictionnary with multiple data used by the action functions
     :type input_data_dict: dict
     :param logger: The passed logger object
@@ -222,18 +318,23 @@ def perfect_forecast_optim(input_data_dict: dict, logger: logging.Logger,
     opt_res = input_data_dict['opt'].perform_perfect_forecast_optim(df_input_data, input_data_dict['days_list'])
     # Save CSV file for analysis
     if save_data_to_file:
-        filename = 'opt_res_perfect_optim_'+input_data_dict['costfun']+'.csv'
-    else: # Just save the latest optimization results
-        filename = 'opt_res_latest.csv'
+        filename = "opt_res_perfect_optim_" + input_data_dict["costfun"] + ".csv"
+    else:  # Just save the latest optimization results
+        filename = "opt_res_latest.csv"
     if not debug:
         opt_res.to_csv(input_data_dict['emhass_conf']['data_path'] / filename, index_label='timestamp')
     return opt_res
-    
-def dayahead_forecast_optim(input_data_dict: dict, logger: logging.Logger,
-    save_data_to_file: Optional[bool] = False, debug: Optional[bool] = False) -> pd.DataFrame:
+
+
+def dayahead_forecast_optim(
+    input_data_dict: dict,
+    logger: logging.Logger,
+    save_data_to_file: Optional[bool] = False,
+    debug: Optional[bool] = False,
+) -> pd.DataFrame:
     """
     Perform a call to the day-ahead optimization routine.
-    
+
     :param input_data_dict:  A dictionnary with multiple data used by the action functions
     :type input_data_dict: dict
     :param logger: The passed logger object
@@ -262,19 +363,26 @@ def dayahead_forecast_optim(input_data_dict: dict, logger: logging.Logger,
         df_input_data_dayahead, input_data_dict['P_PV_forecast'], input_data_dict['P_load_forecast'])
     # Save CSV file for publish_data
     if save_data_to_file:
-        today = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
-        filename = 'opt_res_dayahead_'+today.strftime("%Y_%m_%d")+'.csv'
-    else: # Just save the latest optimization results
-        filename = 'opt_res_latest.csv'
+        today = datetime.now(timezone.utc).replace(
+            hour=0, minute=0, second=0, microsecond=0
+        )
+        filename = "opt_res_dayahead_" + today.strftime("%Y_%m_%d") + ".csv"
+    else:  # Just save the latest optimization results
+        filename = "opt_res_latest.csv"
     if not debug:
         opt_res_dayahead.to_csv(input_data_dict['emhass_conf']['data_path'] / filename, index_label='timestamp')
     return opt_res_dayahead
 
-def naive_mpc_optim(input_data_dict: dict, logger: logging.Logger,
-    save_data_to_file: Optional[bool] = False, debug: Optional[bool] = False) -> pd.DataFrame:
+
+def naive_mpc_optim(
+    input_data_dict: dict,
+    logger: logging.Logger,
+    save_data_to_file: Optional[bool] = False,
+    debug: Optional[bool] = False,
+) -> pd.DataFrame:
     """
     Perform a call to the naive Model Predictive Controller optimization routine.
-    
+
     :param input_data_dict:  A dictionnary with multiple data used by the action functions
     :type input_data_dict: dict
     :param logger: The passed logger object
@@ -299,27 +407,39 @@ def naive_mpc_optim(input_data_dict: dict, logger: logging.Logger,
     if isinstance(df_input_data_dayahead,bool) and not df_input_data_dayahead:
         return False 
     # The specifics params for the MPC at runtime
-    prediction_horizon = input_data_dict['params']['passed_data']['prediction_horizon']
-    soc_init = input_data_dict['params']['passed_data']['soc_init']
-    soc_final = input_data_dict['params']['passed_data']['soc_final']
-    def_total_hours = input_data_dict['params']['passed_data']['def_total_hours']
-    def_start_timestep = input_data_dict['params']['passed_data']['def_start_timestep']
-    def_end_timestep = input_data_dict['params']['passed_data']['def_end_timestep']
-    opt_res_naive_mpc = input_data_dict['opt'].perform_naive_mpc_optim(
-        df_input_data_dayahead, input_data_dict['P_PV_forecast'], input_data_dict['P_load_forecast'],
-        prediction_horizon, soc_init, soc_final, def_total_hours, def_start_timestep, def_end_timestep)
+    prediction_horizon = input_data_dict["params"]["passed_data"]["prediction_horizon"]
+    soc_init = input_data_dict["params"]["passed_data"]["soc_init"]
+    soc_final = input_data_dict["params"]["passed_data"]["soc_final"]
+    def_total_hours = input_data_dict["params"]["passed_data"]["def_total_hours"]
+    def_start_timestep = input_data_dict["params"]["passed_data"]["def_start_timestep"]
+    def_end_timestep = input_data_dict["params"]["passed_data"]["def_end_timestep"]
+    opt_res_naive_mpc = input_data_dict["opt"].perform_naive_mpc_optim(
+        df_input_data_dayahead,
+        input_data_dict["P_PV_forecast"],
+        input_data_dict["P_load_forecast"],
+        prediction_horizon,
+        soc_init,
+        soc_final,
+        def_total_hours,
+        def_start_timestep,
+        def_end_timestep,
+    )
     # Save CSV file for publish_data
     if save_data_to_file:
-        today = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
-        filename = 'opt_res_naive_mpc_'+today.strftime("%Y_%m_%d")+'.csv'
-    else: # Just save the latest optimization results
-        filename = 'opt_res_latest.csv'
+        today = datetime.now(timezone.utc).replace(
+            hour=0, minute=0, second=0, microsecond=0
+        )
+        filename = "opt_res_naive_mpc_" + today.strftime("%Y_%m_%d") + ".csv"
+    else:  # Just save the latest optimization results
+        filename = "opt_res_latest.csv"
     if not debug:
         opt_res_naive_mpc.to_csv(input_data_dict['emhass_conf']['data_path'] / filename, index_label='timestamp')
     return opt_res_naive_mpc
 
-def forecast_model_fit(input_data_dict: dict, logger: logging.Logger,
-    debug: Optional[bool] = False) -> Tuple[pd.DataFrame, pd.DataFrame, MLForecaster]:
+
+def forecast_model_fit(
+    input_data_dict: dict, logger: logging.Logger, debug: Optional[bool] = False
+) -> Tuple[pd.DataFrame, pd.DataFrame, MLForecaster]:
     """Perform a forecast model fit from training data retrieved from Home Assistant.
 
     :param input_data_dict: A dictionnary with multiple data used by the action functions
@@ -341,8 +461,9 @@ def forecast_model_fit(input_data_dict: dict, logger: logging.Logger,
     # The ML forecaster object
     mlf = MLForecaster(data, model_type, var_model, sklearn_model, num_lags, input_data_dict['emhass_conf'], logger)
     # Fit the ML model
-    df_pred, df_pred_backtest = mlf.fit(split_date_delta=split_date_delta, 
-                                        perform_backtest=perform_backtest)
+    df_pred, df_pred_backtest = mlf.fit(
+        split_date_delta=split_date_delta, perform_backtest=perform_backtest
+    )
     # Save model
     if not debug:
         filename = model_type+'_mlf.pkl'
@@ -351,9 +472,14 @@ def forecast_model_fit(input_data_dict: dict, logger: logging.Logger,
             pickle.dump(mlf, outp, pickle.HIGHEST_PROTOCOL)
     return df_pred, df_pred_backtest, mlf
 
-def forecast_model_predict(input_data_dict: dict, logger: logging.Logger,
-    use_last_window: Optional[bool] = True, debug: Optional[bool] = False,
-    mlf: Optional[MLForecaster] = None) -> pd.DataFrame:
+
+def forecast_model_predict(
+    input_data_dict: dict,
+    logger: logging.Logger,
+    use_last_window: Optional[bool] = True,
+    debug: Optional[bool] = False,
+    mlf: Optional[MLForecaster] = None,
+) -> pd.DataFrame:
     r"""Perform a forecast model predict using a previously trained skforecast model.
 
     :param input_data_dict: A dictionnary with multiple data used by the action functions
@@ -380,46 +506,73 @@ def forecast_model_predict(input_data_dict: dict, logger: logging.Logger,
     filename_path = input_data_dict['emhass_conf']['data_path'] / filename
     if not debug:
         if filename_path.is_file():
-            with open(filename_path, 'rb') as inp:
+            with open(filename_path, "rb") as inp:
                 mlf = pickle.load(inp)
         else:
-            logger.error("The ML forecaster file was not found, please run a model fit method before this predict method")
+            logger.error(
+                "The ML forecaster file was not found, please run a model fit method before this predict method",
+            )
             return
     # Make predictions
     if use_last_window:
-        data_last_window = copy.deepcopy(input_data_dict['df_input_data'])
+        data_last_window = copy.deepcopy(input_data_dict["df_input_data"])
     else:
         data_last_window = None
     predictions = mlf.predict(data_last_window)
     # Publish data to a Home Assistant sensor
-    model_predict_publish = input_data_dict['params']['passed_data']['model_predict_publish']
-    model_predict_entity_id = input_data_dict['params']['passed_data']['model_predict_entity_id']
-    model_predict_unit_of_measurement = input_data_dict['params']['passed_data']['model_predict_unit_of_measurement']
-    model_predict_friendly_name = input_data_dict['params']['passed_data']['model_predict_friendly_name']
-    publish_prefix = input_data_dict['params']['passed_data']['publish_prefix']
+    model_predict_publish = input_data_dict["params"]["passed_data"][
+        "model_predict_publish"
+    ]
+    model_predict_entity_id = input_data_dict["params"]["passed_data"][
+        "model_predict_entity_id"
+    ]
+    model_predict_unit_of_measurement = input_data_dict["params"]["passed_data"][
+        "model_predict_unit_of_measurement"
+    ]
+    model_predict_friendly_name = input_data_dict["params"]["passed_data"][
+        "model_predict_friendly_name"
+    ]
+    publish_prefix = input_data_dict["params"]["passed_data"]["publish_prefix"]
     if model_predict_publish is True:
         # Estimate the current index
-        now_precise = datetime.now(input_data_dict['retrieve_hass_conf']['time_zone']).replace(second=0, microsecond=0)
-        if input_data_dict['retrieve_hass_conf']['method_ts_round'] == 'nearest':
-            idx_closest = predictions.index.get_indexer([now_precise], method='nearest')[0]
-        elif input_data_dict['retrieve_hass_conf']['method_ts_round'] == 'first':
-            idx_closest = predictions.index.get_indexer([now_precise], method='ffill')[0]
-        elif input_data_dict['retrieve_hass_conf']['method_ts_round'] == 'last':
-            idx_closest = predictions.index.get_indexer([now_precise], method='bfill')[0]
+        now_precise = datetime.now(
+            input_data_dict["retrieve_hass_conf"]["time_zone"]
+        ).replace(second=0, microsecond=0)
+        if input_data_dict["retrieve_hass_conf"]["method_ts_round"] == "nearest":
+            idx_closest = predictions.index.get_indexer(
+                [now_precise], method="nearest"
+            )[0]
+        elif input_data_dict["retrieve_hass_conf"]["method_ts_round"] == "first":
+            idx_closest = predictions.index.get_indexer([now_precise], method="ffill")[
+                0
+            ]
+        elif input_data_dict["retrieve_hass_conf"]["method_ts_round"] == "last":
+            idx_closest = predictions.index.get_indexer([now_precise], method="bfill")[
+                0
+            ]
         if idx_closest == -1:
-            idx_closest = predictions.index.get_indexer([now_precise], method='nearest')[0]
+            idx_closest = predictions.index.get_indexer(
+                [now_precise], method="nearest"
+            )[0]
         # Publish Load forecast
-        input_data_dict['rh'].post_data(predictions, idx_closest, 
-                                        model_predict_entity_id,
-                                        model_predict_unit_of_measurement, 
-                                        model_predict_friendly_name,
-                                        type_var = 'mlforecaster',
-                                        publish_prefix=publish_prefix)
+        input_data_dict["rh"].post_data(
+            predictions,
+            idx_closest,
+            model_predict_entity_id,
+            model_predict_unit_of_measurement,
+            model_predict_friendly_name,
+            type_var="mlforecaster",
+            publish_prefix=publish_prefix,
+        )
     return predictions
 
-def forecast_model_tune(input_data_dict: dict, logger: logging.Logger,
-    debug: Optional[bool] = False, mlf: Optional[MLForecaster] = None
-    ) -> Tuple[pd.DataFrame, MLForecaster]:
+
+def forecast_model_tune(
+    input_data_dict: dict,
+    logger: logging.Logger,
+    debug: Optional[bool] = False,
+    mlf: Optional[MLForecaster] = None,
+) -> Tuple[pd.DataFrame, MLForecaster]:
     """Tune a forecast model hyperparameters using bayesian optimization.
 
     :param input_data_dict: A dictionnary with multiple data used by the action functions
@@ -440,10 +593,12 @@ def forecast_model_tune(input_data_dict: dict, logger: logging.Logger,
     filename_path = input_data_dict['emhass_conf']['data_path'] / filename
     if not debug:
         if filename_path.is_file():
-            with open(filename_path, 'rb') as inp:
+            with open(filename_path, "rb") as inp:
                 mlf = pickle.load(inp)
         else:
-            logger.error("The ML forecaster file was not found, please run a model fit method before this tune method")
+            logger.error(
+                "The ML forecaster file was not found, please run a model fit method before this tune method",
+            )
             return None, None
     # Tune the model
     df_pred_optim = mlf.tune(debug=debug)
@@ -455,12 +610,139 @@ def forecast_model_tune(input_data_dict: dict, logger: logging.Logger,
             pickle.dump(mlf, outp, pickle.HIGHEST_PROTOCOL)       
     return df_pred_optim, mlf
 
-def publish_data(input_data_dict: dict, logger: logging.Logger,
-    save_data_to_file: Optional[bool] = False, 
-    opt_res_latest: Optional[pd.DataFrame] = None) -> pd.DataFrame:
+
+def regressor_model_fit(
+    input_data_dict: dict,
+    logger: logging.Logger,
+    debug: Optional[bool] = False,
+) -> None:
+    """Perform a forecast model fit from training data retrieved from Home Assistant.
+
+    :param input_data_dict: A dictionnary with multiple data used by the action functions
+    :type input_data_dict: dict
+    :param logger: The passed logger object
+    :type logger: logging.Logger
+    :param debug: True to debug, useful for unit testing, defaults to False
+    :type debug: Optional[bool], optional
     """
-    Publish the data obtained from the optimization results.
+    data = copy.deepcopy(input_data_dict["df_input_data"])
+    if "model_type" in input_data_dict["params"]["passed_data"]:
+        model_type = input_data_dict["params"]["passed_data"]["model_type"]
+    else:
+        logger.error("parameter: 'model_type' not passed")
+        return False
+    if "regression_model" in input_data_dict["params"]["passed_data"]:
+        regression_model = input_data_dict["params"]["passed_data"]["regression_model"]
+    else:
+        logger.error("parameter: 'regression_model' not passed")
+        return False
+    if "features" in input_data_dict["params"]["passed_data"]:
+        features = input_data_dict["params"]["passed_data"]["features"]
+    else:
+        logger.error("parameter: 'features' not passed")
+        return False    
+    if "target" in input_data_dict["params"]["passed_data"]:
+        target = input_data_dict["params"]["passed_data"]["target"]
+    else:
+        logger.error("parameter: 'target' not passed")
+        return False        
+    if "timestamp" in input_data_dict["params"]["passed_data"]:
+        timestamp = input_data_dict["params"]["passed_data"]["timestamp"]
+    else:
+        logger.error("parameter: 'timestamp' not passed")
+        return False       
+    if "date_features" in input_data_dict["params"]["passed_data"]:
+        date_features = input_data_dict["params"]["passed_data"]["date_features"]
+    else:
+        logger.error("parameter: 'date_features' not passed")
+        return False           
+
+    # The MLRegressor object
+    mlr = MLRegressor(
+        data,
+        model_type,
+        regression_model,
+        features,
+        target,
+        timestamp,
+        logger,
+    )
+    # Fit the ML model
+    mlr.fit(date_features=date_features)
+    # Save model
+    if not debug:
+        filename = model_type + "_mlr.pkl"
+        filename_path = input_data_dict["emhass_conf"]["data_path"] / filename
+        with open(filename_path, "wb") as outp:
+            pickle.dump(mlr, outp, pickle.HIGHEST_PROTOCOL)
+    return mlr
+
+
+def regressor_model_predict(
+    input_data_dict: dict,
+    logger: logging.Logger,
+    debug: Optional[bool] = False,
+    mlr: Optional[MLRegressor] = None,
+) -> None:
+    """Perform a prediction from csv file.
+
+    :param input_data_dict: A dictionnary with multiple data used by the action functions
+    :type input_data_dict: dict
+    :param logger: The passed logger object
+    :type logger: logging.Logger
+    :param debug: True to debug, useful for unit testing, defaults to False
+    :type debug: Optional[bool], optional
+    """
+    if "model_type" in input_data_dict["params"]["passed_data"]:
+        model_type = input_data_dict["params"]["passed_data"]["model_type"]
+    else:
+        logger.error("parameter: 'model_type' not passed")
+        return False   
+    filename = model_type + "_mlr.pkl"
+    filename_path = input_data_dict["emhass_conf"]["data_path"] / filename
+    if not debug:
+        if filename_path.is_file():
+            with open(filename_path, "rb") as inp:
+                mlr = pickle.load(inp)
+        else:
+            logger.error(
+                "The ML forecaster file was not found, please run a model fit method before this predict method",
+            )
+            return False
+    if "new_values" in input_data_dict["params"]["passed_data"]:    
+        new_values = input_data_dict["params"]["passed_data"]["new_values"]
+    else:
+        logger.error("parameter: 'new_values' not passed")
+        return False         
+    # Predict from csv file
+    prediction = mlr.predict(new_values)
     
+    mlr_predict_entity_id = input_data_dict["params"]["passed_data"].get("mlr_predict_entity_id","sensor.mlr_predict")
+    mlr_predict_unit_of_measurement = input_data_dict["params"]["passed_data"].get("mlr_predict_unit_of_measurement","h")
+    mlr_predict_friendly_name = input_data_dict["params"]["passed_data"].get("mlr_predict_friendly_name","mlr predictor")
+    # Publish prediction
+    idx = 0
+    if not debug:
+        input_data_dict["rh"].post_data(
+            prediction,
+            idx,
+            mlr_predict_entity_id,
+            mlr_predict_unit_of_measurement,
+            mlr_predict_friendly_name,
+            type_var="mlregressor",
+        )
+    return prediction
+
+
+def publish_data(
+    input_data_dict: dict,
+    logger: logging.Logger,
+    save_data_to_file: Optional[bool] = False,
+    opt_res_latest: Optional[pd.DataFrame] = None,
+) -> pd.DataFrame:
+    """
+    Publish the data obtained from the optimization results.
+
     :param input_data_dict:  A dictionnary with multiple data used by the action functions
     :type input_data_dict: dict
     :param logger: The passed logger object
@@ -474,10 +756,12 @@ def publish_data(input_data_dict: dict, logger: logging.Logger,
     logger.info("Publishing data to HASS instance")
     # Check if a day ahead optimization has been performed (read CSV file)
     if save_data_to_file:
-        today = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
-        filename = 'opt_res_dayahead_'+today.strftime("%Y_%m_%d")+'.csv'
+        today = datetime.now(timezone.utc).replace(
+            hour=0, minute=0, second=0, microsecond=0
+        )
+        filename = "opt_res_dayahead_" + today.strftime("%Y_%m_%d") + ".csv"
     else:
-        filename = 'opt_res_latest.csv'
+        filename = "opt_res_latest.csv"
     if opt_res_latest is None:
         if not os.path.isfile(input_data_dict['emhass_conf']['data_path'] / filename):
             logger.error("File not found error, run an optimization task first.")
@@ -485,144 +769,191 @@ def publish_data(input_data_dict: dict, logger: logging.Logger,
         else:
             opt_res_latest = pd.read_csv(input_data_dict['emhass_conf']['data_path'] / filename, index_col='timestamp')
             opt_res_latest.index = pd.to_datetime(opt_res_latest.index)
-            opt_res_latest.index.freq = input_data_dict['retrieve_hass_conf']['freq']
+            opt_res_latest.index.freq = input_data_dict["retrieve_hass_conf"]["freq"]
     # Estimate the current index
-    now_precise = datetime.now(input_data_dict['retrieve_hass_conf']['time_zone']).replace(second=0, microsecond=0)
-    if input_data_dict['retrieve_hass_conf']['method_ts_round'] == 'nearest':
-        idx_closest = opt_res_latest.index.get_indexer([now_precise], method='nearest')[0]
-    elif input_data_dict['retrieve_hass_conf']['method_ts_round'] == 'first':
-        idx_closest = opt_res_latest.index.get_indexer([now_precise], method='ffill')[0]
-    elif input_data_dict['retrieve_hass_conf']['method_ts_round'] == 'last':
-        idx_closest = opt_res_latest.index.get_indexer([now_precise], method='bfill')[0]
+    now_precise = datetime.now(
+        input_data_dict["retrieve_hass_conf"]["time_zone"]
+    ).replace(second=0, microsecond=0)
+    if input_data_dict["retrieve_hass_conf"]["method_ts_round"] == "nearest":
+        idx_closest = opt_res_latest.index.get_indexer([now_precise], method="nearest")[
+            0
+        ]
+    elif input_data_dict["retrieve_hass_conf"]["method_ts_round"] == "first":
+        idx_closest = opt_res_latest.index.get_indexer([now_precise], method="ffill")[0]
+    elif input_data_dict["retrieve_hass_conf"]["method_ts_round"] == "last":
+        idx_closest = opt_res_latest.index.get_indexer([now_precise], method="bfill")[0]
     if idx_closest == -1:
-        idx_closest = opt_res_latest.index.get_indexer([now_precise], method='nearest')[0]
+        idx_closest = opt_res_latest.index.get_indexer([now_precise], method="nearest")[
+            0
+        ]
     # Publish the data
-    params = json.loads(input_data_dict['params'])
-    publish_prefix = params['passed_data']['publish_prefix']
+    params = json.loads(input_data_dict["params"])
+    publish_prefix = params["passed_data"]["publish_prefix"]
     # Publish PV forecast
-    custom_pv_forecast_id = params['passed_data']['custom_pv_forecast_id']
-    input_data_dict['rh'].post_data(opt_res_latest['P_PV'], idx_closest, 
-                                    custom_pv_forecast_id["entity_id"], 
-                                    custom_pv_forecast_id["unit_of_measurement"],
-                                    custom_pv_forecast_id["friendly_name"],
-                                    type_var = 'power',
-                                    publish_prefix = publish_prefix)
+    custom_pv_forecast_id = params["passed_data"]["custom_pv_forecast_id"]
+    input_data_dict["rh"].post_data(
+        opt_res_latest["P_PV"],
+        idx_closest,
+        custom_pv_forecast_id["entity_id"],
+        custom_pv_forecast_id["unit_of_measurement"],
+        custom_pv_forecast_id["friendly_name"],
+        type_var="power",
+        publish_prefix=publish_prefix,
+    )
     # Publish Load forecast
-    custom_load_forecast_id = params['passed_data']['custom_load_forecast_id']
-    input_data_dict['rh'].post_data(opt_res_latest['P_Load'], idx_closest, 
-                                    custom_load_forecast_id["entity_id"], 
-                                    custom_load_forecast_id["unit_of_measurement"],
-                                    custom_load_forecast_id["friendly_name"],
-                                    type_var = 'power',
-                                    publish_prefix = publish_prefix)
-    cols_published = ['P_PV', 'P_Load']
+    custom_load_forecast_id = params["passed_data"]["custom_load_forecast_id"]
+    input_data_dict["rh"].post_data(
+        opt_res_latest["P_Load"],
+        idx_closest,
+        custom_load_forecast_id["entity_id"],
+        custom_load_forecast_id["unit_of_measurement"],
+        custom_load_forecast_id["friendly_name"],
+        type_var="power",
+        publish_prefix=publish_prefix,
+    )
+    cols_published = ["P_PV", "P_Load"]
     # Publish deferrable loads
-    custom_deferrable_forecast_id = params['passed_data']['custom_deferrable_forecast_id']
-    for k in range(input_data_dict['opt'].optim_conf['num_def_loads']):
+    custom_deferrable_forecast_id = params["passed_data"][
+        "custom_deferrable_forecast_id"
+    ]
+    for k in range(input_data_dict["opt"].optim_conf["num_def_loads"]):
         if "P_deferrable{}".format(k) not in opt_res_latest.columns:
-            logger.error("P_deferrable{}".format(k)+" was not found in results DataFrame. Optimization task may need to be relaunched or it did not converge to a solution.")
+            logger.error(
+                "P_deferrable{}".format(k)
+                + " was not found in results DataFrame. Optimization task may need to be relaunched or it did not converge to a solution.",
+            )
         else:
-            input_data_dict['rh'].post_data(opt_res_latest["P_deferrable{}".format(k)], idx_closest, 
-                                            custom_deferrable_forecast_id[k]["entity_id"], 
-                                            custom_deferrable_forecast_id[k]["unit_of_measurement"],
-                                            custom_deferrable_forecast_id[k]["friendly_name"],
-                                            type_var = 'deferrable',
-                                            publish_prefix = publish_prefix)
-            cols_published = cols_published+["P_deferrable{}".format(k)]
+            input_data_dict["rh"].post_data(
+                opt_res_latest["P_deferrable{}".format(k)],
+                idx_closest,
+                custom_deferrable_forecast_id[k]["entity_id"],
+                custom_deferrable_forecast_id[k]["unit_of_measurement"],
+                custom_deferrable_forecast_id[k]["friendly_name"],
+                type_var="deferrable",
+                publish_prefix=publish_prefix,
+            )
+            cols_published = cols_published + ["P_deferrable{}".format(k)]
     # Publish battery power
-    if input_data_dict['opt'].optim_conf['set_use_battery']:
-        if 'P_batt' not in opt_res_latest.columns:
-            logger.error("P_batt was not found in results DataFrame. Optimization task may need to be relaunched or it did not converge to a solution.")
+    if input_data_dict["opt"].optim_conf["set_use_battery"]:
+        if "P_batt" not in opt_res_latest.columns:
+            logger.error(
+                "P_batt was not found in results DataFrame. Optimization task may need to be relaunched or it did not converge to a solution.",
+            )
         else:
-            custom_batt_forecast_id = params['passed_data']['custom_batt_forecast_id']
-            input_data_dict['rh'].post_data(opt_res_latest['P_batt'], idx_closest,
-                                            custom_batt_forecast_id["entity_id"], 
-                                            custom_batt_forecast_id["unit_of_measurement"],
-                                            custom_batt_forecast_id["friendly_name"],
-                                            type_var = 'batt',
-                                            publish_prefix = publish_prefix)
-            cols_published = cols_published+["P_batt"]
-            custom_batt_soc_forecast_id = params['passed_data']['custom_batt_soc_forecast_id']
-            input_data_dict['rh'].post_data(opt_res_latest['SOC_opt']*100, idx_closest,
-                                            custom_batt_soc_forecast_id["entity_id"], 
-                                            custom_batt_soc_forecast_id["unit_of_measurement"],
-                                            custom_batt_soc_forecast_id["friendly_name"],
-                                            type_var = 'SOC',
-                                            publish_prefix = publish_prefix)
-            cols_published = cols_published+["SOC_opt"]
+            custom_batt_forecast_id = params["passed_data"]["custom_batt_forecast_id"]
+            input_data_dict["rh"].post_data(
+                opt_res_latest["P_batt"],
+                idx_closest,
+                custom_batt_forecast_id["entity_id"],
+                custom_batt_forecast_id["unit_of_measurement"],
+                custom_batt_forecast_id["friendly_name"],
+                type_var="batt",
+                publish_prefix=publish_prefix,
+            )
+            cols_published = cols_published + ["P_batt"]
+            custom_batt_soc_forecast_id = params["passed_data"][
+                "custom_batt_soc_forecast_id"
+            ]
+            input_data_dict["rh"].post_data(
+                opt_res_latest["SOC_opt"] * 100,
+                idx_closest,
+                custom_batt_soc_forecast_id["entity_id"],
+                custom_batt_soc_forecast_id["unit_of_measurement"],
+                custom_batt_soc_forecast_id["friendly_name"],
+                type_var="SOC",
+                publish_prefix=publish_prefix,
+            )
+            cols_published = cols_published + ["SOC_opt"]
     # Publish grid power
-    custom_grid_forecast_id = params['passed_data']['custom_grid_forecast_id']
-    input_data_dict['rh'].post_data(opt_res_latest['P_grid'], idx_closest, 
-                                    custom_grid_forecast_id["entity_id"], 
-                                    custom_grid_forecast_id["unit_of_measurement"],
-                                    custom_grid_forecast_id["friendly_name"],
-                                    type_var = 'power',
-                                    publish_prefix = publish_prefix)
-    cols_published = cols_published+["P_grid"]
+    custom_grid_forecast_id = params["passed_data"]["custom_grid_forecast_id"]
+    input_data_dict["rh"].post_data(
+        opt_res_latest["P_grid"],
+        idx_closest,
+        custom_grid_forecast_id["entity_id"],
+        custom_grid_forecast_id["unit_of_measurement"],
+        custom_grid_forecast_id["friendly_name"],
+        type_var="power",
+        publish_prefix=publish_prefix,
+    )
+    cols_published = cols_published + ["P_grid"]
     # Publish total value of cost function
-    custom_cost_fun_id = params['passed_data']['custom_cost_fun_id']
-    col_cost_fun = [i for i in opt_res_latest.columns if 'cost_fun_' in i]
-    input_data_dict['rh'].post_data(opt_res_latest[col_cost_fun], idx_closest, 
-                                    custom_cost_fun_id["entity_id"], 
-                                    custom_cost_fun_id["unit_of_measurement"],
-                                    custom_cost_fun_id["friendly_name"],
-                                    type_var = 'cost_fun',
-                                    publish_prefix = publish_prefix)
+    custom_cost_fun_id = params["passed_data"]["custom_cost_fun_id"]
+    col_cost_fun = [i for i in opt_res_latest.columns if "cost_fun_" in i]
+    input_data_dict["rh"].post_data(
+        opt_res_latest[col_cost_fun],
+        idx_closest,
+        custom_cost_fun_id["entity_id"],
+        custom_cost_fun_id["unit_of_measurement"],
+        custom_cost_fun_id["friendly_name"],
+        type_var="cost_fun",
+        publish_prefix=publish_prefix,
+    )
     # Publish the optimization status
-    custom_cost_fun_id = params['passed_data']['custom_optim_status_id']
+    custom_cost_fun_id = params["passed_data"]["custom_optim_status_id"]
     if "optim_status" not in opt_res_latest:
-        opt_res_latest["optim_status"] = 'Optimal'
-        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)
-    cols_published = cols_published+["optim_status"]
+        opt_res_latest["optim_status"] = "Optimal"
+        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,
+    )
+    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(opt_res_latest['unit_load_cost'], idx_closest, 
-                                    custom_unit_load_cost_id["entity_id"], 
-                                    custom_unit_load_cost_id["unit_of_measurement"],
-                                    custom_unit_load_cost_id["friendly_name"],
-                                    type_var = 'unit_load_cost',
-                                    publish_prefix = publish_prefix)
-    cols_published = cols_published+["unit_load_cost"]
+    custom_unit_load_cost_id = params["passed_data"]["custom_unit_load_cost_id"]
+    input_data_dict["rh"].post_data(
+        opt_res_latest["unit_load_cost"],
+        idx_closest,
+        custom_unit_load_cost_id["entity_id"],
+        custom_unit_load_cost_id["unit_of_measurement"],
+        custom_unit_load_cost_id["friendly_name"],
+        type_var="unit_load_cost",
+        publish_prefix=publish_prefix,
+    )
+    cols_published = cols_published + ["unit_load_cost"]
     # Publish unit_prod_price
-    custom_unit_prod_price_id = params['passed_data']['custom_unit_prod_price_id']
-    input_data_dict['rh'].post_data(opt_res_latest['unit_prod_price'], idx_closest, 
-                                    custom_unit_prod_price_id["entity_id"], 
-                                    custom_unit_prod_price_id["unit_of_measurement"],
-                                    custom_unit_prod_price_id["friendly_name"],
-                                    type_var = 'unit_prod_price',
-                                    publish_prefix = publish_prefix)
-    cols_published = cols_published+["unit_prod_price"]
+    custom_unit_prod_price_id = params["passed_data"]["custom_unit_prod_price_id"]
+    input_data_dict["rh"].post_data(
+        opt_res_latest["unit_prod_price"],
+        idx_closest,
+        custom_unit_prod_price_id["entity_id"],
+        custom_unit_prod_price_id["unit_of_measurement"],
+        custom_unit_prod_price_id["friendly_name"],
+        type_var="unit_prod_price",
+        publish_prefix=publish_prefix,
+    )
+    cols_published = cols_published + ["unit_prod_price"]
     # Create a DF resuming what has been published
     opt_res = opt_res_latest[cols_published].loc[[opt_res_latest.index[idx_closest]]]
     return opt_res
-    
-        
+
+
 def main():
     r"""Define the main command line entry function.
 
     This function may take several arguments as inputs. You can type `emhass --help` to see the list of options:
-    
+
     - action: Set the desired action, options are: perfect-optim, dayahead-optim,
       naive-mpc-optim, publish-data, forecast-model-fit, forecast-model-predict, forecast-model-tune
-      
+
     - config: Define path to the config.yaml file
-    
+
     - costfun: Define the type of cost function, options are: profit, cost, self-consumption
-    
+
     - log2file: Define if we should log to a file or not
-    
+
     - params: Configuration parameters passed from data/options.json if using the add-on
-    
+
     - runtimeparams: Pass runtime optimization parameters as dictionnary
-    
+
     - debug: Use True for testing purposes
-    
+
     """
     # Parsing arguments
     parser = argparse.ArgumentParser()
@@ -683,39 +1014,66 @@ def main():
 
     # Additionnal argument
     try:
-        parser.add_argument('--version', action='version', version='%(prog)s '+version('emhass'))
+        parser.add_argument(
+            "--version",
+            action="version",
+            version="%(prog)s " + version("emhass"),
+        )
         args = parser.parse_args()
     except Exception:
-        logger.info("Version not found for emhass package. Or importlib exited with PackageNotFoundError.")
+        logger.info(
+            "Version not found for emhass package. Or importlib exited with PackageNotFoundError.",
+        )
     # Setup parameters
     input_data_dict = set_input_data_dict(emhass_conf, 
                                           args.costfun, args.params, args.runtimeparams, args.action, 
                                           logger, args.debug)
     # Perform selected action
-    if args.action == 'perfect-optim':
+    if args.action == "perfect-optim":
         opt_res = perfect_forecast_optim(input_data_dict, logger, debug=args.debug)
-    elif args.action == 'dayahead-optim':
+    elif args.action == "dayahead-optim":
         opt_res = dayahead_forecast_optim(input_data_dict, logger, debug=args.debug)
-    elif args.action == 'naive-mpc-optim':
+    elif args.action == "naive-mpc-optim":
         opt_res = naive_mpc_optim(input_data_dict, logger, debug=args.debug)
-    elif args.action == 'forecast-model-fit':
-        df_fit_pred, df_fit_pred_backtest, mlf = forecast_model_fit(input_data_dict, logger, debug=args.debug)
+    elif args.action == "forecast-model-fit":
+        df_fit_pred, df_fit_pred_backtest, mlf = forecast_model_fit(
+            input_data_dict, logger, debug=args.debug
+        )
         opt_res = None
-    elif args.action == 'forecast-model-predict':
+    elif args.action == "forecast-model-predict":
         if args.debug:
             _, _, mlf = forecast_model_fit(input_data_dict, logger, debug=args.debug)
         else:
             mlf = None
-        df_pred = forecast_model_predict(input_data_dict, logger, debug=args.debug, mlf=mlf)
+        df_pred = forecast_model_predict(
+            input_data_dict, logger, debug=args.debug, mlf=mlf
+        )
         opt_res = None
-    elif args.action == 'forecast-model-tune':
+    elif args.action == "forecast-model-tune":
         if args.debug:
             _, _, mlf = forecast_model_fit(input_data_dict, logger, debug=args.debug)
         else:
             mlf = None
-        df_pred_optim, mlf = forecast_model_tune(input_data_dict, logger, debug=args.debug, mlf=mlf)
+        df_pred_optim, mlf = forecast_model_tune(
+            input_data_dict, logger, debug=args.debug, mlf=mlf
+        )
+        opt_res = None
+    elif args.action == "regressor-model-fit":
+        mlr = regressor_model_fit(input_data_dict, logger, debug=args.debug)
+        opt_res = None
+    elif args.action == "regressor-model-predict":
+        if args.debug:
+            mlr = regressor_model_fit(input_data_dict, logger, debug=args.debug)
+        else:
+            mlr = None
+        prediction = regressor_model_predict(
+            input_data_dict,
+            logger,
+            debug=args.debug,
+            mlr=mlr,
+        )
         opt_res = None
-    elif args.action == 'publish-data':
+    elif args.action == "publish-data":
         opt_res = publish_data(input_data_dict, logger)
     else:
         logger.error("The passed action argument is not valid")
@@ -725,17 +1083,26 @@ def main():
     # Flush the logger
     ch.close()
     logger.removeHandler(ch)
-    if args.action == 'perfect-optim' or args.action == 'dayahead-optim' or \
-        args.action == 'naive-mpc-optim' or args.action == 'publish-data':
+    if (
+        args.action == "perfect-optim"
+        or args.action == "dayahead-optim"
+        or args.action == "naive-mpc-optim"
+        or args.action == "publish-data"
+    ):
         return opt_res
-    elif args.action == 'forecast-model-fit':
+    elif args.action == "forecast-model-fit":
         return df_fit_pred, df_fit_pred_backtest, mlf
-    elif args.action == 'forecast-model-predict':
+    elif args.action == "forecast-model-predict":
         return df_pred
-    elif args.action == 'forecast-model-tune':
+    elif args.action == "regressor-model-fit":
+        return mlr
+    elif args.action == "regressor-model-predict":
+        return prediction
+    elif args.action == "forecast-model-tune":
         return df_pred_optim, mlf
     else: 
         return opt_res
 
-if __name__ == '__main__':
+
+if __name__ == "__main__":
     main()
diff --git a/src/emhass/machine_learning_regressor.py b/src/emhass/machine_learning_regressor.py
new file mode 100644
index 00000000..f0d3c532
--- /dev/null
+++ b/src/emhass/machine_learning_regressor.py
@@ -0,0 +1,290 @@
+"""Machine learning regressor module."""
+
+from __future__ import annotations
+
+import copy
+import time
+import warnings
+from typing import TYPE_CHECKING
+
+import numpy as np
+import pandas as pd
+from sklearn.ensemble import (
+    AdaBoostRegressor,
+    GradientBoostingRegressor,
+    RandomForestRegressor,
+)
+from sklearn.linear_model import Lasso, LinearRegression, Ridge
+from sklearn.metrics import r2_score
+from sklearn.model_selection import GridSearchCV, train_test_split
+from sklearn.pipeline import make_pipeline
+from sklearn.preprocessing import StandardScaler
+
+if TYPE_CHECKING:
+    import logging
+
+warnings.filterwarnings("ignore", category=DeprecationWarning)
+
+REGRESSION_METHODS = {
+    "LinearRegression": {
+        "model": LinearRegression(),
+        "param_grid": {
+            "linearregression__fit_intercept": [True, False],
+            "linearregression__positive": [True, False],
+        },
+    },
+    "RidgeRegression": {
+        "model": Ridge(),
+        "param_grid": {"ridge__alpha": [0.1, 1.0, 10.0]},
+    },
+    "LassoRegression": {
+        "model": Lasso(),
+        "param_grid": {"lasso__alpha": [0.1, 1.0, 10.0]},
+    },
+    "RandomForestRegression": {
+        "model": RandomForestRegressor(),
+        "param_grid": {"randomforestregressor__n_estimators": [50, 100, 200]},
+    },
+    "GradientBoostingRegression": {
+        "model": GradientBoostingRegressor(),
+        "param_grid": {
+            "gradientboostingregressor__n_estimators": [50, 100, 200],
+            "gradientboostingregressor__learning_rate": [0.01, 0.1, 0.2],
+        },
+    },
+    "AdaBoostRegression": {
+        "model": AdaBoostRegressor(),
+        "param_grid": {
+            "adaboostregressor__n_estimators": [50, 100, 200],
+            "adaboostregressor__learning_rate": [0.01, 0.1, 0.2],
+        },
+    },
+}
+
+
+class MLRegressor:
+    r"""A forecaster class using machine learning models.
+
+    This class uses the `sklearn` module and the machine learning models are \
+        from `scikit-learn`.
+
+    It exposes two main methods:
+
+    - `fit`: to train a model with the passed data.
+
+    - `predict`: to obtain a forecast from a pre-trained model.
+
+    """
+
+    def __init__(  # noqa: PLR0913
+        self: MLRegressor,
+        data: pd.DataFrame,
+        model_type: str,
+        regression_model: str,
+        features: list,
+        target: str,
+        timestamp: str,
+        logger: logging.Logger,
+    ) -> None:
+        r"""Define constructor for the forecast class.
+
+        :param data: The data that will be used for train/test
+        :type data: pd.DataFrame
+        :param model_type: A unique name defining this model and useful to identify \
+            for what it will be used for.
+        :type model_type: str
+        :param regression_model: The model that will be used. For now only \
+            this options are possible: `LinearRegression`, `RidgeRegression`, \
+            `LassoRegression`, `RandomForestRegression`, \
+            `GradientBoostingRegression` and `AdaBoostRegression`.
+        :type regression_model: str
+        :param features: A list of features. \
+            Example: [`solar_production`, `degree_days`].
+        :type features: list
+        :param target: The target(to be predicted). \
+            Example: `heating_hours`.
+        :type target: str
+        :param timestamp: If defined, the column key that has to be used of timestamp.
+        :type timestamp: str
+        :param logger: The passed logger object
+        :type logger: logging.Logger
+        """
+        self.data = data
+        self.features = features
+        self.target = target
+        self.timestamp = timestamp
+        self.model_type = model_type
+        self.regression_model = regression_model
+        self.logger = logger
+        self.data = self.data.sort_index()
+        self.data = self.data[~self.data.index.duplicated(keep="first")]
+        self.data_exo = None
+        self.steps = None
+        self.model = None
+        self.grid_search = None
+
+    @staticmethod
+    def add_date_features(
+        data: pd.DataFrame,
+        date_features: list,
+        timestamp: str,
+    ) -> pd.DataFrame:
+        """Add date features from the input DataFrame timestamp.
+
+        :param data: The input DataFrame
+        :type data: pd.DataFrame
+        :param timestamp: The column containing the timestamp
+        :type timestamp: str
+        :return: The DataFrame with the added features
+        :rtype: pd.DataFrame
+        """
+        df = copy.deepcopy(data)  # noqa: PD901
+        df[timestamp] = pd.to_datetime(df["timestamp"])
+        if "year" in date_features:
+            df["year"] = [i.year for i in df["timestamp"]]
+        if "month" in date_features:
+            df["month"] = [i.month for i in df["timestamp"]]
+        if "day_of_week" in date_features:
+            df["day_of_week"] = [i.dayofweek for i in df["timestamp"]]
+        if "day_of_year" in date_features:
+            df["day_of_year"] = [i.dayofyear for i in df["timestamp"]]
+        if "day" in date_features:
+            df["day"] = [i.day for i in df["timestamp"]]
+        if "hour" in date_features:
+            df["hour"] = [i.day for i in df["timestamp"]]
+
+        return df
+
+    def get_regression_model(self: MLRegressor) -> tuple[str, str]:
+        """Get the base model and parameter grid for the specified regression model.
+
+        Returns a tuple containing the base model and parameter grid corresponding to \
+            the specified regression model.
+
+        Args:
+        ----
+            self: The instance of the MLRegressor class.
+
+        Returns:
+        -------
+            A tuple containing the base model and parameter grid.
+
+        """
+        if self.regression_model == "LinearRegression":
+            base_model = REGRESSION_METHODS["LinearRegression"]["model"]
+            param_grid = REGRESSION_METHODS["LinearRegression"]["param_grid"]
+        elif self.regression_model == "RidgeRegression":
+            base_model = REGRESSION_METHODS["RidgeRegression"]["model"]
+            param_grid = REGRESSION_METHODS["RidgeRegression"]["param_grid"]
+        elif self.regression_model == "LassoRegression":
+            base_model = REGRESSION_METHODS["LassoRegression"]["model"]
+            param_grid = REGRESSION_METHODS["LassoRegression"]["param_grid"]
+        elif self.regression_model == "RandomForestRegression":
+            base_model = REGRESSION_METHODS["RandomForestRegression"]["model"]
+            param_grid = REGRESSION_METHODS["RandomForestRegression"]["param_grid"]
+        elif self.regression_model == "GradientBoostingRegression":
+            base_model = REGRESSION_METHODS["GradientBoostingRegression"]["model"]
+            param_grid = REGRESSION_METHODS["GradientBoostingRegression"]["param_grid"]
+        elif self.regression_model == "AdaBoostRegression":
+            base_model = REGRESSION_METHODS["AdaBoostRegression"]["model"]
+            param_grid = REGRESSION_METHODS["AdaBoostRegression"]["param_grid"]
+        else:
+            self.logger.error(
+                "Passed model %s is not valid",
+                self.regression_model,
+            )
+            return None
+        return base_model, param_grid
+
+    def fit(self: MLRegressor, date_features: list | None = None) -> None:
+        """Fit the model using the provided data.
+
+        :param date_features: A list of 'date_features' to take into account when \
+            fitting the model.
+        :type data: list
+        """
+        self.logger.info("Performing a MLRegressor fit for %s", self.model_type)
+        self.data_exo = pd.DataFrame(self.data)
+        self.data_exo[self.features] = self.data[self.features]
+        self.data_exo[self.target] = self.data[self.target]
+        keep_columns = []
+        keep_columns.extend(self.features)
+        if self.timestamp is not None:
+            keep_columns.append(self.timestamp)
+        keep_columns.append(self.target)
+        self.data_exo = self.data_exo[self.data_exo.columns.intersection(keep_columns)]
+        self.data_exo = self.data_exo.reset_index(drop=True)
+        if date_features is not None:
+            if self.timestamp is not None:
+                self.data_exo = MLRegressor.add_date_features(
+                    self.data_exo,
+                    date_features,
+                    self.timestamp,
+                )
+            else:
+                self.logger.error(
+                    "If no timestamp provided, you can't use date_features, going \
+                    further without date_features.",
+                )
+
+        y = self.data_exo[self.target]
+        self.data_exo = self.data_exo.drop(self.target, axis=1)
+        if self.timestamp is not None:
+            self.data_exo = self.data_exo.drop(self.timestamp, axis=1)
+        X = self.data_exo  # noqa: N806
+
+        X_train, X_test, y_train, y_test = train_test_split(  # noqa: N806
+            X,
+            y,
+            test_size=0.2,
+            random_state=42,
+        )
+
+        self.steps = len(X_test)
+
+        base_model, param_grid = self.get_regression_model()
+
+        self.model = make_pipeline(StandardScaler(), base_model)
+
+        # Create a grid search object
+        self.grid_search = GridSearchCV(
+            self.model,
+            param_grid,
+            cv=5,
+            scoring="neg_mean_squared_error",
+            refit=True,
+            verbose=0,
+            n_jobs=-1,
+        )
+
+        # Fit the grid search object to the data
+        self.logger.info("Training a %s model", self.regression_model)
+        start_time = time.time()
+        self.grid_search.fit(X_train.values, y_train.values)
+        self.logger.info("Elapsed time for model fit: %s", time.time() - start_time)
+
+        self.model = self.grid_search.best_estimator_
+
+        # Make predictions
+        predictions = self.model.predict(X_test.values)
+        predictions = pd.Series(predictions, index=X_test.index)
+        pred_metric = r2_score(y_test, predictions)
+        self.logger.info(
+            "Prediction R2 score of fitted model on test data: %s",
+            pred_metric,
+        )
+
+    def predict(self: MLRegressor, new_values: list) -> np.ndarray:
+        """Predict a new value.
+
+        :param new_values: The new values for the features \
+            (in the same order as the features list). \
+            Example: [2.24, 5.68].
+        :type new_values: list
+        :return: The np.ndarray containing the predicted value.
+        :rtype: np.ndarray
+        """
+        self.logger.info("Performing a prediction for %s", self.model_type)
+        new_values = np.array([new_values])
+
+        return self.model.predict(new_values)
diff --git a/src/emhass/retrieve_hass.py b/src/emhass/retrieve_hass.py
index f5fa82de..4bac582f 100644
--- a/src/emhass/retrieve_hass.py
+++ b/src/emhass/retrieve_hass.py
@@ -35,7 +35,7 @@ def __init__(self, hass_url: str, long_lived_token: str, freq: pd.Timedelta,
                  get_data_from_file: Optional[bool] = False) -> None:
         """
         Define constructor for RetrieveHass class.
-        
+
         :param hass_url: The URL of the Home Assistant instance
         :type hass_url: str
         :param long_lived_token: The long lived token retrieved from the configuration pane
@@ -50,7 +50,7 @@ def __init__(self, hass_url: str, long_lived_token: str, freq: pd.Timedelta,
         :type emhass_conf: dict
         :param logger: The passed logger object
         :type logger: logging object
-        :param get_data_from_file: Select if data should be retrieved from a 
+        :param get_data_from_file: Select if data should be retrieved from a
         previously saved pickle useful for testing or directly from connection to
         hass database
         :type get_data_from_file: bool, optional
@@ -65,9 +65,14 @@ def __init__(self, hass_url: str, long_lived_token: str, freq: pd.Timedelta,
         self.logger = logger
         self.get_data_from_file = get_data_from_file
 
-    def get_data(self, days_list: pd.date_range, var_list: list, minimal_response: Optional[bool] = False,
-                 significant_changes_only: Optional[bool] = False, 
-                 test_url: Optional[str] = 'empty') -> None:
+    def get_data(
+        self,
+        days_list: pd.date_range,
+        var_list: list,
+        minimal_response: Optional[bool] = False,
+        significant_changes_only: Optional[bool] = False,
+        test_url: Optional[str] = "empty",
+    ) -> None:
         r"""
         Retrieve the actual data from hass.
         
@@ -92,20 +97,36 @@ def get_data(self, days_list: pd.date_range, var_list: list, minimal_response: O
         """
         self.logger.info("Retrieve hass get data method initiated...")
         self.df_final = pd.DataFrame()
-        x = 0 #iterate based on days
+        x = 0  # iterate based on days
         # Looping on each day from days list
         for day in days_list:
-        
+
             for i, var in enumerate(var_list):
-                
-                if test_url == 'empty':
-                    if self.hass_url == "http://supervisor/core/api": # If we are using the supervisor API
-                        url = self.hass_url+"/history/period/"+day.isoformat()+"?filter_entity_id="+var
-                    else: # Otherwise the Home Assistant Core API it is
-                        url = self.hass_url+"api/history/period/"+day.isoformat()+"?filter_entity_id="+var
-                    if minimal_response: # A support for minimal response
+
+                if test_url == "empty":
+                    if (
+                        self.hass_url == "http://supervisor/core/api"
+                    ):  # If we are using the supervisor API
+                        url = (
+                            self.hass_url
+                            + "/history/period/"
+                            + day.isoformat()
+                            + "?filter_entity_id="
+                            + var
+                        )
+                    else:  # Otherwise the Home Assistant Core API it is
+                        url = (
+                            self.hass_url
+                            + "api/history/period/"
+                            + day.isoformat()
+                            + "?filter_entity_id="
+                            + var
+                        )
+                    if minimal_response:  # A support for minimal response
                         url = url + "?minimal_response"
-                    if significant_changes_only: # And for signicant changes only (check the HASS restful API for more info)
+                    if (
+                        significant_changes_only
+                    ):  # And for signicant changes only (check the HASS restful API for more info)
                         url = url + "?significant_changes_only"
                 else:
                     url = test_url
@@ -116,21 +137,29 @@ def get_data(self, days_list: pd.date_range, var_list: list, minimal_response: O
                 try:
                     response = get(url, headers=headers)
                 except Exception:
-                    self.logger.error("Unable to access Home Assistance instance, check URL")
-                    self.logger.error("If using addon, try setting url and token to 'empty'")
+                    self.logger.error(
+                        "Unable to access Home Assistance instance, check URL"
+                    )
+                    self.logger.error(
+                        "If using addon, try setting url and token to 'empty'"
+                    )
                     return False
                 else:
                     if response.status_code == 401:
-                        self.logger.error("Unable to access Home Assistance instance, TOKEN/KEY")
-                        self.logger.error("If using addon, try setting url and token to 'empty'")
+                        self.logger.error(
+                            "Unable to access Home Assistance instance, TOKEN/KEY"
+                        )
+                        self.logger.error(
+                            "If using addon, try setting url and token to 'empty'"
+                        )
                         return False
                     if response.status_code > 299:
                         return f"Request Get Error: {response.status_code}"
-                '''import bz2 # Uncomment to save a serialized data for tests
+                """import bz2 # Uncomment to save a serialized data for tests
                 import _pickle as cPickle
                 with bz2.BZ2File("data/test_response_get_data_get_method.pbz2", "w") as f: 
-                    cPickle.dump(response, f)'''
-                try: # Sometimes when there are connection problems we need to catch empty retrieved json
+                    cPickle.dump(response, f)"""
+                try:  # Sometimes when there are connection problems we need to catch empty retrieved json
                     data = response.json()[0]
                 except IndexError:
                     if x == 0:
@@ -142,7 +171,11 @@ def get_data(self, days_list: pd.date_range, var_list: list, minimal_response: O
                 # self.logger.info(str(df_raw))
                 if len(df_raw) == 0:
                     if x == 0:
-                        self.logger.error("The retrieved Dataframe is empty, A sensor:" + var + " may have 0 days of history or passed sensor may not be correct")
+                        self.logger.error(
+                            "The retrieved Dataframe is empty, A sensor:"
+                            + var
+                            + " may have 0 days of history or passed sensor may not be correct"
+                        )
                     else:
                         self.logger.error("Retrieved empty Dataframe for day:"+ str(day) +", days_to_retrieve may be larger than the recorded history of sensor:" + var + " (check your recorder settings)")
                     return False
@@ -156,10 +189,17 @@ def get_data(self, days_list: pd.date_range, var_list: list, minimal_response: O
                                         format='%Y-%d-%m %H:%M').round(self.freq, ambiguous='infer', nonexistent='shift_forward')
                     df_day = pd.DataFrame(index = ts)
                 # Caution with undefined string data: unknown, unavailable, etc.
-                df_tp = df_raw.copy()[['state']].replace(
-                    ['unknown', 'unavailable', ''], np.nan).astype(float).rename(columns={'state': var})
+                df_tp = (
+                    df_raw.copy()[["state"]]
+                    .replace(["unknown", "unavailable", ""], np.nan)
+                    .astype(float)
+                    .rename(columns={"state": var})
+                )
                 # Setting index, resampling and concatenation
-                df_tp.set_index(pd.to_datetime(df_raw['last_changed'], format="ISO8601"), inplace=True)
+                df_tp.set_index(
+                    pd.to_datetime(df_raw["last_changed"], format="ISO8601"),
+                    inplace=True,
+                )
                 df_tp = df_tp.resample(self.freq).mean()
                 df_day = pd.concat([df_day, df_tp], axis=1)
             self.df_final = pd.concat([self.df_final, df_day], axis=0)
@@ -196,18 +236,24 @@ def prepare_data(self, var_load: str, load_negative: Optional[bool] = False, set
         
         """
         try:
-            if load_negative: # Apply the correct sign to load power
-                self.df_final[var_load+'_positive'] = -self.df_final[var_load]
+            if load_negative:  # Apply the correct sign to load power
+                self.df_final[var_load + "_positive"] = -self.df_final[var_load]
             else:
-                self.df_final[var_load+'_positive'] = self.df_final[var_load]
+                self.df_final[var_load + "_positive"] = self.df_final[var_load]
             self.df_final.drop([var_load], inplace=True, axis=1)
         except KeyError:
-            self.logger.error("Variable "+var_load+" was not found. This is typically because no data could be retrieved from Home Assistant")
+            self.logger.error(
+                "Variable "
+                + var_load
+                + " was not found. This is typically because no data could be retrieved from Home Assistant"
+            )
             return False
         except ValueError:
-            self.logger.error("sensor.power_photovoltaics and sensor.power_load_no_var_loads should not be the same")
-            return False   
-        if set_zero_min: # Apply minimum values
+            self.logger.error(
+                "sensor.power_photovoltaics and sensor.power_load_no_var_loads should not be the same"
+            )
+            return False
+        if set_zero_min:  # Apply minimum values
             self.df_final.clip(lower=0.0, inplace=True, axis=1)
             self.df_final.replace(to_replace=0.0, value=np.nan, inplace=True)
         new_var_replace_zero = []
@@ -215,59 +261,74 @@ def prepare_data(self, var_load: str, load_negative: Optional[bool] = False, set
         # Just changing the names of variables to contain the fact that they are considered positive
         if var_replace_zero is not None:
             for string in var_replace_zero:
-                new_string = string.replace(var_load, var_load+'_positive')
+                new_string = string.replace(var_load, var_load + "_positive")
                 new_var_replace_zero.append(new_string)
         else:
             new_var_replace_zero = None
         if var_interp is not None:
             for string in var_interp:
-                new_string = string.replace(var_load, var_load+'_positive')
+                new_string = string.replace(var_load, var_load + "_positive")
                 new_var_interp.append(new_string)
         else:
             new_var_interp = None
         # Treating NaN replacement: either by zeros or by linear interpolation
         if new_var_replace_zero is not None:
-            self.df_final[new_var_replace_zero] = self.df_final[new_var_replace_zero].fillna(0.0)
+            self.df_final[new_var_replace_zero] = self.df_final[
+                new_var_replace_zero
+            ].fillna(0.0)
         if new_var_interp is not None:
             self.df_final[new_var_interp] = self.df_final[new_var_interp].interpolate(
-                method='linear', axis=0, limit=None)
+                method="linear", axis=0, limit=None
+            )
             self.df_final[new_var_interp] = self.df_final[new_var_interp].fillna(0.0)
         # Setting the correct time zone on DF index
         if self.time_zone is not None:
             self.df_final.index = self.df_final.index.tz_convert(self.time_zone)
         # Drop datetimeindex duplicates on final DF
-        self.df_final = self.df_final[~self.df_final.index.duplicated(keep='first')]
+        self.df_final = self.df_final[~self.df_final.index.duplicated(keep="first")]
         return True
-    
+
     @staticmethod
-    def get_attr_data_dict(data_df: pd.DataFrame, idx: int, entity_id: str, 
-                           unit_of_measurement: str, friendly_name: str, 
-                           list_name: str, state: float) -> dict:
-        list_df = copy.deepcopy(data_df).loc[data_df.index[idx]:].reset_index()
-        list_df.columns = ['timestamps', entity_id]
-        ts_list = [str(i) for i in list_df['timestamps'].tolist()]
-        vals_list = [str(np.round(i,2)) for i in list_df[entity_id].tolist()]
+    def get_attr_data_dict(
+        data_df: pd.DataFrame,
+        idx: int,
+        entity_id: str,
+        unit_of_measurement: str,
+        friendly_name: str,
+        list_name: str,
+        state: float,
+    ) -> dict:
+        list_df = copy.deepcopy(data_df).loc[data_df.index[idx] :].reset_index()
+        list_df.columns = ["timestamps", entity_id]
+        ts_list = [str(i) for i in list_df["timestamps"].tolist()]
+        vals_list = [str(np.round(i, 2)) for i in list_df[entity_id].tolist()]
         forecast_list = []
         for i, ts in enumerate(ts_list):
             datum = {}
             datum["date"] = ts
-            datum[entity_id.split('sensor.')[1]] = vals_list[i]
+            datum[entity_id.split("sensor.")[1]] = vals_list[i]
             forecast_list.append(datum)
         data = {
             "state": "{:.2f}".format(state),
             "attributes": {
                 "unit_of_measurement": unit_of_measurement,
                 "friendly_name": friendly_name,
-                list_name: forecast_list
-            }
+                list_name: forecast_list,
+            },
         }
         return data
-    
-    def post_data(self, data_df: pd.DataFrame, idx: int, entity_id: str, 
-                  unit_of_measurement: str, friendly_name: str,
-                  type_var: str,
-                  from_mlforecaster: Optional[bool]=False,
-                  publish_prefix: Optional[str]="") -> None:
+
+    def post_data(
+        self,
+        data_df: pd.DataFrame,
+        idx: int,
+        entity_id: str,
+        unit_of_measurement: str,
+        friendly_name: str,
+        type_var: str,
+        from_mlforecaster: Optional[bool] = False,
+        publish_prefix: Optional[str] = "",
+    ) -> None:
         r"""
         Post passed data to hass.
         
@@ -290,72 +351,139 @@ def post_data(self, data_df: pd.DataFrame, idx: int, entity_id: str,
 
         """
         # Add a possible prefix to the entity ID
-        entity_id = entity_id.replace('sensor.', 'sensor.'+publish_prefix)
+        entity_id = entity_id.replace("sensor.", "sensor." + publish_prefix)
         # Set the URL
-        if self.hass_url == "http://supervisor/core/api": # If we are using the supervisor API
-            url = self.hass_url+"/states/"+entity_id
-        else: # Otherwise the Home Assistant Core API it is
-            url = self.hass_url+"api/states/"+entity_id
+        if (
+            self.hass_url == "http://supervisor/core/api"
+        ):  # If we are using the supervisor API
+            url = self.hass_url + "/states/" + entity_id
+        else:  # Otherwise the Home Assistant Core API it is
+            url = self.hass_url + "api/states/" + entity_id
         headers = {
             "Authorization": "Bearer " + self.long_lived_token,
             "content-type": "application/json",
         }
         # Preparing the data dict to be published
-        if type_var == 'cost_fun':
-            state = np.round(data_df.sum()[0],2)
-        elif type_var == 'unit_load_cost' or type_var == 'unit_prod_price':
-            state = np.round(data_df.loc[data_df.index[idx]],4)
-        elif type_var == 'optim_status':
+        if type_var == "cost_fun":
+            state = np.round(data_df.sum()[0], 2)
+        elif type_var == "unit_load_cost" or type_var == "unit_prod_price":
+            state = np.round(data_df.loc[data_df.index[idx]], 4)
+        elif type_var == "optim_status":
             state = data_df.loc[data_df.index[idx]]
+        elif type_var == "mlregressor":
+            state = data_df[idx]
         else:
-            state = np.round(data_df.loc[data_df.index[idx]],2)
-        if type_var == 'power':
-            data = RetrieveHass.get_attr_data_dict(data_df, idx, entity_id, unit_of_measurement, 
-                                                    friendly_name, "forecasts", state)
-        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 == 'batt':
-            data = RetrieveHass.get_attr_data_dict(data_df, idx, entity_id, unit_of_measurement, 
-                                                    friendly_name, "battery_scheduled_power", state)
-        elif type_var == 'SOC':
-            data = RetrieveHass.get_attr_data_dict(data_df, idx, entity_id, unit_of_measurement, 
-                                                    friendly_name, "battery_scheduled_soc", state)
-        elif type_var == 'unit_load_cost':
-            data = RetrieveHass.get_attr_data_dict(data_df, idx, entity_id, unit_of_measurement, 
-                                                    friendly_name, "unit_load_cost_forecasts", state)
-        elif type_var == 'unit_prod_price':
-            data = RetrieveHass.get_attr_data_dict(data_df, idx, entity_id, unit_of_measurement, 
-                                                    friendly_name, "unit_prod_price_forecasts", state)
-        elif type_var == 'mlforecaster':
-            data = RetrieveHass.get_attr_data_dict(data_df, idx, entity_id, unit_of_measurement, 
-                                                    friendly_name, "scheduled_forecast", state)
-        elif type_var == 'optim_status':
+            state = np.round(data_df.loc[data_df.index[idx]], 2)
+        if type_var == "power":
+            data = RetrieveHass.get_attr_data_dict(
+                data_df,
+                idx,
+                entity_id,
+                unit_of_measurement,
+                friendly_name,
+                "forecasts",
+                state,
+            )
+        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 == "batt":
+            data = RetrieveHass.get_attr_data_dict(
+                data_df,
+                idx,
+                entity_id,
+                unit_of_measurement,
+                friendly_name,
+                "battery_scheduled_power",
+                state,
+            )
+        elif type_var == "SOC":
+            data = RetrieveHass.get_attr_data_dict(
+                data_df,
+                idx,
+                entity_id,
+                unit_of_measurement,
+                friendly_name,
+                "battery_scheduled_soc",
+                state,
+            )
+        elif type_var == "unit_load_cost":
+            data = RetrieveHass.get_attr_data_dict(
+                data_df,
+                idx,
+                entity_id,
+                unit_of_measurement,
+                friendly_name,
+                "unit_load_cost_forecasts",
+                state,
+            )
+        elif type_var == "unit_prod_price":
+            data = RetrieveHass.get_attr_data_dict(
+                data_df,
+                idx,
+                entity_id,
+                unit_of_measurement,
+                friendly_name,
+                "unit_prod_price_forecasts",
+                state,
+            )
+        elif type_var == "mlforecaster":
+            data = RetrieveHass.get_attr_data_dict(
+                data_df,
+                idx,
+                entity_id,
+                unit_of_measurement,
+                friendly_name,
+                "scheduled_forecast",
+                state,
+            )
+        elif type_var == "optim_status":
             data = {
                 "state": state,
                 "attributes": {
                     "unit_of_measurement": unit_of_measurement,
-                    "friendly_name": friendly_name
-                }
+                    "friendly_name": friendly_name,
+                },
+            }
+        elif type_var == "mlregressor":
+            data = {
+                "state": state,
+                "attributes": {
+                    "unit_of_measurement": unit_of_measurement,
+                    "friendly_name": friendly_name,
+                },
             }
         else:
             data = {
                 "state": "{:.2f}".format(state),
                 "attributes": {
                     "unit_of_measurement": unit_of_measurement,
-                    "friendly_name": friendly_name
-                }
+                    "friendly_name": friendly_name,
+                },
             }
         # Actually post the data
         if self.get_data_from_file:
-            class response: pass
+
+            class response:
+                pass
+
             response.status_code = 200
             response.ok = True
         else:
             response = post(url, headers=headers, data=json.dumps(data))
         # Treating the response status and posting them on the logger
         if response.ok:
-            self.logger.info("Successfully posted to "+entity_id+" = "+str(state))
+            self.logger.info("Successfully posted to " + entity_id + " = " + str(state))
         else:
-            self.logger.info("The status code for received curl command response is: "+str(response.status_code))
+            self.logger.info(
+                "The status code for received curl command response is: "
+                + str(response.status_code)
+            )
         return response, data
diff --git a/src/emhass/static/advanced.html b/src/emhass/static/advanced.html
index 38371583..6595520c 100644
--- a/src/emhass/static/advanced.html
+++ b/src/emhass/static/advanced.html
@@ -14,6 +14,9 @@ <h4>Use the buttons below to fit, predict and tune a machine learning model for
 <button type="button" id="forecast-model-predict" class="button button2">ML forecast model
     predict</button>
 <button type="button" id="forecast-model-tune" class="button button3">ML forecast model tune</button>
+</br></br>
+<button type="button" id="regressor-model-fit" class="button button1">ML regressor model fit</button>
+<button type="button" id="regressor-model-predict" class="button button2">ML regressor model predict</button>
 <!-- -->
 <!--dynamic input elements section -->
 <h4>Input Runtime Parameters</h4>
diff --git a/src/emhass/static/script.js b/src/emhass/static/script.js
index 05861814..ad02b578 100644
--- a/src/emhass/static/script.js
+++ b/src/emhass/static/script.js
@@ -16,6 +16,8 @@ function loadButtons(page) {
                 "forecast-model-fit",
                 "forecast-model-predict",
                 "forecast-model-tune",
+                "regressor-model-fit",
+                "regressor-model-predict",
                 "perfect-optim",
                 "publish-data",
                 "naive-mpc-optim"
diff --git a/src/emhass/utils.py b/src/emhass/utils.py
index f64161fc..46fa0d46 100644
--- a/src/emhass/utils.py
+++ b/src/emhass/utils.py
@@ -2,10 +2,19 @@
 # -*- coding: utf-8 -*-
 
 from typing import Tuple, Optional
-import numpy as np, pandas as pd
-import yaml, pytz, logging, pathlib, json, copy
 from datetime import datetime, timedelta, timezone
+import logging
+import pathlib
+import json
+import copy
+import numpy as np
+import pandas as pd
+import yaml
+import pytz
+
+
 import plotly.express as px
+
 pd.options.plotting.backend = "plotly"
 
 from emhass.machine_learning_forecaster import MLForecaster
@@ -14,13 +23,13 @@
 def get_root(file: str, num_parent: Optional[int] = 3) -> str:
     """
     Get the root absolute path of the working directory.
-    
+
     :param file: The passed file path with __file__
     :return: The root path
     :param num_parent: The number of parents levels up to desired root folder
     :type num_parent: int, optional
     :rtype: str
-    
+
     """
     if num_parent == 3:
         root = pathlib.Path(file).resolve().parent.parent.parent
@@ -36,7 +45,7 @@ def get_logger(fun_name: str, emhass_conf: dict, save_to_file: Optional[bool] =
                logging_level: Optional[str] = "DEBUG") -> Tuple[logging.Logger, logging.StreamHandler]:
     """
     Create a simple logger object.
-    
+
     :param fun_name: The Python function object name where the logger will be used
     :type fun_name: str
     :param emhass_conf: Dictionary containing the needed emhass paths
@@ -45,9 +54,9 @@ def get_logger(fun_name: str, emhass_conf: dict, save_to_file: Optional[bool] =
     :type save_to_file: bool, optional
     :return: The logger object and the handler
     :rtype: object
-    
+
     """
-	# create logger object
+    # create logger object
     logger = logging.getLogger(fun_name)
     logger.propagate = True
     logger.fileSetting = save_to_file
@@ -70,14 +79,18 @@ def get_logger(fun_name: str, emhass_conf: dict, save_to_file: Optional[bool] =
     else:
         logger.setLevel(logging.DEBUG)
         ch.setLevel(logging.DEBUG)
-    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+    formatter = logging.Formatter(
+        "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+    )
     ch.setFormatter(formatter)
     logger.addHandler(ch)
 
     return logger, ch
 
-def get_forecast_dates(freq: int, delta_forecast: int, 
-                       timedelta_days: Optional[int] = 0) -> pd.core.indexes.datetimes.DatetimeIndex:
+
+def get_forecast_dates(
+    freq: int, delta_forecast: int, timedelta_days: Optional[int] = 0
+) -> pd.core.indexes.datetimes.DatetimeIndex:
     """
     Get the date_range list of the needed future dates using the delta_forecast parameter.
 
@@ -89,7 +102,7 @@ def get_forecast_dates(freq: int, delta_forecast: int,
     :type timedelta_days: Optional[int], optional
     :return: A list of future forecast dates.
     :rtype: pd.core.indexes.datetimes.DatetimeIndex
-    
+
     """
     freq = pd.to_timedelta(freq, "minutes")
     start_forecast = pd.Timestamp(datetime.now()).replace(hour=0, minute=0, second=0, microsecond=0)
@@ -99,11 +112,19 @@ def get_forecast_dates(freq: int, delta_forecast: int,
         freq=freq).round(freq, ambiguous='infer', nonexistent='shift_forward')
     return forecast_dates
 
-def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dict, optim_conf: dict, plant_conf: dict,
-                        set_type: str, logger: logging.Logger) -> Tuple[str, dict]:
+
+def treat_runtimeparams(
+    runtimeparams: str,
+    params: str,
+    retrieve_hass_conf: dict,
+    optim_conf: dict,
+    plant_conf: dict,
+    set_type: str,
+    logger: logging.Logger,
+) -> Tuple[str, dict]:
     """
-    Treat the passed optimization runtime parameters. 
-    
+    Treat the passed optimization runtime parameters.
+
     :param runtimeparams: Json string containing the runtime parameters dict.
     :type runtimeparams: str
     :param params: Configuration parameters passed from data/options.json
@@ -120,93 +141,167 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic
     :type logger: logging.Logger
     :return: Returning the params and optimization parameter container.
     :rtype: Tuple[str, dict]
-    
+
     """
-    if (params != None) and (params != 'null'):
+    if (params != None) and (params != "null"):
         params = json.loads(params)
     else:
         params = {}
     # Some default data needed
     custom_deferrable_forecast_id = []
-    for k in range(optim_conf['num_def_loads']):
-        custom_deferrable_forecast_id.append({
-            "entity_id": "sensor.p_deferrable{}".format(k), 
-            "unit_of_measurement": "W", 
-            "friendly_name": "Deferrable Load {}".format(k)
-        })
-    default_passed_dict = {'custom_pv_forecast_id': {"entity_id": "sensor.p_pv_forecast", "unit_of_measurement": "W", "friendly_name": "PV Power Forecast"},
-                           'custom_load_forecast_id': {"entity_id": "sensor.p_load_forecast", "unit_of_measurement": "W", "friendly_name": "Load Power Forecast"},
-                           'custom_batt_forecast_id': {"entity_id": "sensor.p_batt_forecast", "unit_of_measurement": "W", "friendly_name": "Battery Power Forecast"},
-                           'custom_batt_soc_forecast_id': {"entity_id": "sensor.soc_batt_forecast", "unit_of_measurement": "%", "friendly_name": "Battery SOC Forecast"},
-                           'custom_grid_forecast_id': {"entity_id": "sensor.p_grid_forecast", "unit_of_measurement": "W", "friendly_name": "Grid Power Forecast"},
-                           'custom_cost_fun_id': {"entity_id": "sensor.total_cost_fun_value", "unit_of_measurement": "", "friendly_name": "Total cost function value"},
-                           'custom_optim_status_id': {"entity_id": "sensor.optim_status", "unit_of_measurement": "", "friendly_name": "EMHASS optimization status"},
-                           'custom_unit_load_cost_id': {"entity_id": "sensor.unit_load_cost", "unit_of_measurement": "€/kWh", "friendly_name": "Unit Load Cost"},
-                           'custom_unit_prod_price_id': {"entity_id": "sensor.unit_prod_price", "unit_of_measurement": "€/kWh", "friendly_name": "Unit Prod Price"},
-                           'custom_deferrable_forecast_id': custom_deferrable_forecast_id,
-                           'publish_prefix': ""}
-    if 'passed_data' in params.keys():
+    for k in range(optim_conf["num_def_loads"]):
+        custom_deferrable_forecast_id.append(
+            {
+                "entity_id": "sensor.p_deferrable{}".format(k),
+                "unit_of_measurement": "W",
+                "friendly_name": "Deferrable Load {}".format(k),
+            }
+        )
+    default_passed_dict = {
+        "custom_pv_forecast_id": {
+            "entity_id": "sensor.p_pv_forecast",
+            "unit_of_measurement": "W",
+            "friendly_name": "PV Power Forecast",
+        },
+        "custom_load_forecast_id": {
+            "entity_id": "sensor.p_load_forecast",
+            "unit_of_measurement": "W",
+            "friendly_name": "Load Power Forecast",
+        },
+        "custom_batt_forecast_id": {
+            "entity_id": "sensor.p_batt_forecast",
+            "unit_of_measurement": "W",
+            "friendly_name": "Battery Power Forecast",
+        },
+        "custom_batt_soc_forecast_id": {
+            "entity_id": "sensor.soc_batt_forecast",
+            "unit_of_measurement": "%",
+            "friendly_name": "Battery SOC Forecast",
+        },
+        "custom_grid_forecast_id": {
+            "entity_id": "sensor.p_grid_forecast",
+            "unit_of_measurement": "W",
+            "friendly_name": "Grid Power Forecast",
+        },
+        "custom_cost_fun_id": {
+            "entity_id": "sensor.total_cost_fun_value",
+            "unit_of_measurement": "",
+            "friendly_name": "Total cost function value",
+        },
+        "custom_optim_status_id": {
+            "entity_id": "sensor.optim_status",
+            "unit_of_measurement": "",
+            "friendly_name": "EMHASS optimization status",
+        },
+        "custom_unit_load_cost_id": {
+            "entity_id": "sensor.unit_load_cost",
+            "unit_of_measurement": "€/kWh",
+            "friendly_name": "Unit Load Cost",
+        },
+        "custom_unit_prod_price_id": {
+            "entity_id": "sensor.unit_prod_price",
+            "unit_of_measurement": "€/kWh",
+            "friendly_name": "Unit Prod Price",
+        },
+        "custom_deferrable_forecast_id": custom_deferrable_forecast_id,
+        "publish_prefix": "",
+    }
+    if "passed_data" in params.keys():
         for key, value in default_passed_dict.items():
-            params['passed_data'][key] = value
+            params["passed_data"][key] = value
     else:
-        params['passed_data'] = default_passed_dict
+        params["passed_data"] = default_passed_dict
     if runtimeparams is not None:
         runtimeparams = json.loads(runtimeparams)
-        freq = int(retrieve_hass_conf['freq'].seconds/60.0)
-        delta_forecast = int(optim_conf['delta_forecast'].days)
+        freq = int(retrieve_hass_conf["freq"].seconds / 60.0)
+        delta_forecast = int(optim_conf["delta_forecast"].days)
         forecast_dates = get_forecast_dates(freq, delta_forecast)
+        if set_type == "regressor-model-fit":            
+            if "csv_file" in runtimeparams:
+                csv_file = runtimeparams["csv_file"]
+                params["passed_data"]["csv_file"] = csv_file
+            if "features" in runtimeparams:
+                features = runtimeparams["features"]
+                params["passed_data"]["features"] = features
+            if "target" in runtimeparams:
+                target = runtimeparams["target"]
+                params["passed_data"]["target"] = target
+            if "timestamp" not in runtimeparams:
+                params["passed_data"]["timestamp"] = None
+            else:
+                timestamp = runtimeparams["timestamp"]
+                params["passed_data"]["timestamp"] = timestamp
+            if "date_features" not in runtimeparams:
+                params["passed_data"]["date_features"] = []
+            else:
+                date_features = runtimeparams["date_features"]
+                params["passed_data"]["date_features"] = date_features
+        if set_type == "regressor-model-predict":
+            if "new_values" in runtimeparams:
+                new_values = runtimeparams["new_values"]
+                params["passed_data"]["new_values"] = new_values
+            if "csv_file" in runtimeparams:
+                csv_file = runtimeparams["csv_file"]
+                params["passed_data"]["csv_file"] = csv_file
+            if "features" in runtimeparams:
+                features = runtimeparams["features"]
+                params["passed_data"]["features"] = features
+            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():
-                prediction_horizon = 10 # 10 time steps by default
+        if set_type == "naive-mpc-optim":
+            if "prediction_horizon" not in runtimeparams.keys():
+                prediction_horizon = 10  # 10 time steps by default
             else:
-                prediction_horizon = runtimeparams['prediction_horizon']
-            params['passed_data']['prediction_horizon'] = prediction_horizon
-            if 'soc_init' not in runtimeparams.keys():
-                soc_init = plant_conf['SOCtarget']
+                prediction_horizon = runtimeparams["prediction_horizon"]
+            params["passed_data"]["prediction_horizon"] = prediction_horizon
+            if "soc_init" not in runtimeparams.keys():
+                soc_init = plant_conf["SOCtarget"]
             else:
-                soc_init = runtimeparams['soc_init']
-            params['passed_data']['soc_init'] = soc_init
-            if 'soc_final' not in runtimeparams.keys():
-                soc_final = plant_conf['SOCtarget']
+                soc_init = runtimeparams["soc_init"]
+            params["passed_data"]["soc_init"] = soc_init
+            if "soc_final" not in runtimeparams.keys():
+                soc_final = plant_conf["SOCtarget"]
             else:
-                soc_final = runtimeparams['soc_final']
-            params['passed_data']['soc_final'] = soc_final
-            if 'def_total_hours' not in runtimeparams.keys():
-                def_total_hours = optim_conf['def_total_hours']
+                soc_final = runtimeparams["soc_final"]
+            params["passed_data"]["soc_final"] = soc_final
+            if "def_total_hours" not in runtimeparams.keys():
+                def_total_hours = optim_conf["def_total_hours"]
             else:
-                def_total_hours = runtimeparams['def_total_hours']
-            params['passed_data']['def_total_hours'] = def_total_hours
-            if 'def_start_timestep' not in runtimeparams.keys():
-                def_start_timestep = optim_conf['def_start_timestep']
+                def_total_hours = runtimeparams["def_total_hours"]
+            params["passed_data"]["def_total_hours"] = def_total_hours
+            if "def_start_timestep" not in runtimeparams.keys():
+                def_start_timestep = optim_conf["def_start_timestep"]
             else:
-                def_start_timestep = runtimeparams['def_start_timestep']
-            params['passed_data']['def_start_timestep'] = def_start_timestep
-            if 'def_end_timestep' not in runtimeparams.keys():
-                def_end_timestep = optim_conf['def_end_timestep']
+                def_start_timestep = runtimeparams["def_start_timestep"]
+            params["passed_data"]["def_start_timestep"] = def_start_timestep
+            if "def_end_timestep" not in runtimeparams.keys():
+                def_end_timestep = optim_conf["def_end_timestep"]
             else:
-                def_end_timestep = runtimeparams['def_end_timestep']
-            params['passed_data']['def_end_timestep'] = def_end_timestep
-            if 'alpha' not in runtimeparams.keys():
+                def_end_timestep = runtimeparams["def_end_timestep"]
+            params["passed_data"]["def_end_timestep"] = def_end_timestep
+            if "alpha" not in runtimeparams.keys():
                 alpha = 0.5
             else:
-                alpha = runtimeparams['alpha']
-            params['passed_data']['alpha'] = alpha
-            if 'beta' not in runtimeparams.keys():
+                alpha = runtimeparams["alpha"]
+            params["passed_data"]["alpha"] = alpha
+            if "beta" not in runtimeparams.keys():
                 beta = 0.5
             else:
-                beta = runtimeparams['beta']
-            params['passed_data']['beta'] = beta
+                beta = runtimeparams["beta"]
+            params["passed_data"]["beta"] = beta
             forecast_dates = copy.deepcopy(forecast_dates)[0:prediction_horizon]
         else:
-            params['passed_data']['prediction_horizon'] = None
-            params['passed_data']['soc_init'] = None
-            params['passed_data']['soc_final'] = None
-            params['passed_data']['def_total_hours'] = None
-            params['passed_data']['def_start_timestep'] = None
-            params['passed_data']['def_end_timestep'] = None
-            params['passed_data']['alpha'] = None
-            params['passed_data']['beta'] = None
+            params["passed_data"]["prediction_horizon"] = None
+            params["passed_data"]["soc_init"] = None
+            params["passed_data"]["soc_final"] = None
+            params["passed_data"]["def_total_hours"] = None
+            params["passed_data"]["def_start_timestep"] = None
+            params["passed_data"]["def_end_timestep"] = None
+            params["passed_data"]["alpha"] = None
+            params["passed_data"]["beta"] = None
         # Treat passed forecast data lists
         list_forecast_key = ['pv_power_forecast', 'load_power_forecast', 'load_cost_forecast', 'prod_price_forecast']
         forecast_methods = ['weather_forecast_method', 'load_forecast_method', 'load_cost_forecast_method', 'prod_price_forecast_method']
@@ -226,121 +321,184 @@ def treat_runtimeparams(runtimeparams: str, params: str, retrieve_hass_conf: dic
             else:
                 params['passed_data'][forecast_key] = None
         # Treat passed data for forecast model fit/predict/tune at runtime
-        if 'days_to_retrieve' not in runtimeparams.keys():
+        if "days_to_retrieve" not in runtimeparams.keys():
             days_to_retrieve = 9
         else:
-            days_to_retrieve = runtimeparams['days_to_retrieve']
-        params['passed_data']['days_to_retrieve'] = days_to_retrieve
-        if 'model_type' not in runtimeparams.keys():
+            days_to_retrieve = runtimeparams["days_to_retrieve"]
+        params["passed_data"]["days_to_retrieve"] = days_to_retrieve
+        if "model_type" not in runtimeparams.keys():
             model_type = "load_forecast"
         else:
-            model_type = runtimeparams['model_type']
-        params['passed_data']['model_type'] = model_type
-        if 'var_model' not in runtimeparams.keys():
+            model_type = runtimeparams["model_type"]
+        params["passed_data"]["model_type"] = model_type
+        if "var_model" not in runtimeparams.keys():
             var_model = "sensor.power_load_no_var_loads"
         else:
-            var_model = runtimeparams['var_model']
-        params['passed_data']['var_model'] = var_model
-        if 'sklearn_model' not in runtimeparams.keys():
+            var_model = runtimeparams["var_model"]
+        params["passed_data"]["var_model"] = var_model
+        if "sklearn_model" not in runtimeparams.keys():
             sklearn_model = "KNeighborsRegressor"
         else:
-            sklearn_model = runtimeparams['sklearn_model']
-        params['passed_data']['sklearn_model'] = sklearn_model
-        if 'num_lags' not in runtimeparams.keys():
+            sklearn_model = runtimeparams["sklearn_model"]
+        params["passed_data"]["sklearn_model"] = sklearn_model
+        if "regression_model" not in runtimeparams.keys():
+            regression_model = "AdaBoostRegression"
+        else:
+            regression_model = runtimeparams["regression_model"]
+        params["passed_data"]["regression_model"] = regression_model
+        if "num_lags" not in runtimeparams.keys():
             num_lags = 48
         else:
-            num_lags = runtimeparams['num_lags']
-        params['passed_data']['num_lags'] = num_lags
-        if 'split_date_delta' not in runtimeparams.keys():
-            split_date_delta = '48h'
+            num_lags = runtimeparams["num_lags"]
+        params["passed_data"]["num_lags"] = num_lags
+        if "split_date_delta" not in runtimeparams.keys():
+            split_date_delta = "48h"
         else:
-            split_date_delta = runtimeparams['split_date_delta']
-        params['passed_data']['split_date_delta'] = split_date_delta
-        if 'perform_backtest' not in runtimeparams.keys():
+            split_date_delta = runtimeparams["split_date_delta"]
+        params["passed_data"]["split_date_delta"] = split_date_delta
+        if "perform_backtest" not in runtimeparams.keys():
             perform_backtest = False
         else:
-            perform_backtest = eval(str(runtimeparams['perform_backtest']).capitalize())
-        params['passed_data']['perform_backtest'] = perform_backtest
-        if 'model_predict_publish' not in runtimeparams.keys():
+            perform_backtest = 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())
-        params['passed_data']['model_predict_publish'] = model_predict_publish
-        if 'model_predict_entity_id' not in runtimeparams.keys():
+            model_predict_publish = 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"
         else:
-            model_predict_entity_id = runtimeparams['model_predict_entity_id']
-        params['passed_data']['model_predict_entity_id'] = model_predict_entity_id
-        if 'model_predict_unit_of_measurement' not in runtimeparams.keys():
+            model_predict_entity_id = runtimeparams["model_predict_entity_id"]
+        params["passed_data"]["model_predict_entity_id"] = model_predict_entity_id
+        if "model_predict_unit_of_measurement" not in runtimeparams.keys():
             model_predict_unit_of_measurement = "W"
         else:
-            model_predict_unit_of_measurement = runtimeparams['model_predict_unit_of_measurement']
-        params['passed_data']['model_predict_unit_of_measurement'] = model_predict_unit_of_measurement
-        if 'model_predict_friendly_name' not in runtimeparams.keys():
+            model_predict_unit_of_measurement = runtimeparams[
+                "model_predict_unit_of_measurement"
+            ]
+        params["passed_data"][
+            "model_predict_unit_of_measurement"
+        ] = model_predict_unit_of_measurement
+        if "model_predict_friendly_name" not in runtimeparams.keys():
             model_predict_friendly_name = "Load Power Forecast custom ML model"
         else:
-            model_predict_friendly_name = runtimeparams['model_predict_friendly_name']
-        params['passed_data']['model_predict_friendly_name'] = model_predict_friendly_name
-        # Treat optimization configuration parameters passed at runtime 
-        if 'num_def_loads' in runtimeparams.keys():
-            optim_conf['num_def_loads'] = runtimeparams['num_def_loads']
-        if 'P_deferrable_nom' in runtimeparams.keys():
-            optim_conf['P_deferrable_nom'] = runtimeparams['P_deferrable_nom']
-        if 'def_total_hours' in runtimeparams.keys():
-            optim_conf['def_total_hours'] = runtimeparams['def_total_hours']
-        if 'def_start_timestep' in runtimeparams.keys():
-            optim_conf['def_start_timestep'] = runtimeparams['def_start_timestep']
-        if 'def_end_timestep' in runtimeparams.keys():
-            optim_conf['def_end_timestep'] = runtimeparams['def_end_timestep']
-        if 'treat_def_as_semi_cont' in runtimeparams.keys():
-            optim_conf['treat_def_as_semi_cont'] = [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']]
-        if 'solcast_api_key' in runtimeparams.keys():
-            retrieve_hass_conf['solcast_api_key'] = runtimeparams['solcast_api_key']
-            optim_conf['weather_forecast_method'] = 'solcast'
-        if 'solcast_rooftop_id' in runtimeparams.keys():
-            retrieve_hass_conf['solcast_rooftop_id'] = runtimeparams['solcast_rooftop_id']
-            optim_conf['weather_forecast_method'] = 'solcast'
-        if 'solar_forecast_kwp' in runtimeparams.keys():
-            retrieve_hass_conf['solar_forecast_kwp'] = runtimeparams['solar_forecast_kwp']
-            optim_conf['weather_forecast_method'] = 'solar.forecast'
+            model_predict_friendly_name = runtimeparams["model_predict_friendly_name"]
+        params["passed_data"][
+            "model_predict_friendly_name"
+        ] = model_predict_friendly_name
+        if "mlr_predict_entity_id" not in runtimeparams.keys():
+            mlr_predict_entity_id = "sensor.mlr_predict"
+        else:
+            mlr_predict_entity_id = runtimeparams["mlr_predict_entity_id"]
+        params["passed_data"]["mlr_predict_entity_id"] = mlr_predict_entity_id
+        if "mlr_predict_unit_of_measurement" not in runtimeparams.keys():
+            mlr_predict_unit_of_measurement = None
+        else:
+            mlr_predict_unit_of_measurement = runtimeparams[
+                "mlr_predict_unit_of_measurement"
+            ]
+        params["passed_data"][
+            "mlr_predict_unit_of_measurement"
+        ] = mlr_predict_unit_of_measurement
+        if "mlr_predict_friendly_name" not in runtimeparams.keys():
+            mlr_predict_friendly_name = "mlr predictor"
+        else:
+            mlr_predict_friendly_name = runtimeparams["mlr_predict_friendly_name"]
+        params["passed_data"]["mlr_predict_friendly_name"] = mlr_predict_friendly_name
+        # Treat optimization configuration parameters passed at runtime
+        if "num_def_loads" in runtimeparams.keys():
+            optim_conf["num_def_loads"] = runtimeparams["num_def_loads"]
+        if "P_deferrable_nom" in runtimeparams.keys():
+            optim_conf["P_deferrable_nom"] = runtimeparams["P_deferrable_nom"]
+        if "def_total_hours" in runtimeparams.keys():
+            optim_conf["def_total_hours"] = runtimeparams["def_total_hours"]
+        if "def_start_timestep" in runtimeparams.keys():
+            optim_conf["def_start_timestep"] = runtimeparams["def_start_timestep"]
+        if "def_end_timestep" in runtimeparams.keys():
+            optim_conf["def_end_timestep"] = runtimeparams["def_end_timestep"]
+        if "treat_def_as_semi_cont" in runtimeparams.keys():
+            optim_conf["treat_def_as_semi_cont"] = [
+                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"]
+            ]
+        if "solcast_api_key" in runtimeparams.keys():
+            retrieve_hass_conf["solcast_api_key"] = runtimeparams["solcast_api_key"]
+            optim_conf["weather_forecast_method"] = "solcast"
+        if "solcast_rooftop_id" in runtimeparams.keys():
+            retrieve_hass_conf["solcast_rooftop_id"] = runtimeparams[
+                "solcast_rooftop_id"
+            ]
+            optim_conf["weather_forecast_method"] = "solcast"
+        if "solar_forecast_kwp" in runtimeparams.keys():
+            retrieve_hass_conf["solar_forecast_kwp"] = runtimeparams[
+                "solar_forecast_kwp"
+            ]
+            optim_conf["weather_forecast_method"] = "solar.forecast"
+        if "weight_battery_discharge" in runtimeparams.keys():
+            optim_conf["weight_battery_discharge"] = runtimeparams[
+                "weight_battery_discharge"
+            ]
+        if "weight_battery_charge" in runtimeparams.keys():
+            optim_conf["weight_battery_charge"] = runtimeparams["weight_battery_charge"]
         if 'freq' in runtimeparams.keys():
             retrieve_hass_conf['freq'] = pd.to_timedelta(runtimeparams['freq'], "minutes")
-        if 'weight_battery_discharge' in runtimeparams.keys():
-            optim_conf['weight_battery_discharge'] = runtimeparams['weight_battery_discharge']
-        if 'weight_battery_charge' in runtimeparams.keys():
-            optim_conf['weight_battery_charge'] = runtimeparams['weight_battery_charge']
         # Treat plant configuration parameters passed at runtime
-        if 'SOCtarget' in runtimeparams.keys():
-            plant_conf['SOCtarget'] = runtimeparams['SOCtarget']
+        if "SOCtarget" in runtimeparams.keys():
+            plant_conf["SOCtarget"] = runtimeparams["SOCtarget"]
         # 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['custom_pv_forecast_id']
-        if 'custom_load_forecast_id' in runtimeparams.keys():
-            params['passed_data']['custom_load_forecast_id'] = runtimeparams['custom_load_forecast_id']
-        if 'custom_batt_forecast_id' in runtimeparams.keys():
-            params['passed_data']['custom_batt_forecast_id'] = runtimeparams['custom_batt_forecast_id']
-        if 'custom_batt_soc_forecast_id' in runtimeparams.keys():
-            params['passed_data']['custom_batt_soc_forecast_id'] = runtimeparams['custom_batt_soc_forecast_id']
-        if 'custom_grid_forecast_id' in runtimeparams.keys():
-            params['passed_data']['custom_grid_forecast_id'] = runtimeparams['custom_grid_forecast_id']
-        if 'custom_cost_fun_id' in runtimeparams.keys():
-            params['passed_data']['custom_cost_fun_id'] = runtimeparams['custom_cost_fun_id']
-        if 'custom_optim_status_id' in runtimeparams.keys():
-            params['passed_data']['custom_optim_status_id'] = runtimeparams['custom_optim_status_id']
-        if 'custom_unit_load_cost_id' in runtimeparams.keys():
-            params['passed_data']['custom_unit_load_cost_id'] = runtimeparams['custom_unit_load_cost_id']
-        if 'custom_unit_prod_price_id' in runtimeparams.keys():
-            params['passed_data']['custom_unit_prod_price_id'] = runtimeparams['custom_unit_prod_price_id']
-        if 'custom_deferrable_forecast_id' in runtimeparams.keys():
-            params['passed_data']['custom_deferrable_forecast_id'] = runtimeparams['custom_deferrable_forecast_id']
+        if "custom_pv_forecast_id" in runtimeparams.keys():
+            params["passed_data"]["custom_pv_forecast_id"] = runtimeparams[
+                "custom_pv_forecast_id"
+            ]
+        if "custom_load_forecast_id" in runtimeparams.keys():
+            params["passed_data"]["custom_load_forecast_id"] = runtimeparams[
+                "custom_load_forecast_id"
+            ]
+        if "custom_batt_forecast_id" in runtimeparams.keys():
+            params["passed_data"]["custom_batt_forecast_id"] = runtimeparams[
+                "custom_batt_forecast_id"
+            ]
+        if "custom_batt_soc_forecast_id" in runtimeparams.keys():
+            params["passed_data"]["custom_batt_soc_forecast_id"] = runtimeparams[
+                "custom_batt_soc_forecast_id"
+            ]
+        if "custom_grid_forecast_id" in runtimeparams.keys():
+            params["passed_data"]["custom_grid_forecast_id"] = runtimeparams[
+                "custom_grid_forecast_id"
+            ]
+        if "custom_cost_fun_id" in runtimeparams.keys():
+            params["passed_data"]["custom_cost_fun_id"] = runtimeparams[
+                "custom_cost_fun_id"
+            ]
+        if "custom_optim_status_id" in runtimeparams.keys():
+            params["passed_data"]["custom_optim_status_id"] = runtimeparams[
+                "custom_optim_status_id"
+            ]
+        if "custom_unit_load_cost_id" in runtimeparams.keys():
+            params["passed_data"]["custom_unit_load_cost_id"] = runtimeparams[
+                "custom_unit_load_cost_id"
+            ]
+        if "custom_unit_prod_price_id" in runtimeparams.keys():
+            params["passed_data"]["custom_unit_prod_price_id"] = runtimeparams[
+                "custom_unit_prod_price_id"
+            ]
+        if "custom_deferrable_forecast_id" in runtimeparams.keys():
+            params["passed_data"]["custom_deferrable_forecast_id"] = runtimeparams[
+                "custom_deferrable_forecast_id"
+            ]
         # A condition to put a prefix on all published data
-        if 'publish_prefix' not in runtimeparams.keys():
+        if "publish_prefix" not in runtimeparams.keys():
             publish_prefix = ""
         else:
-            publish_prefix = runtimeparams['publish_prefix']
-        params['passed_data']['publish_prefix'] = publish_prefix
+            publish_prefix = runtimeparams["publish_prefix"]
+        params["passed_data"]["publish_prefix"] = publish_prefix
     # Serialize the final params
     params = json.dumps(params)
     return params, retrieve_hass_conf, optim_conf, plant_conf
@@ -371,40 +529,45 @@ def get_yaml_parse(emhass_conf: dict, use_secrets: Optional[bool] = True,
             with open(emhass_conf["root_path"] / 'secrets_emhass.yaml', 'r') as file: #assume secrets file is in root path 
                 input_secrets = yaml.load(file, Loader=yaml.FullLoader)
         else:
-            input_secrets = input_conf.pop('params_secrets', None)
-   
-    if (type(input_conf['retrieve_hass_conf']) == list): #if using old config version 
-        retrieve_hass_conf = dict({key:d[key] for d in input_conf['retrieve_hass_conf'] for key in d})
+            input_secrets = input_conf.pop("params_secrets", None)
+
+    if type(input_conf["retrieve_hass_conf"]) == list:  # if using old config version
+        retrieve_hass_conf = dict(
+            {key: d[key] for d in input_conf["retrieve_hass_conf"] for key in d}
+        )
     else:
-        retrieve_hass_conf = input_conf.get('retrieve_hass_conf', {})
-        
+        retrieve_hass_conf = input_conf.get("retrieve_hass_conf", {})
+
     if use_secrets:
         retrieve_hass_conf.update(input_secrets)
     else:
-        retrieve_hass_conf['hass_url'] = 'http://supervisor/core/api'
-        retrieve_hass_conf['long_lived_token'] = '${SUPERVISOR_TOKEN}'
-        retrieve_hass_conf['time_zone'] = 'Europe/Paris'
-        retrieve_hass_conf['lat'] = 45.83
-        retrieve_hass_conf['lon'] = 6.86
-        retrieve_hass_conf['alt'] = 4807.8
-    retrieve_hass_conf['freq'] = pd.to_timedelta(retrieve_hass_conf['freq'], "minutes")
-    retrieve_hass_conf['time_zone'] = pytz.timezone(retrieve_hass_conf['time_zone'])
-    
-    if (type(input_conf['optim_conf']) == list):
-        optim_conf = dict({key:d[key] for d in input_conf['optim_conf'] for key in d})
+        retrieve_hass_conf["hass_url"] = "http://supervisor/core/api"
+        retrieve_hass_conf["long_lived_token"] = "${SUPERVISOR_TOKEN}"
+        retrieve_hass_conf["time_zone"] = "Europe/Paris"
+        retrieve_hass_conf["lat"] = 45.83
+        retrieve_hass_conf["lon"] = 6.86
+        retrieve_hass_conf["alt"] = 4807.8
+    retrieve_hass_conf["freq"] = pd.to_timedelta(retrieve_hass_conf["freq"], "minutes")
+    retrieve_hass_conf["time_zone"] = pytz.timezone(retrieve_hass_conf["time_zone"])
+
+    if type(input_conf["optim_conf"]) == list:
+        optim_conf = dict({key: d[key] for d in input_conf["optim_conf"] for key in d})
     else:
-        optim_conf = input_conf.get('optim_conf', {})
+        optim_conf = input_conf.get("optim_conf", {})
 
-    optim_conf['list_hp_periods'] = dict((key,d[key]) for d in optim_conf['list_hp_periods'] for key in d)
-    optim_conf['delta_forecast'] = pd.Timedelta(days=optim_conf['delta_forecast'])
-    
-    if (type(input_conf['plant_conf']) == list):
-        plant_conf = dict({key:d[key] for d in input_conf['plant_conf'] for key in d})
+    optim_conf["list_hp_periods"] = dict(
+        (key, d[key]) for d in optim_conf["list_hp_periods"] for key in d
+    )
+    optim_conf["delta_forecast"] = pd.Timedelta(days=optim_conf["delta_forecast"])
+
+    if type(input_conf["plant_conf"]) == list:
+        plant_conf = dict({key: d[key] for d in input_conf["plant_conf"] for key in d})
     else:
-        plant_conf = input_conf.get('plant_conf', {})
-    
+        plant_conf = input_conf.get("plant_conf", {})
+
     return retrieve_hass_conf, optim_conf, plant_conf
 
+
 def get_injection_dict(df: pd.DataFrame, plot_size: Optional[int] = 1366) -> dict:
     """
     Build a dictionary with graphs and tables for the webui.
@@ -415,61 +578,86 @@ def get_injection_dict(df: pd.DataFrame, plot_size: Optional[int] = 1366) -> dic
     :type plot_size: Optional[int], optional
     :return: A dictionary containing the graphs and tables in html format
     :rtype: dict
-    
+
     """
-    cols_p = [i for i in df.columns.to_list() if 'P_' in i]
+    cols_p = [i for i in df.columns.to_list() if "P_" in i]
     # Let's round the data in the DF
-    optim_status = df['optim_status'].unique().item()
-    df.drop('optim_status', axis=1, inplace=True)
-    cols_else = [i for i in df.columns.to_list() if 'P_' not in i]
+    optim_status = df["optim_status"].unique().item()
+    df.drop("optim_status", axis=1, inplace=True)
+    cols_else = [i for i in df.columns.to_list() if "P_" not in i]
     df = df.apply(pd.to_numeric)
     df[cols_p] = df[cols_p].astype(int)
     df[cols_else] = df[cols_else].round(3)
     # Create plots
     n_colors = len(cols_p)
-    colors = px.colors.sample_colorscale("jet", [n/(n_colors -1) for n in range(n_colors)])
-    fig_0 = px.line(df[cols_p], title='Systems powers schedule after optimization results', 
-                    template='presentation', line_shape="hv",
-                    color_discrete_sequence=colors)
-    fig_0.update_layout(xaxis_title='Timestamp', yaxis_title='System powers (W)')
-    if 'SOC_opt' in df.columns.to_list():
-        fig_1 = px.line(df['SOC_opt'], title='Battery state of charge schedule after optimization results', 
-                        template='presentation',  line_shape="hv",
-                        color_discrete_sequence=colors)
-        fig_1.update_layout(xaxis_title='Timestamp', yaxis_title='Battery SOC (%)')
-    cols_cost = [i for i in df.columns.to_list() if 'cost_' in i or 'unit_' in i]
+    colors = px.colors.sample_colorscale(
+        "jet", [n / (n_colors - 1) for n in range(n_colors)]
+    )
+    fig_0 = px.line(
+        df[cols_p],
+        title="Systems powers schedule after optimization results",
+        template="presentation",
+        line_shape="hv",
+        color_discrete_sequence=colors,
+    )
+    fig_0.update_layout(xaxis_title="Timestamp", yaxis_title="System powers (W)")
+    if "SOC_opt" in df.columns.to_list():
+        fig_1 = px.line(
+            df["SOC_opt"],
+            title="Battery state of charge schedule after optimization results",
+            template="presentation",
+            line_shape="hv",
+            color_discrete_sequence=colors,
+        )
+        fig_1.update_layout(xaxis_title="Timestamp", yaxis_title="Battery SOC (%)")
+    cols_cost = [i for i in df.columns.to_list() if "cost_" in i or "unit_" in i]
     n_colors = len(cols_cost)
-    colors = px.colors.sample_colorscale("jet", [n/(n_colors -1) for n in range(n_colors)])
-    fig_2 = px.line(df[cols_cost], title='Systems costs obtained from optimization results', 
-                    template='presentation', line_shape="hv",
-                    color_discrete_sequence=colors)
-    fig_2.update_layout(xaxis_title='Timestamp', yaxis_title='System costs (currency)')
+    colors = px.colors.sample_colorscale(
+        "jet", [n / (n_colors - 1) for n in range(n_colors)]
+    )
+    fig_2 = px.line(
+        df[cols_cost],
+        title="Systems costs obtained from optimization results",
+        template="presentation",
+        line_shape="hv",
+        color_discrete_sequence=colors,
+    )
+    fig_2.update_layout(xaxis_title="Timestamp", yaxis_title="System costs (currency)")
     # Get full path to image
-    image_path_0 = fig_0.to_html(full_html=False, default_width='75%')
-    if 'SOC_opt' in df.columns.to_list():
-        image_path_1 = fig_1.to_html(full_html=False, default_width='75%')
-    image_path_2 = fig_2.to_html(full_html=False, default_width='75%')
+    image_path_0 = fig_0.to_html(full_html=False, default_width="75%")
+    if "SOC_opt" in df.columns.to_list():
+        image_path_1 = fig_1.to_html(full_html=False, default_width="75%")
+    image_path_2 = fig_2.to_html(full_html=False, default_width="75%")
     # The tables
-    table1 = df.reset_index().to_html(classes='mystyle', index=False)
-    cost_cols = [i for i in df.columns if 'cost_' in i]
+    table1 = df.reset_index().to_html(classes="mystyle", index=False)
+    cost_cols = [i for i in df.columns if "cost_" in i]
     table2 = df[cost_cols].reset_index().sum(numeric_only=True)
-    table2['optim_status'] = optim_status
-    table2 = table2.to_frame(name='Value').reset_index(names='Variable').to_html(classes='mystyle', index=False)
+    table2["optim_status"] = optim_status
+    table2 = (
+        table2.to_frame(name="Value")
+        .reset_index(names="Variable")
+        .to_html(classes="mystyle", index=False)
+    )
     # The dict of plots
     injection_dict = {}
-    injection_dict['title'] = '<h2>EMHASS optimization results</h2>'
-    injection_dict['subsubtitle0'] = '<h4>Plotting latest optimization results</h4>'
-    injection_dict['figure_0'] = image_path_0
-    if 'SOC_opt' in df.columns.to_list():
-        injection_dict['figure_1'] = image_path_1
-    injection_dict['figure_2'] = image_path_2
-    injection_dict['subsubtitle1'] = '<h4>Last run optimization results table</h4>'
-    injection_dict['table1'] = table1
-    injection_dict['subsubtitle2'] = '<h4>Summary table for latest optimization results</h4>'
-    injection_dict['table2'] = table2
+    injection_dict["title"] = "<h2>EMHASS optimization results</h2>"
+    injection_dict["subsubtitle0"] = "<h4>Plotting latest optimization results</h4>"
+    injection_dict["figure_0"] = image_path_0
+    if "SOC_opt" in df.columns.to_list():
+        injection_dict["figure_1"] = image_path_1
+    injection_dict["figure_2"] = image_path_2
+    injection_dict["subsubtitle1"] = "<h4>Last run optimization results table</h4>"
+    injection_dict["table1"] = table1
+    injection_dict["subsubtitle2"] = (
+        "<h4>Summary table for latest optimization results</h4>"
+    )
+    injection_dict["table2"] = table2
     return injection_dict
 
-def get_injection_dict_forecast_model_fit(df_fit_pred: pd.DataFrame, mlf: MLForecaster) -> dict:
+
+def get_injection_dict_forecast_model_fit(
+    df_fit_pred: pd.DataFrame, mlf: MLForecaster
+) -> dict:
     """
     Build a dictionary with graphs and tables for the webui for special MLF fit case.
 
@@ -481,19 +669,26 @@ def get_injection_dict_forecast_model_fit(df_fit_pred: pd.DataFrame, mlf: MLFore
     :rtype: dict
     """
     fig = df_fit_pred.plot()
-    fig.layout.template = 'presentation'
-    fig.update_yaxes(title_text = mlf.model_type)
-    fig.update_xaxes(title_text = "Time")
-    image_path_0 = fig.to_html(full_html=False, default_width='75%')
+    fig.layout.template = "presentation"
+    fig.update_yaxes(title_text=mlf.model_type)
+    fig.update_xaxes(title_text="Time")
+    image_path_0 = fig.to_html(full_html=False, default_width="75%")
     # The dict of plots
     injection_dict = {}
-    injection_dict['title'] = '<h2>Custom machine learning forecast model fit</h2>'
-    injection_dict['subsubtitle0'] = '<h4>Plotting train/test forecast model results for '+mlf.model_type+'</h4>'
-    injection_dict['subsubtitle0'] = '<h4>Forecasting variable '+mlf.var_model+'</h4>'
-    injection_dict['figure_0'] = image_path_0
+    injection_dict["title"] = "<h2>Custom machine learning forecast model fit</h2>"
+    injection_dict["subsubtitle0"] = (
+        "<h4>Plotting train/test forecast model results for " + mlf.model_type + "</h4>"
+    )
+    injection_dict["subsubtitle0"] = (
+        "<h4>Forecasting variable " + mlf.var_model + "</h4>"
+    )
+    injection_dict["figure_0"] = image_path_0
     return injection_dict
 
-def get_injection_dict_forecast_model_tune(df_pred_optim: pd.DataFrame, mlf: MLForecaster) -> dict:
+
+def get_injection_dict_forecast_model_tune(
+    df_pred_optim: pd.DataFrame, mlf: MLForecaster
+) -> dict:
     """
     Build a dictionary with graphs and tables for the webui for special MLF tune case.
 
@@ -505,19 +700,32 @@ def get_injection_dict_forecast_model_tune(df_pred_optim: pd.DataFrame, mlf: MLF
     :rtype: dict
     """
     fig = df_pred_optim.plot()
-    fig.layout.template = 'presentation'
-    fig.update_yaxes(title_text = mlf.model_type)
-    fig.update_xaxes(title_text = "Time")
-    image_path_0 = fig.to_html(full_html=False, default_width='75%')
+    fig.layout.template = "presentation"
+    fig.update_yaxes(title_text=mlf.model_type)
+    fig.update_xaxes(title_text="Time")
+    image_path_0 = fig.to_html(full_html=False, default_width="75%")
     # The dict of plots
     injection_dict = {}
-    injection_dict['title'] = '<h2>Custom machine learning forecast model tune</h2>'
-    injection_dict['subsubtitle0'] = '<h4>Performed a tuning routine using bayesian optimization for '+mlf.model_type+'</h4>'
-    injection_dict['subsubtitle0'] = '<h4>Forecasting variable '+mlf.var_model+'</h4>'
-    injection_dict['figure_0'] = image_path_0
+    injection_dict["title"] = "<h2>Custom machine learning forecast model tune</h2>"
+    injection_dict["subsubtitle0"] = (
+        "<h4>Performed a tuning routine using bayesian optimization for "
+        + mlf.model_type
+        + "</h4>"
+    )
+    injection_dict["subsubtitle0"] = (
+        "<h4>Forecasting variable " + mlf.var_model + "</h4>"
+    )
+    injection_dict["figure_0"] = image_path_0
     return injection_dict
 
-def build_params(params: dict, params_secrets: dict, options: dict, addon: int, logger: logging.Logger) -> dict:
+
+def build_params(
+    params: dict,
+    params_secrets: dict,
+    options: dict,
+    addon: int,
+    logger: logging.Logger,
+) -> dict:
     """
     Build the main params dictionary from the loaded options.json when using the add-on.
 
@@ -536,45 +744,120 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int,
     """
     if addon == 1:
         # Updating variables in retrieve_hass_conf
-        params['retrieve_hass_conf']['freq'] = options.get('optimization_time_step',params['retrieve_hass_conf']['freq'])
-        params['retrieve_hass_conf']['days_to_retrieve'] = options.get('historic_days_to_retrieve',params['retrieve_hass_conf']['days_to_retrieve'])
-        params['retrieve_hass_conf']['var_PV'] = options.get('sensor_power_photovoltaics',params['retrieve_hass_conf']['var_PV'])
-        params['retrieve_hass_conf']['var_load'] = options.get('sensor_power_load_no_var_loads',params['retrieve_hass_conf']['var_load'])
-        params['retrieve_hass_conf']['load_negative'] = options.get('load_negative',params['retrieve_hass_conf']['load_negative'])
-        params['retrieve_hass_conf']['set_zero_min'] = options.get('set_zero_min',params['retrieve_hass_conf']['set_zero_min'])
-        params['retrieve_hass_conf']['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'])]
-        params['retrieve_hass_conf']['method_ts_round'] = options.get('method_ts_round',params['retrieve_hass_conf']['method_ts_round'])
+        params["retrieve_hass_conf"]["freq"] = options.get(
+            "optimization_time_step", params["retrieve_hass_conf"]["freq"]
+        )
+        params["retrieve_hass_conf"]["days_to_retrieve"] = options.get(
+            "historic_days_to_retrieve",
+            params["retrieve_hass_conf"]["days_to_retrieve"],
+        )
+        params["retrieve_hass_conf"]["var_PV"] = options.get(
+            "sensor_power_photovoltaics", params["retrieve_hass_conf"]["var_PV"]
+        )
+        params["retrieve_hass_conf"]["var_load"] = options.get(
+            "sensor_power_load_no_var_loads", params["retrieve_hass_conf"]["var_load"]
+        )
+        params["retrieve_hass_conf"]["load_negative"] = options.get(
+            "load_negative", params["retrieve_hass_conf"]["load_negative"]
+        )
+        params["retrieve_hass_conf"]["set_zero_min"] = options.get(
+            "set_zero_min", params["retrieve_hass_conf"]["set_zero_min"]
+        )
+        params["retrieve_hass_conf"]["var_replace_zero"] = [
+            options.get(
+                "sensor_power_photovoltaics",
+                params["retrieve_hass_conf"]["var_replace_zero"],
+            )
+        ]
+        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"],
+            ),
+        ]
+        params["retrieve_hass_conf"]["method_ts_round"] = options.get(
+            "method_ts_round", params["retrieve_hass_conf"]["method_ts_round"]
+        )
         # Update params Secrets if specified
-        params['params_secrets'] = params_secrets
-        params['params_secrets']['time_zone'] = options.get('time_zone',params_secrets['time_zone'])
-        params['params_secrets']['lat'] = options.get('Latitude',params_secrets['lat'])
-        params['params_secrets']['lon'] = options.get('Longitude',params_secrets['lon'])
-        params['params_secrets']['alt'] = options.get('Altitude',params_secrets['alt'])
+        params["params_secrets"] = params_secrets
+        params["params_secrets"]["time_zone"] = options.get(
+            "time_zone", params_secrets["time_zone"]
+        )
+        params["params_secrets"]["lat"] = options.get("Latitude", params_secrets["lat"])
+        params["params_secrets"]["lon"] = options.get(
+            "Longitude", params_secrets["lon"]
+        )
+        params["params_secrets"]["alt"] = options.get("Altitude", params_secrets["alt"])
         # Updating variables in optim_conf
-        params['optim_conf']['set_use_battery'] = options.get('set_use_battery',params['optim_conf']['set_use_battery'])
-        params['optim_conf']['num_def_loads'] = options.get('number_of_deferrable_loads',params['optim_conf']['num_def_loads'])
-        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')]
-        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')]
-        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']['weather_forecast_method'] = options.get('weather_forecast_method',params['optim_conf']['weather_forecast_method'])
+        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")
+            ]
+        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")
+            ]
+        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"]["weather_forecast_method"] = options.get(
+            "weather_forecast_method", params["optim_conf"]["weather_forecast_method"]
+        )
         # Update optional param secrets
-        if params['optim_conf']['weather_forecast_method'] == "solcast":
-            params['params_secrets']['solcast_api_key'] = options.get('optional_solcast_api_key',params_secrets.get('solcast_api_key',"123456"))
-            params['params_secrets']['solcast_rooftop_id'] = options.get('optional_solcast_rooftop_id',params_secrets.get('solcast_rooftop_id',"123456"))
-        elif params['optim_conf']['weather_forecast_method'] == "solar.forecast":    
-            params['params_secrets']['solar_forecast_kwp'] = options.get('optional_solar_forecast_kwp',params_secrets.get('solar_forecast_kwp',5))
-        params['optim_conf']['load_forecast_method'] = options.get('load_forecast_method',params['optim_conf']['load_forecast_method'])
-        params['optim_conf']['delta_forecast'] = options.get('delta_forecast_daily',params['optim_conf']['delta_forecast'])
-        params['optim_conf']['load_cost_forecast_method'] = options.get('load_cost_forecast_method',params['optim_conf']['load_cost_forecast_method'])
-        if options.get('list_set_deferrable_load_single_constant',None) != None: 
-            params['optim_conf']['set_def_constant'] = [i['set_deferrable_load_single_constant'] for i in options.get('list_set_deferrable_load_single_constant')]
-        if options.get('list_peak_hours_periods_start_hours',None) != None and options.get('list_peak_hours_periods_end_hours',None) != None:
-            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']]
+        if params["optim_conf"]["weather_forecast_method"] == "solcast":
+            params["params_secrets"]["solcast_api_key"] = options.get(
+                "optional_solcast_api_key",
+                params_secrets.get("solcast_api_key", "123456"),
+            )
+            params["params_secrets"]["solcast_rooftop_id"] = options.get(
+                "optional_solcast_rooftop_id",
+                params_secrets.get("solcast_rooftop_id", "123456"),
+            )
+        elif params["optim_conf"]["weather_forecast_method"] == "solar.forecast":
+            params["params_secrets"]["solar_forecast_kwp"] = options.get(
+                "optional_solar_forecast_kwp",
+                params_secrets.get("solar_forecast_kwp", 5),
+            )
+        params["optim_conf"]["load_forecast_method"] = options.get(
+            "load_forecast_method", params["optim_conf"]["load_forecast_method"]
+        )
+        params["optim_conf"]["delta_forecast"] = options.get(
+            "delta_forecast_daily", params["optim_conf"]["delta_forecast"]
+        )
+        params["optim_conf"]["load_cost_forecast_method"] = options.get(
+            "load_cost_forecast_method",
+            params["optim_conf"]["load_cost_forecast_method"],
+        )
+        if options.get("list_set_deferrable_load_single_constant", None) != None:
+            params["optim_conf"]["set_def_constant"] = [
+                i["set_deferrable_load_single_constant"]
+                for i in options.get("list_set_deferrable_load_single_constant")
+            ]
+        if (
+            options.get("list_peak_hours_periods_start_hours", None) != None
+            and options.get("list_peak_hours_periods_end_hours", None) != None
+        ):
+            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
@@ -646,20 +929,35 @@ def build_params(params: dict, params_secrets: dict, options: dict, addon: int,
             for x in range(len(params['optim_conf']['P_deferrable_nom']), params['optim_conf']['num_def_loads']):
                 params['optim_conf']['P_deferrable_nom'].append(0)   
         # days_to_retrieve should be no less then 2
-        if params['retrieve_hass_conf']['days_to_retrieve'] < 2:
-            params['retrieve_hass_conf']['days_to_retrieve'] = 2
-            logger.warning("days_to_retrieve should not be lower then 2, setting days_to_retrieve to 2. Make sure your sensors also have at least 2 days of history")
+        if params["retrieve_hass_conf"]["days_to_retrieve"] < 2:
+            params["retrieve_hass_conf"]["days_to_retrieve"] = 2
+            logger.warning(
+                "days_to_retrieve should not be lower then 2, setting days_to_retrieve to 2. Make sure your sensors also have at least 2 days of history"
+            )
     else:
-        params['params_secrets'] = params_secrets
+        params["params_secrets"] = params_secrets
     # The params dict
-    params['passed_data'] = {'pv_power_forecast':None,'load_power_forecast':None,'load_cost_forecast':None,'prod_price_forecast':None,
-                             'prediction_horizon':None,'soc_init':None,'soc_final':None,'def_total_hours':None,'def_start_timestep':None,'def_end_timestep':None,'alpha':None,'beta':None}
+    params["passed_data"] = {
+        "pv_power_forecast": None,
+        "load_power_forecast": None,
+        "load_cost_forecast": None,
+        "prod_price_forecast": None,
+        "prediction_horizon": None,
+        "soc_init": None,
+        "soc_final": None,
+        "def_total_hours": None,
+        "def_start_timestep": None,
+        "def_end_timestep": None,
+        "alpha": None,
+        "beta": None,
+    }
     return params
 
+
 def get_days_list(days_to_retrieve: int) -> pd.date_range:
     """
     Get list of past days from today to days_to_retrieve.
-    
+
     :param days_to_retrieve: Total number of days to retrieve from the past
     :type days_to_retrieve: int
     :return: The list of days
@@ -668,19 +966,20 @@ def get_days_list(days_to_retrieve: int) -> pd.date_range:
     """
     today = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0)
     d = (today - timedelta(days=days_to_retrieve)).isoformat()
-    days_list = pd.date_range(start=d, end=today.isoformat(), freq='D')
-    
+    days_list = pd.date_range(start=d, end=today.isoformat(), freq="D")
+
     return days_list
 
+
 def set_df_index_freq(df: pd.DataFrame) -> pd.DataFrame:
     """
     Set the freq of a DataFrame DateTimeIndex.
-    
+
     :param df: Input DataFrame
     :type df: pd.DataFrame
     :return: Input DataFrame with freq defined
     :rtype: pd.DataFrame
-    
+
     """
     idx_diff = np.diff(df.index)
     sampling = pd.to_timedelta(np.median(idx_diff))
diff --git a/src/emhass/web_server.py b/src/emhass/web_server.py
index 2fdd2861..afb5370a 100644
--- a/src/emhass/web_server.py
+++ b/src/emhass/web_server.py
@@ -13,6 +13,7 @@
 from emhass.command_line import set_input_data_dict
 from emhass.command_line import perfect_forecast_optim, dayahead_forecast_optim, naive_mpc_optim
 from emhass.command_line import forecast_model_fit, forecast_model_predict, forecast_model_tune
+from emhass.command_line import regressor_model_fit, regressor_model_predict
 from emhass.command_line import publish_data
 from emhass.utils import get_injection_dict, get_injection_dict_forecast_model_fit, \
     get_injection_dict_forecast_model_tune, build_params
@@ -193,6 +194,22 @@ def action_call(action_name):
         if not checkFileLog(ActionStr):
             return make_response(msg, 201)
         return make_response(grabLog(ActionStr), 400)
+    elif action_name == 'regressor-model-fit':
+        ActionStr = " >> Performing a machine learning regressor fit..."
+        app.logger.info(ActionStr)
+        regressor_model_fit(input_data_dict, app.logger)
+        msg = f'EMHASS >> Action regressor-model-fit executed... \n'
+        if not checkFileLog(ActionStr):
+            return make_response(msg, 201)
+        return make_response(grabLog(ActionStr), 400)
+    elif action_name == 'regressor-model-predict':
+        ActionStr = " >> Performing a machine learning regressor predict..."
+        app.logger.info(ActionStr)
+        regressor_model_predict(input_data_dict, app.logger)
+        msg = f'EMHASS >> Action regressor-model-predict executed... \n'
+        if not checkFileLog(ActionStr):
+            return make_response(msg, 201)
+        return make_response(grabLog(ActionStr), 400)
     else:
         app.logger.error("ERROR: passed action is not valid")
         msg = f'EMHASS >> ERROR: Passed action is not valid... \n'


Test patch:
diff --git a/tests/test_command_line_utils.py b/tests/test_command_line_utils.py
index fb775f86..91aa9f00 100644
--- a/tests/test_command_line_utils.py
+++ b/tests/test_command_line_utils.py
@@ -5,10 +5,21 @@
 from unittest.mock import patch
 import pandas as pd
 import pathlib, json, yaml, copy
+import numpy as np
 
 from emhass.command_line import set_input_data_dict
-from emhass.command_line import perfect_forecast_optim, dayahead_forecast_optim, naive_mpc_optim
-from emhass.command_line import forecast_model_fit, forecast_model_predict, forecast_model_tune
+from emhass.command_line import (
+    perfect_forecast_optim,
+    dayahead_forecast_optim,
+    naive_mpc_optim,
+)
+from emhass.command_line import (
+    forecast_model_fit,
+    forecast_model_predict,
+    forecast_model_tune,
+    regressor_model_fit,
+    regressor_model_predict,
+)
 from emhass.command_line import publish_data
 from emhass.command_line import main
 from emhass import utils
@@ -311,13 +322,99 @@ def test_forecast_model_fit_predict_tune(self):
         self.assertIsInstance(df_pred, pd.Series)
         self.assertTrue(df_pred.isnull().sum().sum() == 0)
         # Test the tune method
-        df_pred_optim, mlf = forecast_model_tune(input_data_dict, logger, debug=True, mlf=mlf)
+        df_pred_optim, mlf = forecast_model_tune(
+            input_data_dict, logger, debug=True, mlf=mlf
+        )
         self.assertIsInstance(df_pred_optim, pd.DataFrame)
         self.assertTrue(mlf.is_tuned == True)
-        # Test ijection_dict for tune method on webui
+        # Test injection_dict for tune method on webui
         injection_dict = utils.get_injection_dict_forecast_model_tune(df_fit_pred, mlf)
         self.assertIsInstance(injection_dict, dict)
-        self.assertIsInstance(injection_dict['figure_0'], str)
+        self.assertIsInstance(injection_dict["figure_0"], str)
+
+    def test_regressor_model_fit_predict(self):
+        costfun = "profit"
+        action = "regressor-model-fit"  # fit and predict methods
+        params = TestCommandLineUtils.get_test_params()
+        runtimeparams = {
+            "csv_file": "heating_prediction.csv",
+            "features": ["degreeday", "solar"],
+            "target": "hour",
+            "regression_model": "AdaBoostRegression",
+            "model_type": "heating_hours_degreeday",
+            "timestamp": "timestamp",
+            "date_features": ["month", "day_of_week"],
+            "mlr_predict_entity_id": "sensor.predicted_hours_test",
+            "mlr_predict_unit_of_measurement": "h",
+            "mlr_predict_friendly_name": "Predicted hours",
+            "new_values": [12.79, 4.766, 1, 2],
+        }
+        runtimeparams_json = json.dumps(runtimeparams)
+        params_json = json.dumps(params)
+        input_data_dict = set_input_data_dict(
+            emhass_conf,
+            costfun,
+            params_json,
+            runtimeparams_json,
+            action,
+            logger,
+            get_data_from_file=True,
+        )
+        self.assertTrue(
+            input_data_dict["params"]["passed_data"]["model_type"]
+            == "heating_hours_degreeday",
+        )
+        self.assertTrue(
+            input_data_dict["params"]["passed_data"]["regression_model"]
+            == "AdaBoostRegression",
+        )
+        self.assertTrue(
+            input_data_dict["params"]["passed_data"]["csv_file"]
+            == "heating_prediction.csv",
+        )
+        mlr = regressor_model_fit(input_data_dict, logger, debug=True)
+
+        # def test_regressor_model_predict(self):
+        costfun = "profit"
+        action = "regressor-model-predict"  # predict methods
+        params = TestCommandLineUtils.get_test_params()
+        runtimeparams = {
+            "csv_file": "heating_prediction.csv",
+            "features": ["degreeday", "solar"],
+            "target": "hour",
+            "regression_model": "AdaBoostRegression",
+            "model_type": "heating_hours_degreeday",
+            "timestamp": "timestamp",
+            "date_features": ["month", "day_of_week"],
+            "mlr_predict_entity_id": "sensor.predicted_hours_test",
+            "mlr_predict_unit_of_measurement": "h",
+            "mlr_predict_friendly_name": "Predicted hours",
+            "new_values": [12.79, 4.766, 1, 2],
+        }
+        runtimeparams_json = json.dumps(runtimeparams)
+        params["passed_data"] = runtimeparams
+        params_json = json.dumps(params)
+
+        input_data_dict = set_input_data_dict(
+            emhass_conf,
+            costfun,
+            params_json,
+            runtimeparams_json,
+            action,
+            logger,
+            get_data_from_file=True,
+        )
+        self.assertTrue(
+            input_data_dict["params"]["passed_data"]["model_type"]
+            == "heating_hours_degreeday",
+        )
+        self.assertTrue(
+            input_data_dict["params"]["passed_data"]["mlr_predict_friendly_name"]
+            == "Predicted hours",
+        )
+
+        regressor_model_predict(input_data_dict, logger, debug=True, mlr=mlr)
+
     
     @patch('sys.argv', ['main', '--action', 'test', '--config', str(emhass_conf['config_path']), 
                         '--debug', 'True'])
@@ -330,27 +427,30 @@ def test_main_wrong_action(self):
     def test_main_perfect_forecast_optim(self):
         opt_res = main()
         self.assertIsInstance(opt_res, pd.DataFrame)
-        self.assertTrue(opt_res.isnull().sum().sum()==0)
+        self.assertTrue(opt_res.isnull().sum().sum() == 0)
         self.assertIsInstance(opt_res.index, pd.core.indexes.datetimes.DatetimeIndex)
-        self.assertIsInstance(opt_res.index.dtype, pd.core.dtypes.dtypes.DatetimeTZDtype)
-        
+        self.assertIsInstance(
+            opt_res.index.dtype,
+            pd.core.dtypes.dtypes.DatetimeTZDtype,
+        )
+
     def test_main_dayahead_forecast_optim(self):
         with patch('sys.argv', ['main', '--action', 'dayahead-optim', '--config', str(emhass_conf['config_path']), 
                                 '--params', self.params_json, '--runtimeparams', self.runtimeparams_json,
                                 '--debug', 'True']):
             opt_res = main()
         self.assertIsInstance(opt_res, pd.DataFrame)
-        self.assertTrue(opt_res.isnull().sum().sum()==0)
-        
+        self.assertTrue(opt_res.isnull().sum().sum() == 0)
+
     def test_main_naive_mpc_optim(self):
         with patch('sys.argv', ['main', '--action', 'naive-mpc-optim', '--config', str(emhass_conf['config_path']), 
                                 '--params', self.params_json, '--runtimeparams', self.runtimeparams_json,
                                 '--debug', 'True']):
             opt_res = main()
         self.assertIsInstance(opt_res, pd.DataFrame)
-        self.assertTrue(opt_res.isnull().sum().sum()==0)
-        self.assertTrue(len(opt_res)==10)
-        
+        self.assertTrue(opt_res.isnull().sum().sum() == 0)
+        self.assertTrue(len(opt_res) == 10)
+
     def test_main_forecast_model_fit(self):
         params = copy.deepcopy(json.loads(self.params_json))
         runtimeparams = {
@@ -381,12 +481,12 @@ def test_main_forecast_model_predict(self):
             "var_model": "sensor.power_load_no_var_loads",
             "sklearn_model": "KNeighborsRegressor",
             "num_lags": 48,
-            "split_date_delta": '48h',
-            "perform_backtest": False
+            "split_date_delta": "48h",
+            "perform_backtest": False,
         }
         runtimeparams_json = json.dumps(runtimeparams)
-        params['passed_data'] = runtimeparams
-        params['optim_conf']['load_forecast_method'] = 'skforecast'
+        params["passed_data"] = runtimeparams
+        params["optim_conf"]["load_forecast_method"] = "skforecast"
         params_json = json.dumps(params)
         with patch('sys.argv', ['main', '--action', 'forecast-model-predict', '--config', str(emhass_conf['config_path']), 
                                 '--params', params_json, '--runtimeparams', runtimeparams_json,
@@ -394,7 +494,7 @@ def test_main_forecast_model_predict(self):
             df_pred = main()
         self.assertIsInstance(df_pred, pd.Series)
         self.assertTrue(df_pred.isnull().sum().sum() == 0)
-        
+
     def test_main_forecast_model_tune(self):
         params = copy.deepcopy(json.loads(self.params_json))
         runtimeparams = {
@@ -403,12 +503,12 @@ def test_main_forecast_model_tune(self):
             "var_model": "sensor.power_load_no_var_loads",
             "sklearn_model": "KNeighborsRegressor",
             "num_lags": 48,
-            "split_date_delta": '48h',
-            "perform_backtest": False
+            "split_date_delta": "48h",
+            "perform_backtest": False,
         }
         runtimeparams_json = json.dumps(runtimeparams)
-        params['passed_data'] = runtimeparams
-        params['optim_conf']['load_forecast_method'] = 'skforecast'
+        params["passed_data"] = runtimeparams
+        params["optim_conf"]["load_forecast_method"] = "skforecast"
         params_json = json.dumps(params)
         with patch('sys.argv', ['main', '--action', 'forecast-model-tune', '--config', str(emhass_conf['config_path']), 
                                 '--params', params_json, '--runtimeparams', runtimeparams_json,
@@ -416,6 +516,74 @@ def test_main_forecast_model_tune(self):
             df_pred_optim, mlf = main()
         self.assertIsInstance(df_pred_optim, pd.DataFrame)
         self.assertTrue(mlf.is_tuned == True)
+
+    def test_main_regressor_model_fit(self):
+        params = copy.deepcopy(json.loads(self.params_json))
+        runtimeparams = {
+            "csv_file": "heating_prediction.csv",
+            "features": ["degreeday", "solar"],
+            "target": "hour",
+            "regression_model": "AdaBoostRegression",
+            "model_type": "heating_hours_degreeday",
+            "timestamp": "timestamp",
+            "date_features": ["month", "day_of_week"],
+        }
+        runtimeparams_json = json.dumps(runtimeparams)
+        params["passed_data"] = runtimeparams
+        params_json = json.dumps(params)
+        with patch(
+            "sys.argv",
+            [
+                "main",
+                "--action",
+                "regressor-model-fit",
+                "--config",
+                str(emhass_conf["config_path"]),
+                "--params",
+                params_json,
+                "--runtimeparams",
+                runtimeparams_json,
+                "--debug",
+                "True",
+            ],
+        ):
+            mlr = main()
+
+    def test_main_regressor_model_predict(self):
+        params = copy.deepcopy(json.loads(self.params_json))
+        runtimeparams = {
+            "csv_file": "heating_prediction.csv",
+            "features": ["degreeday", "solar"],
+            "target": "hour",
+            "regression_model": "AdaBoostRegression",
+            "model_type": "heating_hours_degreeday",
+            "timestamp": "timestamp",
+            "date_features": ["month", "day_of_week"],
+            "new_values": [12.79, 4.766, 1, 2],
+        }
+        runtimeparams_json = json.dumps(runtimeparams)
+        params["passed_data"] = runtimeparams
+        params["optim_conf"]["load_forecast_method"] = "skforecast"
+        params_json = json.dumps(params)
+        with patch(
+            "sys.argv",
+            [
+                "main",
+                "--action",
+                "regressor-model-predict",
+                "--config",
+                str(emhass_conf["config_path"]),
+                "--params",
+                params_json,
+                "--runtimeparams",
+                runtimeparams_json,
+                "--debug",
+                "True",
+            ],
+        ):
+            prediction = main()
+        self.assertIsInstance(prediction, np.ndarray)
+
         
     @patch('sys.argv', ['main', '--action', 'publish-data', '--config', str(emhass_conf['config_path']), 
                         '--debug', 'True'])
diff --git a/tests/test_machine_learning_regressor.py b/tests/test_machine_learning_regressor.py
new file mode 100644
index 00000000..0d40ac0b
--- /dev/null
+++ b/tests/test_machine_learning_regressor.py
@@ -0,0 +1,111 @@
+"""Machine learning regressor test module."""
+
+import copy
+import json
+import pathlib
+import unittest
+
+import numpy as np
+import pandas as pd
+import yaml
+from emhass import utils
+from emhass.command_line import set_input_data_dict
+from emhass.machine_learning_regressor import MLRegressor
+from sklearn.pipeline import Pipeline
+
+# the root folder
+root = str(utils.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 = utils.get_logger(__name__, emhass_conf, save_to_file=False)
+
+
+class TestMLRegressor(unittest.TestCase):
+    @staticmethod
+    def get_test_params():
+        with open(emhass_conf["config_path"]) as file:
+            params = yaml.safe_load(file)
+        params.update(
+            {
+                "params_secrets": {
+                    "hass_url": "http://supervisor/core/api",
+                    "long_lived_token": "${SUPERVISOR_TOKEN}",
+                    "time_zone": "Europe/Paris",
+                    "lat": 45.83,
+                    "lon": 6.86,
+                    "alt": 8000.0,
+                },
+            },
+        )
+        return params
+
+    def setUp(self):
+        params = TestMLRegressor.get_test_params()
+        params_json = json.dumps(params)
+        costfun = "profit"
+        action = "regressor-model-fit"  # fit and predict methods
+        params = copy.deepcopy(json.loads(params_json))
+        runtimeparams = {
+            "csv_file": "heating_prediction.csv",
+            "features": ["degreeday", "solar"],
+            "target": "hour",
+            "regression_model": "AdaBoostRegression",
+            "model_type": "heating_hours_degreeday",
+            "timestamp": "timestamp",
+            "date_features": ["month", "day_of_week"],
+            "new_values": [12.79, 4.766, 1, 2]
+        }
+        runtimeparams_json = json.dumps(runtimeparams)
+        params["passed_data"] = runtimeparams
+        params["optim_conf"]["load_forecast_method"] = "skforecast"
+        params_json = json.dumps(params)
+        self.input_data_dict = set_input_data_dict(
+            emhass_conf,
+            costfun,
+            params_json,
+            runtimeparams_json,
+            action,
+            logger,
+            get_data_from_file=True,
+        )
+        data = copy.deepcopy(self.input_data_dict["df_input_data"])
+        self.assertIsInstance(data, pd.DataFrame)
+        self.csv_file = self.input_data_dict["params"]["passed_data"]["csv_file"]
+        features = self.input_data_dict["params"]["passed_data"]["features"]
+        target = self.input_data_dict["params"]["passed_data"]["target"]
+        regression_model = self.input_data_dict["params"]["passed_data"][
+            "regression_model"
+        ]
+        model_type = self.input_data_dict["params"]["passed_data"]["model_type"]
+        timestamp = self.input_data_dict["params"]["passed_data"]["timestamp"]
+        self.date_features = self.input_data_dict["params"]["passed_data"][
+            "date_features"
+        ]
+        self.new_values = self.input_data_dict["params"]["passed_data"]["new_values"]
+        self.mlr = MLRegressor(
+            data,
+            model_type,
+            regression_model,
+            features,
+            target,
+            timestamp,
+            logger,
+        )
+
+    def test_fit(self):
+        self.mlr.fit(self.date_features)
+        self.assertIsInstance(self.mlr.model, Pipeline)
+
+    def test_predict(self):
+        self.mlr.fit(self.date_features)
+        predictions = self.mlr.predict(self.new_values)
+        self.assertIsInstance(predictions, np.ndarray)
+
+
+if __name__ == "__main__":
+    unittest.main()
+    ch.close()
+    logger.removeHandler(ch)