Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .cspell/custom-dictionary-workspace.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ chargehold
chargelater
Chrg
citem
clearsky
clipping
Codespaces
collapsable
compareform
Expand Down Expand Up @@ -93,6 +95,7 @@ dischargeenergytotal
dischargefreeze
diverter
dlimit
dno
dnoregion
docstrings
dstamp
Expand Down Expand Up @@ -166,6 +169,7 @@ homeassistant
houseb
htmlcov
husforbrukning
hybrid
hypervolt
iboost
idag
Expand All @@ -177,6 +181,7 @@ INTELLI
intelligentdevice
interp
invbatpower
inverter
invname
isoformat
isort
Expand Down
4 changes: 2 additions & 2 deletions apps/predbat/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,10 @@ def run_scenario(self, end_record):

my_predbat.calculate_plan(recompute=True, debug_mode=False, publish=False)

cost, import_kwh_battery, import_kwh_house, export_kwh, soc_min, soc, soc_min_minute, battery_cycle, metric_keep, final_iboost, final_carbon_g = my_predbat.run_prediction(
cost, import_kwh_battery, import_kwh_house, export_kwh, soc_min, soc, soc_min_minute, battery_cycle, metric_keep, final_iboost, final_carbon_g, *_ = my_predbat.run_prediction(
my_predbat.charge_limit_best, my_predbat.charge_window_best, my_predbat.export_window_best, my_predbat.export_limits_best, False, end_record=end_record, save="compare"
)
cost10, import_kwh_battery10, import_kwh_house10, export_kwh10, soc_min10, soc10, soc_min_minute10, battery_cycle10, metric_keep10, final_iboost10, final_carbon_g10 = my_predbat.run_prediction(
cost10, import_kwh_battery10, import_kwh_house10, export_kwh10, soc_min10, soc10, soc_min_minute10, battery_cycle10, metric_keep10, final_iboost10, final_carbon_g10, *_ = my_predbat.run_prediction(
my_predbat.charge_limit_best,
my_predbat.charge_window_best,
my_predbat.export_window_best,
Expand Down
5 changes: 5 additions & 0 deletions apps/predbat/component_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ def midnight_utc(self):
"""Get today's midnight time in UTC"""
return self.base.midnight_utc

@property
def midnight(self):
"""Get today's midnight time in local time"""
return self.base.midnight

@property
def now_utc_exact(self):
"""Get the current time in the local timezone"""
Expand Down
150 changes: 150 additions & 0 deletions apps/predbat/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,144 @@
"enable": "num_cars",
"enable_condition": "num_cars > 0",
},
{
"name": "clipping_buffer_enable",
"friendly_name": "Clipping Buffer Enable",
"type": "switch",
"default": False,
},
{
"name": "clipping_buffer_forecast",
"friendly_name": "Clipping Buffer Forecast",
"type": "select",
"options": ["pv_estimate", "pv_estimate10", "pv_estimate90", "pv_clearsky", "pv_historical"],
"icon": "mdi:solar-power",
"default": "pv_estimate90",
"enable": "clipping_buffer_enable",
},
{
"name": "clipping_buffer_min_kwh",
"friendly_name": "Clipping Buffer Min kWh",
"type": "input_number",
"min": 0,
"max": 50.0,
"step": 0.1,
"unit": "kWh",
"icon": "mdi:battery-50",
"default": 0,
"enable": "clipping_buffer_enable",
},
{
"name": "clipping_buffer_max_kwh",
"friendly_name": "Clipping Buffer Max kWh",
"type": "input_number",
"min": 0,
"max": 50.0,
"step": 0.1,
"unit": "kWh",
"icon": "mdi:battery-50",
"default": 0,
"enable": "clipping_buffer_enable",
},
{
"name": "clipping_buffer_limit_override",
"friendly_name": "Clipping Buffer Limit Override",
"type": "input_number",
"min": 0,
"max": 100000.0,
"step": 100,
"unit": "W",
"icon": "mdi:speedometer",
"default": 0,
"enable": "clipping_buffer_enable",
},
{
"name": "clipping_buffer_window_offset",
"friendly_name": "Clipping Window Offset",
"type": "input_number",
"min": 0,
"max": 120.0,
"step": 5,
"unit": "min",
"icon": "mdi:clock-fast",
"default": 15,
"enable": "clipping_buffer_enable",
},
{
"name": "clipping_buffer_fallback_window",
"friendly_name": "Clipping Fallback Duration",
"type": "input_number",
"min": 0,
"max": 12.0,
"step": 0.5,
"unit": "h",
"icon": "mdi:clock-alert-outline",
"default": 2.0,
"enable": "clipping_buffer_enable",
},
{
"name": "clipping_buffer_risk_threshold",
"friendly_name": "Clipping Risk Threshold",
"type": "input_number",
"min": 0,
"max": 1.0,
"step": 0.05,
"unit": "factor",
"icon": "mdi:alert-outline",
"default": 0.8,
"enable": "clipping_buffer_enable",
},
{
"name": "clipping_buffer_duration_threshold",
"friendly_name": "Clipping Duration Threshold",
"type": "input_number",
"min": 0,
"max": 1.0,
"step": 0.05,
"unit": "factor",
"icon": "mdi:timer-sand",
"default": 0.2,
"enable": "clipping_buffer_enable",
},
{
"name": "clipping_buffer_safety_margin",
"friendly_name": "Clipping Safety Margin",
"type": "input_number",
"min": 0,
"max": 1.0,
"step": 0.01,
"unit": "factor",
"icon": "mdi:shield-check",
"default": 0.05,
"enable": "clipping_buffer_enable",
},
{
"name": "clipping_buffer_can_discharge",
"friendly_name": "Clipping Buffer Discharge",
"type": "select",
"options": ["None", "Cost Optimal", "Always"],
"icon": "mdi:battery-arrow-down",
"default": "Cost Optimal",
"enable": "clipping_buffer_enable",
},
{
"name": "clipping_buffer_start_time",
"friendly_name": "Clipping Buffer Start Time",
"type": "select",
"options": ["None"] + OPTIONS_TIME,
"icon": "mdi:clock-start",
"default": "None",
"enable": "clipping_buffer_enable",
},
{
"name": "clipping_buffer_end_time",
"friendly_name": "Clipping Buffer End Time",
"type": "select",
"options": ["None"] + OPTIONS_TIME,
"icon": "mdi:clock-end",
"default": "None",
"enable": "clipping_buffer_enable",
},
{
"name": "mode",
"friendly_name": "Predbat mode",
Expand Down Expand Up @@ -2110,6 +2248,18 @@
"inverter_limit_discharge": {"type": "sensor_list", "sensor_type": "integer", "modify": False, "zero": False, "entries": "num_inverters"},
"inverter_limit_export": {"type": "sensor_list", "sensor_type": "integer", "modify": False, "zero": False, "entries": "num_inverters"},
"inverter_limit_override": {"type": "sensor_list", "sensor_type": "integer", "modify": False, "zero": False, "entries": "num_inverters"},
"clipping_buffer_enable": {"type": "boolean"},
"clipping_buffer_forecast": {"type": "string", "allowed": ["pv_estimate", "pv_estimate10", "pv_estimate90", "pv_clearsky", "pv_historical"]},
"clipping_buffer_min_kwh": {"type": "float"},
"clipping_buffer_max_kwh": {"type": "float"},
"clipping_buffer_limit_override": {"type": "integer"},
"clipping_buffer_window_offset": {"type": "integer"},
"clipping_buffer_fallback_window": {"type": "float"},
"clipping_buffer_risk_threshold": {"type": "float"},
"clipping_buffer_duration_threshold": {"type": "float"},
"clipping_buffer_can_discharge": {"type": "string", "allowed": ["None", "Cost Optimal", "Always"]},
"clipping_buffer_start_time": {"type": "string"},
"clipping_buffer_end_time": {"type": "string"},
"battery_rate_max": {"type": "sensor_list", "sensor_type": "float", "modify": False, "zero": False, "entries": "num_inverters"},
"export_limit": {"type": "sensor_list", "sensor_type": "float", "entries": "num_inverters"},
"inverter_battery_rate_min": {"type": "integer", "zero": False, "entries": "num_inverters"},
Expand Down
34 changes: 34 additions & 0 deletions apps/predbat/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,40 @@ def publish_inverter_data(self):
},
)

# Individual inverter data
if self.inverters:
for inverter in self.inverters:
self.dashboard_item(
self.prefix + ".pv_power_{}".format(inverter.id),
state=dp3(inverter.pv_power / 1000.0),
attributes={
"friendly_name": "Current PV Power Inverter {}".format(inverter.id),
"state_class": "measurement",
"unit_of_measurement": "kW",
"icon": "mdi:solar-power",
},
)
self.dashboard_item(
self.prefix + ".load_power_{}".format(inverter.id),
state=dp3(inverter.load_power / 1000.0),
attributes={
"friendly_name": "Current Load Power Inverter {}".format(inverter.id),
"state_class": "measurement",
"unit_of_measurement": "kW",
"icon": "mdi:home-lightning-bolt",
},
)
self.dashboard_item(
self.prefix + ".battery_power_{}".format(inverter.id),
state=dp3(inverter.battery_power / 1000.0),
attributes={
"friendly_name": "Current Battery Power Inverter {}".format(inverter.id),
"state_class": "measurement",
"unit_of_measurement": "kW",
"icon": "mdi:battery",
},
)

def balance_inverters(self, test_mode=False):
"""
Attempt to balance multiple inverters
Expand Down
62 changes: 60 additions & 2 deletions apps/predbat/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -1020,7 +1020,7 @@ def fetch_sensor_data(self, save=True):
self.cost_today_sofar, self.carbon_today_sofar = self.today_cost(self.import_today, self.export_today, self.car_charging_energy, self.load_minutes, save=save)

# Fetch PV forecast if enabled, today must be enabled, other days are optional
self.pv_forecast_minute, self.pv_forecast_minute10 = self.fetch_pv_forecast()
self.pv_forecast_minute, self.pv_forecast_minute10, self.pv_forecast_minute90, self.pv_forecast_minuteCS, self.pv_forecast_minuteMAX = self.fetch_pv_forecast()

if self.load_minutes and not self.load_forecast_only:
# Apply modal filter to historical data
Expand Down Expand Up @@ -1233,11 +1233,21 @@ def fetch_pv_forecast(self):
"""
pv_forecast_minute = {}
pv_forecast_minute10 = {}
pv_forecast_minute90 = {}
pv_forecast_minuteCS = {}
pv_forecast_minuteHIST = {}

# Get data from forecast sensor
entity_id = "sensor." + self.prefix + "_pv_forecast_raw"
pv_forecast_packed_ld = self.get_state_wrapper(entity_id=entity_id, attribute="forecast")
pv_forecast10_packed_ld = self.get_state_wrapper(entity_id=entity_id, attribute="forecast10")
pv_forecast90_packed_ld = self.get_state_wrapper(entity_id=entity_id, attribute="forecast90")
pv_forecastCS_packed_ld = self.get_state_wrapper(entity_id=entity_id, attribute="forecast_clearsky")
pv_forecastHIST_packed_ld = self.get_state_wrapper(entity_id=entity_id, attribute="forecast_historical")
if pv_forecastCS_packed_ld is None:
pv_forecastCS_packed_ld = self.get_state_wrapper(entity_id=entity_id, attribute="forecastCS")
if pv_forecastHIST_packed_ld is None:
pv_forecastHIST_packed_ld = self.get_state_wrapper(entity_id=entity_id, attribute="forecastMAX")
relative_time = self.get_state_wrapper(entity_id=entity_id, attribute="relative_time")
try:
relative_time = datetime.strptime(relative_time, TIME_FORMAT)
Expand All @@ -1248,6 +1258,9 @@ def fetch_pv_forecast(self):
# Convert keys to integers and values to floats
pv_forecast_packed = {}
pv_forecast10_packed = {}
pv_forecast90_packed = {}
pv_forecastCS_packed = {}
pv_forecastHIST_packed = {}

if pv_forecast_packed_ld:
for key, value in pv_forecast_packed_ld.items():
Expand All @@ -1265,10 +1278,37 @@ def fetch_pv_forecast(self):
except (ValueError, TypeError):
pass

if pv_forecast90_packed_ld:
for key, value in pv_forecast90_packed_ld.items():
try:
minute = int(key)
pv_forecast90_packed[minute] = float(value)
except (ValueError, TypeError):
pass

if pv_forecastCS_packed_ld:
for key, value in pv_forecastCS_packed_ld.items():
try:
minute = int(key)
pv_forecastCS_packed[minute] = float(value)
except (ValueError, TypeError):
pass

if pv_forecastHIST_packed_ld:
for key, value in pv_forecastHIST_packed_ld.items():
try:
minute = int(key)
pv_forecastHIST_packed[minute] = float(value)
except (ValueError, TypeError):
pass

# Unpack the forecast data
max_minute = max(pv_forecast_packed.keys()) if pv_forecast_packed else 0
last_value = 0
last_value10 = 0
last_value90 = 0
last_valueCS = 0
last_valueHIST = 0
# The forecast could be for a different time to our relative time, so we need to offset the minutes to align with our midnight_utc.
# relative_time is the midnight at which the forecast was saved, so stored minute keys are relative to that midnight.
# We subtract the offset so that stored minute X (= relative_time + X) maps to (relative_time + X - midnight_utc) minutes from today's midnight.
Expand All @@ -1277,10 +1317,16 @@ def fetch_pv_forecast(self):
target_minute = minute - minute_offset
last_value = pv_forecast_packed.get(minute, last_value)
last_value10 = pv_forecast10_packed.get(minute, last_value10)
last_value90 = pv_forecast90_packed.get(minute, last_value90)
last_valueCS = pv_forecastCS_packed.get(minute, last_valueCS)
last_valueHIST = pv_forecastHIST_packed.get(minute, last_valueHIST)
pv_forecast_minute[target_minute] = last_value
pv_forecast_minute10[target_minute] = last_value10
pv_forecast_minute90[target_minute] = last_value90
pv_forecast_minuteCS[target_minute] = last_valueCS
pv_forecast_minuteHIST[target_minute] = last_valueHIST

return pv_forecast_minute, pv_forecast_minute10
return pv_forecast_minute, pv_forecast_minute10, pv_forecast_minute90, pv_forecast_minuteCS, pv_forecast_minuteHIST

def predict_battery_temperature(self, battery_temperature_history, step):
"""
Expand Down Expand Up @@ -2235,6 +2281,18 @@ def fetch_config_options(self):
self.set_read_only = True
self.set_read_only_axle = True

# Clipping Buffer options
self.clipping_buffer_enable = self.get_arg("clipping_buffer_enable")
self.clipping_buffer_forecast = self.get_arg("clipping_buffer_forecast")
self.clipping_buffer_min_kwh = self.get_arg("clipping_buffer_min_kwh")
self.clipping_buffer_max_kwh = self.get_arg("clipping_buffer_max_kwh")
self.clipping_buffer_limit_override = self.get_arg("clipping_buffer_limit_override", 0.0) / 60.0 / 1000.0 # Convert W to kW/min
self.clipping_buffer_window_offset = self.get_arg("clipping_buffer_window_offset", 15)
self.clipping_buffer_fallback_window = self.get_arg("clipping_buffer_fallback_window", 2.0)
self.clipping_buffer_can_discharge = self.get_arg("clipping_buffer_can_discharge", "Cost Optimal")
self.clipping_buffer_start_time = self.get_arg("clipping_buffer_start_time")
self.clipping_buffer_end_time = self.get_arg("clipping_buffer_end_time")

# hard wired options, can be configured per inverter later on
self.set_soc_enable = True
self.set_reserve_enable = self.get_arg("set_reserve_enable")
Expand Down
3 changes: 2 additions & 1 deletion apps/predbat/load_ml_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ def initialize(self, load_ml_enable, load_ml_source=True, load_ml_max_days_histo
# Data state
self.load_data = None
self.load_data_age_days = 0
self.load_minutes_now = 0
self.pv_data = None
self.temperature_data = None
self.import_rates_data = None
Expand Down Expand Up @@ -404,7 +405,7 @@ async def _fetch_load_data(self):
energy = self.get_from_incrementing(pv_data_cumulative, m, PREDICT_STEP, backwards=True)
pv_data[m] = dp4(energy)

pv_forecast_minute, pv_forecast_minute10 = self.base.fetch_pv_forecast()
pv_forecast_minute, pv_forecast_minute10, _, _, _ = self.base.fetch_pv_forecast()
# Add future PV forecast as per-5-min energy with negative keys (negative = future)
# key -5 = first future step, -10 = second, etc.
if pv_forecast_minute:
Expand Down
Loading