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
20 changes: 14 additions & 6 deletions custom_components/pool_controller/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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"):
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
6 changes: 5 additions & 1 deletion custom_components/pool_controller/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
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
32 changes: 30 additions & 2 deletions custom_components/pool_controller/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion custom_components/pool_controller/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down