Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added multiple days calculations to battery model #91

Merged
merged 2 commits into from
Nov 21, 2022
Merged
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
46 changes: 13 additions & 33 deletions custom_components/foxess_em/battery/battery_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,51 +94,31 @@ def charge_to_perc(self) -> int:
self.charge_total() + self.state_at_eco_start()
)

def state_at_dawn(self) -> float:
"""Battery state at dawn"""
dawn = self._model.state_at_dawn()
return round(dawn, 2)

def state_at_eco_end(self) -> float:
"""Battery state at end of eco period"""
eco_end = self._model.state_at_eco_end()
return round(eco_end, 2)

def state_at_eco_start(self) -> float:
"""Battery state at start of eco period"""
battery_value = self._model.state_at_eco_start()
return round(battery_value, 2)

def todays_dawn_time(self) -> datetime:
"""Return todays dawn time"""
return self._model.todays_dawn_time()

def todays_dawn_time_str(self) -> str:
"""Return todays dawn time in ISO format"""
return self._model.todays_dawn_time().isoformat()

def next_dawn_time(self) -> datetime:
"""Return next dawn time"""
return self._model.next_dawn_time()
return round(self._model.state_at_eco_start(), 2)

def dawn_charge_needs(self) -> float:
"""Dawn charge needs"""
return self._model.dawn_charge_needs()
return self._model.dawn_charge()

def day_charge_needs(self) -> float:
"""Day charge needs"""
house_load = self._average_controller.average_peak_house_load()
forecast_today = self._forecast_controller.total_kwh_forecast_today()
forecast_tomorrow = self._forecast_controller.total_kwh_forecast_tomorrow()
return self._model.day_charge()

return self._model.day_charge_needs(
forecast_today, forecast_tomorrow, house_load
)
def next_dawn_time(self) -> datetime:
"""Day charge needs"""
return self._model.next_dawn_time()

def todays_dawn_time_str(self) -> datetime:
"""Day charge needs"""
return self._model.todays_dawn_time().isoformat()

def charge_total(self) -> float:
"""Total kWh required to charge"""
eco_start = self.state_at_eco_start()
if self._full:
return self._model.ceiling_charge_total(float("inf"))
return self._model.ceiling_charge_total(float("inf"), eco_start)

dawn_charge = self.dawn_charge_needs()
day_charge = self.day_charge_needs()
Expand All @@ -147,7 +127,7 @@ def charge_total(self) -> float:

if self._boost:
total += _BOOST
total = self._model.ceiling_charge_total(total)
total = self._model.ceiling_charge_total(total, eco_start)

return total

Expand Down
224 changes: 108 additions & 116 deletions custom_components/foxess_em/battery/battery_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,104 +67,110 @@ def refresh_battery_model(self, forecast: pd.DataFrame, load: pd.DataFrame) -> N
load = load.groupby(load["time"]).mean()
load["time"] = load.index.values

now = datetime.utcnow()

forecast = forecast[
(forecast["date"] >= now.date())
& (forecast["date"] <= (now + timedelta(days=2)).date())
]
# limit forecast values to future only
forecast = forecast[forecast["date"] >= datetime.utcnow().date()]

# reset indexes
load.reset_index(drop=True, inplace=True)
forecast.reset_index(drop=True, inplace=True)

# merge load and forecast to produce a delta
merged = pd.merge(load, forecast, how="right", on=["time"])
merged["delta"] = merged["pv_estimate"] - merged["load"]

merged = merged.reset_index(drop=True)

merged = merged.sort_values(by="period_start")

# set global model
self._model = merged

battery = self.battery_capacity_remaining()
battery_states = []
grid_usage = []
available_capacity = self._capacity - (self._min_soc * self._capacity)
for index, _ in merged.iterrows():
if merged.iloc[index]["period_start"] >= datetime.now().astimezone():
delta = merged.iloc[index]["delta"]
new_state = battery + delta
battery = max([0, min([available_capacity, new_state])])
battery_states.append(battery)
if new_state <= 0 or new_state >= available_capacity:
# import (-) or excess (+)
grid_usage.append(delta)
period = merged.iloc[index]["period_start"].to_pydatetime()

if period > datetime.now().astimezone():
if period.time() == self._eco_start_time:
# landed on the start of the eco period
dawn_charge, day_charge = self._charge_totals(period, index)
battery += max([dawn_charge, day_charge])
# store in dataframe for retrieval later
merged.at[index, "charge_dawn"] = dawn_charge
merged.at[index, "charge_day"] = day_charge
merged.at[index, "battery"] = battery
elif (
period.time() > self._eco_start_time
and period.time() <= self._eco_end_time
):
# still in eco period, don't update the battery
merged.at[index, "battery"] = merged.at[index - 1, "battery"]
else:
# battery usage
grid_usage.append(0)
else:
grid_usage.append(0)
battery_states.append(0)

merged["battery"] = battery_states
merged["grid"] = grid_usage
delta = merged.iloc[index]["delta"]
new_state = battery + delta
battery = max([0, min([available_capacity, new_state])])
merged.at[index, "battery"] = battery
if new_state <= 0 or new_state >= available_capacity:
# import (-) or excess (+)
merged.at[index, "grid"] = delta
else:
# battery usage
merged.at[index, "grid"] = 0

self._model = merged
self._ready = True

def state_at_eco_start(self) -> float:
"""State at eco end"""
return self._state_at_datetime(self._next_eco_start_time())
def _charge_totals(self, period, index):
"""Return charge totals for dawn/day"""
# calculate start/end of the next peak period
eco_end = self._next_eco_end_time(period)
next_eco_start = period + timedelta(days=1)
# grab all peak values
peak = self._model[
(self._model["period_start"] > eco_end)
& (self._model["period_start"] < next_eco_start)
]
# sum forecast and house load
forecast_sum = peak.pv_estimate.sum()
load_sum = peak.load.sum()
dawn_load = self._dawn_load(eco_end)
eco_start = self._model.iloc[index - 1].battery
dawn_charge = self.dawn_charge_needs(dawn_load, eco_start)
day_charge = self.day_charge_needs(forecast_sum, load_sum, eco_start)
_LOGGER.debug(
f"Period: {period.date()} - EcoStart: {eco_start} Dawn: {dawn_charge} Day: {day_charge}"
)
return dawn_charge, day_charge

def state_at_eco_end(self) -> float:
def state_at_eco_start(self) -> float:
"""State at eco end"""
return self._state_at_datetime(self._next_eco_end_time())
eco_time = self._next_eco_start_time().replace(second=0, microsecond=0)
eco_time -= timedelta(minutes=1)
return self._model[self._model["period_start"] == eco_time].battery.iloc[0]

def state_at_dawn(self) -> float:
"""State at eco end"""
return self._state_at_datetime(self.next_dawn_time())
def dawn_charge(self):
"""Dawn charge required"""
return self._charge_info().iloc[0].charge_dawn

def dawn_load(self) -> float:
"""Dawn load"""
dawn_date = datetime.now().astimezone()
if self._is_after_todays_eco_start():
dawn_date += timedelta(days=1)
def day_charge(self):
"""Day charge required"""
return self._charge_info().iloc[0].charge_day

eco_time = dawn_date.replace(
hour=self._eco_end_time.hour,
minute=self._eco_end_time.minute,
second=0,
microsecond=0,
)
def _charge_info(self):
"""Charge info"""
return self._model[self._model["period_start"] == self._next_eco_start_time()]

dawn_time = self._dawn_time(dawn_date)
def _dawn_load(self, eco_end_time) -> float:
"""Dawn load"""
dawn_time = self._dawn_time(eco_end_time)

dawn_load = self._model[
(
(self._model["period_start"] > eco_time)
& (self._model["period_start"] < dawn_time)
)
(self._model["period_start"] > eco_end_time)
& (self._model["period_start"] < dawn_time)
]

load_sum = abs(dawn_load.delta.sum())

return round(load_sum, 2)

def next_dawn_time(self) -> datetime:
"""Calculate dawn time"""
now = datetime.now().astimezone()

dawn_today = self._dawn_time(now)
dawn_tomorrow = self._dawn_time(now + timedelta(days=1))

if now > dawn_today:
return dawn_tomorrow
else:
return dawn_today

def todays_dawn_time(self) -> datetime:
"""Calculate dawn time"""
now = datetime.now().astimezone()
return self._dawn_time(now)

def _dawn_time(self, date: datetime) -> datetime:
"""Calculate dawn time"""
filtered = self._model[self._model["date"] == date.date()]
Expand All @@ -174,53 +180,56 @@ def _dawn_time(self, date: datetime) -> datetime:
# Solar never reaches house load... return mid-day
return date.replace(hour=12, minute=0, second=0, microsecond=0)
else:
return self._model.iloc[dawn["period_start"].idxmin()].period_start
return dawn.iloc[0].period_start.to_pydatetime()

def dawn_charge_needs(self) -> float:
def dawn_charge_needs(self, dawn_load, eco_start) -> float:
"""Dawn charge needs"""

eco_start = self.state_at_eco_start()
dawn_load = self.dawn_load()

dawn_charge_needs = eco_start - dawn_load

dawn_buffer_top_up = self._dawn_buffer - dawn_charge_needs
dawn_buffer = self._dawn_buffer - dawn_charge_needs

ceiling = self.ceiling_charge_total(dawn_buffer_top_up)
ceiling = self.ceiling_charge_total(dawn_buffer, eco_start)

return round(ceiling, 2)

def day_charge_needs(
self,
forecast_today: float,
forecast_tomorrow: float,
house_load: float,
self, forecast: float, house_load: float, eco_start: float
) -> float:
"""Day charge needs"""
if self._is_after_todays_eco_start():
forecast = forecast_tomorrow
else:
forecast = forecast_today

day_charge_needs = (self.state_at_eco_start() - house_load) + forecast
day_charge_needs = (eco_start - house_load) + forecast

day_buffer_top_up = self._day_buffer - day_charge_needs

ceiling = self.ceiling_charge_total(day_buffer_top_up)
ceiling = self.ceiling_charge_total(day_buffer_top_up, eco_start)

return round(ceiling, 2)

def ceiling_charge_total(self, charge_total: float) -> float:
def ceiling_charge_total(self, charge_total: float, eco_start: float) -> float:
"""Ceiling total charge"""
available_capacity = round(
self._capacity
- (self._min_soc * self._capacity)
- self.state_at_eco_start(),
self._capacity - (self._min_soc * self._capacity) - eco_start,
2,
)

return min(available_capacity, charge_total)

def next_dawn_time(self) -> datetime:
"""Calculate dawn time"""
now = datetime.now().astimezone()

dawn_today = self._dawn_time(now)
dawn_tomorrow = self._dawn_time(now + timedelta(days=1))

if now > dawn_today:
return dawn_tomorrow
else:
return dawn_today

def todays_dawn_time(self) -> datetime:
"""Calculate dawn time"""
now = datetime.now().astimezone()
return self._dawn_time(now)

def battery_depleted_time(self) -> datetime:
"""Time battery capacity is 0"""

Expand Down Expand Up @@ -265,25 +274,6 @@ def peak_grid_export(self) -> float:

return round(grid_export.grid.sum(), 2)

def _state_at_datetime(self, state_time: datetime) -> float:
"""Battery and forecast remaining meets load until dawn"""
state_time = state_time.replace(second=0, microsecond=0)
return self._model[self._model["period_start"] == state_time].battery.iloc[0]

def _next_eco_end_time(self) -> datetime:
"""Next eco end time"""
now = datetime.now().astimezone()
eco_end = now.replace(
hour=self._eco_end_time.hour,
minute=self._eco_end_time.minute,
second=0,
microsecond=0,
)
if now > eco_end:
eco_end += timedelta(days=1)

return eco_end

def _next_eco_start_time(self) -> datetime:
"""Next eco start time"""
now = datetime.now().astimezone()
Expand All @@ -298,13 +288,15 @@ def _next_eco_start_time(self) -> datetime:

return eco_start

def _is_after_todays_eco_start(self) -> bool:
"""Is current time after eco period start"""
now = datetime.now().astimezone()
eco_start = now.replace(
hour=self._eco_start_time.hour,
minute=self._eco_start_time.minute,
def _next_eco_end_time(self, period) -> datetime:
"""Next eco end time"""
eco_end = period.replace(
hour=self._eco_end_time.hour,
minute=self._eco_end_time.minute,
second=0,
microsecond=0,
)
return now > eco_start
if period > eco_end:
eco_end += timedelta(days=1)

return eco_end
Loading