emhass 0.13.0__tar.gz → 0.13.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {emhass-0.13.0 → emhass-0.13.2}/.gitignore +2 -0
- {emhass-0.13.0 → emhass-0.13.2}/PKG-INFO +13 -4
- {emhass-0.13.0 → emhass-0.13.2}/README.md +1 -1
- emhass-0.13.2/pyproject.toml +105 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/command_line.py +104 -63
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/data/associations.csv +2 -0
- emhass-0.13.2/src/emhass/data/cec_inverters.pbz2 +0 -0
- emhass-0.13.2/src/emhass/data/cec_modules.pbz2 +0 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/data/config_defaults.json +5 -3
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/forecast.py +281 -131
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/machine_learning_forecaster.py +12 -14
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/machine_learning_regressor.py +1 -3
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/optimization.py +217 -171
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/retrieve_hass.py +62 -45
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/data/param_definitions.json +12 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/utils.py +62 -56
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/web_server.py +3 -6
- emhass-0.13.0/emhass/data/cec_inverters.pbz2 +0 -0
- emhass-0.13.0/emhass/data/cec_modules.pbz2 +0 -0
- emhass-0.13.0/emhass/data/emhass_inverters.csv +0 -8
- emhass-0.13.0/emhass/data/emhass_modules.csv +0 -6
- emhass-0.13.0/pyproject.toml +0 -84
- {emhass-0.13.0 → emhass-0.13.2}/LICENSE +0 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/__init__.py +0 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/img/emhass_icon.png +0 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/advanced.html +0 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/basic.html +0 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/configuration_list.html +0 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/configuration_script.js +0 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/img/emhass_icon.png +0 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/img/emhass_logo_short.svg +0 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/img/feather-sprite.svg +0 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/script.js +0 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/style.css +0 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/templates/configuration.html +0 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/templates/index.html +0 -0
- {emhass-0.13.0 → emhass-0.13.2/src}/emhass/templates/template.html +0 -0
@@ -1,18 +1,25 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: emhass
|
3
|
-
Version: 0.13.
|
3
|
+
Version: 0.13.2
|
4
4
|
Summary: An Energy Management System for Home Assistant
|
5
5
|
Project-URL: Homepage, https://github.com/davidusb-geek/emhass
|
6
|
+
Project-URL: Source, https://github.com/davidusb-geek/emhass
|
7
|
+
Project-URL: Issues, https://github.com/davidusb-geek/emhass/issues
|
8
|
+
Project-URL: Documentation, https://emhass.readthedocs.io/en/latest/
|
9
|
+
Project-URL: Community, https://community.home-assistant.io/t/emhass-an-energy-management-for-home-assistant
|
6
10
|
Author-email: David HERNANDEZ <davidusb@gmail.com>
|
7
|
-
License
|
11
|
+
License: MIT
|
8
12
|
License-File: LICENSE
|
9
13
|
Keywords: energy,hass,management,optimization
|
10
14
|
Classifier: Development Status :: 5 - Production/Stable
|
11
15
|
Classifier: Intended Audience :: Developers
|
12
16
|
Classifier: License :: OSI Approved :: MIT License
|
13
17
|
Classifier: Operating System :: OS Independent
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
14
21
|
Classifier: Programming Language :: Python :: 3.12
|
15
|
-
Classifier: Topic :: Software Development ::
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
16
23
|
Requires-Python: <3.13,>=3.10
|
17
24
|
Requires-Dist: flask>=3.1.0
|
18
25
|
Requires-Dist: gunicorn>=23.0.0
|
@@ -30,6 +37,8 @@ Requires-Dist: scipy>=1.15.0
|
|
30
37
|
Requires-Dist: skforecast>=0.14.0
|
31
38
|
Requires-Dist: tables>=3.10.0
|
32
39
|
Requires-Dist: waitress>=3.0.2
|
40
|
+
Provides-Extra: dev
|
41
|
+
Requires-Dist: ruff; extra == 'dev'
|
33
42
|
Provides-Extra: docs
|
34
43
|
Requires-Dist: myst-parser; extra == 'docs'
|
35
44
|
Requires-Dist: sphinx; extra == 'docs'
|
@@ -456,7 +465,7 @@ Below you can find a list of the variables resulting from EMHASS computation, sh
|
|
456
465
|
| P_grid_pos | Forecasted power imported from the grid (Watts). This indicates the amount of energy you are expected to draw from the grid when your solar production is insufficient to meet your needs or it is advantageous to consume from the grid. | - |
|
457
466
|
| P_grid_neg | Forecasted power exported to the grid (Watts). This indicates the amount of excess solar energy you are expected to send back to the grid during the forecast period. | - |
|
458
467
|
| P_batt | Forecasted (dis)charge power load (Watts) for the battery (if installed). If negative it indicates the battery is charging, if positive that the battery is discharging. | sensor.p_batt_forecast |
|
459
|
-
| P_grid | Forecasted net power flow between your home and the grid (Watts). This is calculated as P_grid_pos
|
468
|
+
| P_grid | Forecasted net power flow between your home and the grid (Watts). This is calculated as P_grid_pos + P_grid_neg. A positive value indicates net import, while a negative value indicates net export. | sensor.p_grid_forecast |
|
460
469
|
| SOC_opt | Forecasted battery optimized Status Of Charge (SOC) percentage level | sensor.soc_batt_forecast |
|
461
470
|
| unit_load_cost | Forecasted cost per unit of energy you pay to the grid (typically "Currency"/kWh). This helps you understand the expected energy cost during the forecast period. | sensor.unit_load_cost |
|
462
471
|
| unit_prod_price | Forecasted price you receive for selling excess solar energy back to the grid (typically "Currency"/kWh). This helps you understand the potential income from your solar production. | sensor.unit_prod_price |
|
@@ -410,7 +410,7 @@ Below you can find a list of the variables resulting from EMHASS computation, sh
|
|
410
410
|
| P_grid_pos | Forecasted power imported from the grid (Watts). This indicates the amount of energy you are expected to draw from the grid when your solar production is insufficient to meet your needs or it is advantageous to consume from the grid. | - |
|
411
411
|
| P_grid_neg | Forecasted power exported to the grid (Watts). This indicates the amount of excess solar energy you are expected to send back to the grid during the forecast period. | - |
|
412
412
|
| P_batt | Forecasted (dis)charge power load (Watts) for the battery (if installed). If negative it indicates the battery is charging, if positive that the battery is discharging. | sensor.p_batt_forecast |
|
413
|
-
| P_grid | Forecasted net power flow between your home and the grid (Watts). This is calculated as P_grid_pos
|
413
|
+
| P_grid | Forecasted net power flow between your home and the grid (Watts). This is calculated as P_grid_pos + P_grid_neg. A positive value indicates net import, while a negative value indicates net export. | sensor.p_grid_forecast |
|
414
414
|
| SOC_opt | Forecasted battery optimized Status Of Charge (SOC) percentage level | sensor.soc_batt_forecast |
|
415
415
|
| unit_load_cost | Forecasted cost per unit of energy you pay to the grid (typically "Currency"/kWh). This helps you understand the expected energy cost during the forecast period. | sensor.unit_load_cost |
|
416
416
|
| unit_prod_price | Forecasted price you receive for selling excess solar energy back to the grid (typically "Currency"/kWh). This helps you understand the potential income from your solar production. | sensor.unit_prod_price |
|
@@ -0,0 +1,105 @@
|
|
1
|
+
[project]
|
2
|
+
name = "emhass"
|
3
|
+
version = "0.13.2"
|
4
|
+
description = "An Energy Management System for Home Assistant"
|
5
|
+
readme = "README.md"
|
6
|
+
requires-python = ">=3.10, <3.13"
|
7
|
+
authors = [
|
8
|
+
{ name = "David HERNANDEZ", email = "davidusb@gmail.com" },
|
9
|
+
]
|
10
|
+
license = { text = "MIT" }
|
11
|
+
keywords = ["energy", "management", "optimization", "hass"]
|
12
|
+
classifiers = [
|
13
|
+
"Development Status :: 5 - Production/Stable",
|
14
|
+
"Intended Audience :: Developers",
|
15
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
16
|
+
"License :: OSI Approved :: MIT License",
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
18
|
+
"Programming Language :: Python :: 3.10",
|
19
|
+
"Programming Language :: Python :: 3.11",
|
20
|
+
"Programming Language :: Python :: 3.12",
|
21
|
+
"Operating System :: OS Independent",
|
22
|
+
]
|
23
|
+
dependencies = [
|
24
|
+
"numpy>=2.0.0, <2.3.0",
|
25
|
+
"scipy>=1.15.0",
|
26
|
+
"pandas>=2.2.0",
|
27
|
+
"pvlib>=0.11.0",
|
28
|
+
"protobuf>=5.29.3",
|
29
|
+
"pytz>=2024.2",
|
30
|
+
"requests>=2.32.2",
|
31
|
+
"h5py>=3.12.1",
|
32
|
+
"pulp>=2.8.0",
|
33
|
+
"pyyaml>=6.0.2",
|
34
|
+
"tables>=3.10.0",
|
35
|
+
"skforecast>=0.14.0",
|
36
|
+
"flask>=3.1.0",
|
37
|
+
"waitress>=3.0.2",
|
38
|
+
"plotly>=6.0.0",
|
39
|
+
"gunicorn>=23.0.0",
|
40
|
+
]
|
41
|
+
[build-system]
|
42
|
+
requires = ["hatchling"]
|
43
|
+
build-backend = "hatchling.build"
|
44
|
+
|
45
|
+
|
46
|
+
[tool.uv.workspace]
|
47
|
+
members = ["emhass"]
|
48
|
+
|
49
|
+
[tool.uv.sources]
|
50
|
+
emhass = { workspace = true }
|
51
|
+
|
52
|
+
[tool.uv]
|
53
|
+
default-groups = "all"
|
54
|
+
package = true
|
55
|
+
|
56
|
+
[project.optional-dependencies]
|
57
|
+
docs = ["sphinx", "sphinx-rtd-theme", "myst-parser"]
|
58
|
+
test = ["requests-mock", "pytest", "coverage", "snakeviz", "ruff", "tabulate", "hatchling"]
|
59
|
+
dev = ["ruff"]
|
60
|
+
|
61
|
+
[tool.hatch.build.targets.wheel]
|
62
|
+
packages = ["src/emhass"]
|
63
|
+
package-data = { "emhass" = [
|
64
|
+
"templates/*",
|
65
|
+
"static/*",
|
66
|
+
"img/*",
|
67
|
+
"data/cec_modules.pbz2",
|
68
|
+
"data/cec_inverters.pbz2",
|
69
|
+
"data/associations.csv",
|
70
|
+
"data/config_defaults.json"]}
|
71
|
+
|
72
|
+
[tool.hatch.build.targets.sdist]
|
73
|
+
include = [
|
74
|
+
"src/emhass/*.py",
|
75
|
+
"src/emhass/templates/",
|
76
|
+
"src/emhass/static/",
|
77
|
+
"src/emhass/img/",
|
78
|
+
"src/emhass/data/cec_modules.pbz2",
|
79
|
+
"src/emhass/data/cec_inverters.pbz2",
|
80
|
+
"src/emhass/data/associations.csv",
|
81
|
+
"src/emhass/data/config_defaults.json",
|
82
|
+
]
|
83
|
+
|
84
|
+
[project.scripts]
|
85
|
+
emhass = "emhass.command_line:main"
|
86
|
+
|
87
|
+
[project.urls]
|
88
|
+
Homepage = "https://github.com/davidusb-geek/emhass"
|
89
|
+
Source = "https://github.com/davidusb-geek/emhass"
|
90
|
+
Issues = "https://github.com/davidusb-geek/emhass/issues"
|
91
|
+
Documentation = "https://emhass.readthedocs.io/en/latest/"
|
92
|
+
Community = "https://community.home-assistant.io/t/emhass-an-energy-management-for-home-assistant"
|
93
|
+
|
94
|
+
[tool.ruff.lint]
|
95
|
+
select = ["E", "W", "F", "I", "C", "B", "Q", "UP", "YTT", "PYI"]
|
96
|
+
ignore = ["E501", "B008", "B905", "C901"]
|
97
|
+
|
98
|
+
[dependency-groups]
|
99
|
+
dev = [
|
100
|
+
"emhass",
|
101
|
+
]
|
102
|
+
|
103
|
+
[tool.ruff]
|
104
|
+
target-version = "py311"
|
105
|
+
src = ["src", "tests"]
|
@@ -1,5 +1,4 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
|
4
3
|
import argparse
|
5
4
|
import copy
|
@@ -10,9 +9,8 @@ import pathlib
|
|
10
9
|
import pickle
|
11
10
|
import re
|
12
11
|
import time
|
13
|
-
from datetime import
|
12
|
+
from datetime import UTC, datetime
|
14
13
|
from importlib.metadata import version
|
15
|
-
from typing import Optional, Tuple
|
16
14
|
|
17
15
|
import numpy as np
|
18
16
|
import pandas as pd
|
@@ -28,19 +26,21 @@ default_csv_filename = "opt_res_latest.csv"
|
|
28
26
|
default_pkl_suffix = "_mlf.pkl"
|
29
27
|
default_metadata_json = "metadata.json"
|
30
28
|
|
29
|
+
|
31
30
|
def retrieve_home_assistant_data(
|
32
31
|
set_type: str,
|
33
|
-
get_data_from_file: bool,
|
32
|
+
get_data_from_file: bool,
|
34
33
|
retrieve_hass_conf: dict,
|
35
34
|
optim_conf: dict,
|
36
35
|
rh: RetrieveHass,
|
37
36
|
emhass_conf: dict,
|
38
|
-
test_df_literal: pd.DataFrame
|
37
|
+
test_df_literal: pd.DataFrame,
|
39
38
|
) -> dict:
|
40
39
|
"""Retrieve data from Home Assistant or file and prepare it for optimization."""
|
41
40
|
if get_data_from_file:
|
42
41
|
with open(emhass_conf["data_path"] / test_df_literal, "rb") as inp:
|
43
42
|
rh.df_final, days_list, var_list, rh.ha_config = pickle.load(inp)
|
43
|
+
rh.var_list = var_list
|
44
44
|
# Assign variables based on set_type
|
45
45
|
retrieve_hass_conf["sensor_power_load_no_var_loads"] = str(var_list[0])
|
46
46
|
if optim_conf.get("set_use_pv", True):
|
@@ -61,7 +61,9 @@ def retrieve_home_assistant_data(
|
|
61
61
|
else:
|
62
62
|
# Determine days_list based on set_type
|
63
63
|
if set_type == "perfect-optim" or set_type == "adjust_pv":
|
64
|
-
days_list = utils.get_days_list(
|
64
|
+
days_list = utils.get_days_list(
|
65
|
+
retrieve_hass_conf["historic_days_to_retrieve"]
|
66
|
+
)
|
65
67
|
elif set_type == "naive-mpc-optim":
|
66
68
|
days_list = utils.get_days_list(1)
|
67
69
|
else:
|
@@ -69,7 +71,11 @@ def retrieve_home_assistant_data(
|
|
69
71
|
var_list = [retrieve_hass_conf["sensor_power_load_no_var_loads"]]
|
70
72
|
if optim_conf.get("set_use_pv", True):
|
71
73
|
var_list.append(retrieve_hass_conf["sensor_power_photovoltaics"])
|
72
|
-
|
74
|
+
if optim_conf.get("set_use_adjusted_pv", True):
|
75
|
+
var_list.append(retrieve_hass_conf["sensor_power_photovoltaics_forecast"])
|
76
|
+
if not rh.get_data(
|
77
|
+
days_list, var_list, minimal_response=False, significant_changes_only=False
|
78
|
+
):
|
73
79
|
return False, None, days_list
|
74
80
|
rh.prepare_data(
|
75
81
|
retrieve_hass_conf["sensor_power_load_no_var_loads"],
|
@@ -80,6 +86,7 @@ def retrieve_home_assistant_data(
|
|
80
86
|
)
|
81
87
|
return True, rh.df_final.copy(), days_list
|
82
88
|
|
89
|
+
|
83
90
|
def adjust_pv_forecast(
|
84
91
|
logger: logging.Logger,
|
85
92
|
fcst: Forecast,
|
@@ -121,7 +128,13 @@ def adjust_pv_forecast(
|
|
121
128
|
logger.info("Adjusting PV forecast, retrieving history data for model fit")
|
122
129
|
# Retrieve data from Home Assistant
|
123
130
|
success, df_input_data, _ = retrieve_home_assistant_data(
|
124
|
-
"adjust_pv",
|
131
|
+
"adjust_pv",
|
132
|
+
get_data_from_file,
|
133
|
+
retrieve_hass_conf,
|
134
|
+
optim_conf,
|
135
|
+
rh,
|
136
|
+
emhass_conf,
|
137
|
+
test_df_literal,
|
125
138
|
)
|
126
139
|
if not success:
|
127
140
|
return False
|
@@ -138,6 +151,7 @@ def adjust_pv_forecast(
|
|
138
151
|
# Update the PV forecast
|
139
152
|
return P_PV_forecast["adjusted_forecast"].rename(None)
|
140
153
|
|
154
|
+
|
141
155
|
def set_input_data_dict(
|
142
156
|
emhass_conf: dict,
|
143
157
|
costfun: str,
|
@@ -145,7 +159,7 @@ def set_input_data_dict(
|
|
145
159
|
runtimeparams: str,
|
146
160
|
set_type: str,
|
147
161
|
logger: logging.Logger,
|
148
|
-
get_data_from_file:
|
162
|
+
get_data_from_file: bool | None = False,
|
149
163
|
) -> dict:
|
150
164
|
"""
|
151
165
|
Set up some of the data needed for the different actions.
|
@@ -247,7 +261,13 @@ def set_input_data_dict(
|
|
247
261
|
if set_type == "perfect-optim":
|
248
262
|
# Retrieve data from hass
|
249
263
|
success, df_input_data, days_list = retrieve_home_assistant_data(
|
250
|
-
set_type,
|
264
|
+
set_type,
|
265
|
+
get_data_from_file,
|
266
|
+
retrieve_hass_conf,
|
267
|
+
optim_conf,
|
268
|
+
rh,
|
269
|
+
emhass_conf,
|
270
|
+
test_df_literal,
|
251
271
|
)
|
252
272
|
if not success:
|
253
273
|
return False
|
@@ -269,8 +289,14 @@ def set_input_data_dict(
|
|
269
289
|
if optim_conf["set_use_adjusted_pv"]:
|
270
290
|
# Update the PV forecast
|
271
291
|
P_PV_forecast = adjust_pv_forecast(
|
272
|
-
logger,
|
273
|
-
|
292
|
+
logger,
|
293
|
+
fcst,
|
294
|
+
P_PV_forecast,
|
295
|
+
get_data_from_file,
|
296
|
+
retrieve_hass_conf,
|
297
|
+
optim_conf,
|
298
|
+
rh,
|
299
|
+
emhass_conf,
|
274
300
|
test_df_literal,
|
275
301
|
)
|
276
302
|
else:
|
@@ -321,10 +347,11 @@ def set_input_data_dict(
|
|
321
347
|
df_input_data, days_list = None, None
|
322
348
|
elif set_type == "naive-mpc-optim":
|
323
349
|
if (
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
350
|
+
optim_conf.get("load_forecast_method", None) == "list"
|
351
|
+
and optim_conf.get("weather_forecast_method", None) == "list"
|
352
|
+
) or (
|
353
|
+
optim_conf.get("load_forecast_method", None) == "list"
|
354
|
+
and not (optim_conf["set_use_pv"])
|
328
355
|
):
|
329
356
|
days_list = None
|
330
357
|
set_mix_forecast = False
|
@@ -332,7 +359,13 @@ def set_input_data_dict(
|
|
332
359
|
else:
|
333
360
|
# Retrieve data from hass
|
334
361
|
success, df_input_data, days_list = retrieve_home_assistant_data(
|
335
|
-
set_type,
|
362
|
+
set_type,
|
363
|
+
get_data_from_file,
|
364
|
+
retrieve_hass_conf,
|
365
|
+
optim_conf,
|
366
|
+
rh,
|
367
|
+
emhass_conf,
|
368
|
+
test_df_literal,
|
336
369
|
)
|
337
370
|
if not success:
|
338
371
|
return False
|
@@ -354,8 +387,14 @@ def set_input_data_dict(
|
|
354
387
|
if optim_conf["set_use_adjusted_pv"]:
|
355
388
|
# Update the PV forecast
|
356
389
|
P_PV_forecast = adjust_pv_forecast(
|
357
|
-
logger,
|
358
|
-
|
390
|
+
logger,
|
391
|
+
fcst,
|
392
|
+
P_PV_forecast,
|
393
|
+
get_data_from_file,
|
394
|
+
retrieve_hass_conf,
|
395
|
+
optim_conf,
|
396
|
+
rh,
|
397
|
+
emhass_conf,
|
359
398
|
test_df_literal,
|
360
399
|
)
|
361
400
|
else:
|
@@ -547,8 +586,8 @@ def weather_forecast_cache(
|
|
547
586
|
def perfect_forecast_optim(
|
548
587
|
input_data_dict: dict,
|
549
588
|
logger: logging.Logger,
|
550
|
-
save_data_to_file:
|
551
|
-
debug:
|
589
|
+
save_data_to_file: bool | None = True,
|
590
|
+
debug: bool | None = False,
|
552
591
|
) -> pd.DataFrame:
|
553
592
|
"""
|
554
593
|
Perform a call to the perfect forecast optimization routine.
|
@@ -570,14 +609,14 @@ def perfect_forecast_optim(
|
|
570
609
|
df_input_data = input_data_dict["fcst"].get_load_cost_forecast(
|
571
610
|
input_data_dict["df_input_data"],
|
572
611
|
method=input_data_dict["fcst"].optim_conf["load_cost_forecast_method"],
|
573
|
-
list_and_perfect
|
612
|
+
list_and_perfect=True,
|
574
613
|
)
|
575
614
|
if isinstance(df_input_data, bool) and not df_input_data:
|
576
615
|
return False
|
577
616
|
df_input_data = input_data_dict["fcst"].get_prod_price_forecast(
|
578
617
|
df_input_data,
|
579
618
|
method=input_data_dict["fcst"].optim_conf["production_price_forecast_method"],
|
580
|
-
list_and_perfect
|
619
|
+
list_and_perfect=True,
|
581
620
|
)
|
582
621
|
if isinstance(df_input_data, bool) and not df_input_data:
|
583
622
|
return False
|
@@ -612,8 +651,8 @@ def perfect_forecast_optim(
|
|
612
651
|
def dayahead_forecast_optim(
|
613
652
|
input_data_dict: dict,
|
614
653
|
logger: logging.Logger,
|
615
|
-
save_data_to_file:
|
616
|
-
debug:
|
654
|
+
save_data_to_file: bool | None = False,
|
655
|
+
debug: bool | None = False,
|
617
656
|
) -> pd.DataFrame:
|
618
657
|
"""
|
619
658
|
Perform a call to the day-ahead optimization routine.
|
@@ -655,9 +694,7 @@ def dayahead_forecast_optim(
|
|
655
694
|
)
|
656
695
|
# Save CSV file for publish_data
|
657
696
|
if save_data_to_file:
|
658
|
-
today = datetime.now(
|
659
|
-
hour=0, minute=0, second=0, microsecond=0
|
660
|
-
)
|
697
|
+
today = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0)
|
661
698
|
filename = "opt_res_dayahead_" + today.strftime("%Y_%m_%d") + ".csv"
|
662
699
|
else: # Just save the latest optimization results
|
663
700
|
filename = default_csv_filename
|
@@ -685,8 +722,8 @@ def dayahead_forecast_optim(
|
|
685
722
|
def naive_mpc_optim(
|
686
723
|
input_data_dict: dict,
|
687
724
|
logger: logging.Logger,
|
688
|
-
save_data_to_file:
|
689
|
-
debug:
|
725
|
+
save_data_to_file: bool | None = False,
|
726
|
+
debug: bool | None = False,
|
690
727
|
) -> pd.DataFrame:
|
691
728
|
"""
|
692
729
|
Perform a call to the naive Model Predictive Controller optimization routine.
|
@@ -751,9 +788,7 @@ def naive_mpc_optim(
|
|
751
788
|
)
|
752
789
|
# Save CSV file for publish_data
|
753
790
|
if save_data_to_file:
|
754
|
-
today = datetime.now(
|
755
|
-
hour=0, minute=0, second=0, microsecond=0
|
756
|
-
)
|
791
|
+
today = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0)
|
757
792
|
filename = "opt_res_naive_mpc_" + today.strftime("%Y_%m_%d") + ".csv"
|
758
793
|
else: # Just save the latest optimization results
|
759
794
|
filename = default_csv_filename
|
@@ -779,8 +814,8 @@ def naive_mpc_optim(
|
|
779
814
|
|
780
815
|
|
781
816
|
def forecast_model_fit(
|
782
|
-
input_data_dict: dict, logger: logging.Logger, debug:
|
783
|
-
) ->
|
817
|
+
input_data_dict: dict, logger: logging.Logger, debug: bool | None = False
|
818
|
+
) -> tuple[pd.DataFrame, pd.DataFrame, MLForecaster]:
|
784
819
|
"""Perform a forecast model fit from training data retrieved from Home Assistant.
|
785
820
|
|
786
821
|
:param input_data_dict: A dictionnary with multiple data used by the action functions
|
@@ -819,15 +854,16 @@ def forecast_model_fit(
|
|
819
854
|
filename_path = input_data_dict["emhass_conf"]["data_path"] / filename
|
820
855
|
with open(filename_path, "wb") as outp:
|
821
856
|
pickle.dump(mlf, outp, pickle.HIGHEST_PROTOCOL)
|
857
|
+
logger.debug("saved model to " + str(filename_path))
|
822
858
|
return df_pred, df_pred_backtest, mlf
|
823
859
|
|
824
860
|
|
825
861
|
def forecast_model_predict(
|
826
862
|
input_data_dict: dict,
|
827
863
|
logger: logging.Logger,
|
828
|
-
use_last_window:
|
829
|
-
debug:
|
830
|
-
mlf:
|
864
|
+
use_last_window: bool | None = True,
|
865
|
+
debug: bool | None = False,
|
866
|
+
mlf: MLForecaster | None = None,
|
831
867
|
) -> pd.DataFrame:
|
832
868
|
r"""Perform a forecast model predict using a previously trained skforecast model.
|
833
869
|
|
@@ -857,9 +893,12 @@ def forecast_model_predict(
|
|
857
893
|
if filename_path.is_file():
|
858
894
|
with open(filename_path, "rb") as inp:
|
859
895
|
mlf = pickle.load(inp)
|
896
|
+
logger.debug("loaded saved model from " + str(filename_path))
|
860
897
|
else:
|
861
898
|
logger.error(
|
862
|
-
"The ML forecaster file
|
899
|
+
"The ML forecaster file ("
|
900
|
+
+ str(filename_path)
|
901
|
+
+ ") was not found, please run a model fit method before this predict method",
|
863
902
|
)
|
864
903
|
return
|
865
904
|
# Make predictions
|
@@ -923,9 +962,9 @@ def forecast_model_predict(
|
|
923
962
|
def forecast_model_tune(
|
924
963
|
input_data_dict: dict,
|
925
964
|
logger: logging.Logger,
|
926
|
-
debug:
|
927
|
-
mlf:
|
928
|
-
) ->
|
965
|
+
debug: bool | None = False,
|
966
|
+
mlf: MLForecaster | None = None,
|
967
|
+
) -> tuple[pd.DataFrame, MLForecaster]:
|
929
968
|
"""Tune a forecast model hyperparameters using bayesian optimization.
|
930
969
|
|
931
970
|
:param input_data_dict: A dictionnary with multiple data used by the action functions
|
@@ -948,9 +987,12 @@ def forecast_model_tune(
|
|
948
987
|
if filename_path.is_file():
|
949
988
|
with open(filename_path, "rb") as inp:
|
950
989
|
mlf = pickle.load(inp)
|
990
|
+
logger.debug("loaded saved model from " + str(filename_path))
|
951
991
|
else:
|
952
992
|
logger.error(
|
953
|
-
"The ML forecaster file
|
993
|
+
"The ML forecaster file ("
|
994
|
+
+ str(filename_path)
|
995
|
+
+ ") was not found, please run a model fit method before this tune method",
|
954
996
|
)
|
955
997
|
return None, None
|
956
998
|
# Tune the model
|
@@ -961,11 +1003,12 @@ def forecast_model_tune(
|
|
961
1003
|
filename_path = input_data_dict["emhass_conf"]["data_path"] / filename
|
962
1004
|
with open(filename_path, "wb") as outp:
|
963
1005
|
pickle.dump(mlf, outp, pickle.HIGHEST_PROTOCOL)
|
1006
|
+
logger.debug("Saved model to " + str(filename_path))
|
964
1007
|
return df_pred_optim, mlf
|
965
1008
|
|
966
1009
|
|
967
1010
|
def regressor_model_fit(
|
968
|
-
input_data_dict: dict, logger: logging.Logger, debug:
|
1011
|
+
input_data_dict: dict, logger: logging.Logger, debug: bool | None = False
|
969
1012
|
) -> MLRegressor:
|
970
1013
|
"""Perform a forecast model fit from training data retrieved from Home Assistant.
|
971
1014
|
|
@@ -1027,8 +1070,8 @@ def regressor_model_fit(
|
|
1027
1070
|
def regressor_model_predict(
|
1028
1071
|
input_data_dict: dict,
|
1029
1072
|
logger: logging.Logger,
|
1030
|
-
debug:
|
1031
|
-
mlr:
|
1073
|
+
debug: bool | None = False,
|
1074
|
+
mlr: MLRegressor | None = None,
|
1032
1075
|
) -> np.ndarray:
|
1033
1076
|
"""Perform a prediction from csv file.
|
1034
1077
|
|
@@ -1092,10 +1135,10 @@ def regressor_model_predict(
|
|
1092
1135
|
def publish_data(
|
1093
1136
|
input_data_dict: dict,
|
1094
1137
|
logger: logging.Logger,
|
1095
|
-
save_data_to_file:
|
1096
|
-
opt_res_latest:
|
1097
|
-
entity_save:
|
1098
|
-
dont_post:
|
1138
|
+
save_data_to_file: bool | None = False,
|
1139
|
+
opt_res_latest: pd.DataFrame | None = None,
|
1140
|
+
entity_save: bool | None = False,
|
1141
|
+
dont_post: bool | None = False,
|
1099
1142
|
) -> pd.DataFrame:
|
1100
1143
|
"""
|
1101
1144
|
Publish the data obtained from the optimization results.
|
@@ -1123,9 +1166,7 @@ def publish_data(
|
|
1123
1166
|
|
1124
1167
|
# Check if a day ahead optimization has been performed (read CSV file)
|
1125
1168
|
if save_data_to_file:
|
1126
|
-
today = datetime.now(
|
1127
|
-
hour=0, minute=0, second=0, microsecond=0
|
1128
|
-
)
|
1169
|
+
today = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0)
|
1129
1170
|
filename = "opt_res_dayahead_" + today.strftime("%Y_%m_%d") + ".csv"
|
1130
1171
|
# If publish_prefix is passed, check if there is saved entities in data_path/entities with prefix, publish to results
|
1131
1172
|
elif params["passed_data"].get("publish_prefix", "") != "" and not dont_post:
|
@@ -1270,14 +1311,14 @@ def publish_data(
|
|
1270
1311
|
"custom_deferrable_forecast_id"
|
1271
1312
|
]
|
1272
1313
|
for k in range(input_data_dict["opt"].optim_conf["number_of_deferrable_loads"]):
|
1273
|
-
if "P_deferrable{}"
|
1314
|
+
if f"P_deferrable{k}" not in opt_res_latest.columns:
|
1274
1315
|
logger.error(
|
1275
|
-
"P_deferrable{}"
|
1316
|
+
f"P_deferrable{k}"
|
1276
1317
|
+ " was not found in results DataFrame. Optimization task may need to be relaunched or it did not converge to a solution.",
|
1277
1318
|
)
|
1278
1319
|
else:
|
1279
1320
|
input_data_dict["rh"].post_data(
|
1280
|
-
opt_res_latest["P_deferrable{}"
|
1321
|
+
opt_res_latest[f"P_deferrable{k}"],
|
1281
1322
|
idx_closest,
|
1282
1323
|
custom_deferrable_forecast_id[k]["entity_id"],
|
1283
1324
|
"power",
|
@@ -1288,7 +1329,7 @@ def publish_data(
|
|
1288
1329
|
save_entities=entity_save,
|
1289
1330
|
dont_post=dont_post,
|
1290
1331
|
)
|
1291
|
-
cols_published = cols_published + ["P_deferrable{}"
|
1332
|
+
cols_published = cols_published + [f"P_deferrable{k}"]
|
1292
1333
|
# Publish thermal model data (predicted temperature)
|
1293
1334
|
custom_predicted_temperature_id = params["passed_data"][
|
1294
1335
|
"custom_predicted_temperature_id"
|
@@ -1300,7 +1341,7 @@ def publish_data(
|
|
1300
1341
|
in input_data_dict["opt"].optim_conf["def_load_config"][k]
|
1301
1342
|
):
|
1302
1343
|
input_data_dict["rh"].post_data(
|
1303
|
-
opt_res_latest["predicted_temp_heater{}"
|
1344
|
+
opt_res_latest[f"predicted_temp_heater{k}"],
|
1304
1345
|
idx_closest,
|
1305
1346
|
custom_predicted_temperature_id[k]["entity_id"],
|
1306
1347
|
"temperature",
|
@@ -1311,7 +1352,7 @@ def publish_data(
|
|
1311
1352
|
save_entities=entity_save,
|
1312
1353
|
dont_post=dont_post,
|
1313
1354
|
)
|
1314
|
-
cols_published = cols_published + ["predicted_temp_heater{}"
|
1355
|
+
cols_published = cols_published + [f"predicted_temp_heater{k}"]
|
1315
1356
|
# Publish battery power
|
1316
1357
|
if input_data_dict["opt"].optim_conf["set_use_battery"]:
|
1317
1358
|
if "P_batt" not in opt_res_latest.columns:
|
@@ -1484,7 +1525,7 @@ def continual_publish(
|
|
1484
1525
|
)
|
1485
1526
|
# Retrieve entity metadata from file
|
1486
1527
|
if os.path.isfile(entity_path / default_metadata_json):
|
1487
|
-
with open(entity_path / default_metadata_json
|
1528
|
+
with open(entity_path / default_metadata_json) as file:
|
1488
1529
|
metadata = json.load(file)
|
1489
1530
|
# Check if freq should be shorter
|
1490
1531
|
if metadata.get("lowest_time_step", None) is not None:
|
@@ -1499,7 +1540,7 @@ def publish_json(
|
|
1499
1540
|
input_data_dict: dict,
|
1500
1541
|
entity_path: pathlib.Path,
|
1501
1542
|
logger: logging.Logger,
|
1502
|
-
reference:
|
1543
|
+
reference: str | None = "",
|
1503
1544
|
):
|
1504
1545
|
"""
|
1505
1546
|
Extract saved entity data from .json (in data_path/entities), build entity, post results to post_data
|
@@ -1518,7 +1559,7 @@ def publish_json(
|
|
1518
1559
|
"""
|
1519
1560
|
# Retrieve entity metadata from file
|
1520
1561
|
if os.path.isfile(entity_path / default_metadata_json):
|
1521
|
-
with open(entity_path / default_metadata_json
|
1562
|
+
with open(entity_path / default_metadata_json) as file:
|
1522
1563
|
metadata = json.load(file)
|
1523
1564
|
else:
|
1524
1565
|
logger.error("unable to located metadata.json in:" + entity_path)
|
@@ -1560,7 +1601,7 @@ def publish_json(
|
|
1560
1601
|
data_df=entity_data[metadata[entity_id]["name"]],
|
1561
1602
|
idx=idx_closest,
|
1562
1603
|
entity_id=entity_id,
|
1563
|
-
device_class=dict.get(metadata[entity_id],"device_class"),
|
1604
|
+
device_class=dict.get(metadata[entity_id], "device_class"),
|
1564
1605
|
unit_of_measurement=metadata[entity_id]["unit_of_measurement"],
|
1565
1606
|
friendly_name=metadata[entity_id]["friendly_name"],
|
1566
1607
|
type_var=metadata[entity_id].get("type_var", ""),
|
@@ -38,6 +38,7 @@ optim_conf,prod_sell_price,photovoltaic_production_sell_price
|
|
38
38
|
optim_conf,set_total_pv_sell,set_total_pv_sell
|
39
39
|
optim_conf,lp_solver,lp_solver
|
40
40
|
optim_conf,lp_solver_path,lp_solver_path
|
41
|
+
optim_conf,lp_solver_timeout,lp_solver_timeout
|
41
42
|
optim_conf,set_nocharge_from_grid,set_nocharge_from_grid
|
42
43
|
optim_conf,set_nodischarge_to_grid,set_nodischarge_to_grid
|
43
44
|
optim_conf,set_battery_dynamic,set_battery_dynamic
|
@@ -46,6 +47,7 @@ optim_conf,battery_dynamic_min,battery_dynamic_min
|
|
46
47
|
optim_conf,weight_battery_discharge,weight_battery_discharge
|
47
48
|
optim_conf,weight_battery_charge,weight_battery_charge
|
48
49
|
optim_conf,weather_forecast_method,weather_forecast_method
|
50
|
+
optim_conf,open_meteo_cache_max_age,open_meteo_cache_max_age
|
49
51
|
optim_conf,def_start_timestep,start_timesteps_of_each_deferrable_load,list_start_timesteps_of_each_deferrable_load
|
50
52
|
optim_conf,def_end_timestep,end_timesteps_of_each_deferrable_load,list_end_timesteps_of_each_deferrable_load
|
51
53
|
optim_conf,list_hp_periods,load_peak_hour_periods
|
Binary file
|
Binary file
|