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
78 changes: 78 additions & 0 deletions apps/predbat/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,50 @@
"enable": "num_cars",
"enable_condition": "num_cars > 0",
},
{
"name": "car_charging_solar",
"friendly_name": "Charge car on excess solar",
"type": "switch",
"default": False,
"enable": "num_cars",
"enable_condition": "num_cars > 0",
},
{
"name": "car_charging_solar_excess",
"friendly_name": "Car solar charging surplus threshold",
"type": "input_number",
"min": 0.1,
"max": 20.0,
"step": 0.1,
"unit": "kW",
"icon": "mdi:solar-power",
"default": 1.0,
"enable": "car_charging_solar",
},
{
"name": "car_charging_plan_min_soc",
"friendly_name": "Car minimum charge from any source",
"type": "input_number",
"min": 0,
"max": 100,
"step": 1,
"unit": "%",
"icon": "mdi:battery-charging-low",
"default": 100,
"enable": "car_charging_solar",
},
{
"name": "car_charging_rate_threshold_export",
"friendly_name": "Car solar charging export rate threshold",
"type": "input_number",
"min": -10,
"max": 99,
"step": 1,
"unit": "p/kWh",
"icon": "mdi:currency-usd",
"default": 99,
"enable": "car_charging_solar",
},
{
"name": "car_charging_plan_max_price",
"friendly_name": "Car Charging Plan max price",
Expand Down Expand Up @@ -1362,6 +1406,40 @@
"restore": False,
"manual": True,
},
{
"name": "manual_car_away",
"friendly_name": "Manual car away",
"type": "select",
"options": ["off"],
"icon": "mdi:car-off",
"default": "off",
"restore": False,
"manual": True,
"enable": "num_cars",
},
{
"name": "manual_car_deadline",
"friendly_name": "Manual car ready by",
"type": "select",
"options": ["off"],
"icon": "mdi:car-clock",
"default": "off",
"restore": False,
"manual_rate": True,
"enable": "num_cars",
},
{
"name": "manual_car_deadline_value",
"friendly_name": "Manual car ready by level",
"type": "input_number",
"min": 0,
"max": 100,
"step": 1,
"unit": "%",
"icon": "mdi:car-electric",
"default": 80,
"enable": "num_cars",
},
{
"name": "manual_import_rates",
"friendly_name": "Manual import rates",
Expand Down
31 changes: 24 additions & 7 deletions apps/predbat/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -1312,12 +1312,6 @@ def fetch_sensor_data(self, save=True):
# "everything but the most expensive" threshold, which the scan then replaces with the real
# low-rate band. Rendering on the provisional value painted most of the day as cheap.
self.log_dispatch_timelines()

# Work out car plan?
self.fetch_sensor_data_car_planning()
# Publish the car plan
self.publish_car_plan()

# Work out iBoost plan
if self.iboost_enable and (((not self.iboost_solar) and (not self.iboost_charging)) or self.iboost_smart):
self.iboost_plan = self.plan_iboost_smart()
Expand Down Expand Up @@ -1372,6 +1366,15 @@ def fetch_sensor_data(self, save=True):
else:
self.load_inday_adjustment = 1.0

# Work out car plan? Runs after the PV forecast so car_charging_solar can place slots on predicted
# sunshine, and after the load forecast is finished - the modal filter above rewrites load_minutes,
# the history forecast adds to load_forecast, and load_inday_adjustment is only computed here, all
# three of which feed the house load that solar surplus is measured against. Nothing between the
# PV forecast and this point uses the car plan.
self.fetch_sensor_data_car_planning()
# Publish the car plan
self.publish_car_plan()

force_replan = dynamic_load_car_changed
# Compare on the change-detection signature, not the raw slots, so the per-cycle re-clocking
# of an in-progress dispatch (start advanced to now, energy scaled to remaining time) does not
Expand Down Expand Up @@ -1409,12 +1412,17 @@ def fetch_sensor_data_car_planning(self):
self.log("Car {} on Octopus Intelligent, no active plan".format(car_n))
elif self.car_charging_planned[car_n] or self.car_charging_now[car_n]:
limit_percent = dp1(self.car_charging_limit[car_n] / self.car_charging_battery_size[car_n] * 100) if self.car_charging_battery_size[car_n] else 0
# Report the bought-energy target as well as the overall limit: with car_charging_plan_min_soc
# lowered they differ, and a log showing only the limit makes a correct plan look wrong
min_soc_kwh = min(dp3(self.car_charging_plan_min_soc * self.car_charging_battery_size[car_n] / 100.0), self.car_charging_limit[car_n])
self.log(
"Car {} plan charging from {}kWh to {}% ({}kWh), with slots {}, ready by {}".format(
"Car {} plan charging from {}kWh to {}% ({}kWh), buying only up to {}% ({}kWh), with slots {}, ready by {}".format(
car_n,
self.car_charging_soc[car_n],
limit_percent,
self.car_charging_limit[car_n],
self.car_charging_plan_min_soc,
min_soc_kwh,
self.low_rates,
self.car_charging_plan_time[car_n],
)
Expand Down Expand Up @@ -2613,6 +2621,10 @@ def get_car_charging_planned(self):
self.car_charging_planned_response = [str(response).lower() for response in self.get_arg("car_charging_planned_response", ["yes", "on", "enable", "true"])]
self.car_charging_now_response = [str(response).lower() for response in self.get_arg("car_charging_now_response", ["yes", "on", "enable", "true", "charging"])]
self.car_charging_from_battery = self.get_arg("car_charging_from_battery")
self.car_charging_solar = self.get_arg("car_charging_solar")
self.car_charging_solar_excess = self.get_arg("car_charging_solar_excess")
self.car_charging_rate_threshold_export = self.get_arg("car_charging_rate_threshold_export")
self.car_charging_plan_min_soc = self.get_arg("car_charging_plan_min_soc")

# Car charging planned sensor
for car_n in range(self.num_cars):
Expand Down Expand Up @@ -3392,6 +3404,11 @@ def fetch_config_options(self):
self.manual_freeze_charge_times = self.manual_times("manual_freeze_charge")
self.manual_freeze_export_times = self.manual_times("manual_freeze_export")
self.manual_demand_times = self.manual_times("manual_demand")
# Deliberately not folded into manual_all_times below: that set forces charge and export
# windows into existence, and this does the opposite - a slot the car is away for should not
# have a window manufactured for it.
self.manual_car_away_times = self.manual_times("manual_car_away")
self.manual_car_deadline_keep = self.manual_rates("manual_car_deadline", default_rate=self.get_arg("manual_car_deadline_value"))
self.manual_all_times = self.manual_charge_times + self.manual_export_times + self.manual_demand_times + self.manual_freeze_charge_times + self.manual_freeze_export_times
self.manual_api = self.api_select_update("manual_api")
self.manual_import_rates = self.manual_rates("manual_import_rates", default_rate=self.get_arg("manual_import_value"))
Expand Down
1 change: 1 addition & 0 deletions apps/predbat/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ def publish_car_plan(self):
show["cost"] = cost
if window.get("kwh_cancelled"):
show["kwh_cancelled"] = dp2(window["kwh_cancelled"])
show["solar"] = window.get("solar", False)
total_cost += cost
total_kwh += kwh
plan.append(show)
Expand Down
Loading