diff --git a/custom_components/pool_controller/climate.py b/custom_components/pool_controller/climate.py index 00213f5..b8c872b 100644 --- a/custom_components/pool_controller/climate.py +++ b/custom_components/pool_controller/climate.py @@ -41,7 +41,7 @@ class WhirlpoolClimate(CoordinatorEntity, ClimateEntity): _attr_has_entity_name = True _attr_translation_key = "pool_climate" # Ermöglicht Übersetzung via de.json _attr_temperature_unit = UnitOfTemperature.CELSIUS - _attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT] + _attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL] _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE # Defaults; will be overridden from entry config/options in __init__. _attr_min_temp = DEFAULT_MIN_TEMP @@ -123,7 +123,7 @@ def hvac_mode(self): return HVACMode.OFF if not bool(getattr(self.coordinator, "hvac_enabled", True)): return HVACMode.OFF - return HVACMode.HEAT + return HVACMode.COOL if bool(getattr(self.coordinator, "cool_mode", False)) else HVACMode.HEAT @property def hvac_action(self): @@ -145,8 +145,11 @@ def hvac_action(self): except Exception: power_saving_stage = 0 + cool = bool(getattr(self.coordinator, "cool_mode", False)) wants_heat = heat_reason not in ("", "off", "disabled") temp_below_target = (cur is not None and tgt is not None and cur < tgt) + temp_above_target = (cur is not None and tgt is not None and cur > tgt) + temp_demand = temp_above_target if cool else temp_below_target heating_active = False if data.get("frost_timer_active"): @@ -155,12 +158,14 @@ def hvac_action(self): heating_active = True elif bool(data.get("should_pump_on")) and ( power_saving_stage >= 1 or run_reason == "power_saving" - ) and (temp_below_target or cur is None or tgt is None): + ) and (temp_demand or cur is None or tgt is None): heating_active = True - elif wants_heat and temp_below_target and bool(data.get("should_pump_on")): + elif wants_heat and temp_demand and bool(data.get("should_pump_on")): heating_active = True - return HVACAction.HEATING if heating_active else HVACAction.IDLE + if not heating_active: + return HVACAction.IDLE + return HVACAction.COOLING if cool else HVACAction.HEATING @property def preset_mode(self): @@ -209,9 +214,12 @@ async def async_set_hvac_mode(self, hvac_mode): if hvac_mode == HVACMode.OFF: await self.coordinator.stop_manual_heat() await self.coordinator.set_hvac_enabled(False) + elif hvac_mode == HVACMode.COOL: + await self.coordinator.set_cool_mode(True) + await self.coordinator.set_hvac_enabled(True) else: + await self.coordinator.set_cool_mode(False) await self.coordinator.set_hvac_enabled(True) - await self.coordinator.start_manual_heat_to_target() await self.coordinator.async_request_refresh() async def async_set_preset_mode(self, preset_mode: str): diff --git a/custom_components/pool_controller/const.py b/custom_components/pool_controller/const.py index 753c316..0cfeb4b 100644 --- a/custom_components/pool_controller/const.py +++ b/custom_components/pool_controller/const.py @@ -395,4 +395,8 @@ OPT_KEY_DERIVED_COST_NET_MONTH_TOTAL = "derived_cost_net_month_total" OPT_KEY_DERIVED_COST_NET_YEAR_TOTAL = "derived_cost_net_year_total" OPT_KEY_DERIVED_COST_NET_MONTH_ID = "derived_cost_net_month_id" -OPT_KEY_DERIVED_COST_NET_YEAR_ID = "derived_cost_net_year_id" \ No newline at end of file +OPT_KEY_DERIVED_COST_NET_YEAR_ID = "derived_cost_net_year_id" +# --- Heating/Cooling mode (ice bath) -------------------------------- +# Live-toggled via the climate entity hvac_mode (heat vs cool); persisted here. +OPT_KEY_COOL_MODE = "cool_mode" +DEFAULT_COOL_MODE = False diff --git a/custom_components/pool_controller/coordinator.py b/custom_components/pool_controller/coordinator.py index e8a9feb..2aefbc7 100644 --- a/custom_components/pool_controller/coordinator.py +++ b/custom_components/pool_controller/coordinator.py @@ -66,6 +66,7 @@ def __init__(self, hass, entry): self.maintenance_active = False # HVAC enabled state (thermostat-style heating when PV optimization is disabled). self.hvac_enabled = True + self.cool_mode = DEFAULT_COOL_MODE # Away mode (reduced activity) self.away_active = False self.away_temp = DEFAULT_AWAY_TEMP @@ -209,6 +210,7 @@ def __init__(self, hass, entry): if entry and entry.options: self.maintenance_active = bool(entry.options.get(OPT_KEY_MAINTENANCE_ACTIVE, False)) self.hvac_enabled = bool(entry.options.get(OPT_KEY_HVAC_ENABLED, True)) + self.cool_mode = bool(entry.options.get(OPT_KEY_COOL_MODE, False)) self.away_active = bool(entry.options.get(OPT_KEY_AWAY_ACTIVE, False)) self.power_saving_active = bool(entry.options.get(OPT_KEY_POWER_SAVING_ACTIVE, False)) self.manual_mode_active = bool(entry.options.get(OPT_KEY_MANUAL_MODE_ACTIVE, False)) @@ -1932,6 +1934,20 @@ async def set_boost(self, active: bool): except Exception: _LOGGER.exception("Fehler beim Speichern von boost mode") + async def set_cool_mode(self, cool: bool): + """Switch between heating and cooling regulation (persisted).""" + cool = bool(cool) + if cool == bool(getattr(self, "cool_mode", False)): + return + self.cool_mode = cool + try: + new_opts = {**(self.entry.options or {})} + new_opts[OPT_KEY_COOL_MODE] = cool + await self._async_update_entry_options(new_opts) + except Exception: + pass + await self.async_request_refresh() + async def set_hvac_enabled(self, enabled: bool): """Enable/disable thermostat behavior (independent from maintenance).""" enabled = bool(enabled) @@ -2114,6 +2130,12 @@ def _thermostat_demand(self, current_temp: float | None, target_temp: float, col hot = float(hot_tolerance) except Exception: return prev_on + if getattr(self, "cool_mode", False): + # Cooling: ON above (target+hot), OFF at/below (target-cold). + if prev_on: + return ct > (tt - cold) + return ct > (tt + hot) + # Heating (unchanged): if prev_on: return ct < (tt + hot) return ct < (tt - cold) @@ -4170,7 +4192,10 @@ def _quiet_end_for(dt_obj, cfg): # PV-based run should stop once target is reached (with same hysteresis). try: - if water_temp is not None and float(water_temp) >= float(target_temp_effective): + if water_temp is not None and ( + (float(water_temp) <= float(target_temp_effective)) if getattr(self, "cool_mode", False) + else (float(water_temp) >= float(target_temp_effective)) + ): pv_heat_demand = False else: pv_heat_demand = self._thermostat_demand( @@ -4238,7 +4263,10 @@ def _quiet_end_for(dt_obj, cfg): # Aux heater is strictly for heating. Once target is reached, keep it OFF # even if a manual timer (e.g. bathing/filter/chlorine) is active. try: - if water_temp is not None and float(water_temp) >= float(target_temp_effective): + if water_temp is not None and ( + (float(water_temp) <= float(target_temp_effective)) if getattr(self, "cool_mode", False) + else (float(water_temp) >= float(target_temp_effective)) + ): aux_heat_demand = False else: aux_heat_demand = self._thermostat_demand( diff --git a/custom_components/pool_controller/sensor.py b/custom_components/pool_controller/sensor.py index 8150b3c..ac77dd1 100644 --- a/custom_components/pool_controller/sensor.py +++ b/custom_components/pool_controller/sensor.py @@ -82,7 +82,7 @@ async def async_setup_entry(hass, entry, async_add_entities): PoolChemSensor(coordinator, "tds_water_change_percent", None, "%", "mdi:water-percent", state_class=None, entity_category=EntityCategory.DIAGNOSTIC), PoolChemSensor(coordinator, "ph_minus_g", None, "g", "mdi:pill", device_class=SensorDeviceClass.WEIGHT, state_class=None, entity_category=EntityCategory.DIAGNOSTIC), PoolChemSensor(coordinator, "ph_plus_g", None, "g", "mdi:pill", device_class=SensorDeviceClass.WEIGHT, state_class=None, entity_category=EntityCategory.DIAGNOSTIC), - PoolChemSensor(coordinator, "chlor_spoons", None, "Löffel", "mdi:spoon-sugar", state_class=None, entity_category=EntityCategory.DIAGNOSTIC), + PoolChemSensor(coordinator, "chlor_spoons", None, "spoons", "mdi:spoon-sugar", state_class=None, entity_category=EntityCategory.DIAGNOSTIC), PoolChemSensor(coordinator, "next_start_mins", None, "min", "mdi:clock-start", device_class=SensorDeviceClass.DURATION), PoolChemSensor(coordinator, "next_frost_mins", None, "min", "mdi:clock-start", device_class=SensorDeviceClass.DURATION, state_class=None, entity_category=EntityCategory.DIAGNOSTIC), PoolChemSensor(coordinator, "event_rain_probability", None, "%", "mdi:weather-rainy", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC),