Skip to content

Commit

Permalink
redo nextwaterboost to datetimes
Browse files Browse the repository at this point in the history
  • Loading branch information
magnuselden authored and magnuselden committed Dec 6, 2023
1 parent 419e55b commit 1b333a5
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ class NextWaterBoostModel:
non_hours_raw: list[int] = field(default_factory=lambda: [], repr=False, compare=False)
demand_hours_raw: list[int] = field(default_factory=lambda: [], repr=False, compare=False)
initialized: bool = False
prices: list = field(default_factory=lambda: [])

price_dict:dict = field(default_factory=lambda: {})
preset: HvacPresets = HvacPresets.Normal
_now_dt: datetime = None # type: ignore
latest_boost: datetime = None # type: ignore
Expand All @@ -80,8 +79,6 @@ class NextWaterBoostModel:

def __post_init__(self):
self._now_dt = datetime.now() if self.now_dt is None else self.now_dt
self.non_hours = self._set_hours(self.non_hours_raw, self.preset)
self.demand_hours = self._set_hours(self.demand_hours_raw, self.preset)
self.latest_boost = self.now_dt if self.latest_boost is None else self.latest_boost

@property
Expand Down Expand Up @@ -110,13 +107,17 @@ def demand_minutes(self) -> int:
def now_dt(self) -> datetime:
return self._now_dt.replace(second=0, microsecond=0) if self._now_dt else None

def _create_price_dict(self, prices) -> dict:
startofday = self.now_dt.replace(hour=0, minute=0)
return {startofday + timedelta(hours=i): prices[i] for i in range(0, len(prices))}

def update(self, temp, temp_trend, target_temp, prices_today: list, prices_tomorrow: list, preset: HvacPresets,
now_dt=None, latest_boost: datetime = None) -> None:
_old_dt = self.now_dt
self.set_now_dt(now_dt)
new_prices = prices_today + prices_tomorrow
if new_prices != self.prices:
self.prices = new_prices
new_price_dict = self._create_price_dict(prices_today + prices_tomorrow)
if new_price_dict != self.price_dict:
self.price_dict = new_price_dict
self.should_update = True
new_non_hours = self._set_hours(self.non_hours_raw, preset)
new_demand_hours = self._set_hours(self.demand_hours_raw, preset)
Expand All @@ -134,7 +135,6 @@ def update(self, temp, temp_trend, target_temp, prices_today: list, prices_tomor
]) and not self.should_update:
self.should_update = True

self.prices = new_prices
self.latest_boost = latest_boost
self.non_hours = new_non_hours
self.demand_hours = new_demand_hours
Expand All @@ -149,17 +149,12 @@ def get_demand_minutes(self, expected_temp) -> int:
return DEMAND_MINUTES[self.preset][get_demand(expected_temp)]

def _set_hours(self, input_hours: list, preset: HvacPresets) -> set:
ret = set()
if preset == HvacPresets.Away:
return ret
start_dt = self.now_dt.replace(hour=0, minute=0)
for i in range(0, len(self.prices)):
if i in input_hours:
ret.add(start_dt + timedelta(hours=i))
return ret
return set()
return {k for k in self.price_dict.keys() if k.hour in input_hours}

def set_now_dt(self, now_dt=None) -> None:
self._now_dt = datetime.now() if now_dt is None else now_dt

def set_floating_mean(self, now_dt=None) -> None:
self.floating_mean = mean(self.prices[self.now_dt.hour:])
self.floating_mean = mean([v for k,v in self.price_dict.items() if k >=self.now_dt])
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

REQUIRED_DEMAND_DELAY = 6


class NextWaterBoost:
def __init__(self, min_price: float = None, non_hours: list[int] = None, demand_hours: list[int] = None):
self.model = NextWaterBoostModel(min_price=min_price, non_hours_raw=non_hours, demand_hours_raw=demand_hours)
Expand Down Expand Up @@ -58,18 +59,16 @@ def _get_next_start(self, delay_dt=None) -> tuple[datetime, int | None]:
next_dt, override_demand = self._calculate_next_start(delay_dt) # todo: must also use latestboost +24h in this.
intersecting1 = self._check_intersecting(next_dt, last_known)
if intersecting1[0] or next_dt == datetime.max:
#_LOGGER.debug(f"returning next boost based on intersection of hours. original: {next_dt}, inter: {intersecting1}")
# _LOGGER.debug(f"returning next boost based on intersection of hours. original: {next_dt}, inter: {intersecting1}")
return intersecting1

expected_temp = min(self._get_temperature_at_datetime(next_dt), 39)
retval = min(next_dt, (latest_limit if latest_limit < last_known else datetime.max))
return self._set_start_dt(
low_period=0,
delayed_dt=retval,
new_demand=self.model.get_demand_minutes(expected_temp)
), override_demand

def _check_intersecting(self, next_dt, last_known) -> tuple[datetime, int | None]:
def _check_intersecting(self, next_dt: datetime, last_known: datetime) -> tuple[datetime, int | None]:
intersecting_non_hours = self._intersecting_special_hours(self.model.non_hours, min(next_dt, last_known))
intersecting_demand_hours = self._intersecting_special_hours(self.model.demand_hours, min(next_dt, last_known))
if intersecting_demand_hours:
Expand All @@ -78,12 +77,13 @@ def _check_intersecting(self, next_dt, last_known) -> tuple[datetime, int | None
# print(f"best match: {best_match}")
expected_temp = min(self._get_temperature_at_datetime(best_match), 39)
ret = self._set_start_dt(
low_period=0,
delayed_dt=min(best_match, next_dt),
new_demand=self.model.get_demand_minutes(expected_temp)
# special demand because demand_hour
), self.model.get_demand_minutes(expected_temp)
if (ret[0] - self.model.latest_boost > timedelta(hours=REQUIRED_DEMAND_DELAY) and self.model.current_temp < 50) or ret[0] > self.model.cold_limit:
if (ret[0] - self.model.latest_boost > timedelta(
hours=REQUIRED_DEMAND_DELAY) and self.model.current_temp < 50) or ret[
0] > self.model.cold_limit:
return ret
return None, None

Expand Down Expand Up @@ -119,119 +119,105 @@ def _get_list_of_hours(self, start_dt: datetime, end_dt: datetime) -> set[dateti

def _last_known_price(self) -> datetime:
try:
td = timedelta(hours=len(self.model.prices) - 1)
ret = self.model.now_dt.replace(hour=0, minute=0) + td
return ret
return max(self.model.price_dict.keys())
except Exception as e:
_LOGGER.error(
f"Error on getting last known price with {self.model.now_dt} and len prices {len(self.model.prices)}: {e}")
f"Error on getting last known price with {self.model.now_dt} and len prices {len(self.model.price_dict.items())}: {e}")
return datetime.max

def _set_minute_start(self, now_dt=None, low_period=0, delayed=False, new_demand: int = None) -> int:
def _set_minute_start(self, now_dt=None, new_demand: int = None) -> int:
now_dt = self.model.now_dt if now_dt is None else now_dt
demand = new_demand if new_demand is not None else self.model.demand_minutes
if low_period >= 60 - now_dt.minute and not delayed:
start_minute = max(now_dt.minute, min(60 - int(demand / 2), 59))
else:
start_minute = min(60 - int(demand / 2), 59)
return start_minute

def _set_start_dt(self, low_period: int, delayed_dt: datetime = None, delayed: bool = False,
new_demand: int = None) -> datetime:
return min(60 - int(demand / 2), 59)

def _set_start_dt(self, delayed_dt: datetime = None, delayed: bool = False, new_demand: int = None) -> datetime:
now_dt = self.model.now_dt if delayed_dt is None else delayed_dt
start_minute: int = self._set_minute_start(now_dt, low_period, delayed, new_demand)
start_minute: int = self._set_minute_start(now_dt, new_demand)
return now_dt.replace(minute=start_minute)

def _get_low_period(self, override_dt=None) -> int:
dt = self.model.now_dt if override_dt is None else override_dt
if override_dt is not None:
_start_hour = dt.hour + (int(self.model.now_dt.day != override_dt.day) * 24)
else:
_start_hour = dt.hour
low_period: int = 0
for i in range(_start_hour, len(self.model.prices)):
if self.model.prices[i] > self.model.floating_mean:
break
if i == dt.hour:
low_period = 60 - dt.minute
else:
low_period += 60
return low_period

def _values_are_good(self, i, use_floating_mean) -> bool:
checklist = [i, i + 1, i - 23, i - 24]
non_hours = [dt.hour for dt in self.model.non_hours]
def _values_are_good(self, i: datetime, use_floating_mean: bool) -> bool:
return all([
(self.model.prices[i] < self.model.floating_mean if use_floating_mean else True) or self.model.prices[
i] < self.model.min_price,
(self.model.prices[i + 1] < self.model.floating_mean if use_floating_mean else True) or self.model.prices[
i + 1] < self.model.min_price,
not any(item in checklist for item in non_hours)
(self.model.price_dict[i] < self.model.floating_mean if use_floating_mean else True) or
self.model.price_dict[i] < self.model.min_price,
(self.model.price_dict[i + timedelta(hours=1)] < self.model.floating_mean if use_floating_mean else True) or
self.model.price_dict[i + timedelta(hours=1)] < self.model.min_price,
i not in self.model.non_hours,
(i + timedelta(hours=1)) not in self.model.non_hours
])

def _calculate_next_start(self, delay_dt=None) -> tuple[datetime, int | None]:
check_dt = (delay_dt if delay_dt else self.model.now_dt).replace(minute=0)
# print("is cold") if self.model.is_cold else print("is not cold- will be at:", self.model.cold_limit)
check_dt = self.norm_dt(delay_dt if delay_dt else self.model.now_dt)
try:
if self.model.prices[check_dt.hour] < self.model.floating_mean and self.model.is_cold and not any(
if self.model.price_dict[check_dt] < self.model.floating_mean and self.model.is_cold and not any(
[
check_dt in self.model.non_hours,
(check_dt + timedelta(hours=1)) in self.model.non_hours
]
):
"""This hour is cheap enough to start and it is cold"""
low_period = self._get_low_period()
return self._set_start_dt(low_period=low_period), None
return self._set_start_dt(), None

if len(self.model.demand_hours):
required_delay = self.model.latest_boost + timedelta(hours=REQUIRED_DEMAND_DELAY)
loopstart = max(self.model.now_dt, min(self.model.cold_limit, required_delay)).hour
loopstart = max(self.model.now_dt,
min(self.norm_dt(self.model.cold_limit), self.norm_dt(required_delay)))
use_floating_mean = False
min_demand_hour = min((hour for hour in self.model.demand_hours if hour > self.model.now_dt),default=None)
min_demand_hour = min(
(hour for hour in self.model.demand_hours if hour > max(loopstart, self.model.cold_limit)),
default=None)
if min_demand_hour is None: # If there's no demand hour later today, find the earliest one tomorrow
min_demand_hour = min([h + timedelta(hours=24) for h in self.model.demand_hours],default=self.model.now_dt)
loopend = min(len(self.model.prices) - 1, int((min_demand_hour - self.model.now_dt).total_seconds() / 3600) + self.model.now_dt.hour)
min_demand_hour = min([h + timedelta(hours=24) for h in self.model.demand_hours],
default=self.model.now_dt)
loopend = min(max(self.model.price_dict.keys()), min_demand_hour + timedelta(hours=-1))
override_demand = max(self.model.get_demand_minutes(self.model.current_temp), 26)
else:
"""start looping when we expect it to be cold"""
loopstart = int((self.model.cold_limit - self.model.now_dt).total_seconds() / 3600) + self.model.now_dt.hour
loopend = len(self.model.prices) - 1
loopstart = self.norm_dt(self.model.cold_limit)
loopend = max(self.model.price_dict.keys())
use_floating_mean = True
override_demand = None
i = self.find_lowest_2hr_combination(loopstart, loopend, use_floating_mean)
if i:
return self._set_start_dt_params(i), override_demand
return datetime.max, None
except Exception as e:
print(e)
return datetime.max, None

def find_lowest_2hr_combination(self, start_index: int, end_index: int, use_floating_mean: bool = True) -> int:
@staticmethod
def norm_dt(dt: datetime) -> datetime:
return dt.replace(minute=0, second=0, microsecond=0)

def find_lowest_2hr_combination(self, start_index: datetime, end_index: datetime,
use_floating_mean: bool = True) -> datetime:
min_sum = float('inf')
min_start_index = None
for i in range(start_index, end_index):
current: datetime = start_index
while current < end_index:
try:
current_sum = self.model.prices[i] + self.model.prices[i + 1]
current_sum = self.model.price_dict[current] + self.model.price_dict[current + timedelta(hours=1)]
if current_sum < min_sum:
if self._values_are_good(i, use_floating_mean):
if self._values_are_good(current, use_floating_mean):
min_sum = current_sum
min_start_index = i
if self._stop_2hr_combination(i):
min_start_index = current
if self._stop_2hr_combination(current):
break
except IndexError:
current += timedelta(hours=1)
except Exception as e:
print("2hr combo", e)
break
return min_start_index

def _stop_2hr_combination(self, i: int) -> bool:
def _stop_2hr_combination(self, i: datetime) -> bool:
return any([
self.model.is_cold,
self.model.cold_limit < (self.model.now_dt.replace(hour=0) + timedelta(hours=i))
self.model.cold_limit < i
])

def _set_start_dt_params(self, i: int) -> datetime:
delay = (i - self.model.now_dt.hour)
def _set_start_dt_params(self, i: datetime) -> datetime:
delay = (i - self.model.now_dt) / timedelta(hours=1)
delayed_dt = self.model.now_dt + timedelta(hours=delay)
low_period = self._get_low_period(override_dt=delayed_dt)
expected_temp = self.model.current_temp + (delay * self.model.temp_trend)
new_demand = max(self.model.get_demand_minutes(expected_temp),
DEMAND_MINUTES[self.model.preset][Demand.LowDemand])
return self._set_start_dt(low_period, delayed_dt, True, new_demand)
return self._set_start_dt(delayed_dt, True, new_demand)

0 comments on commit 1b333a5

Please sign in to comment.