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.
Files changed (37) hide show
  1. {emhass-0.13.0 → emhass-0.13.2}/.gitignore +2 -0
  2. {emhass-0.13.0 → emhass-0.13.2}/PKG-INFO +13 -4
  3. {emhass-0.13.0 → emhass-0.13.2}/README.md +1 -1
  4. emhass-0.13.2/pyproject.toml +105 -0
  5. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/command_line.py +104 -63
  6. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/data/associations.csv +2 -0
  7. emhass-0.13.2/src/emhass/data/cec_inverters.pbz2 +0 -0
  8. emhass-0.13.2/src/emhass/data/cec_modules.pbz2 +0 -0
  9. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/data/config_defaults.json +5 -3
  10. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/forecast.py +281 -131
  11. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/machine_learning_forecaster.py +12 -14
  12. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/machine_learning_regressor.py +1 -3
  13. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/optimization.py +217 -171
  14. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/retrieve_hass.py +62 -45
  15. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/data/param_definitions.json +12 -0
  16. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/utils.py +62 -56
  17. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/web_server.py +3 -6
  18. emhass-0.13.0/emhass/data/cec_inverters.pbz2 +0 -0
  19. emhass-0.13.0/emhass/data/cec_modules.pbz2 +0 -0
  20. emhass-0.13.0/emhass/data/emhass_inverters.csv +0 -8
  21. emhass-0.13.0/emhass/data/emhass_modules.csv +0 -6
  22. emhass-0.13.0/pyproject.toml +0 -84
  23. {emhass-0.13.0 → emhass-0.13.2}/LICENSE +0 -0
  24. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/__init__.py +0 -0
  25. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/img/emhass_icon.png +0 -0
  26. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/advanced.html +0 -0
  27. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/basic.html +0 -0
  28. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/configuration_list.html +0 -0
  29. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/configuration_script.js +0 -0
  30. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/img/emhass_icon.png +0 -0
  31. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/img/emhass_logo_short.svg +0 -0
  32. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/img/feather-sprite.svg +0 -0
  33. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/script.js +0 -0
  34. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/static/style.css +0 -0
  35. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/templates/configuration.html +0 -0
  36. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/templates/index.html +0 -0
  37. {emhass-0.13.0 → emhass-0.13.2/src}/emhass/templates/template.html +0 -0
@@ -47,6 +47,8 @@ MANIFEST
47
47
 
48
48
  # Local session data
49
49
  data/actionLogs.txt
50
+ data/debug-*.csv
51
+ data/cached-open-meteo-forecast.json
50
52
  data/entities/*.json
51
53
 
52
54
  # PyInstaller
@@ -1,18 +1,25 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: emhass
3
- Version: 0.13.0
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-Expression: MIT
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 :: Build Tools
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 - P_grid_neg. A positive value indicates net export, while a negative value indicates net import. | sensor.p_grid_forecast |
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 - P_grid_neg. A positive value indicates net export, while a negative value indicates net import. | sensor.p_grid_forecast |
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 datetime, timezone
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(retrieve_hass_conf["historic_days_to_retrieve"])
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
- if not rh.get_data(days_list, var_list, minimal_response=False, significant_changes_only=False):
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", get_data_from_file, retrieve_hass_conf, optim_conf, rh, emhass_conf, test_df_literal
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: Optional[bool] = False,
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, get_data_from_file, retrieve_hass_conf, optim_conf, rh, emhass_conf, test_df_literal
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, fcst, P_PV_forecast, get_data_from_file,
273
- retrieve_hass_conf, optim_conf, rh, emhass_conf,
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
- (optim_conf.get("load_forecast_method", None) == "list"
325
- and optim_conf.get("weather_forecast_method", None) == "list")
326
- or (optim_conf.get("load_forecast_method", None) == "list"
327
- and not(optim_conf["set_use_pv"]))
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, get_data_from_file, retrieve_hass_conf, optim_conf, rh, emhass_conf, test_df_literal
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, fcst, P_PV_forecast, get_data_from_file,
358
- retrieve_hass_conf, optim_conf, rh, emhass_conf,
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: Optional[bool] = True,
551
- debug: Optional[bool] = False,
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 = True
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 = True
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: Optional[bool] = False,
616
- debug: Optional[bool] = False,
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(timezone.utc).replace(
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: Optional[bool] = False,
689
- debug: Optional[bool] = False,
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(timezone.utc).replace(
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: Optional[bool] = False
783
- ) -> Tuple[pd.DataFrame, pd.DataFrame, MLForecaster]:
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: Optional[bool] = True,
829
- debug: Optional[bool] = False,
830
- mlf: Optional[MLForecaster] = None,
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 was not found, please run a model fit method before this predict method",
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: Optional[bool] = False,
927
- mlf: Optional[MLForecaster] = None,
928
- ) -> Tuple[pd.DataFrame, MLForecaster]:
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 was not found, please run a model fit method before this tune method",
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: Optional[bool] = False
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: Optional[bool] = False,
1031
- mlr: Optional[MLRegressor] = None,
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: Optional[bool] = False,
1096
- opt_res_latest: Optional[pd.DataFrame] = None,
1097
- entity_save: Optional[bool] = False,
1098
- dont_post: Optional[bool] = False,
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(timezone.utc).replace(
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{}".format(k) not in opt_res_latest.columns:
1314
+ if f"P_deferrable{k}" not in opt_res_latest.columns:
1274
1315
  logger.error(
1275
- "P_deferrable{}".format(k)
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{}".format(k)],
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{}".format(k)]
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{}".format(k)],
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{}".format(k)]
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, "r") as file:
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: Optional[str] = "",
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, "r") as file:
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