emhass 0.13.2__py3-none-any.whl → 0.13.4__py3-none-any.whl

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/command_line.py CHANGED
@@ -72,7 +72,9 @@ def retrieve_home_assistant_data(
72
72
  if optim_conf.get("set_use_pv", True):
73
73
  var_list.append(retrieve_hass_conf["sensor_power_photovoltaics"])
74
74
  if optim_conf.get("set_use_adjusted_pv", True):
75
- var_list.append(retrieve_hass_conf["sensor_power_photovoltaics_forecast"])
75
+ var_list.append(
76
+ retrieve_hass_conf["sensor_power_photovoltaics_forecast"]
77
+ )
76
78
  if not rh.get_data(
77
79
  days_list, var_list, minimal_response=False, significant_changes_only=False
78
80
  ):
@@ -302,7 +304,8 @@ def set_input_data_dict(
302
304
  else:
303
305
  P_PV_forecast = pd.Series(0, index=fcst.forecast_dates)
304
306
  P_load_forecast = fcst.get_load_forecast(
305
- method=optim_conf["load_forecast_method"]
307
+ days_min_load_forecast=optim_conf["delta_forecast_daily"].days,
308
+ method=optim_conf["load_forecast_method"],
306
309
  )
307
310
  if isinstance(P_load_forecast, bool) and not P_load_forecast:
308
311
  logger.error(
@@ -400,6 +403,7 @@ def set_input_data_dict(
400
403
  else:
401
404
  P_PV_forecast = pd.Series(0, index=fcst.forecast_dates)
402
405
  P_load_forecast = fcst.get_load_forecast(
406
+ days_min_load_forecast=optim_conf["delta_forecast_daily"].days,
403
407
  method=optim_conf["load_forecast_method"],
404
408
  set_mix_forecast=set_mix_forecast,
405
409
  df_now=df_input_data,
@@ -39,6 +39,7 @@ 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
41
  optim_conf,lp_solver_timeout,lp_solver_timeout
42
+ optim_conf,num_threads,num_threads
42
43
  optim_conf,set_nocharge_from_grid,set_nocharge_from_grid
43
44
  optim_conf,set_nodischarge_to_grid,set_nodischarge_to_grid
44
45
  optim_conf,set_battery_dynamic,set_battery_dynamic
@@ -60,6 +61,10 @@ plant_conf,surface_azimuth,surface_azimuth,list_surface_azimuth
60
61
  plant_conf,modules_per_string,modules_per_string,list_modules_per_string
61
62
  plant_conf,strings_per_inverter,strings_per_inverter,list_strings_per_inverter
62
63
  plant_conf,inverter_is_hybrid,inverter_is_hybrid
64
+ plant_conf,inverter_ac_output_max,inverter_ac_output_max
65
+ plant_conf,inverter_ac_input_max,inverter_ac_input_max
66
+ plant_conf,inverter_efficiency_dc_ac,inverter_efficiency_dc_ac
67
+ plant_conf,inverter_efficiency_ac_dc,inverter_efficiency_ac_dc
63
68
  plant_conf,compute_curtailment,compute_curtailment
64
69
  plant_conf,Pd_max,battery_discharge_power_max
65
70
  plant_conf,Pc_max,battery_charge_power_max
@@ -68,4 +73,4 @@ plant_conf,eta_ch,battery_charge_efficiency
68
73
  plant_conf,Enom,battery_nominal_energy_capacity
69
74
  plant_conf,SOCmin,battery_minimum_state_of_charge
70
75
  plant_conf,SOCmax,battery_maximum_state_of_charge
71
- plant_conf,SOCtarget,battery_target_state_of_charge
76
+ plant_conf,SOCtarget,battery_target_state_of_charge
@@ -10,6 +10,7 @@
10
10
  "lp_solver": "default",
11
11
  "lp_solver_path": "empty",
12
12
  "lp_solver_timeout": 45,
13
+ "num_threads": 0,
13
14
  "set_nocharge_from_grid": false,
14
15
  "set_nodischarge_to_grid": true,
15
16
  "set_battery_dynamic": false,
@@ -115,6 +116,10 @@
115
116
  1
116
117
  ],
117
118
  "inverter_is_hybrid": false,
119
+ "inverter_ac_output_max": 1000,
120
+ "inverter_ac_input_max": 1000,
121
+ "inverter_efficiency_dc_ac": 1.0,
122
+ "inverter_efficiency_ac_dc": 1.0,
118
123
  "compute_curtailment": false,
119
124
  "set_use_battery": false,
120
125
  "battery_discharge_power_max": 1000,
emhass/forecast.py CHANGED
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env python3
2
-
3
1
  import bz2
4
2
  import copy
5
3
  import json
@@ -216,8 +214,7 @@ class Forecast:
216
214
  ]
217
215
 
218
216
  def get_cached_open_meteo_forecast_json(
219
- self,
220
- max_age: int | None = 30,
217
+ self, max_age: int | None = 30, forecast_days: int = 3
221
218
  ) -> dict:
222
219
  r"""
223
220
  Get weather forecast json from Open-Meteo and cache it for re-use.
@@ -235,10 +232,30 @@ class Forecast:
235
232
  before it is discarded and a new version fetched from Open-Meteo.
236
233
  Defaults to 30 minutes.
237
234
  :type max_age: int, optional
235
+ :param forecast_days: The number of days of forecast data required from Open-Meteo.
236
+ One additional day is always fetched from Open-Meteo so there is an extra data in the cache.
237
+ Defaults to 2 days (3 days fetched) to match the prior default.
238
+ :type forecast_days: int, optional
238
239
  :return: The json containing the Open-Meteo forecast data
239
240
  :rtype: dict
240
241
 
241
242
  """
243
+
244
+ # Ensure at least 3 weather forecast days (and 1 more than requested)
245
+ if forecast_days is None:
246
+ self.logger.warning(
247
+ "Open-Meteo forecast_days is missing so defaulting to 3 days"
248
+ )
249
+ forecast_days = 3
250
+ elif forecast_days < 3:
251
+ self.logger.warning(
252
+ "Open-Meteo forecast_days is too low (%s) so defaulting to 3 days",
253
+ forecast_days,
254
+ )
255
+ forecast_days = 3
256
+ else:
257
+ forecast_days = forecast_days + 1
258
+
242
259
  json_path = os.path.abspath(
243
260
  self.emhass_conf["data_path"] / "cached-open-meteo-forecast.json"
244
261
  )
@@ -287,10 +304,13 @@ class Forecast:
287
304
  + "shortwave_radiation_instant,"
288
305
  + "diffuse_radiation_instant,"
289
306
  + "direct_normal_irradiance_instant"
307
+ + "&forecast_days="
308
+ + str(forecast_days)
290
309
  + "&timezone="
291
310
  + quote(str(self.time_zone), safe="")
292
311
  )
293
312
  try:
313
+ self.logger.debug("Fetching data from Open-Meteo using URL: %s", url)
294
314
  response = get(url, headers=headers)
295
315
  self.logger.debug("Returned HTTP status code: %s", response.status_code)
296
316
  response.raise_for_status()
@@ -349,7 +369,8 @@ class Forecast:
349
369
  ): # The scrapper option is being left here for backward compatibility
350
370
  if not os.path.isfile(w_forecast_cache_path):
351
371
  data_raw = self.get_cached_open_meteo_forecast_json(
352
- self.optim_conf["open_meteo_cache_max_age"]
372
+ self.optim_conf["open_meteo_cache_max_age"],
373
+ self.optim_conf["delta_forecast_daily"].days,
353
374
  )
354
375
  data_15min = pd.DataFrame.from_dict(data_raw["minutely_15"])
355
376
  data_15min["time"] = pd.to_datetime(data_15min["time"])
@@ -671,6 +692,7 @@ class Forecast:
671
692
  alpha: float,
672
693
  beta: float,
673
694
  col: str,
695
+ ignore_pv_feedback: bool = False,
674
696
  ) -> pd.DataFrame:
675
697
  """A simple correction method for forecasted data using the current real values of a variable.
676
698
 
@@ -684,9 +706,15 @@ class Forecast:
684
706
  :type beta: float
685
707
  :param col: The column variable name
686
708
  :type col: str
709
+ :param ignore_pv_feedback: If True, bypass mixing and return original forecast (used during curtailment)
710
+ :type ignore_pv_feedback: bool
687
711
  :return: The output DataFrame with the corrected values
688
712
  :rtype: pd.DataFrame
689
713
  """
714
+ # If ignoring PV feedback (e.g., during curtailment), return original forecast
715
+ if ignore_pv_feedback:
716
+ return df_forecast
717
+
690
718
  first_fcst = alpha * df_forecast.iloc[0] + beta * df_now[col].iloc[-1]
691
719
  df_forecast.iloc[0] = int(round(first_fcst))
692
720
  return df_forecast
@@ -787,12 +815,14 @@ class Forecast:
787
815
  # Extracting results for AC power
788
816
  P_PV_forecast = mc.results.ac
789
817
  if set_mix_forecast:
818
+ ignore_pv_feedback = self.params["passed_data"].get("ignore_pv_feedback_during_curtailment", False)
790
819
  P_PV_forecast = Forecast.get_mix_forecast(
791
820
  df_now,
792
821
  P_PV_forecast,
793
822
  self.params["passed_data"]["alpha"],
794
823
  self.params["passed_data"]["beta"],
795
824
  self.var_PV,
825
+ ignore_pv_feedback,
796
826
  )
797
827
  P_PV_forecast[P_PV_forecast < 0] = 0 # replace any negative PV values with zero
798
828
  self.logger.debug("get_power_from_weather returning:\n%s", P_PV_forecast)
@@ -1410,14 +1440,22 @@ class Forecast:
1410
1440
  forecast_out.index.name = "ts"
1411
1441
  forecast_out = forecast_out.rename(columns={"load": "yhat"})
1412
1442
  elif method == "naive": # using a naive approach
1413
- mask_forecast_out = (
1414
- df.index > days_list[-1] - self.optim_conf["delta_forecast_daily"]
1443
+ # Old code logic (shifted timestamp problem)
1444
+ # mask_forecast_out = (
1445
+ # df.index > days_list[-1] - self.optim_conf["delta_forecast_daily"]
1446
+ # )
1447
+ # forecast_out = df.copy().loc[mask_forecast_out]
1448
+ # forecast_out = forecast_out.rename(columns={self.var_load_new: "yhat"})
1449
+ # forecast_out = forecast_out.iloc[0 : len(self.forecast_dates)]
1450
+ # forecast_out.index = self.forecast_dates
1451
+ # New code logic
1452
+ forecast_horizon = len(self.forecast_dates)
1453
+ historical_values = df.iloc[-forecast_horizon:]
1454
+ forecast_out = pd.DataFrame(
1455
+ historical_values.values,
1456
+ index=self.forecast_dates,
1457
+ columns=["yhat"]
1415
1458
  )
1416
- forecast_out = df.copy().loc[mask_forecast_out]
1417
- forecast_out = forecast_out.rename(columns={self.var_load_new: "yhat"})
1418
- # Force forecast_out length to avoid mismatches
1419
- forecast_out = forecast_out.iloc[0 : len(self.forecast_dates)]
1420
- forecast_out.index = self.forecast_dates
1421
1459
  elif (
1422
1460
  method == "mlforecaster"
1423
1461
  ): # using a custom forecast model with machine learning
@@ -1506,12 +1544,14 @@ class Forecast:
1506
1544
  return False
1507
1545
  P_Load_forecast = copy.deepcopy(forecast_out["yhat"])
1508
1546
  if set_mix_forecast:
1547
+ # Load forecasts don't need curtailment protection - always use feedback
1509
1548
  P_Load_forecast = Forecast.get_mix_forecast(
1510
1549
  df_now,
1511
1550
  P_Load_forecast,
1512
1551
  self.params["passed_data"]["alpha"],
1513
1552
  self.params["passed_data"]["beta"],
1514
1553
  self.var_load_new,
1554
+ False, # Never ignore feedback for load forecasts
1515
1555
  )
1516
1556
  self.logger.debug("get_load_forecast returning:\n%s", P_Load_forecast)
1517
1557
  return P_Load_forecast
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env python3
2
-
3
1
  import logging
4
2
  import time
5
3
  import warnings