From 177759b7012536556f7819f6e4be57d66eb9ac2e Mon Sep 17 00:00:00 2001 From: magnuselden Date: Thu, 12 Sep 2024 20:43:45 +0200 Subject: [PATCH] misc --- .../peaqhvac/sensors/min_maxsensor.py | 2 + .../peaqhvac/sensors/money_data_sensor.py | 2 + .../peaqhvac/sensors/offsetsensor.py | 3 + .../peaqhvac/sensors/peaqsensor.py | 2 + .../peaqhvac/sensors/simple_money_sensor.py | 2 + .../peaqhvac/sensors/simple_sensor.py | 2 + .../peaqhvac/sensors/trendsensor.py | 2 + .../peaqhvac/service/hub/hub_factory.py | 12 +++- .../peaqhvac/service/hub/hubsensors.py | 2 +- .../peaqhvac/service/hub/weather_prognosis.py | 3 +- .../house_heater/house_heater_coordinator.py | 1 + .../service/hvac/interfaces/ihvactype.py | 3 +- .../service/hvac/offset/offset_coordinator.py | 8 ++- .../service/hvac/offset/offset_utils.py | 61 ++++++++++++++++-- .../peaqhvac/test/test_offsets.py | 64 +++++++------------ 15 files changed, 115 insertions(+), 54 deletions(-) diff --git a/custom_components/peaqhvac/sensors/min_maxsensor.py b/custom_components/peaqhvac/sensors/min_maxsensor.py index fab9e2b..a02506c 100644 --- a/custom_components/peaqhvac/sensors/min_maxsensor.py +++ b/custom_components/peaqhvac/sensors/min_maxsensor.py @@ -35,6 +35,8 @@ def icon(self) -> str: return "mdi:thermometer" def update(self) -> None: + if not self._hub.is_initialized: + return if self._sensorname == AVERAGESENSOR_INDOORS: self._state = self._hub.sensors.average_temp_indoors.value self._min = self._hub.sensors.average_temp_indoors.min diff --git a/custom_components/peaqhvac/sensors/money_data_sensor.py b/custom_components/peaqhvac/sensors/money_data_sensor.py index 971b2df..32674b3 100644 --- a/custom_components/peaqhvac/sensors/money_data_sensor.py +++ b/custom_components/peaqhvac/sensors/money_data_sensor.py @@ -37,6 +37,8 @@ def icon(self) -> str: return "mdi:database-outline" async def async_update(self) -> None: + if not self.hub.is_initialized: + return try: ret = self.hub.spotprice.average_data except: diff --git a/custom_components/peaqhvac/sensors/offsetsensor.py b/custom_components/peaqhvac/sensors/offsetsensor.py index 0f2dfc2..0d85f5b 100644 --- a/custom_components/peaqhvac/sensors/offsetsensor.py +++ b/custom_components/peaqhvac/sensors/offsetsensor.py @@ -24,6 +24,7 @@ def __init__(self, hub, entry_id, name): self._peaks_today = [] self._peaks_tomorrow = [] self._prognosis = [] + self._aux_dict = None @property def unit_of_measurement(self): @@ -38,6 +39,8 @@ def icon(self) -> str: return "mdi:stairs" async def async_update(self) -> None: + if not self._hub.is_initialized: + return state_result = await self._hub.hvac_service.house_heater.async_adjusted_offset( self._hub.hvac_service.model.current_offset ) diff --git a/custom_components/peaqhvac/sensors/peaqsensor.py b/custom_components/peaqhvac/sensors/peaqsensor.py index 56fda34..56da578 100644 --- a/custom_components/peaqhvac/sensors/peaqsensor.py +++ b/custom_components/peaqhvac/sensors/peaqsensor.py @@ -38,6 +38,8 @@ def icon(self) -> str: return self._icon async def async_update(self) -> None: + if not self._hub.is_initialized: + return if self._sensorname == HEATINGDEMAND: self._state = self._hub.hvac_service.house_heater.demand.value elif self._sensorname == WATERDEMAND: diff --git a/custom_components/peaqhvac/sensors/simple_money_sensor.py b/custom_components/peaqhvac/sensors/simple_money_sensor.py index aa4f199..34897de 100644 --- a/custom_components/peaqhvac/sensors/simple_money_sensor.py +++ b/custom_components/peaqhvac/sensors/simple_money_sensor.py @@ -45,6 +45,8 @@ def unit_of_measurement(self): return self._attr_unit_of_measurement async def async_update(self) -> None: + if not self.hub.is_initialized: + return self._use_cent = self.hub.spotprice.use_cent self._attr_unit_of_measurement = getattr(self.hub.spotprice, "currency") ret = getattr(self.hub.spotprice, self._caller_attribute) diff --git a/custom_components/peaqhvac/sensors/simple_sensor.py b/custom_components/peaqhvac/sensors/simple_sensor.py index f1349d7..ceaaa36 100644 --- a/custom_components/peaqhvac/sensors/simple_sensor.py +++ b/custom_components/peaqhvac/sensors/simple_sensor.py @@ -39,6 +39,8 @@ def icon(self) -> str: return self._icon async def async_update(self) -> None: + if not self._hub.is_initialized: + return ret = await self._hub.async_get_internal_sensor(self._internal_entity) if ret is not None: if self._internal_entity == NEXT_WATER_START: diff --git a/custom_components/peaqhvac/sensors/trendsensor.py b/custom_components/peaqhvac/sensors/trendsensor.py index 82f10ae..c835feb 100644 --- a/custom_components/peaqhvac/sensors/trendsensor.py +++ b/custom_components/peaqhvac/sensors/trendsensor.py @@ -58,6 +58,8 @@ def extra_state_attributes(self) -> dict: return attr_dict async def async_update(self) -> None: + if not self._hub.is_initialized: + return self._state = getattr(self.datasensor, "trend") self._samples = getattr(self.datasensor, "samples") self._oldest_sample = getattr(self.datasensor, "oldest_sample") diff --git a/custom_components/peaqhvac/service/hub/hub_factory.py b/custom_components/peaqhvac/service/hub/hub_factory.py index 87af371..1bb9907 100644 --- a/custom_components/peaqhvac/service/hub/hub_factory.py +++ b/custom_components/peaqhvac/service/hub/hub_factory.py @@ -27,11 +27,15 @@ def __init__(self, hass: HomeAssistant): self.hub = None async def async_create(self, options: ConfigModel) -> Hub: + _LOGGER.debug("Entering async_create in hub_factory") observer = Observer(self.hass) + _LOGGER.debug("Hubfactory > Created observer") options.observer = observer options.misc_options.peaqev_discovered = self._get_peaqev() + _LOGGER.debug("Hubfactory > Created options and set peaqev_discovered") hub = Hub(self.hass, observer, options) + _LOGGER.debug("Hubfactory > Created hub") spotprice = SpotPriceFactory.create( hub=hub, observer=observer, @@ -39,8 +43,11 @@ async def async_create(self, options: ConfigModel) -> Hub: test=False, is_active=True, ) + _LOGGER.debug("Hubfactory > Created spotprice") sensors = HubSensors(observer=observer, options=options, hass=self.hass) + _LOGGER.debug("Hubfactory > Created sensors") states = StateChanges(hub, self.hass) + _LOGGER.debug("Hubfactory > Created states") self.hub = hub await self.async_setup(spotprice, sensors, states) return hub @@ -53,12 +60,13 @@ async def async_setup(self, spotprice, sensors, states) -> None: self.hub.hvac_service = HvacFactory.create( self.hass, self.hub.options, self.hub, self.hub.observer ) - + _LOGGER.debug("Hubfactory > Created hvacfactory") self.hub.prognosis = WeatherPrognosis( self.hass, sensors.average_temp_outdoors, self.hub.observer ) + _LOGGER.debug("Hubfactory > Created prognosis") self.hub.offset = OffsetFactory.create(self.hub, self.hub.observer) - + _LOGGER.debug("Hubfactory > Created offset") await self.async_setup_trackers() async def async_setup_trackers(self): diff --git a/custom_components/peaqhvac/service/hub/hubsensors.py b/custom_components/peaqhvac/service/hub/hubsensors.py index ddb9817..570e762 100644 --- a/custom_components/peaqhvac/service/hub/hubsensors.py +++ b/custom_components/peaqhvac/service/hub/hubsensors.py @@ -32,7 +32,7 @@ def __init__(self, observer, options: ConfigModel, hass): initval=options.misc_options.enabled_on_boot, data_type=bool ) self.hvac_tolerance = options.hvac_tolerance - self.average_temp_indoors = Average(entities=options.indoor_tempsensors) + self.average_temp_indoors = Average(entities=options.indoor_tempsensors, observer=observer) self.average_temp_outdoors = Average( entities=options.outdoor_tempsensors, observer_message=ObserverTypes.TemperatureOutdoorsChanged, diff --git a/custom_components/peaqhvac/service/hub/weather_prognosis.py b/custom_components/peaqhvac/service/hub/weather_prognosis.py index c8366fc..d25f7f3 100644 --- a/custom_components/peaqhvac/service/hub/weather_prognosis.py +++ b/custom_components/peaqhvac/service/hub/weather_prognosis.py @@ -202,7 +202,8 @@ def _get_two_hour_prog(self, thishour: datetime) -> PrognosisExportModel | None: def _setup_weather_prognosis( self, ): # todo: this must be handled with weeather servicecall. - pass + self._is_initialized = True + # try: # entities = template.integration_entities(self._hass, "met") # if len(entities) < 1: diff --git a/custom_components/peaqhvac/service/hvac/house_heater/house_heater_coordinator.py b/custom_components/peaqhvac/service/hvac/house_heater/house_heater_coordinator.py index c8f34a9..59a4d83 100644 --- a/custom_components/peaqhvac/service/hvac/house_heater/house_heater_coordinator.py +++ b/custom_components/peaqhvac/service/hvac/house_heater/house_heater_coordinator.py @@ -132,6 +132,7 @@ async def async_calculated_offsetdata( self.hub.sensors.set_temp_indoors.adjusted_temp, ) + _LOGGER.debug(f"current_offset: {current_offset}, tempdiff: {tempdiff}, tempextremas: {tempextremas}, temptrend: {temptrend}") return CalculatedOffsetModel( current_offset=current_offset, current_tempdiff=tempdiff, diff --git a/custom_components/peaqhvac/service/hvac/interfaces/ihvactype.py b/custom_components/peaqhvac/service/hvac/interfaces/ihvactype.py index 3934d4a..193d985 100644 --- a/custom_components/peaqhvac/service/hvac/interfaces/ihvactype.py +++ b/custom_components/peaqhvac/service/hvac/interfaces/ihvactype.py @@ -155,7 +155,8 @@ async def async_update_offset(self, raw_offset:int|None = None) -> bool: if self.model.current_offset != _hvac_offset: await self.observer.async_broadcast(ObserverTypes.OffsetsChanged) if self._force_update: - await self.observer.async_broadcast(ObserverTypes.UpdateOperation) + # await self.observer.async_broadcast(ObserverTypes.UpdateOperation) + await self.request_periodic_updates() ret = True except Exception as e: _LOGGER.exception(f"Error in updating offsets: {e}") diff --git a/custom_components/peaqhvac/service/hvac/offset/offset_coordinator.py b/custom_components/peaqhvac/service/hvac/offset/offset_coordinator.py index 156f8b1..161fa43 100644 --- a/custom_components/peaqhvac/service/hvac/offset/offset_coordinator.py +++ b/custom_components/peaqhvac/service/hvac/offset/offset_coordinator.py @@ -57,9 +57,11 @@ def current_offset(self) -> int: try: self._set_offset() if len(self.model.raw_offsets): - ret = self.model.raw_offsets.get( - datetime.now().replace(minute=0, second=0, microsecond=0), 0 - ) + latest_key = max((key for key in self.model.raw_offsets if key <= datetime.now()), default=None) + if latest_key is not None: + ret = self.model.raw_offsets[latest_key] + else: + ret = 0 except KeyError as e: _LOGGER.error( f"Unable to get current offset: {e}. raw_offsets: {self.model.raw_offsets}" diff --git a/custom_components/peaqhvac/service/hvac/offset/offset_utils.py b/custom_components/peaqhvac/service/hvac/offset/offset_utils.py index c5c5f30..2d9d57b 100644 --- a/custom_components/peaqhvac/service/hvac/offset/offset_utils.py +++ b/custom_components/peaqhvac/service/hvac/offset/offset_utils.py @@ -55,9 +55,9 @@ def get_offset_dict(offset_dict, dt_now) -> dict: } -def set_offset_dict(prices: list[float], dt: datetime, min_price: float) -> dict: +def set_offset_dict(prices: list[float], dt: datetime, min_price: float, split_into_parts: bool = False) -> dict: dt = dt.replace(minute=0, second=0, microsecond=0) - all_offsets = _deviation_from_mean(prices, min_price, dt) + all_offsets = _deviation_from_mean(prices, min_price, dt, split_into_parts) return all_offsets @@ -71,7 +71,7 @@ def _get_timedelta(prices: list[float]) -> int: def _deviation_from_mean( - prices: list[float], min_price: float, dt: datetime + prices: list[float], min_price: float, dt: datetime, split_into_parts: bool = False ) -> dict[datetime, float]: if not len(prices): return {} @@ -103,7 +103,60 @@ def _deviation_from_mean( else: setval = round(deviation, 2) deviation_dict[dt.replace(hour=0) + timedelta(minutes=delta * i)] = setval - return deviation_dict + if not split_into_parts: + return deviation_dict + return handle_splitting(deviation_dict) + + +def handle_splitting(data: dict[datetime, float]) -> dict[datetime, float]: + sorted_keys = sorted(data.keys()) + result = {} + + for i in range(len(sorted_keys) - 1): + current_key = sorted_keys[i] + next_key = sorted_keys[i + 1] + current_value = data[current_key] + next_value = data[next_key] + + # Calculate the difference + diff = next_value - current_value + + # Determine the split intervals + if abs(diff) > 1.0: # Large difference, split into half-hour intervals + interval = timedelta(minutes=30) + num_splits = 2 + elif abs(diff) > 0.5: # Moderate difference, split into 20-minute intervals + interval = timedelta(minutes=20) + num_splits = 3 + elif abs(diff) > 0.2: # Small difference, split into quarterly intervals + interval = timedelta(minutes=15) + num_splits = 4 + else: + interval = timedelta(minutes=60) + num_splits = 1 + + + # Calculate the split values + split_values = [current_value] * num_splits + + # Adjust the last split value to be closer to the next hour's deviation + if num_splits > 1: + split_values[-1] = (split_values[-1] + next_value) / 2 + + # Ensure the average of the split values equals the original value + total_adjustment = current_value * num_splits - sum(split_values) + adjustment_per_split = total_adjustment / num_splits + split_values = [v + adjustment_per_split for v in split_values] + + # Add the split values to the result + for j in range(num_splits): + split_key = current_key + interval * j + result[split_key] = split_values[j] + + # Add the last key-value pair + result[sorted_keys[-1]] = data[sorted_keys[-1]] + + return result def max_price_lower_internal(tempdiff: float, peaks_today: list) -> bool: diff --git a/custom_components/peaqhvac/test/test_offsets.py b/custom_components/peaqhvac/test/test_offsets.py index 5995b1b..8dd48d5 100644 --- a/custom_components/peaqhvac/test/test_offsets.py +++ b/custom_components/peaqhvac/test/test_offsets.py @@ -228,50 +228,30 @@ def test_assert_cheaper_hours_tomorrow_not_lower_offset_than_today(): # assert 1 > 2 +def test_split_into_periods(): + _tolerance = 10 + indoors_preset = HvacPresets.Normal + prices = P231218 + prices_tomorrow = P231219 + now_dt = datetime(2023, 12, 18, 20, 43, 0) + offset_dict = set_offset_dict(prices + prices_tomorrow, now_dt, 0, False) + offs2 = offset_per_day( + all_prices=prices + prices_tomorrow, + day_values=offset_dict, + tolerance=_tolerance, + indoors_preset=indoors_preset, + ) -# def test_offsets_correct_curve_over_night_cached_today(): -# _tolerance = 3 -# indoors_preset = HvacPresets.Normal -# prices = P231215 -# prices_tomorrow = P231216 -# now_dt = datetime(2023,12,15,0,3,0) -# offset_dict1 = set_offset_dict(prices, now_dt, 0, {}) -# today1 = offset_per_day( -# all_prices=prices, -# day_values=offset_dict1, -# tolerance=_tolerance, -# indoors_preset=indoors_preset, -# ) -# -# ret1 = smooth_transitions( -# vals=today1, -# tolerance=_tolerance, -# ) -# key_today_only = [1,1, 2, 2, 3, 2, -2, -# -4, -7, -6, -4, -3, -1, 0, 1, 0, -# -2, -1, 1, 2, 2, 3, 6, 4] -# assert [v for k,v in ret1.items()] == key_today_only -# -# now_dt = datetime(2023,12,15,13,3,0) -# offset_dict2 = set_offset_dict(prices+prices_tomorrow, now_dt, 0, ret1) -# today2 = offset_per_day( -# all_prices=prices + prices_tomorrow, -# day_values=offset_dict2, -# tolerance=_tolerance, -# indoors_preset=indoors_preset, -# ) -# -# ret2 = smooth_transitions( -# vals=today2, -# tolerance=_tolerance, -# ) -# -# key_today = [-3, -2, -2, -2, -2, -2, -# -3, -4, -5, -5, -4, -4, -3, -2, -2, -2, -3, -3, -2, -2, -2, -2, -1, -2, -# 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2] -# -# assert [v for k, v in ret2.items()] == key_today + ret = smooth_transitions( + vals=offs2, + tolerance=_tolerance, + ) + + print(offset_dict) + print(offs2) + print(ret) + assert 1 > 2 def test_smooth_transistions_no_weather_prog_nothing_exceeds_tolerance(): _tolerance = 3